Skip to content

Verify Clang against the older libstdc++ releases in the declared range - #24

Merged
skipbit merged 4 commits into
mainfrom
ci/verify-clang-against-older-libstdcxx
Aug 14, 2026
Merged

Verify Clang against the older libstdc++ releases in the declared range#24
skipbit merged 4 commits into
mainfrom
ci/verify-clang-against-older-libstdcxx

Conversation

@skipbit

@skipbit skipbit commented Aug 14, 2026

Copy link
Copy Markdown
Owner

What

The required Linux jobs built Clang only against the libstdc++ that ships with
the runner image, so the two older releases the declared range promises — 13
and 14 — went unverified. This adds jobs that pin them, and settles the contract
documents around what the matrix actually covers.

Why this axis needs jobs of its own

Which libstdc++ Clang uses does not follow from the Clang version the way it
does for GCC: the two come from different upstreams, and neither of them tests
the pairing. Both breakages this project has hit on that axis — std::expected
first, std::views::split later — happened there. -Werror is public, so a
header difference or a newly added diagnostic on this axis reaches consumers.

The jobs

Six configurations, each in Debug and Release:

Compiler Standard library
Clang 20 / 22 whatever the image provides (libstdc++ 15) — unchanged: no flag, no package step
Clang 20 / 22 libstdc++ 13, pinned with --gcc-install-dir
Clang 20 / 22 libstdc++ 14, pinned with --gcc-install-dir

libstdc++-13-dev and libstdc++-14-dev come from the distribution's own
repository (universe), so no external source enters a required gate. Build-system
jobs go from 20 to 28.

A green build is not evidence for the release a job's name claims: were the pin
to resolve to another release, the build would still succeed, just not against
what the name says. Each pinned job therefore reads the include search path back
and compares its first standard library entry against the pin. The default jobs
report which installation the driver selected without asserting on it — they
exist to build against whatever the image provides, so a change of release there
is news rather than a failure.

Measured

Ubuntu 26.04, along the path CI takes (configure, build, run the tests):

Release Debug
Clang 20.1.8 × libstdc++ 13 405 passed 405 passed
Clang 20.1.8 × libstdc++ 14 405 passed 405 passed
Clang 22.1.2 × libstdc++ 13 405 passed 405 passed
Clang 22.1.2 × libstdc++ 14 405 passed 405 passed

The lower bound holds against every declared release, so it stays at 20.

Documents

The three files that publish the support range now state three axes — the C++
standard a consumer builds with, the compiler, and the standard library — and
list the pairings that hold instead of a product of two axes. GCC with libc++ is
noted as a pairing upstream does not support, along with what would make it
worth revisiting. Coverage is stated per quadrant: every libstdc++ release in
range is covered in the Clang pairings, while the GCC ones cover 13 and 15,
since the libstdc++ version follows the compiler version there. The provisional
notes that called this combination unverified are gone.

The version in each pairing is the version of the headers built against; the
runtime a binary loads comes from the system's runtime package, which is
versioned separately. The documents now say so, since the pin does not reach the
loader.

The required jobs built Clang only against the libstdc++ that ships with
the runner image, so 13 and 14 went unverified even though the declared
range promises them. Which libstdc++ Clang uses is an axis of its own --
it does not follow from the Clang version the way it does for GCC -- so
pin those two releases in a job of their own, leaving the default jobs to
build without a flag or a package step. A green build is not evidence for
the release a job claims, so the include search path is read back and its
first standard library entry compared against the pin.

Settle the contract documents accordingly: state the C++ standard, the
compiler and the standard library as three axes, list the pairings that
hold instead of a product of two axes, and record that GCC with libc++ is
not among them because upstream has no -stdlib option to select it with.
Coverage is now stated per quadrant: every libstdc++ release in range is
covered in the Clang pairings, while the GCC ones cover 13 and 15 only.
The check added with the pinned jobs dies without printing anything when
the driver refuses the directory it was given: the assignment fails under
set -e with the driver's output already captured, so a run keeps only its
exit code. Report it instead. The awk there also took the first field of
each search path entry, which turns a path containing a space into a
wrong comparison rather than an error.

The default jobs name no standard library at all, and Clang picks the
newest GCC installation it finds rather than the distribution's alias, so
nothing in a run recorded which release that arm built against even
though the documents name it. Report the selection there too, without
asserting on it -- those jobs exist to build against whatever the image
provides, so a change of release is news rather than a failure.

The documents now also say that the version in each pairing is the one
whose headers are compiled against, the runtime a binary loads being
versioned separately, and the configure step names the shared linker
variable rather than resting on CMake carrying the flag there on its own.
The observation line added with the pinned jobs filtered the driver's
output through grep, so a driver that failed left nothing behind: its
diagnostics went into the pipe and were dropped, and the fallback text
claimed the driver had reported no selection -- a statement about what
the driver said, made in the one case where nothing it said survived.
That is the same disappearance this branch had just removed from the
pinned check, reintroduced one step away.

Capture first, then read the capture. Whenever the marker cannot be
reported, whether because the driver failed or because it stopped
printing that line, the captured output reaches the log instead of being
dropped. Reading the capture with a here-string rather than a pipe also
removes the step's dependence on pipefail being unset, which is what the
current behaviour rests on.

Two comments elsewhere claimed more than the code does. The configure
step called its linker variables the same shape the libc++ jobs use,
though those name no shared-linker variable. The pin check said no other
entry is examined, when what it means is that the entries are walked in
order and only the first one under the standard library prefix is
compared.
One of them said this workflow does not set pipefail, while the pinned
check sets it eighty lines further down. The claim holds for the step,
whose default shell is the one that has no pipefail, and not for the
file; shell options do not carry between steps, so narrowing the scope
keeps the reasoning and drops the part a reader would find contradicted.

The other explained why the libc++ jobs name their linker variable. That
is an attribution those jobs do not make anywhere; their comments cover
why both ends are built and where the packages come from. What can be
read off them is which variables they set, so the comment now says that
and stops there.
@skipbit
skipbit merged commit 34d41c1 into main Aug 14, 2026
31 checks passed
@skipbit
skipbit deleted the ci/verify-clang-against-older-libstdcxx branch August 14, 2026 09:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant