The crt-building hack in `postPatch` does two things, both of which assume ELF:
- it forces `libgcc.a` to depend on `crti.o`/`crtn.o`. Those are an ELF
convention. PE/COFF targets have no such files, so the rule makes the
build assemble the generic ELF `config/i386/crti.S` with a PE
assembler, which fails with `junk at end of line`.
- it blanks `SHLIB_LC`, which for ELF is `-lc` and stands in the way of
linking `libgcc_s.so` before libc exists. On Cygwin and MinGW
`SHLIB_LC` is instead the list of system import libraries the DLL
genuinely needs, so blanking it drops real dependencies.
So apply it only on ELF.
That leaves the pre-libc stage on PE/COFF with no way to link a shared
`libgcc`: every symbol in a DLL has to resolve at link time, and the
libc's import library does not exist yet. Build only `libgcc.a` there --
which is all that stage is used for -- and let the shared library be
built as usual once the real libc is present. Which case applies is
read from `stdenv.cc.libc`, as everything else in this package is, with
headers-only stand-ins marking themselves via `passthru.headersOnly`.
ELF targets build the same thing as before but do not keep the same
derivation: `--enable-shared` is now stated outright where configure was
previously left to default to it. `libgcc` for `x86_64-unknown-netbsd`
gains that one flag and nothing else.
Assisted-by: Claude Code (Claude Opus 5)
We don't really want to share Nix logic with the monolithic GCC build,
as GCC NG is supposed to be unconstrained to go in different directions.
Inline `libgcc-buildstuff.nix` and read `hostPlatform` throughout.
Assisted-by: Claude Code (Claude Opus 5)
One question in three places: which part of the split set carries the
target libc, and what happens where there is none yet.
The compiler was configured with
`--with-sysroot=${lib.getDev targetPackages.libc}`, so every libc change
moved `gcc-unwrapped` -- the most expensive thing here to rebuild, and
exactly the coupling this split exists to remove. cc-wrapper already
supplies the target libc, the same way the LLVM set leaves `clang`
carrying none. Measured with a behaviour-free libc change: before,
`gcc-unwrapped` moves, hours; after, `gcc-unwrapped` and `libgcc` are
byte-identical and only `libstdcxx` and the wrapper move, minutes.
libgcc is where the headers are wanted, and it wants them twice. First,
`gcc/configure` sets `inhibit_libc=true` when host != target and
`$target_header_dir/stdio.h` is absent, which makes `tsystem.h` skip
`<unistd.h>` and friends. That is fine for the generic sources and fatal
for the target-specific ones that need libc declarations, which fail as
`implicit declaration of function 'profil'` and the like.
`target_header_dir` comes from `--with-sysroot`, not `--with-headers`, so
point the sysroot pair at whichever libc the compiler carries -- read
off `stdenv.cc.libc`, which in the pre-libc build is `preLibcHeaders`,
the header-only package nixpkgs already keeps for this stage (NetBSD,
MinGW, Cygwin), and nothing at all elsewhere. Being headers-only, a libc
change still leaves the pre-libc libgcc byte-identical.
Reading it off the compiler rather than naming `preLibcHeaders` also
fixes glibc and musl, which matched no branch of `preLibcHeaders`, got no
sysroot at all, and so built their real libgcc with `inhibit_libc` set.
That is silent -- it compiles, links and installs cleanly, and
`_eprintf.o` simply comes out empty:
aarch64-unknown-linux-gnu libgcc.a
before 413014 bytes, `__eprintf` absent
after 415276 bytes, `__eprintf` present
The real libc is already an input to that build through `stdenv.cc`, so
naming it here adds no coupling. Dropping the flag instead was the other
option; NetBSD shows the cost, where all 334 libgcc compiles gain
`-Dinhibit_libc` and the same symbol disappears there too.
Second, `limits.h`. gcc's chains to the target libc's with
`#include_next`, and that is the right header to ship: it names no libc,
so it stays correct for whatever is on the include path later. Where the
compiler has a libc -- headers-only or real -- the chain resolves, and
must: those same sources want `PATH_MAX` from it. Where it does not, the
chain has nowhere
to land and even configure's `AC_PROG_CPP` probe fails, since it includes
`<limits.h>` precisely because that "exists even on freestanding
compilers", after which it falls back to `/lib/cpp` and reports that:
checking how to run the C preprocessor... /lib/cpp
configure: error: C preprocessor "/lib/cpp" fails sanity check
Only then, put gcc's own `glimits.h` earlier on the include path for this
build. That is the self-contained variant, the same file gcc installs
when configured against no libc. It is build-local: the compiler keeps
shipping the chained header.
Fixing the compiler instead is wrong, and glibc catches it at once --
`bits/stdlib.h:98: #error "Assumed value of MB_LEN_MAX wrong"` while
building libgomp. `limits.h` is the only header libgcc wanted on glibc
and musl, which is why this is one build-local include path rather than a
header package per libc.
Verified by building `stdenv.cc` for `aarch64-unknown-linux-gnu` and
`aarch64-unknown-linux-musl`, neither of which worked before;
`x86_64-unknown-netbsd`, whose bootstrap compiler carries
`preLibcHeaders`, uses those headers and carries no freestanding include
path.
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)
`libgcc` runs `gcc/configure` itself, to generate the makefile fragments
the split build has nobody else to produce. That configure identifies the
target assembler and linker from `AS_FOR_TARGET`/`LD_FOR_TARGET` and
probes them for capabilities.
`preConfigure` set those *after* assigning `AS=$AS_FOR_BUILD` and
`LD=$LD_FOR_BUILD`, deriving them with `$(basename $AS)`. The build
assignments stomp on the variables the target ones are read from, so it
took the basename of the *build* tools -- plain `as` and `ld` -- and
looked for them inside the *target* wrappers, which install
machine-prefixed names only. Neither path existed. Nothing about that is
target-specific; it catches every cross target.
`gcc/configure` does not treat it as an error: a probe it cannot run
records "no", so every `gcc_cv_as_*`/`gcc_cv_ld_*` capability came back
"no". The silently fatal one is `HAVE_LD_EH_FRAME_HDR`, which
`unwind-dw2-fde-dip.c` gates `USE_PT_GNU_EH_FRAME` on, leaving the
unwinder with only the `__register_frame` registry, which nothing
populates for normally linked objects. libgcc and libstdc++ still build,
link and install cleanly, and every C++ `throw` finds no FDE and calls
`std::terminate`.
Snapshot the target tool names before the `*_FOR_BUILD` assignments
clobber them. `CPP` is not always exported, so fall back to the
machine-prefixed name the wrappers install.
Assisted-by: Claude Code (Claude Opus 5)
A few patches of mine were just merged (yay!). Let's upgrade the
`fetchpatch` invocations from the original mailing list submission to
the accepted commit, accordingly.
I narrowed down the the files to patch because patching
autoconf-generated files will break, and is not needed because we are
regenerating those files anyways.
Because we're building things separately, we don't need the fancy
lib/... namespacing tricks that GCC normally does to squeeze itself in
the FHS. We can just use the normal autotools libdir and include dir,
and the nixpkgs infra will sort everything out.
Where possible I submitted patches to the mailing list, and fetched
those. The ones I am vendoring are the residuals which I don't think are
ready for upstreaming yet. (I can imagine a further reworking upstream
such that we wouldn't need our own patches of that sort, but it would be
good to get the first crop merged first before discussing that.)