Measuring the clang-tidy lanes¶
The whole-tree clang-tidy ratchet (ADR-1142, CI guide) compares every file with a committed count in scripts/ci/tidy-baseline-<lane>.json. A count is only meaningful on the toolchain it was measured with, so every lane is measured in one place: the dev container (ADR-1471).
Measure a lane¶
make tidy-lane LANE=cpu # compare with the committed baseline
make tidy-lane LANE=all # cpu, clang, cuda, hip, sycl and arm64, one after the other
make tidy-lane-write LANE=hip # rewrite scripts/ci/tidy-baseline-hip.json
or call the script the targets wrap:
scripts/dev/tidy-lane.sh cpu cuda
scripts/dev/tidy-lane.sh --write all
scripts/dev/tidy-lane.sh --write --only core/src/feature/hip/ciede_hip.c hip
scripts/dev/tidy-lane.sh --help
You need Docker and the dev image vmaf-dev-mcp:local (dev container guide); another tag goes in --image or VMAFX_DEV_IMAGE. The first step of a run downloads clang-tidy 22 from apt.llvm.org and meson from PyPI (hash-locked, requirements/locks/build.txt), so the run needs network access; the arm64 lane also installs the aarch64 cross compiler and qemu-user from the Ubuntu archive. A lane takes about five minutes on eight cores. --jobs N (default 8) sets the cores for the build and for clang-tidy and caps the container at the same number.
The exit code is the ratchet's:
| Code | Meaning |
|---|---|
0 | The baseline matches. |
2 | A file is above its baseline. |
3 | A file is below its baseline. |
4 | The lane did not build, or a translation unit did not parse. |
5 | A usage error, or a clang-tidy version the baseline was not measured with (below). |
With several lanes the highest code wins. Reports (tidy-ratchet-<lane>.json, with every diagnostic behind the counts) and logs (<lane>.log) land in ~/.cache/vmafx-tidy-lanes/, or in the directory given with --out.
After a change to C, C++, CUDA, HIP or SYCL sources¶
- Fix the findings in the files you touched; a touched file ends at zero (agent hard rules, rule 10).
- Run the lanes that read the file. A file under
core/src/feature/cuda/is read bycuda; a file every build compiles (core/src/libvmaf.c) is read by all of them.make tidy-lane LANE=allis the safe choice. - Exit
3means the file is cleaner than its baseline: runmake tidy-lane-write LANE=<lane>and commit the JSON with your change. To tighten only your files and leave the rest of the baseline untouched (ADR-1243), pass--only <path>once per translation unit. - Exit
2means a file got worse. Fix the code. A baseline is never raised by hand.
--write copies the rewritten baseline back into your checkout. Nothing else in the checkout is written, and nothing is mounted into the container.
The clang-tidy version¶
A baseline records the clang-tidy it was measured with (clang_tidy_version, 22.1.8 today), and the counts of two versions are not comparable. apt.llvm.org serves only the newest build of the 22 series, the same one the hosted job gets.
When that build moves on, a check and a scoped write stop with exit 5 and name both versions; nothing is measured.
Moving the baselines to the new version is one deliberate step, reviewed like any other change of the counts:
The hosted job then measures against the same version. A workstation's own clang-tidy plays no part: the container never uses the host's tools.
Why not on the host¶
The numbers depend on the C library, the compilers and the device toolchains, not only on clang-tidy's version:
- C library. glibc 2.44 expands
assert(e)in C++ so that clang-tidy 22 reportsmisc-static-assert/cert-dcl03-cfor every runtime assertion (T-TIDY-GLIBC-244-STATIC-ASSERT-FALSE-POSITIVE-2026-10-02indocs/state.md); glibc 2.43 in the container and on the hosted runners does not. A baseline written on such a host carries those findings, and the hosted job then fails every file as "below its baseline". That is what the first complete hosted run after two days of local landing showed: 24 files below a baseline that named gcc 16 as its compiler. - Device compilers. A host without
hipccconfigures the HIP backend with-Denable_hipcc=false, where 21 host files compile to-ENOSYSstubs. The lane then lints the stubs, not the code a device runs. - Optional libraries. With ONNX Runtime installed, the DNN sources compile their real bodies and two more translation units exist. The hosted runner has none.
- clang-tidy itself. A workstation's package manager moves it: on 2026-10-02 the workstation's clang-tidy went from 22.1.8 to 23.1.1, which reports more and which the scoped write refuses against a 22.1.8 baseline.
The container pins all of that: Ubuntu 26.04 (the hosted runners' release), gcc-15, CUDA with nvcc, ROCm with hipcc, oneAPI with icpx, ONNX Runtime, and clang-tidy 22 from the hosted job's source.
What the script does¶
scripts/dev/tidy-lane.sh:
- creates a throwaway container of the dev image, capped to
--jobsCPUs; - copies the checkout's tracked files and the files not yet added (not the ignored ones) into it as a tar stream, so uncommitted edits are measured and a host build directory is not;
- installs clang-tidy 22 from apt.llvm.org's
llvm-toolchain-<codename>-22, the package the hosted job installs withllvm.sh 22, unless the image already has it. The archive key is checked against a pinned SHA-256; - runs
make tidy-ratchet-build LANE=<lane>:meson setupwith the lane's configuration, then a full build, which leavescompile_commands.jsonand the generated headers; - runs
make tidy-ratchetormake tidy-ratchet-writefor the lane; - copies the report, the log and, with
--write, the baseline out and removes the container.
Lane configurations¶
One definition, in the Makefile: TIDY_RATCHET_COMPILERS_<lane> and TIDY_RATCHET_SETUP_<lane>.
| Lane | Compilers | meson setup options | Parsed by |
|---|---|---|---|
cpu | gcc-15 / g++-15 | -Denable_cuda=false -Denable_sycl=false -Denable_dnn=disabled -Denable_mcp=true -Denable_mcp_sse=enabled -Denable_mcp_uds=true -Denable_mcp_stdio=true -Db_lto=false | clang-tidy 22 |
clang | clang-22 / clang++-22 | the cpu options plus -Dfuzz=true | clang-tidy 22, --select core/test/fuzz/ --select core/src/read_json_model.c |
cuda | gcc-15 / g++-15, nvcc | -Denable_cuda=true -Denable_nvcc=true -Denable_sycl=false -Denable_hip=false -Denable_dnn=enabled -Db_lto=false | clang-tidy 22, --cuda-host-only -nocudalib |
hip | gcc-15 / g++-15, hipcc | -Denable_hip=true -Denable_hipcc=true -Denable_cuda=false -Denable_sycl=false -Denable_dnn=enabled -Db_lto=false | clang-tidy 22 for the host files, ROCm's clang-tidy for the .hip kernels, on their host compilation (--cuda-host-only: LLVM 24 lists the device job first, and clang-tidy analyses the first) |
sycl | icx / icpx | -Denable_sycl=true -Dsycl_icpx_aot_targets= -Denable_cuda=false -Denable_hip=false -Denable_dnn=enabled -Db_lto=false | clang-tidy 22 through scripts/ci/clang-tidy-sycl.sh |
arm64 | aarch64-linux-gnu-gcc / g++ 15 (cross) | --cross-file build-aux/aarch64-linux-gnu.ini --cross-file build-aux/aarch64-linux-gnu-qemu-user.ini -Denable_cuda=false -Denable_sycl=false -Denable_dnn=disabled -Db_lto=false | clang-tidy 22, --target=aarch64-linux-gnu --sysroot=/usr/aarch64-linux-gnu |
Notes per lane¶
-Db_lto=falseeverywhere: the project default renders as GCC's-flto=4(ADR-1172), which clang rejects, so every translation unit would fail to parse.cpudisables the DNN runtime because the hosted runner has no ONNX Runtime and the two must measure the same translation units. The GPU lanes enable it, so the DNN bodies are measured there.cpualso measures the twelve MATLAB MEX sources ofcompat/python-vmaf/matlab/. meson never builds them and the MATLAB SDK is on no runner, soscripts/ci/gen-mex-compile-commands.pyappends compile-database entries whose include path starts with the self-authored stub headers ofscripts/ci/lint-stubs/matlab/(ADR-2062). The stubs are for clang-tidy only: nothing builds or links against them.syclcompiles SPIR-V only. The ahead-of-time device list changes backend arguments thatscripts/ci/gen-sycl-compile-commands.pyremoves from the lint database anyway.- Kernels. meson compiles
.cu,.hipand the SYCL sources through custom commands, whichcompile_commands.jsondoes not list.scripts/ci/gen-gpu-compile-commands.py(cuda, hip) andscripts/ci/gen-sycl-compile-commands.py(sycl) add them; both stop the lane when a kernel rule exists that they could not read, so a lane never reports a clean measurement of a database without its kernels. .hipkernels and ROCm's clang-tidy. ROCm 10's device headers call__builtin_amdgcn_is_invocable, a builtin of the LLVM that ROCm ships. Stock clang-tidy 22 stops there ("builtin functions must be directly called"), soscripts/ci/clang-tidy-hip.shsends.hipfiles to/opt/rocm/llvm/bin/clang-tidy(hipcc's own LLVM) and everything else to clang-tidy 22. The version a baseline records is clang-tidy 22's; the kernel tool's version follows the ROCm image pinned inbuild-config.env.-
arm64is the cross lane of ADR-1283: the NEON and SVE2 sources that no x86 build compiles. It uses the in-tree cross file and a second one,build-aux/aarch64-linux-gnu-qemu-user.ini, that names Ubuntu'sqemu-aarch64(the first namesqemu-aarch64-static, which Ubuntu 26.04 does not package); meson runs its compiler check through it. -
clangexists because libFuzzer is a clang feature:-Dfuzz=trueis a configure error under gcc, so the five harnesses ofcore/test/fuzz/(andcore/src/read_json_model.c, which only they compile) are in no gcc database. The lane builds thecpuconfiguration with clang and measures those files only (--select, a path prefix ofscripts/ci/tidy-ratchet.py), so a file the other lanes own is not counted twice. The container installsclang-22andlibclang-rt-22-devfrom the same apt.llvm.org archive as clang-tidy. cpuconfigures the embedded MCP server (-Denable_mcp=trueand its three transports), socore/src/mcp/andcore/test/test_mcp_*.care read.cpubuildscore/tools/vmaf_vpl_core.cand its test when thevplpkg-config module is found: the container haslibvpl-dev, so the hosted jobs install it too.
What every translation unit is read by¶
Every tracked .c, .cc, .cpp, .cxx, .cu, .hip, .mm and .metal file is in the measured_sources of at least one baseline (scripts/ci/tidy-baseline-<lane>.json) or in the shared lint exception list (.config/lint-exceptions.d/clang-tidy-coverage.toml). python3 scripts/ci/check-tidy-coverage.py fails otherwise; it is a pre-commit hook (check-tidy-coverage) and runs in CI with the rest of the hooks.
A new translation unit therefore has to land in a lane: configure the option that builds it in the lane that reads it, and write its allowance with scripts/dev/tidy-lane.sh --write --only <path> <lane> (the scoped write also records the file as measured). A file no lane can read gets one entry in the exception list: the path, the rule clang-tidy-coverage (the file name), a reason that names the missing tool or toolchain, and an expiry date. An entry fails the check once it has expired, when its file is gone, and when a lane reads the file after all; extend or delete it, never leave it.
Praetor's copy of the same facts¶
Since the pin 04cc813ff054 (ADR-2153) praetor's own translation-unit gate (praetorctl audit, praetorctl ci tidy-coverage --dir=.) reads a lane file list and the exceptions list of .standards.yaml. Neither is a second source of truth: python3 scripts/ci/praetor_tidy_coverage.py --write renders .config/clang-tidy/measured-sources.txt (the union of every baseline's measured_sources) and the block of .standards.yaml between its BEGIN / END generated markers (one entry per unit no lane measures, from the shared exception list, plus the exact-twin fragments and the included HIP header) from the baselines and the exception list above. The pre-commit hook check-praetor-tidy-coverage fails on a stale rendering. Praetor refuses an exception that expires more than 90 days out, so each rendered entry expires on the earlier of its own date and PRAETOR_EXPIRY_CAP in the script (2027-01-04); renew the cap with the entries. Edit the baselines and the exception list, never the generated block.
Praetor reads that one exceptions key for its other rules too. Since the pin afb739ed81f3 (ADR-2321) the script also copies the exception list's entries for the rules in PRAETOR_RULES into the block, with the same expiry cap: HISS-11 (.config/lint-exceptions.d/HISS-11.toml, a declared supply-chain gap) and, since the pin 3a766f2d56ad (ADR-2784), HISS-10 (.config/lint-exceptions.d/HISS-10.toml, the workflows whose lanes praetor's build-warnings gate reads as compiling without warnings as errors).
| Group | Read by | Why the rest is excepted |
|---|---|---|
| C, C++ and the device kernels | cpu, clang, cuda, hip, sycl, arm64 (the six container lanes) | |
Metal host code (core/src/metal/*.mm, core/src/feature/metal/*_metal.mm) and the Metal-only C tests | metal (macOS, below) | |
Metal kernels (core/src/feature/metal/*.metal) | nothing | Upstream clang-tidy has no Metal language mode (clang -x metal answers "language not recognized"). |
Pelorus mirror (core/src/interop/pelorus_*.c, core/test/test_pelorus_interop.c) | nothing here | Byte-identical mirror of VMAFx/pelorus (ADR-1113); scripts/ci/pelorus-mirror-paths.txt keeps it out of every lane. It is tidied in the pelorus repository's own CI at the pinned SHA, fixed there and re-vendored. One entry per file; the check fails on a mirror file without one. |
.config/hiss/testdata/ | nothing | The planted defect is the fixture. |
cmd/vmafx-node/bpf/ | nothing | Includes vmlinux.h, generated from a running kernel's BTF. |
core/tools/compat/win32/getopt.c, core/tools/test/test_vmaf_windows_utf8_argv.cpp | nothing | Built on Windows only. |
core/src/feature/tad_rust.c, core/test/test_tad_rust.c | nothing | Need -Denable_rust_features=true (cargo and cbindgen), absent from the image. |
Pelorus interop mirror (scripts/ci/pelorus-mirror-paths.txt) | nothing | Byte-exact mirror (ADR-1113); fixes go upstream. |
scripts/dev/upstream_parity_harness.c, scripts/dev/hip_dispatch_drop_probe.hip | nothing | Built by hand or by a script, outside meson. |
core/src/feature/hip/integer_adm/adm_decouple_inline.hip is a header that two kernels include; the check does not count it as a unit.
The macOS metal lane¶
The Objective-C++ sources of the Metal backend need Apple's SDK, which no container has. The Tidy Metal workflow (tidy-metal.yml) runs on macos-latest weekly, on a pull request that touches the Metal host sources, and on dispatch (a macOS runner bills ten times a Linux one, so it is not a per-pull-request gate, as for the tester bundle): Homebrew's llvm@22 clang-tidy (the Linux lanes' major), Apple clang's compile commands, -isysroot named explicitly because Homebrew's clang-tidy has no implicit SDK. It measures core/src/metal/, core/src/feature/metal/ and the four Metal-only tests (--select) against scripts/ci/tidy-baseline-metal.json. The configuration is TIDY_RATCHET_COMPILERS_metal / TIDY_RATCHET_SETUP_metal in the Makefile, repeated by the job; test_tidy_lane_container.py compares them. The job is not a required check yet. It becomes one after it has passed on master, the path the SYCL lane took (ADR-1297). The artifact of a run (tidy-ratchet-metal) holds every diagnostic behind the counts; the baseline is the same JSON without them.
The lane reads the translation units and the one header only Objective-C++ includes (core/src/metal/objc_handle.h). Every other header is owned by the cpu lane, which reads it as C: the Metal shader compiler (C++14) and the C translation units include the Metal math headers, so the C++-only fixes modernize-use-designated-initializers, modernize-use-using and modernize-loop-convert would break one or the other. The workflow narrows clang-tidy's --header-filter accordingly.
Dispatch the workflow with fix=true to have the runner apply clang-tidy's own fixes (to a fixed point), build the fixed tree, measure it and upload the result: tidy-metal-fixes.patch, tidy-metal-fix.log, tidy-metal-build.log and tidy-ratchet-metal.json. Review the patch before applying it: misc-const-correctness writes T const *const p for a pointer whose pointee the code later changes; declare it T *const p. Push, then dispatch again without fix to build and compare with the baseline.
What the hosted job covers¶
The required check Tidy Ratchet (.github/workflows/lint-and-format.yml) measures the cpu lane on every change to the C core and fails when a file differs from scripts/ci/tidy-baseline-cpu.json. It runs the same two targets as the container, make tidy-ratchet-build LANE=cpu and make tidy-ratchet LANE=cpu, so it configures, builds and extends the compile database (the MATLAB MEX sources) exactly as the lane's Makefile variables say; so does the nightly full scan. scripts/ci/tests/test_tidy_lane_container.py pins both jobs to those targets and to the clang-tidy major. A translation unit the baseline measured and the job did not is reported by name as not measured and fails the job (exit 4); before 2026-10-06 it counted as 0 and the job asked to tighten the baseline of five MEX files it had never read (T-TIDY-RATCHET-UNMEASURED-AS-CLEAN-2026-10-06). The container reproduces the hosted measurement byte for byte: for master 513d2a6fc the report of scripts/dev/tidy-lane.sh cpu and the tidy-ratchet-cpu artifact of hosted run 37011276599 are the same file (SHA-256 ffb5ca1819a3…, 327 translation units, 322 findings).
No hosted runner has nvcc, hipcc or icpx with a full build, so cuda, hip and sycl are not required checks, and neither is arm64. Their baselines still hold: a change that raises a count shows up the next time the lane runs, locally or in the nightly run below. A lane that did not run is never reported as clean.
Nightly run on the workstation¶
Until a runner with the device toolchains exists, the lanes without a hosted job run from a timer on the workstation that has the dev image. The job measures a clean clone of master, not a working tree:
git -C ~/.cache/vmafx-tidy-nightly/vmafx fetch --quiet origin master
git -C ~/.cache/vmafx-tidy-nightly/vmafx checkout --quiet --detach origin/master
~/.cache/vmafx-tidy-nightly/vmafx/scripts/dev/tidy-lane.sh \
--out ~/.cache/vmafx-tidy-nightly/$(date +%F) all
Run it from a systemd user timer. The service is a Type=oneshot unit whose ExecStart= lines are the three commands above:
# ~/.config/systemd/user/vmafx-tidy-lanes.service
[Unit]
Description=Measure the clang-tidy lanes on master
[Service]
Type=oneshot
ExecStart=/usr/bin/git -C %h/.cache/vmafx-tidy-nightly/vmafx fetch --quiet origin master
ExecStart=/usr/bin/git -C %h/.cache/vmafx-tidy-nightly/vmafx checkout --quiet --detach origin/master
ExecStart=%h/.cache/vmafx-tidy-nightly/vmafx/scripts/dev/tidy-lane.sh --out %h/.cache/vmafx-tidy-nightly/nightly all
# ~/.config/systemd/user/vmafx-tidy-lanes.timer
[Timer]
OnCalendar=*-*-* 03:30:00
[Install]
WantedBy=timers.target
The unit files above are illustrative: %h is systemd's home directory, and a fixed --out directory replaces the $(date +%F) of the shell form (systemd does not expand shell substitutions). A non-zero result shows in systemctl --user status vmafx-tidy-lanes.service and the lane logs name the files. An exit 2 or 3 on master means a change landed without its lane being measured; open a row in docs/state.md and fix the file or tighten the baseline through make tidy-lane-write.