Prebuilt compiler toolchains for the MeshInspector CI: -O3 + PGO builds of
LLVM/Clang that are considerably faster at compiling C++ than the stock distro
packages or Homebrew bottles of the same version.
Everything is published as GitHub release assets in this repository. There is
no package index and no authentication — every asset is a plain
https://github.com/MeshInspector/toolchains/releases/download/<tag>/<asset>
URL that curl can fetch anonymously.
| Tag | Platform | Archs | Unpacks to | Size | Status |
|---|---|---|---|---|---|
clang-22.1.8-pgo-dylib-linux |
Linux, glibc >= 2.28 | x86_64, aarch64 |
llvm-pgo-dylib-22.1.8/ |
~185 MB | current |
clang-emsdk-4.0.19-pgo |
emscripten/emsdk:4.0.19 (jammy, glibc >= 2.35) |
x86_64, aarch64 |
llvm-emsdk-4.0.19-pgo/ |
~114 MB | current |
clang-18.1.8-pgo-dylib-linux |
Linux, glibc >= 2.28 | x86_64, aarch64 |
llvm-pgo-dylib-18.1.8/ |
~178 MB | current |
clang-22.1.8-pgo-rockylinux8 |
Linux, glibc >= 2.28 | x64, arm64 |
llvm-pgo-22.1.8/ |
~1 GB | superseded |
llvm-pgo-22.1.8_2-arm64 |
macOS arm64 | arm64 |
Homebrew keg under /opt/homebrew |
~438 MB | current |
llvm-pgo-22.1.8_2-x64 |
macOS Intel | x86_64 |
Homebrew keg under /usr/local |
~468 MB | current |
lld-22.1.8-pgo-macos |
macOS >= 12.0 (Intel), >= 13.0 (arm64) | x86_64, arm64 |
one ld64.lld binary, into the keg's bin/ |
54-59 MB | current |
To list them from the command line:
gh release list --repo MeshInspector/toolchains
gh release view clang-22.1.8-pgo-dylib-linux --repo MeshInspector/toolchains
gh release download clang-22.1.8-pgo-dylib-linux --repo MeshInspector/toolchains --pattern "*x86_64*"
Asset names are stable once published, but a tag can be renamed in place (that
is how clang-22.1.8-pgo-dylib-rockylinux8 became clang-22.1.8-pgo-dylib-linux),
which 404s the old URLs. Pin the tag in exactly one place per consumer so a
rename stays a one-line fix.
The compiler emscripten/emsdk:4.0.19 ships, rebuilt with PGO. Upstream builds
that toolchain with ThinLTO and assertions off but no PGO at all
(emscripten-releases src/build.py has no LLVM_BUILD_INSTRUMENTED, no
LLVM_PROFDATA_FILE, no BOLT), so this is -O3 + IR-PGO + ThinLTO on top of the
same configuration.
It is the same llvm-project revision emsdk pins, 12f392cff /
clang version 22.0.0git, read out of the emscripten-releases DEPS for the
4.0.19 tag. That matters: it keeps emcc's EXPECTED_LLVM_VERSION = 22 check
satisfied and keeps the image's prebuilt wasm sysroot and resource headers
valid, so dropping it in is a compiler swap and nothing else.
Point emscripten at it with EM_LLVM_ROOT — emscripten honours EM_<KEY> for
any config key, so nothing under /emsdk has to be modified:
TOOLCHAIN=clang-emsdk-4.0.19-pgo
curl -fsSL --retry 5 --retry-all-errors "https://github.com/MeshInspector/toolchains/releases/download/${TOOLCHAIN}/${TOOLCHAIN}-$(uname -m).tar.gz" | tar -C /opt -xz
export EM_LLVM_ROOT=/opt/llvm-emsdk-4.0.19-pgo/bin
emcc --version
Compile time against the stock emsdk compiler, same runner, template-heavy
translation unit, em++ -O3 -std=c++20 -c, min of 5 with both caches warm:
| arch | stock emsdk clang | this toolchain | delta |
|---|---|---|---|
x86_64 |
1647 ms | 1336 ms | -18.9% |
aarch64 |
2159 ms | 1670 ms | -22.6% |
sha256 of the tarballs:
65c70c0d997a9ab6a82c8ea6af5c69f46ac64bc96bd8816a3a29f445c7edc21d clang-emsdk-4.0.19-pgo-x86_64.tar.gz
a740be411d2333189f28783af80a35b9efcbcabcdb5ef2ebd5d0650c9d9b764a clang-emsdk-4.0.19-pgo-aarch64.tar.gz
Two things to know before reusing this recipe for another emsdk version:
- It is built inside the target image, not in
rockylinux:8like the kegs above, so the glibc floor is jammy's 2.35 rather than 2.28. That is deliberate: the training step drives the image's ownembuilderto profile the real WebAssembly code paths (system libraries for wasm32 and wasm64), alongside a slice of LLVM itself for the frontend and middle-end. - The emsdk clang cannot bootstrap it. Its compiler-rt is wasm-only, so it
cannot link
-fprofile-generatebinaries, and the arm64 one reports its own host target asunknownbecause it is cross-built — a native CMake configure with it fails outright. The recipe therefore builds a stage1 clang with the distro gcc first.
Only the bin/ tree is needed by emscripten; the tarball also carries
lib/clang/22 from the same revision, which is why overlaying it onto
/emsdk/upstream works too if EM_LLVM_ROOT is inconvenient.
clang/lld 22.1.8 built with -O3 + IR-PGO (no LTO), LLVM_ENABLE_RTTI=ON, and
distro-style shared libLLVM.so / libclang-cpp.so, which is what mrbind links
against. Built in a rockylinux:8 container, so the glibc floor is 2.28 and one
tarball runs on every Linux image we use (RockyLinux 8/9, Ubuntu 22/24).
Assets are named after uname -m, so no arch mapping is needed:
TOOLCHAIN=clang-22.1.8-pgo-dylib-linux
curl -fsSL "https://github.com/MeshInspector/toolchains/releases/download/${TOOLCHAIN}/${TOOLCHAIN}-$(uname -m).tar.gz" | tar -C /opt -xz
/opt/llvm-pgo-dylib-22.1.8/bin/clang++ --version
Point the build at it with LLVM_PREFIX=/opt/llvm-pgo-dylib-22.1.8 (mrbind and
the Python bindings read it), or with
-DCMAKE_CXX_COMPILER=/opt/llvm-pgo-dylib-22.1.8/bin/clang++.
sha256 of the tarballs:
97b40230ff3c65e567b532b3a7cff009b0f3d397b3b5311816743a50f5641727 clang-22.1.8-pgo-dylib-linux-x86_64.tar.gz
41e405b76822e19b7d6cf00db648d290ad78b274fd03a970b91a0bbbb76af5d7 clang-22.1.8-pgo-dylib-linux-aarch64.tar.gz
Each tarball has a .sha256 sidecar asset that names the tarball itself, so
fetching both side by side and running sha256sum -c just works:
TOOLCHAIN=clang-22.1.8-pgo-dylib-linux
ASSET=${TOOLCHAIN}-$(uname -m).tar.gz
BASE=https://github.com/MeshInspector/toolchains/releases/download/${TOOLCHAIN}
curl -fsSL -O "${BASE}/${ASSET}" -O "${BASE}/${ASSET}.sha256"
sha256sum -c "${ASSET}.sha256"
tar -C /opt -xzf "${ASSET}"
One host requirement: the keg picks its default libstdc++ from /opt/rh, and
clang 22 only scans that tree up to gcc-toolset-13. Install
gcc-toolset-13-gcc-c++ — the -gcc-c++ package, not just libstdc++-devel,
because detection keys on crtbegin.o — or the C++ standard library is not found.
The same recipe, container and flags as the 22.1.8 dylib keg above, built from llvmorg-18.1.8 (the last 18.x release) for consumers that need clang 18 rather than 22. Install it the same way — only the version differs:
TOOLCHAIN=clang-18.1.8-pgo-dylib-linux
ASSET=${TOOLCHAIN}-$(uname -m).tar.gz
BASE=https://github.com/MeshInspector/toolchains/releases/download/${TOOLCHAIN}
curl -fsSL -O "${BASE}/${ASSET}" -O "${BASE}/${ASSET}.sha256"
sha256sum -c "${ASSET}.sha256"
tar -C /opt -xzf "${ASSET}"
/opt/llvm-pgo-dylib-18.1.8/bin/clang++ --version
eb544c6ebe6db2a5ebc3cb288c021bbbc6242ae324245d74e221d94d539bbcd4 clang-18.1.8-pgo-dylib-linux-x86_64.tar.gz
684e54fe27645450dd9b63f9cdf7b9c30d09cb69a72e07d5aaa9f9b1f94fc6fc clang-18.1.8-pgo-dylib-linux-aarch64.tar.gz
This major installs the dylibs as libLLVM.so.18.1 and libclang-cpp.so.18.1,
with libLLVM-18.so and the unsuffixed libLLVM.so as symlinks beside them.
Same sources and container, but a static-LLVM + ThinLTO build: ~4-7% faster compiles than the dylib flavor, at ~1 GB per tarball (+0.8 GiB on a Docker image) and only usable by mrbind through its static-LLVM machinery. Unused since the dylib switch; kept for reference and A/B measurements.
ARCH=x64 # or arm64 -- this release predates the uname-style asset names
TOOLCHAIN=clang-22.1.8-pgo-rockylinux8
ASSET=${TOOLCHAIN}-${ARCH}.tar.gz
BASE=https://github.com/MeshInspector/toolchains/releases/download/${TOOLCHAIN}
curl -fsSL -O "${BASE}/${ASSET}" -O "${BASE}/${ASSET}.sha256"
sha256sum -c "${ASSET}.sha256"
tar -C /opt -xzf "${ASSET}"
/opt/llvm-pgo-22.1.8/bin/clang++ --version
3deac7c02e46b7d048e80c78d097622eab7927671d99e4307d1c3a7a9f48a7d2 clang-22.1.8-pgo-rockylinux8-x64.tar.gz
7867aa7d6603234d336caaf13bb629e3672cfe9aae7117ab43fe47f29c4af2b6 clang-22.1.8-pgo-rockylinux8-arm64.tar.gz
PGO + ThinLTO LLVM/Clang 22.1.8, built from a local-tap copy of homebrew-core's
llvm.rb (llvmorg-22.1.8) with ENV.O3 and brew install --build-bottle, which
is what enables the formula's stage1 -> instrumented stage2 -> train on the
clang/LLVM test suites -> final build pipeline. The formula gates PGO to arm, so
the x64 build removes that gate. z3 excluded; the x64 superenv baseline is
-march=nehalem. Roughly 25-30% faster at compiling C++ than a plain build of the
same version.
These are Homebrew kegs relocated only to the standard prefix they were built
for: /opt/homebrew on arm64, /usr/local on Intel. A machine with a custom brew
prefix has to build its own.
arm64 (/opt/homebrew):
curl -fsSL -o /tmp/k.tar.gz https://github.com/MeshInspector/toolchains/releases/download/llvm-pgo-22.1.8_2-arm64/llvm-pgo-22.1.8_2.arm64_opt-homebrew.tar.gz
echo "94f76fb55ec64490605d0e68115259426a428a78f269f7e9160726d90a22e197 /tmp/k.tar.gz" | shasum -a 256 -c -
tar -xzf /tmp/k.tar.gz -C /opt/homebrew/Cellar
ln -sfn ../Cellar/llvm-pgo/22.1.8_2 /opt/homebrew/opt/llvm-pgo
brew install --quiet zstd xz
/opt/homebrew/Cellar/llvm-pgo/22.1.8_2/bin/clang++ --version
Intel (/usr/local):
curl -fsSL -o /tmp/k.tar.gz https://github.com/MeshInspector/toolchains/releases/download/llvm-pgo-22.1.8_2-x64/llvm-pgo-22.1.8_2.x86_64_usr-local.tar.gz
echo "8f1f1d97e4a5391c7f871bc5cf9601a9b4a1262e6e61f1538327bb48d7ef71ed /tmp/k.tar.gz" | shasum -a 256 -c -
tar -xzf /tmp/k.tar.gz -C /usr/local/Cellar
ln -sfn ../Cellar/llvm-pgo/22.1.8_2 /usr/local/opt/llvm-pgo
brew install --quiet zstd xz
/usr/local/Cellar/llvm-pgo/22.1.8_2/bin/clang++ --version
zstd and xz are the runtime deps that actually matter; python@3.14 is only
needed for lldb. The full set of external brew deps the keg links against is the
deps.txt asset of each release. Each release also has a sha256.txt naming its
own tarball, so shasum -a 256 -c sha256.txt works if you download both into the
same directory instead of pasting the digest.
In MeshLib CI this is wrapped as a composite action,
.github/actions/install-llvm-pgo-keg, which no-ops when the keg is already in
the local Cellar (self-hosted macOS runners build it themselves) and otherwise
runs exactly the commands above.
Homebrew stopped bottling lld for Intel macOS when it became a Tier 3
configuration, and neither the kegs above nor the bottled llvm@22 contains
ld64.lld — the lld formula is separate and only depends on llvm. So the
linker is built here, -O3 + IR-PGO by the same Clang 22.1.8 from the keg that
calls it, and trained on the link it actually does in our CI: a Mach-O dylib of
~165 objects, plain and with ThinLTO.
Each binary is built on the oldest machine of its architecture we have, because a Mach-O runs on systems newer than the one it was built against and not the other way round:
| asset | arch | built on | SDK | minos |
|---|---|---|---|---|
ld64.lld-22.1.8-pgo.x86_64-macos12 |
x86_64 |
macOS 12.7.6 | 12.3 | 12.0 |
ld64.lld-22.1.8-pgo.arm64-macos13 |
arm64 |
macOS 13.4.1 | 13.3 | 13.0 |
Both link only /usr/lib/libSystem.B.dylib, libz.1.dylib and libc++.1.dylib
(LLVM_ENABLE_ZSTD, LIBXML2 and TERMINFO are off), so a plain download is
enough — no brew deps, unlike the kegs. Each was verified by linking a ThinLTO
dylib on the hosted macos-15-intel, macos-26-intel and macos-15 runners.
Install it into the keg's bin, next to the Clang that calls it:
curl -fsSL -o "$( brew --prefix )/Cellar/llvm-pgo/22.1.8_2/bin/ld64.lld" \
https://github.com/MeshInspector/toolchains/releases/download/lld-22.1.8-pgo-macos/ld64.lld-22.1.8-pgo.x86_64-macos12
chmod +x "$( brew --prefix )/Cellar/llvm-pgo/22.1.8_2/bin/ld64.lld"
That location is the point: Clang resolves -fuse-ld=lld in its own directory
before PATH, so this shadows any Homebrew ld64.lld without replacing it, and
uninstalling is deleting the one file. Verify with
clang++ -print-prog-name=ld64.lld.
Measured on the real mrmeshpy.so ThinLTO link in MeshInspector CI, min of 3,
against the Homebrew lld each machine had:
| runner | before | after |
|---|---|---|
Mac Pro 2013, macOS 12, x86_64 |
164.0 s | 115.4 s (-30%) |
MacBook Pro M1 Pro, macOS 13, arm64 |
47.8 s | 36.0 s (-25%) |
Ordinary dylib links of 0.1-0.4 s show no difference at all; the gain is in ThinLTO codegen, which is what the profile covers.
docker/rockylinux8-vcpkgDockerfilein our Linux images fetches the Linux dylib keg into/opt. The version is pinned by the singleTOOLCHAIN=line there, plus oneLLVM_PREFIXenv per workflow.- MeshLib
.github/workflows/build-test-macos.ymlandpip-build.ymlinstall the macOS kegs on the GitHub-hosted runners through the composite action and use them for mrbind and the Python bindings. - The three self-hosted macOS runners (
macos-12-intel,macos-13,our-macos-15) carry the PGOld64.lldinside their own kegs, installed once by the workflow below, so MeshInspector builds link with it without downloading anything per job.
The Linux recipe is a two-job chain per arch, in a rockylinux:8 container:
stage1 gcc-toolset-11 -> IR-instrumented stage2 trained on compiling
LLVMSupport/Core/Analysis -> final build with the profile. It lives on a branch
per version, and pushing to that branch is what triggers the build:
clang-18-linux-pgobuilt 18.1.8.LLVM_VERis the only knob — it drives the source tarball, the install prefix, the release tag and the asset names — so the next version is a one-line change on a new branch.clang-linux-pgobuilt 22.1.8, and its history also holds the static+ThinLTO config.
Budget roughly 3h of hosted-runner wall clock for a dylib build (18.1.8 took 2h56m for all four jobs); the static+ThinLTO flavor took closer to 8h.
ld64.lld has two workflows of its own, both triggered by pushing to their file
and re-runnable by hand:
.github/workflows/build-lld-macos.ymlbuilds it on the two oldest self-hosted machines — instrumented build, training links, then a plain and a PGO build of the same sources so the benchmark isolates the profile — and verifies the result on the hosted runners. About 2.5h per machine..github/workflows/install-lld-macos.ymlputs the published binary into the keg on each self-hosted runner. It is checksum-verified and idempotent; re-run it after a machine is reimaged or its keg is rebuilt, since either wipes the keg'sbin.
The macOS kegs are built on the self-hosted macOS runners by
brew install --build-bottle from a local tap, then packed straight out of the
Cellar. The Validate llvm-pgo keg
workflow on main re-downloads the published macOS kegs on four hosted images and
benchmarks them against the llvm@22 bottle.