Formatting And CI

Use the checked-out configuration as the source of truth. .gitlab-ci.yml defines stages and includes; ci/*.gitlab-ci.yml and ci/scripts/ define the actual jobs. This guide covers what a change needs from CI and how to run the same checks locally; how the selection, pass cache, and artifact plumbing work inside the jobs is in ci-internals.md, and the blocking documentation job is in docs.md.

Default MR pipelines run a limited smoke matrix; labels such as affected-tests, all-tests and gpu-tests, plus scheduled or manually started pipelines, enable broader jobs, and affected-tests composes with the *-tests platform labels and all-platforms to pick where it runs. A green default MR pipeline is not proof that every supported configuration was exercised.

A pipeline is evidence only for the commit it ran on: after a push, amend, or rebase, check which SHA the pipeline and the merge request point at before citing either — a green run on a superseded revision proves nothing about the current head, and a reported failure should be reproduced at the current head too.

Three things to know when reading a test report. A job that failed and then passed on retry still reports the failed first attempt, exits 42, and is a soft warning: a green pipeline showing a failed test is the flake being surfaced, not a regression. The widget's comparison needs a base-branch report for the same job name, and default-branch pushes run only a small subset of jobs, so most jobs show a summary without one. And in merge-request pipelines the Linux test jobs skip tests whose binary and environment match a first-attempt pass recorded by an earlier MR pipeline, so a test count that falls between pipelines, or a test job reporting “No tests were found”, is expected rather than a regression; scheduled and web pipelines never skip, and EIGEN_CI_TEST_CACHE: "off" opts a job out.

Build jobs keep a second GitLab cache, the ccache pool holding .ccache/, keyed $EIGEN_CI_CCACHE_POOL$EIGEN_CI_CCACHE_SCOPE-ccache. The pool is the job‘s own slug, or its parent’s for a job that compiles a subset of another‘s targets with the same flags (the :affected tier); the scope is -mr<iid> in a merge request pipeline, -<ref slug> off the default branch, and empty on it, so only default-branch builds write the unscoped pool that fallback_keys names. A restore replaces .ccache/ rather than merging into it, so one pool shared by every pipeline is last-writer-wins: merge requests on unrelated bases overwrote each other’s objects and compiled at a 0.35% hit rate. GitLab appends its clear-cache index and -protected or -non_protected to both keys, so a Developer-started merge request pipeline cannot read the pool the scheduled builds fill. Two tiers opt out by setting EIGEN_CI_CCACHE_SCOPE back to "" in the job: the smoke tier (.smoketest:build), because it runs on merge request events only and scoping it would leave its shared pool with no writer at all, and Windows, whose runner has no distributed cache, so per-merge-request archives would accumulate on that disk unbounded. Any self-hosted runner without [runners.cache] keeps one archive per key forever — prune_runner_cache.py caps such a directory (--max-gb, LRU by mtime) and drops superseded clear-cache generations (--stale-index-below); its unit tests, test_prune_runner_cache.py, run in checkformat:scripts.

Test Tiers On Merge Requests

Three tiers, in increasing cost:

TierTriggerWhat runs
smokeevery MR with neither label belowthe fixed list in cmake/EigenSmokeTestList.cmake, usually one part per test, at baseline ISA on x86-64, aarch64 and riscv64, under gcc and clang
affectedaffected-tests labelevery test the diff can reach, all parts, on x86-64 (gcc AVX2, clang baseline) and aarch64 (gcc, clang), plus any platform the diff or a *-tests label selects
fullall-tests labelthe whole suite across the entire compiler and ISA matrix, minus the schedule-only jobs below

One configuration sits outside all three tiers and runs only on schedules and web pipelines: the NVHPC (nvc++) build and test pair, whose frontend is slow enough that those two builds alone once took roughly a quarter of the project's hosted-runner minutes. Start a web pipeline when a change plausibly affects nvc++ rather than waiting for the scheduled run to find it.

The affected tier exists because the smoke list samples: it is broad but shallow, so a change confined to one module gets only the one part of each related test that the list happens to name. Reach for affected-tests when a change is module-local and you want depth without paying for the full matrix.

The tiers do not stack: affected-tests and all-tests each suppress the smoke jobs, and the affected tier's four unconditional jobs use the smoke compilers, gcc-10 and clang-14 on x86-64 and aarch64. Two smoke configurations come back only with a label: riscv64 (rvv-tests or all-platforms), and x86-64 gcc at baseline ISA (sse-tests or all-platforms), since the unconditional gcc job is AVX2.

scripts/affected_tests.py computes the selection in the select:tests job. Run it locally the same way CI does:

python3 scripts/affected_tests.py --base-sha $(git merge-base origin/master HEAD)

Selection follows the textual #include graph, ignoring preprocessor guards, so it is a strict superset of the real compile dependency and never drops an affected test. Because Eigen is header-only and the umbrella headers are hubs, a change under Eigen/src/Core typically reaches every test and the selector degrades to the full suite — that is the correct answer, not a failure. Changes to CMake, ci/scripts/, ci/docker/, or the BLAS/LAPACK shims also force the full suite; the ci/*.gitlab-ci.yml files select nothing.

Platform-Triggered Configurations

Every job in the default smoke matrix builds at baseline ISA, so a change under Eigen/src/Core/arch/AVX512 gets no AVX-512 compilation at all unless someone applies all-tests. Under the affected-tests label the tier adds platforms beyond the four unconditional jobs on two independent triggers, either of which is enough:

  • the diff, through rules:changes: on the backend directory — automatic, and the common case;
  • a label, through $CI_MERGE_REQUEST_LABELS — the axis orthogonal to the include graph. The graph decides which tests run; the labels decide where. Use this to run the affected tests somewhere the diff does not point at: a Core change on ppc64le, a Geometry change on Windows.
Backend directoryLabelAdded configurationIn all-platforms
arch/SSEsse-testsx86-64 gcc-10 baseline, AVX, and AVX-512DQyes
arch/AVXavx-testsx86-64 gcc-10 AVX and AVX-512DQyes
arch/AVX512avx512-testsx86-64 gcc-10 AVX-512DQyes
arch/AVX512/*FP16*avx512-teststhe split gcc-13 AVX512-FP16 compile buildsno
arch/NEONneon-tests32-bit arm (aarch64 already runs unconditionally)yes
arch/AltiVecaltivec-testsppc64le gcc-14, under qemuyes
arch/LSXlsx-testsloongarch64 gcc-14, under qemuyes
arch/RVV10rvv-testsriscv64 gcc-15, on the native runneryes
arch/SVEsve-testsSVE cross builds and test runs at 128, 256 and 512 bits under qemuyes
arch/SMEsme-teststhe full SME build, compile-onlyno
windows-testsMSVC 14.29 x64 baselineyes
arch/GPU, the Half.h/BFloat16.h scalar headers, the GpuHipCuda*.inc alias files, cmake/EigenTesting.cmake, the Tensor *Gpu*.h headers, the GPU tests and their harness headers (.rules:libeigen:gpu in ci/common.gitlab-ci.yml has the exact list)gpu-teststhe CUDA build and test jobsno

Several labels select the union of their platforms — neon-tests with altivec-tests runs 32-bit arm and ppc64le and nothing else. Apart from gpu-tests, none of them does anything without affected-tests. all-platforms is a shorthand for every row that runs the affected selection; the three rows marked “no” ignore the selection and compile the whole suite, so reaching them means naming their label, and all-platforms on a one-line change cannot silently buy hours of whole-suite compilation.

Rows worth knowing before relying on them:

  • A wider x86 configuration compiles the narrower backends' headers, which is why SSE fans out to three builds. AVX512-FP16 headers are guarded by EIGEN_VECTORIZE_AVX512FP16, so an AVX-512DQ build does not parse them, and the *FP16* row is compile-only because no current runner can execute those instructions.
  • SVE is a fixed-length backend, so each vector length is a separate build with different fold counts and transpose networks, and test/sve_vector_length fails the run when a binary meets a different length than it was built for — a mismatch otherwise computes wrong answers while the suite passes.
  • Windows has no changes: trigger: what MSVC catches — template instantiation limits, EIGEN_STRONG_INLINE behaviour, optimizer heap exhaustion — is whole-library, so windows-tests is the only way in, and only MSVC x64 at baseline ISA is wired up. The 32-bit, AVX2 and AVX-512DQ Windows configurations stay in all-tests.
  • No affected-tier configuration enables CUDA, HIP or SYCL, so a diff confined to GPU test sources would select targets no host build has and read green having run nothing. Those paths add the existing CUDA jobs instead, which build buildtests_gpu and run the whole gpu label: coverage of the GPU suite, not of the affected subset.
  • arch/ZVector, arch/MSA, arch/HVX and the arch/SYCL backend have no matching test configuration, so a change there gets only the four unconditional jobs and the same hollow result; gpu-tests is no help either, since the GPU jobs it gates are all CUDA or ROCm.

The CUDA matrix is CUDA 11.8 with gcc-10 and clang-14 on GitLab‘s SaaS T4 runners (sm_75), and CUDA 12.6 with gcc-13 and clang-19 plus CUDA 13.3 with gcc-13 on the project’s L4 runner (sm_89); the ROCm job is build-only. The Linux CUDA test jobs are allow_failure: true, so a red GPU job renders as a warning and a green pipeline is not evidence that the GPU tests passed. Hence the policy for a merge request that touches any path in the GPU row: apply gpu-tests (and affected-tests when it also changes shared headers), name the GPU jobs that ran and their status in the description, and re-run the L4 jobs after rebasing onto another GPU-touching change. A scheduled pipeline whose EIGEN_CI_SCHEDULE_SCOPE variable is gpu runs only these jobs, which is how a second, cheaper GPU schedule coexists with the weekly full run.

Worktree-Safe Formatting

Inspect git status --short before formatting and preserve unrelated changes. Eigen requires clang-format-17 exactly; the pin lives in ci/checkformat.gitlab-ci.yml, which installs clang17-extra-tools. CI checks only the lines a merge request changes, and the tree is not uniformly clang-format-17 clean (a whole-file pass rewrites > > closers in a couple of dozen headers), so format the diff:

git clang-format --binary clang-format-17 --force <base-sha> -- path/to/file.cpp path/to/header.h
git clang-format --binary clang-format-17 --diff <base-sha> -- path/to/file.cpp path/to/header.h
clang-format-17 -i path/to/new-file.h
clang-format-17 --dry-run --Werror path/to/new-file.h

Inspect the selected files' diffs first: every change being formatted must belong to the task. --force permits unstaged edits; without it, files that need formatting must be staged or committed first. Untracked files are absent from the Git diff, so the whole-file commands above cover task-created files. git clang-format exits 1 when it makes or reports formatting changes; rerun the --diff check after applying them.

.clang-format intentionally disables include sorting and registers Eigen-specific macros and attributes. Do not reorder includes or restyle those macros manually.

scripts/format.sh rewrites every matching file in the tree in parallel. Run it only when the worktree is clean and a whole-tree pass is intentional. Review git diff afterward in either case.

Local Checks

Run checks relevant to the changed files and report unavailable tools:

codespell --config setup.cfg path/to/changed-file
reuse lint
python3 scripts/check_style.py --diff <base-sha>
python3 scripts/clang_tidy_hook.py --diff <base-sha>   # needs clang-tidy

Both report only on the lines a change adds, and both are advisory. check_style.py covers the conventions clang-tidy cannot state — comment verbosity, and the declaration forms still awaiting a CustomChecks query (see the parked block in .clang-tidy). clang_tidy_hook.py runs clang-tidy itself, restricted to added lines with --line-filter; it needs no build directory, generating a driver that includes the module umbrella and then the edited Eigen/src header, the way ci/scripts/run-clang-tidy.sh does for merge requests. It skips silently when clang-tidy is absent, and shows the user a non-blocking notice when a file's translation unit does not compile.

Claude Code sessions run both automatically through the hooks registered in .claude/settings.json. Their unit tests, scripts/test_check_style.py and scripts/test_clang_tidy_hook.py, run in checkformat:scripts; run them after changing either script.

The whole-tree codespell invocation used by CI can expose pre-existing findings. Do not modify unrelated files merely to make a local broad scan clean. In the current CI configuration, clang-format, codespell, and clang-tidy jobs are allow_failure; treat their diagnostics as review findings anyway. The REUSE job is blocking.

Source files carry the inline SPDX header conventions.md records; files that cannot need coverage in REUSE.toml. To stamp selected new files with the repository helper, pass them explicitly because its default scan considers tracked files:

python3 scripts/add_spdx_headers.py --paths path/to/new-file.cpp

Clang-Tidy

Use the CI driver rather than invoking clang-tidy directly on an implementation header; the driver routes such a header through its public umbrella include.

cmake -G Ninja -S . -B .tidy-build \
  -DCMAKE_CXX_COMPILER=clang++ \
  -DCMAKE_C_COMPILER=clang \
  -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
  -DEIGEN_BUILD_TESTING=ON
ci/scripts/run-clang-tidy.sh <base-sha> .tidy-build

The driver examines files committed between <base-sha> and HEAD; uncommitted-only edits are not included. Eigen's .clang-tidy policy is authoritative. Do not apply generic modernize-* or cppcoreguidelines-* campaigns.

A module that reaches a third-party header the machine does not install — <cuda_runtime.h> from contrib/Eigen/src/GPU, <cholmod.h> from CholmodSupport — is still checked, but clang parses a truncated translation unit, so the driver marks the heading — partial: <header> is not installed and reports that file's findings without failing the job. Installing the dependency gets the module checked in full; for CUDA the driver looks under CUDAToolkit_ROOT, CUDA_HOME, CUDA_PATH, then /usr/local/cuda, and so does clang_tidy_hook.py. An unresolved in-tree include is a defect in the change and stays a hard error.

A header under arch/<ISA>/ other than arch/Default/ is not forced into the driver: it parses only under the -march/-mcpu that selects it, which this job does not pass. Such a header is linted only when the host target selects the backend — SSE2 on the x86-64 runner — and the heading says which backend went unchecked. Validate a change to one with a build that enables the ISA rather than relying on this job.

For a split test the driver checks only the parts that compile the added lines and prints beside the file name the parts it left out, so a capped run names what it did not check rather than reporting the file clean; ci-internals.md has the reduction.

Before Review

  1. Inspect git diff and git diff --check.
  2. Format and check the task's changed lines and new files using the Worktree-Safe Formatting recipes above.
  3. Run the focused builds and tests documented in testing.md.
  4. Run applicable spelling, REUSE, and clang-tidy checks.
  5. Build the doc target when the change touches Doxygen markup, a documented name, or a snippet, and label the merge request all-tests so the blocking documentation job (docs.md) runs before the merge rather than after it.
  6. State what ran, what did not run, and why. Do not claim coverage from jobs or hardware that were unavailable.