- `gccNGPackages.libbacktrace`: Remove, in favor of the `libbacktrace`
package in `pkgs/by-name`
- `stdenvNoCxx`: New, a compiler with a libc but no C++ standard library
- `libbacktrace`: Build with `stdenvNoCxx`, so `libstdc++` can link it
without a cycle
- `gccNGPackages.gcc`, `gccNGPackages.libgfortran`,
`gccNGPackages.libstdcxx`: Take `libbacktrace` as a build input and pass
`--with-system-libbacktrace`
That option is not upstream; it comes from a patch posted to gcc-patches[^1],
which we fetch from the archive rather than vendor.
One posting covers every component that links libbacktrace, so each
package filters it down to the files its own `src` carries, and leaves
out the generated files because we regenerate those locally anyway.
`gcc` and `libgfortran` were already using this `libbacktrace`, but by
faking up the sibling directory an in-tree build would have had --
archive, libtool archive and headers -- for the relative paths to resolve
against. Passing the flag lets that go.
Note that this means our libstdc++ is getting `std::stacktrace` support
for the first time; we were unwittingly excluding the vendored support.
Nothing failed, which is why it went unnoticed:
`--enable-libstdcxx-backtrace` defaults to `auto`, and `auto` means yes
wherever the library is hosted, so the feature was asked for. But the
probe that picks the object format runs `libbacktrace/filetype.awk`,
which was not among the files we copied, so it got nothing back, warned
"could not determine output file type", and quietly settled on
`enable_libstdcxx_backtrace=no`.
Note also that with this approach, a downstream `libbacktrace`
*cannot* be built by the regular `stdenv` with `useGccNG = true`,
because its symbols would conflict. We'll see whether that is acceptable
or not. If it isn't, we can still avoid vendoring, but we will need to
replicate the symbol renaming that GCC would have done externally.
[^1]: https://inbox.sourceware.org/gcc-patches/20260814013206.3818461-1-git@JohnEricson.me/
Assisted-by: Claude Code (Claude Opus 5)
The chain is the part of this package set that cannot be read off any one
file: four compilers, each a step further along, and two `libgcc`s either
side of the libc. Until now it was only recoverable by reading the
`overrideCC` call sites in order and reconstructing the argument.
Write it down once, as a table, next to why the cycle needs breaking at
all -- a libc's sources call into libgcc, and a libgcc that can use the
libc's threads needs the libc. Also record the two things that are easy
to get wrong and hard to notice: that the pre-libc stage is written down
in exactly one place (`binutilsNoLibc` carrying `preLibcHeaders` as its
`libc`), and that a libgcc built with `inhibit_libc` still compiles,
links and installs, just with pieces silently missing.
The per-wrapper comments shrink to what is local to each -- which stage it
is and what keeps the libc out -- since the chain-wide narrative now has
somewhere better to live.
Assisted-by: Claude Code (Claude Opus 5)
Two halves of how the compiler finds libstdc++.
The library was not on the link path. `wrapCCWith` was given
`libcxx = libstdcxx`, which tells cc-wrapper where the C++ headers live
but does not put the library where a GNU compiler looks, so every C++
link failed with `cannot find -lstdc++`. Add `-B${libstdcxx}/lib`, as
libgcc, libssp, libatomic and libgomp already are.
The headers had the opposite problem: found too eagerly, by the wrong
language. libstdc++ deliberately ships headers named after C headers --
`math.h`, `stdlib.h`, `complex.h`, `stdckdint.h`, `stdbit.h`,
`stdatomic.h`, `tgmath.h`, `fenv.h` -- whose purpose is to shadow the C
ones when compiling C++. Installed into `$dev/include` they shadowed
unconditionally, since `libcxx` is a propagated target-target dep of
cc-wrapper and the setup hook puts that directory on the **C** include
path of everything built with the compiler. Under C, libstdc++'s
`<stdckdint.h>` is an empty shell, so its functions vanish:
bash: braces.c: implicit declaration of function 'ckd_sub'
coreutils: randperm.c: implicit declaration of function 'stdc_bit_width'
Both build fine with monolithic GCC, whose libstdc++ headers live in
`include/c++/$ver` and are added by the g++ driver alone. Install them
into a sibling `include-cxx`, which keeps them off C's include path while
staying findable for C++; no version or target subdirectory is needed,
since the store path already separates one libstdc++ from another.
Assisted-by: Claude Code (Claude Opus 5)
The libc bootstrap had every stage except the last. `gccNoLibgcc` builds
libgcc, `gccWithLibgcc` builds the libc with it — and then nothing goes
back to rebuild libgcc now that the libc exists. So the only libgcc
anyone ever got was the one from before there was a libc, which is
necessarily single-threaded: `gthr-posix.h` includes `<pthread.h>`
unconditionally, and at that point there is no libc to provide it.
`libstdcxx` above it assumed `posix` regardless, so the two disagreed by
construction — the unwinder's registry locking compiled away to nothing
underneath a `libstdc++` handing out `std::thread`.
Add the last stage the way the LLVM package set does with
`compiler-rt-no-libc` and `compiler-rt-libc`: instantiate the same
package twice. `libgcc-no-libc` is the existing one and now feeds only
the three bootstrap compilers. `libgcc-libc` is built after the libc, by
a new `gccWithLibcAndBasicLibgcc` — real libc, bootstrap libgcc — and is
what `libgcc` resolves to for any platform that has a libc.
Nothing is passed to the package to say which of the two it is. The
distinction is the compiler it is handed, exactly as in the LLVM set:
`gccNoLibgcc` is wrapped with `binutilsNoLibc`, whose `libc` is
`preLibcHeaders` -- the header-only stand-in, or nothing at all on
platforms without one -- and `wrapCCWith` defaults a wrapper's `libc` to
its bintools'. So the package reads `stdenv.cc.libc` and needs no
bootstrap flag of its own.
Which threading model is available is a property of the libc, so the libc
declares it as `passthru.threadModel` and `libgcc` reads it from there.
That is what makes the pre-libc build single-threaded without being told
to be: a headers-only package declares no `threadModel`, a real libc
does.
`libstdcxx` in turn takes both the model and the generated
`gthr-default.h` from `libgcc`, replacing a `$CXX -v` probe that asked
the compiler — which in this package set is configured separately from
libgcc and so answers for the wrong component. Platforms that declare
nothing keep the single-threaded model they already had.
The second libgcc is also the first one that can be shared. Previously
only the static library was built, and the compiler was configured
`--disable-shared` for every target, which is compiled into the driver's
specs, so it named bare `-lgcc` and never `-lgcc_s`:
$ x86_64-unknown-netbsd-g++ -### eh.cc
collect2 ... -lstdc++ -lm -lgcc -lc -lgcc
Both halves have to move together. Build the shared library without
changing the specs and the unwinder leaves `libgcc.a` while the specs
still name it:
libgcc.a _Unwind_RaiseException: absent
libgcc_eh.a _Unwind_RaiseException: present
libgcc_s.so.1 _Unwind_RaiseException: present
so every throwing C++ program fails to link and `rustc` fails on
`-lgcc_s` outright. Either standard arm would do, `-lgcc_s -lgcc` shared
or `-lgcc -lgcc_eh` static; the halves disagreeing is what breaks.
`--disable-shared` was passed to libgcc with the comment "Do not have
dynamic linker without libc". That does not hold: the monolithic build
ships `libgcc_s` even from its *nolibc* cross stage, and the result needs
nothing at run time --
$ readelf -d .../nolibc-gcc-15.3.0-libgcc/.../libgcc_s.so.1
0x...0e (SONAME) Library soname: [libgcc_s.so.1]
(no NEEDED entries at all)
What genuinely blocks it is libgcc's own makefile: `SHLIB_LC` defaults to
`-lc`, so the `libgcc_s.so` rule links against a libc that does not exist
yet and fails with `cannot find -lc`. The monolithic build clobbers that
variable -- see `common/libgcc-buildstuff.nix` -- so reuse that helper
rather than reinventing it. On the compiler side the flag derives from
`hasSharedLibraries`, as the monolithic build does, so a target genuinely
without shared libraries keeps the old behaviour; it is spelled
`enableTargetShared` there because it describes the target's libgcc
rather than anything about the compiler being built.
Verified on `x86_64-unknown-netbsd`, where `libgcc_s.so.1` and the
`GROUP ( libgcc_s.so.1 -lgcc )` script now appear in the output, and by
building `stdenv.cc` for `aarch64-unknown-linux-gnu` and
`aarch64-unknown-linux-musl`.
Assisted-by: Claude Code (Claude Opus 5)
The package set offered two compilers either side of a gap:
- `gccNoLibgcc` — no libgcc, no libc. Builds libgcc.
- `gccWithLibc` — libgcc *and* libc. Builds libssp and everything above.
Nothing in between, and the missing stage is the one a libc is compiled
with. A libc's own sources call into libgcc — 128-bit and soft-float
helpers, `__stack_chk`-adjacent bits, unwinder support — so
`gccNoLibgcc` cannot compile it. `gccWithLibc` obviously cannot either:
it is defined in terms of the libc that does not exist yet. Any platform
bringing up a new libc therefore has no compiler to express the step
with, and the bootstrap simply cannot be written down.
Add `gccWithLibgcc`: libgcc present, libc absent. `binutilsNoLibc` is
what enforces the second half — it supplies the header-only
`preLibcHeaders` rather than a real libc, so the derivation refers to no
libc at all and the dependency cycle stays broken. Verified on a cross
target: the wrapper's `orig-libc` is the headers package, and a
translation unit needing `__udivti3` compiles and links against libgcc.
This is additive; no existing derivation changes.
Assisted-by: Claude Code (Claude Opus 5)