blob: df3422407fba91ba98efb028c1b685206b0ae003 [file] [view]
# Formatting And CI
Use the checked-out configuration as the source of truth. [`.gitlab-ci.yml`](../.gitlab-ci.yml) defines stages and
includes; [`ci/*.gitlab-ci.yml`](../ci) and [`ci/scripts/`](../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`](ci-internals.md), and the blocking documentation job is in
[`docs.md`](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`](../ci/scripts/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`](../ci/scripts/test_prune_runner_cache.py), run in `checkformat:scripts`.
## Test Tiers On Merge Requests
Three tiers, in increasing cost:
| Tier | Trigger | What runs |
|---|---|---|
| smoke | every MR with neither label below | the fixed list in [`cmake/EigenSmokeTestList.cmake`](../cmake/EigenSmokeTestList.cmake), usually one part per test, at baseline ISA on x86-64, aarch64 and riscv64, under gcc and clang |
| affected | `affected-tests` label | every 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 |
| full | `all-tests` label | the 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`](../scripts/affected_tests.py) computes the selection in the `select:tests` job. Run it
locally the same way CI does:
```bash
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 directory | Label | Added configuration | In `all-platforms` |
|---|---|---|---|
| `arch/SSE` | `sse-tests` | x86-64 gcc-10 baseline, AVX, and AVX-512DQ | yes |
| `arch/AVX` | `avx-tests` | x86-64 gcc-10 AVX and AVX-512DQ | yes |
| `arch/AVX512` | `avx512-tests` | x86-64 gcc-10 AVX-512DQ | yes |
| `arch/AVX512/*FP16*` | `avx512-tests` | the split gcc-13 AVX512-FP16 compile builds | no |
| `arch/NEON` | `neon-tests` | 32-bit arm (aarch64 already runs unconditionally) | yes |
| `arch/AltiVec` | `altivec-tests` | ppc64le gcc-14, under qemu | yes |
| `arch/LSX` | `lsx-tests` | loongarch64 gcc-14, under qemu | yes |
| `arch/RVV10` | `rvv-tests` | riscv64 gcc-15, on the native runner | yes |
| `arch/SVE` | `sve-tests` | SVE cross builds and test runs at 128, 256 and 512 bits under qemu | yes |
| `arch/SME` | `sme-tests` | the full SME build, compile-only | no |
| — | `windows-tests` | MSVC 14.29 x64 baseline | yes |
| `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`](../ci/common.gitlab-ci.yml) has the exact list) | `gpu-tests` | the CUDA build and test jobs | no |
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`](../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:
```bash
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`](../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:
```bash
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`](../scripts/test_check_style.py) and
[`scripts/test_clang_tidy_hook.py`](../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`](conventions.md) records; files that cannot need coverage
in [`REUSE.toml`](../REUSE.toml). To stamp selected new files with the repository helper, pass them explicitly because
its default scan considers tracked files:
```bash
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.
```bash
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`](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`](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`](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.