make preflight — the local gate that matches CI¶
make preflight # everything, on the files you changed
scripts/dev/preflight.sh --full # everything, on the whole tree
scripts/dev/preflight.sh --stage clang # just one stage
scripts/dev/preflight.sh --list # which CI context each stage mirrors
Run it before you push. make lint and make test-fast build with one compiler; CI builds with several, and the gap is where portability bugs live.
Why it exists¶
On 2026-09-07 a single branch shipped three portability breaks in one afternoon. All three were green under local gcc:
| What was written | Locally | In CI |
|---|---|---|
static_assert(UINT_MAX <= SIZE_MAX/2/sizeof(ptr)) | fine on 64-bit | the i686 lane of the time could not compile it (that lane is retired: the fork is 64-bit only, ADR-1258) |
__declspec(align((x))) | gcc never compiles the MSVC branch | Windows MSVC+CUDA → C2059 on every use; align() needs a literal |
__attribute__(noinline) | gcc accepts the single paren | Ubuntu clang, clang+DNN and four Sanitizer lanes fail to compile |
nullptr in a .c file | gcc and clang accept it under -std=c23 | Windows MSVC+CUDA → C2065 at every site; ADR-1138 keeps C TUs on NULL |
static const double used in a static aggregate initialiser | no diagnostic at all, even with -pedantic-errors -Weverything | Windows MSVC+CUDA → C2099, then cascading C2440s as the members shift |
M_PI in a new test file | glibc exposes it because meson passes -D_GNU_SOURCE | Windows MinGW64 → 'M_PI' undeclared; MinGW ignores _GNU_SOURCE and -std=c23 sets __STRICT_ANSI__ |
const ulong half = ... in a .metal shader | the C / CUDA / HIP twins it was ported from have no such reserved name | macOS Clang+Metal → cannot combine with previous 'type-name' declaration specifier; half is MSL's 16-bit float type |
The last three are from the same evening: PR #1340's new test file carried the first two (21 errors on Windows MSVC+CUDA), and PR #1342's carried the third. All are new files added by a PR, which is the pattern: fresh code is where portable-looking constructs that exactly one lane rejects get written. The fourth arrived on the same branch as the third.
Each cost a full CI round-trip. Because only one PR is in flight at a time, that is queue time for every other PR too — the branch held the merge window for about three hours.
Stages¶
| Stage | Mirrors | Catches |
|---|---|---|
gcc | Ubuntu gcc(+DNN) | the baseline build and fast tests |
clang | Ubuntu clang(+DNN) | clang-only syntax; gcc is far more permissive about attributes and extensions |
msvcism | Windows MSVC+CUDA / +SYCL | constructs MSVC rejects, checked statically so no MSVC is needed |
sanitizers | Sanitizers (address) / (undefined) | UB the plain build hides |
tidy | Tidy Changed | clang-tidy on the touched files, using the workflow's own exclusion list |
cppcheck | Cppcheck | cppcheck's findings |
A stage whose toolchain is missing is skipped with a notice, not failed, so the script is still useful on a partially provisioned machine.
There is no 32-bit stage. The fork is 64-bit only, and the m32 stage went with the i686 lane it mirrored (ADR-1258).
Behaviours worth knowing¶
It looks at uncommitted work. Not just origin/master...HEAD — the edit you are about to commit is exactly what you want checked. (The first version of this script did not, and cheerfully passed against a deliberately planted break.)
sanitizers needs -Db_lundef=false. Clang links the sanitizer runtime into executables, not shared libraries, so libvmaf.so is left with undefined __asan_report_* / __ubsan_handle_* symbols and -Wl,--no-undefined refuses the link. The repo's own fuzz.yml pairs the same two options. Without it the stage fails on every branch, including ones that change no code at all — and a stage that cries wolf is a stage people learn to ignore.
What it does not cover¶
Windows MinGW64, the Windows MSVC lanes proper, and Ubuntu HIP have no local equivalent here. msvcism is a set of pattern matches plus one small scanner (scripts/dev/find-nonconst-static-init.py) over known rejection classes, not a compiler — Windows MSVC+* remains the authority. For HIP and the other GPU backends use the dev container (dev-mcp.md, ADR-0451).
Keeping it honest¶
If you add a required CI context, add a stage or record why it cannot be mirrored. --list is the single place that mapping is written down, and ADR-1234 records the reasoning.