|
cpp-toolchain documentation
Up-to-date C++ toolchain images for the complete development cycle - GNU and LLVM side by side
|
Scripts that ask a question about a compiler, or about a built image, and report the answer.
The two under details/ are also the image validation gate:
| Script | Purpose |
|---|---|
| cxx-standards.sh | Which C++ standards a compiler accepts, ordered by __cplusplus |
| cxx-stdlibs.sh | Which C++ standard libraries are installed, and what ABI they expose |
These depend on nothing in this repository - point them at any compiler on any machine.
Fetch either on its own when you want the answer without an image or a checkout:
From a checkout it is the same script:
--stable drops draft spellings such as c++2c, --greatest keeps only the highest standard, and --format narrows each line to one field - std for c++26, cplusplus for 202400. --format=std is spelled the way the compiler spells it, so it feeds straight back in:
--help is the full reference.
Which standard libraries are installed, and the two things that actually break a build:
One row is one installed runtime, not one implementation.
libstdc++ appears three times because gcc.sh installs multilib by default, which adds a 32-bit and an x32 build beside the 64-bit one: same release and same ABI, a different secondary ABI, and package is the field that tells them apart.
These images carry them, so expect these rows.
Two libc++ rows are not a conflict either, and soname is why: apt.llvm.org gives its versioned runtimes a SONAME of their own, libc++.so.1.0.20 against libc++.so.1, which is what lets both sit on one host.
A binary records one of those names and the loader looks up only that one, so the other is inert rather than competing. cxx-stdlib-parity.sh compares that field across two images - docs/IMAGES_VALIDATION.md covers what it buys there.
The two implementations answer in different currencies, because only one of them has GNU symbol versions:
| Implementation | abi | cxxabi |
|---|---|---|
| libstdc++ | greatest GLIBCXX_ in the ELF ex: GLIBCXX_3.4.35 | greatest CXXABI_ in that same ELF ex: CXXABI_1.3.17 - its C++ ABI lives inside libstdc++.so |
| libc++ | the std::__N inline namespace in the ELF ex: LIBCPP_ABI_1 | the SONAME of the C++ ABI library it keeps apart ex: libc++abi.so.1 |
libc++ has no GNU symbol versions, but it puts its ABI in an inline namespace, std::__1, that every mangled name in the library repeats - so the binary does say which ABI it is. Mangling is length-prefixed, which is what makes it readable: St3__1 is the substitution for std:: followed by a three-character identifier, so the digits after it belong to the next component's length and not to the namespace.
_LIBCPP_ABI_VERSION in the headers says the same thing and is kept as the fallback, for a build whose namespace was renamed - the Android NDK's __ndk1, say - or an ABI numbered past a single digit.
Everything above is the library view: what is installed. The compiler view answers a different question - what each compiler actually compiles against, which is not always the newest one installed, because a compiler picks headers off its own search path. --compilers selects it, and says which compilers to ask. It takes the same shape as --versions and --targets do in install/: all, or a space-separated list.
Clang 18 compiling against libc++ 22 is not a misreading: /usr/include/c++/v1 is a symlink into the newest LLVM's tree, so every clang installed shares one libc++.
libc++ 20.1.7 is installed and no compiler here reaches it. A gap only two separate views can show.
Bare --compilers is --compilers=all, every g++/clang++ on PATH. Naming the ones you care about instead is worth it - asking every compiler a host has costs about a minute. --view=compiler is the same thing spelled out, for when no filter is wanted.
--format=fields is the parseable form, one key=value set per line, tagged with the view it came from, so --view=all can be read off one stream. The library view needs neither a compiler nor binutils, so it still answers inside a runtime-only image:
A field this host cannot answer reads - rather than being dropped, so every line of a view keeps its shape.
The whole surface, of which the examples above use a part:
| Option | Values | Default |
|---|---|---|
| --view | library, compiler, all | library; compiler if --compilers |
| --stdlib | all, libstdc++, libc++ | all |
| --compilers | all, or a space-separated list of compilers | all if bare |
| --format | default, name, version, soname, abi, fields | default |
The four narrowing formats print one field per line, deduplicated: name gives libstdc++, version gives 16, soname gives libstdc++.so.6, abi gives GLIBCXX_3.4.35. They are what makes the script scriptable - the sample above collapses to a single answer:
They print nothing that says which view a line came from, so they answer for one view and --view=all is refused rather than answered - an installed runtime's GLIBCXX_3.4.35 and a compiler's cxx11 are two different facts, and one flat list holding both cannot say which is which. Narrow to a view, and --stdlib narrows the rest of the way:
--format=soname is refused against the compiler view for the same reason: a compiler names the headers it reaches, and headers have no SONAME.
--help is the full reference.
The image validation gate, in details/:
| Script | Purpose |
|---|---|
package-origins.sh <build|runtime> | Every toolchain package comes from the repository that owns it, not from the Ubuntu archive |
cxx-runtime.sh <compile|inspect|run> <directory> | Compiles the payload for every standard, proves it links against the expected C++ runtime dynamically, then runs it |
cxx-stdlib-parity.sh <record|verify> <file> | The image that runs the binaries carries the same standard libraries the image that built them used |
Both package-origins.sh and cxx-stdlib-parity.sh build on cxx-stdlibs.sh above: one to discover which package owns the installed libc++ - a name apt.llvm.org has changed twice - and the other to compare SONAME, version and ABI either side of a COPY --from.
Implementation details of this gate, in the C++ sense of a nested detail namespace: they know this repo's expected package origins, and they ask scripts/install/ which compilers are installed rather than guessing. That reach is why the validate stages copy scripts/ whole - a layout that separates the two fails loudly rather than silently finding no compilers.
Each reports every failure before exiting, so one run tells you everything that is wrong. They also run against a plain checkout, which is the quickest way to iterate on them; they discover whatever the host has, so expect a wider matrix than an image produces.