From d8ad24b1cd45120d49e2e5b2fe900efb7fc06840 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Mon, 10 Aug 2026 18:37:43 -0400 Subject: [PATCH 01/11] gcc/ng: point libgcc's `gcc/configure` at the real target `as` and `ld` `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) --- .../gcc/ng/common/libgcc/default.nix | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/pkgs/development/compilers/gcc/ng/common/libgcc/default.nix b/pkgs/development/compilers/gcc/ng/common/libgcc/default.nix index d88c2e6c0ae5..9cd1561d76ff 100644 --- a/pkgs/development/compilers/gcc/ng/common/libgcc/default.nix +++ b/pkgs/development/compilers/gcc/ng/common/libgcc/default.nix @@ -100,6 +100,35 @@ stdenv.mkDerivation (finalAttrs: { cd "$buildRoot/gcc" ( + '' + # `AS`, `CC`, `CPP` and `LD` still name the *target* tools at this point, + # under their machine-prefixed names. Snapshot them before the + # `*_FOR_BUILD` assignments below overwrite them. + # + # Deriving `AS_FOR_TARGET` from `$AS` *after* `AS=$AS_FOR_BUILD` asked for + # the basename of the build assembler -- plain `as` -- inside the target + # compiler's `bin`, and a cross wrapper installs only prefixed names, so + # the path did not exist. Likewise `ld`. + # + # That is not an error `gcc/configure` reports. It probes the target + # assembler and linker for capabilities, and a probe it cannot run simply + # records "no"; with neither tool found, *every* `gcc_cv_as_*`/`gcc_cv_ld_*` + # answer came back "no". The one that matters here is + # `HAVE_LD_EH_FRAME_HDR`: `unwind-dw2-fde-dip.c` gates `USE_PT_GNU_EH_FRAME` + # on it, so without it the unwinder is compiled with no `dl_iterate_phdr` + # lookup at all -- only the `__register_frame` registry, which nothing + # populates for normally linked objects. libgcc and libstdc++ then build, + # link and install perfectly cleanly, and every C++ `throw` finds no FDE + # and calls `std::terminate`. + # + # `CPP` in particular is not always exported, so fall back to the + # machine-prefixed name the wrappers install. + + '' + targetAs=$(basename "''${AS:-${stdenv.hostPlatform.config}-as}") + targetCc=$(basename "''${CC:-${stdenv.hostPlatform.config}-cc}") + targetCpp=$(basename "''${CPP:-${stdenv.hostPlatform.config}-cpp}") + targetLd=$(basename "''${LD:-${stdenv.hostPlatform.config}-ld}") + export AS_FOR_BUILD=${lib.getExe' buildPackages.stdenv.cc "$(basename $AS_FOR_BUILD)"} export CC_FOR_BUILD=${lib.getExe' buildPackages.stdenv.cc "$(basename $CC_FOR_BUILD)"} export CPP_FOR_BUILD=${lib.getExe' buildPackages.stdenv.cc "$(basename $CPP_FOR_BUILD)"} @@ -112,10 +141,10 @@ stdenv.mkDerivation (finalAttrs: { export CXX=$CXX_FOR_BUILD export LD=$LD_FOR_BUILD - export AS_FOR_TARGET=${lib.getExe' stdenv.cc "$(basename $AS)"} - export CC_FOR_TARGET=${lib.getExe' stdenv.cc "$(basename $CC)"} - export CPP_FOR_TARGET=${lib.getExe' stdenv.cc "$(basename $CPP)"} - export LD_FOR_TARGET=${lib.getExe' stdenv.cc.bintools "$(basename $LD)"} + export AS_FOR_TARGET=${lib.getExe' stdenv.cc "$targetAs"} + export CC_FOR_TARGET=${lib.getExe' stdenv.cc "$targetCc"} + export CPP_FOR_TARGET=${lib.getExe' stdenv.cc "$targetCpp"} + export LD_FOR_TARGET=${lib.getExe' stdenv.cc.bintools "$targetLd"} export NIX_CFLAGS_COMPILE_FOR_BUILD+=' -DGENERATOR_FILE=1' From 92144940a3acaddbb3af8694e1fc576cb6a24491 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Mon, 10 Aug 2026 18:37:43 -0400 Subject: [PATCH 02/11] gcc/ng: let a platform opt in with `useGccNG` Nothing could select the split GCC package set, so none of the fixes below this commit were reachable from a normal build. Add `useGccNG` alongside the existing `useLLVM`/`useZig`/`useArocc` choices, and honour it where a compiler is picked: the cross stdenv takes `gccNGPackages.gcc`, and `stdenvNoLibs`/`stdenvNoLibc` map to `gccNoLibgcc` and `gccWithLibgcc`. That last pair is the point of the split. Both currently fall back to `gccCrossLibcStdenv`, because the monolithic compiler cannot distinguish them; `lib/systems/default.nix` says as much next to `linker`, noting that we would like to choose the C compiler and runtime library orthogonally but "due to the monolithic GCC build we cannot actually make those choices independently". This is what lifts that, the way the LLVM set already splits `clangNoCompilerRt` from `clangNoLibc`. `gccNGPackages` tracks `default-gcc-version` rather than naming a release, so the split set and the monolithic `gcc` stay on the same one. `useGccNG` defaults to `false` and no platform derives it, so no existing build changes. Setting it on a platform spec is enough to exercise the whole stack: nix-build . -A stdenv.cc --arg crossSystem \ '{ config = "aarch64-unknown-linux-musl"; useGccNG = true; }' The intent is to switch obscure low-tier platforms over to it soon -- NetBSD first -- so `ng` gets dogfooded somewhere the blast radius is small. Those switches come separately. Assisted-by: Claude Code (Claude Opus 5) --- lib/systems/default.nix | 10 ++++++++++ pkgs/stdenv/cross/default.nix | 2 ++ pkgs/top-level/all-packages.nix | 9 +++++++++ 3 files changed, 21 insertions(+) diff --git a/lib/systems/default.nix b/lib/systems/default.nix index 262cb722a090..0d00ff953530 100644 --- a/lib/systems/default.nix +++ b/lib/systems/default.nix @@ -153,6 +153,16 @@ let # Derived meta-data useLLVM = final.isFreeBSD || final.isOpenBSD; + # Use the split GCC package set (`gccNGPackages`) instead of the + # monolithic `gcc`. No platform selects it yet; it is opt-in, set + # explicitly on a platform spec, so that the split set can be exercised + # before anything depends on it. + # + # I (@Ericson2314) plan on making obscure low-tier platforms (e.g. + # NetBSD) use it soon, so we can dogfood GCC NG and thereby iron out its + # bugs. + useGccNG = false; + libc = if final.isDarwin then "libSystem" diff --git a/pkgs/stdenv/cross/default.nix b/pkgs/stdenv/cross/default.nix index f2adc91e2bb1..98d93014faf2 100644 --- a/pkgs/stdenv/cross/default.nix +++ b/pkgs/stdenv/cross/default.nix @@ -127,6 +127,8 @@ lib.init bootStages buildPackages.zig.cc else if crossSystem.useArocc or false then buildPackages.arocc + else if crossSystem.useGccNG or false then + buildPackages.gccNGPackages.gcc else buildPackages.gcc; diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index 57f5bbbe422b..3bc94c889614 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -88,6 +88,8 @@ with pkgs; ( if stdenvNoCC.hostPlatform.isDarwin || stdenvNoCC.hostPlatform.useLLVM or false then overrideCC stdenvNoCC buildPackages.llvmPackages.clangNoCompilerRt + else if stdenvNoCC.hostPlatform.useGccNG or false then + overrideCC stdenvNoCC buildPackages.gccNGPackages.gccNoLibgcc else gccCrossLibcStdenv ) @@ -99,6 +101,11 @@ with pkgs; ( if stdenvNoCC.hostPlatform.isDarwin || stdenvNoCC.hostPlatform.useLLVM or false then overrideCC stdenvNoCC buildPackages.llvmPackages.clangNoLibc + else if stdenvNoCC.hostPlatform.useGccNG or false then + # The split package set can express the two rungs separately, the way + # the LLVM set does: no libgcc above, libgcc but no libc here. The + # monolithic `gccCrossLibcStdenv` has to serve both. + overrideCC stdenvNoCC buildPackages.gccNGPackages.gccWithLibgcc else gccCrossLibcStdenv ) @@ -3244,9 +3251,11 @@ with pkgs; # NOTE: keep this with the "NG" label until we're ready to drop the monolithic GCC gccNGPackagesSet = recurseIntoAttrs (callPackages ../development/compilers/gcc/ng { }); gccNGPackages_15 = gccNGPackagesSet."15"; + gccNGPackages = gccNGPackagesSet.${toString default-gcc-version}; mkGCCNGPackages = gccNGPackagesSet.mkPackage; }) gccNGPackages_15 + gccNGPackages mkGCCNGPackages ; From ab281c3fd468477ae06a2ed9975f724635d6f4ed Mon Sep 17 00:00:00 2001 From: John Ericson Date: Mon, 10 Aug 2026 18:37:31 -0400 Subject: [PATCH 03/11] gcc/ng: let GCC probe the real tools, and bake neither answers nor paths GCC decides at configure time what its assembler and linker can do and writes the answers into the compiler. A probe it cannot run is not an error -- it silently records "no". We passed `--with-as` but put nothing on `PATH`, so the probes had no tools to ask, and **31 capability macros** came out wrong in the installed `auto-host.h`: `HAVE_GAS_HIDDEN`, `HAVE_LD_PIE`, `HAVE_LD_RELRO_SUPPORT`, `HAVE_LD_NOW_SUPPORT`, `HAVE_LTO_PLUGIN`, `HAVE_COMDAT_GROUP`, `HAVE_GAS_CFI_DIRECTIVE`, `HAVE_AS_LEB128`, `HAVE_LD_EH_FRAME_HDR` and more. nixpkgs' hardening flags were quietly inert, LTO unavailable, C++ without COMDAT. `HAVE_GAS_HIDDEN` is how it surfaced: without it `-fvisibility=hidden` is not rejected but becomes a no-op that merely warns, so every symbol stays preemptible while GCC still emits direct `R_X86_64_PC32` references, which the linker then rejects: libc_pic.a(stats.pico): relocation R_X86_64_PC32 against symbol `opt_stats_interval_opts' can not be used when making a shared object Found on NetBSD, where it stops `libc.so` linking at all. Targets matching configure's hardcoded `*-*-solaris2*` case escape it, which is what made it look target-specific. Put the unwrapped bintools on `PATH` via `depsBuildTarget` -- they run on the build machine and act on target artifacts -- so the probes ask the real tools. Unwrapped keeps the earlier decoupling: `as` and `ld` proper carry no target-libc reference, unlike the bintools wrapper. Drop `--with-as` rather than adding `--with-ld`. Those bake `DEFAULT_ASSEMBLER`/`DEFAULT_LINKER` as absolute store paths, so the driver runs exactly those binaries instead of deferring to the wrapped ones. Finding them at use time is what the three `find_a_program` patches are for: `PATH` was left to `execvp`, which matches `NAME` alone, and `PATH` is exactly where we expose a cross toolchain under prefixed names. They come from the posting to `gcc-patches`; the second needs a `postFetch` tweak, being against trunk where one cast is spelled with the C++ operator rather than `CONST_CAST`. All three configurations were built and compared: - `--with-as` only (before): 31 macros wrong, `libc.so` unlinkable - `--with-as` + `--with-ld`: capabilities recovered, but ten probes still fail, because probes need the rest of binutils on `PATH`, not one absolute path - `PATH` only (this): all 31 recovered, `DEFAULT_ASSEMBLER` and `DEFAULT_LINKER` undefined, `-print-prog-name=as`/`ld` return bare names, so the wrapper stays in charge Assisted-by: Claude Code (Claude Opus 5) --- .../compilers/gcc/ng/common/gcc/default.nix | 63 ++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/pkgs/development/compilers/gcc/ng/common/gcc/default.nix b/pkgs/development/compilers/gcc/ng/common/gcc/default.nix index 1c7a57d9a6cc..d74be1b6a521 100644 --- a/pkgs/development/compilers/gcc/ng/common/gcc/default.nix +++ b/pkgs/development/compilers/gcc/ng/common/gcc/default.nix @@ -81,6 +81,36 @@ stdenv.mkDerivation (finalAttrs: { hash = "sha256-54/HzM+aeWq8CTkQu8Pualqc/LgRLS0+8EY8uPUsD+s="; }) + # Not upstream yet; a follow-up to the series above (drop the `/raw` to + # read them). They extend that series' `-as` preference to `PATH`, + # where we put the cross toolchain, so a cross compiler finds its tools + # the way a native one does. See below for the problems `--with-as` and + # `--with-ld` cause, and thus why we want to avoid them. + (fetchpatch { + name = "driver-factor-out-env-path-parsing.patch"; + url = "https://inbox.sourceware.org/gcc-patches/20260810065714.2215299-1-git@JohnEricson.me/raw"; + hash = "sha256-2qUUMWuyxX4mVaBPeNnHIiMl/aN7ejWM5stTSFWxD7g="; + }) + (fetchpatch { + name = "driver-search-PATH-ourselves.patch"; + url = "https://inbox.sourceware.org/gcc-patches/20260810065714.2215299-2-git@JohnEricson.me/raw"; + # The posted patch is against trunk, which spells this cast with the C++ + # operator. GCC 15 still uses the CONST_CAST macro, and the line is + # context rather than a change, so it cannot fuzz-match. Rewrite it + # here rather than keeping a forked copy of the whole patch. + postFetch = '' + substituteInPlace "$out" \ + --replace-fail 'string, const_cast (commands[i].argv),' \ + 'string, CONST_CAST (char **, commands[i].argv),' + ''; + hash = "sha256-uD8xJxQus2qyNgNDN/63WnURNuUJFDkhaXPph7g/DIk="; + }) + (fetchpatch { + name = "driver-search-PATH-machine-prefix.patch"; + url = "https://inbox.sourceware.org/gcc-patches/20260810065714.2215299-3-git@JohnEricson.me/raw"; + hash = "sha256-Q5CJpJKD11kadIKselQdHgNe26GqojpyAAmlAyHnsB0="; + }) + (getVersionFile "gcc/fix-collect2-paths.diff") ]; @@ -93,6 +123,29 @@ stdenv.mkDerivation (finalAttrs: { strictDeps = true; depsBuildBuild = [ buildPackages.stdenv.cc ]; + + # The target assembler and linker have to be *runnable here*, during + # configure. GCC probes them for capabilities, and a probe it cannot run is + # not an error -- it silently records "no". Without the target `ld` on + # `PATH`, every `gcc_cv_ld_*` probe fails that way, and the two that matter + # most are `HAVE_GAS_HIDDEN` (which needs `gcc_cv_as_hidden` *and* + # `gcc_cv_ld_hidden`) and `HAVE_LD_EH_FRAME_HDR`. + # + # Losing `HAVE_GAS_HIDDEN` is the nasty one: `-fvisibility=hidden` degrades + # into a no-op that merely warns, so every symbol stays preemptible and GCC's + # own same-translation-unit `R_X86_64_PC32` references become invalid when + # linking a shared library. + # + # The *unwrapped* bintools: `as` and `ld` proper carry no target-libc + # reference, so the decoupling this package set exists for still holds. + # + # Note this is `PATH` rather than `--with-as`/`--with-ld` on purpose. We want + # configure to *ask* the real tools what they support, and we want it to bake + # nothing else: those flags record `DEFAULT_ASSEMBLER`/`DEFAULT_LINKER` as + # absolute store paths, and the driver then runs exactly those binaries + # instead of the wrapped ones it is meant to defer to. + depsBuildTarget = [ (bintools.bintools or bintools) ]; + nativeBuildInputs = [ texinfo which @@ -218,7 +271,15 @@ stdenv.mkDerivation (finalAttrs: { "--without-headers" "--with-gnu-as" "--with-gnu-ld" - "--with-as=${lib.getExe' bintools "${bintools.targetPrefix}as"}" + # Deliberately no `--with-as` / `--with-ld`. Those bake + # `DEFAULT_ASSEMBLER` and `DEFAULT_LINKER` -- absolute store paths -- into + # the compiler, so the driver runs exactly those binaries and stops + # deferring to the wrapped tools it is meant to use. + # + # Nothing has to be baked. At configure time the tools are on `PATH` via + # `depsBuildTarget`, which is what the capability probes need; at use time + # the driver finds them on `PATH` under their target-prefixed names, via + # the `find_a_program` patches above. "--with-system-zlib" "--without-included-gettext" "--enable-linker-build-id" From be82056397df8e2e4ba1ed2caa09237ca3fa94c5 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Mon, 10 Aug 2026 03:22:39 -0400 Subject: [PATCH 04/11] gcc/ng: cope with a source tree that is a VCS checkout A release tarball carries generated files that a git tree does not, and two of those bit us. `MD5SUMS` is generated when a tarball is rolled. Each sub-source extractor asserted `[[ -f MD5SUMS ]]` before copying it, so every one of them failed outright the moment `monorepoSrc` was a git tree: unpacking source archive /nix/store/...-source source root is source Copy it only when it is there. The generated sources are the other half: a checkout lacks `gengtype-lex.cc` and friends, which a tarball ships pre-built, so they have to be regenerated. Take flex and bison as native inputs when `fromVCS` says the source is a checkout. Tarball builds are unaffected either way. Assisted-by: Claude Code (Claude Opus 5) --- .../compilers/gcc/ng/common/gcc/default.nix | 12 ++++- .../gcc/ng/common/libatomic/default.nix | 41 ++++++++------- .../gcc/ng/common/libbacktrace/default.nix | 41 ++++++++------- .../gcc/ng/common/libgomp/default.nix | 43 +++++++++------- .../gcc/ng/common/libiberty/default.nix | 37 ++++++++------ .../gcc/ng/common/libquadmath/default.nix | 35 +++++++------ .../gcc/ng/common/libsanitizer/default.nix | 35 +++++++------ .../gcc/ng/common/libssp/default.nix | 39 +++++++------- .../gcc/ng/common/libstdcxx/default.nix | 51 ++++++++++--------- 9 files changed, 192 insertions(+), 142 deletions(-) diff --git a/pkgs/development/compilers/gcc/ng/common/gcc/default.nix b/pkgs/development/compilers/gcc/ng/common/gcc/default.nix index d74be1b6a521..48b3401d21c8 100644 --- a/pkgs/development/compilers/gcc/ng/common/gcc/default.nix +++ b/pkgs/development/compilers/gcc/ng/common/gcc/default.nix @@ -27,6 +27,12 @@ texinfo, which, gettext, + flex, + bison, + # Whether `monorepoSrc` is a VCS checkout rather than a release tarball. A + # checkout lacks the generated sources (gengtype-lex.cc and friends) that a + # tarball ships pre-built, so they have to be regenerated with flex and bison. + fromVCS ? false, getVersionFile, buildGccPackages, targetPackages, @@ -151,7 +157,11 @@ stdenv.mkDerivation (finalAttrs: { which gettext ] - ++ lib.optional (perl != null) perl; + ++ lib.optional (perl != null) perl + ++ lib.optionals fromVCS [ + flex + bison + ]; buildInputs = [ gmp diff --git a/pkgs/development/compilers/gcc/ng/common/libatomic/default.nix b/pkgs/development/compilers/gcc/ng/common/libatomic/default.nix index db4261a186cc..ce51cf4dd520 100644 --- a/pkgs/development/compilers/gcc/ng/common/libatomic/default.nix +++ b/pkgs/development/compilers/gcc/ng/common/libatomic/default.nix @@ -14,29 +14,34 @@ stdenv.mkDerivation (finalAttrs: { pname = "libatomic"; inherit version; - src = runCommand "libatomic-src-${version}" { src = monorepoSrc; } '' - runPhase unpackPhase + src = runCommand "libatomic-src-${version}" { src = monorepoSrc; } ( + '' + runPhase unpackPhase - mkdir -p "$out/gcc" - cp gcc/BASE-VER "$out/gcc" - cp gcc/DATESTAMP "$out/gcc" + mkdir -p "$out/gcc" + cp gcc/BASE-VER "$out/gcc" + cp gcc/DATESTAMP "$out/gcc" - cp -r libatomic "$out" + cp -r libatomic "$out" - cp -r config "$out" - cp -r multilib.am "$out" - cp -r libtool.m4 "$out" + cp -r config "$out" + cp -r multilib.am "$out" + cp -r libtool.m4 "$out" - cp config.guess "$out" - cp config.rpath "$out" - cp config.sub "$out" - cp config-ml.in "$out" - cp ltmain.sh "$out" - cp install-sh "$out" - cp mkinstalldirs "$out" + cp config.guess "$out" + cp config.rpath "$out" + cp config.sub "$out" + cp config-ml.in "$out" + cp ltmain.sh "$out" + cp install-sh "$out" + cp mkinstalldirs "$out" - [[ -f MD5SUMS ]]; cp MD5SUMS "$out" - ''; + '' + # `MD5SUMS` exists only in release tarballs, not in a VCS checkout. + + '' + if [[ -f MD5SUMS ]]; then cp MD5SUMS "$out"; fi + '' + ); patches = [ (fetchpatch { diff --git a/pkgs/development/compilers/gcc/ng/common/libbacktrace/default.nix b/pkgs/development/compilers/gcc/ng/common/libbacktrace/default.nix index 307a7061bc57..75cf777746a7 100644 --- a/pkgs/development/compilers/gcc/ng/common/libbacktrace/default.nix +++ b/pkgs/development/compilers/gcc/ng/common/libbacktrace/default.nix @@ -11,28 +11,33 @@ stdenv.mkDerivation (finalAttrs: { pname = "libbacktrace"; inherit version; - src = runCommand "libbacktrace-src-${version}" { src = monorepoSrc; } '' - runPhase unpackPhase + src = runCommand "libbacktrace-src-${version}" { src = monorepoSrc; } ( + '' + runPhase unpackPhase - mkdir -p "$out/gcc" - cp gcc/BASE-VER "$out/gcc" - cp gcc/DATESTAMP "$out/gcc" + mkdir -p "$out/gcc" + cp gcc/BASE-VER "$out/gcc" + cp gcc/DATESTAMP "$out/gcc" - cp -r include "$out" - cp -r libbacktrace "$out" + cp -r include "$out" + cp -r libbacktrace "$out" - cp config.guess "$out" - cp config.rpath "$out" - cp config.sub "$out" - cp config-ml.in "$out" - cp ltmain.sh "$out" - cp install-sh "$out" - cp move-if-change "$out" - cp mkinstalldirs "$out" - cp test-driver "$out" + cp config.guess "$out" + cp config.rpath "$out" + cp config.sub "$out" + cp config-ml.in "$out" + cp ltmain.sh "$out" + cp install-sh "$out" + cp move-if-change "$out" + cp mkinstalldirs "$out" + cp test-driver "$out" - [[ -f MD5SUMS ]]; cp MD5SUMS "$out" - ''; + '' + # `MD5SUMS` exists only in release tarballs, not in a VCS checkout. + + '' + if [[ -f MD5SUMS ]]; then cp MD5SUMS "$out"; fi + '' + ); outputs = [ "out" diff --git a/pkgs/development/compilers/gcc/ng/common/libgomp/default.nix b/pkgs/development/compilers/gcc/ng/common/libgomp/default.nix index 9053e88a80f9..19b74a3da0d9 100644 --- a/pkgs/development/compilers/gcc/ng/common/libgomp/default.nix +++ b/pkgs/development/compilers/gcc/ng/common/libgomp/default.nix @@ -13,30 +13,35 @@ stdenv.mkDerivation (finalAttrs: { pname = "libgomp"; inherit version; - src = runCommand "libgomp-src-${version}" { src = monorepoSrc; } '' - runPhase unpackPhase + src = runCommand "libgomp-src-${version}" { src = monorepoSrc; } ( + '' + runPhase unpackPhase - mkdir -p "$out/gcc" - cp gcc/BASE-VER "$out/gcc" - cp gcc/DATESTAMP "$out/gcc" + mkdir -p "$out/gcc" + cp gcc/BASE-VER "$out/gcc" + cp gcc/DATESTAMP "$out/gcc" - cp -r libgomp "$out" - cp -r include "$out" + cp -r libgomp "$out" + cp -r include "$out" - cp -r config "$out" - cp -r multilib.am "$out" - cp -r libtool.m4 "$out" + cp -r config "$out" + cp -r multilib.am "$out" + cp -r libtool.m4 "$out" - cp config.guess "$out" - cp config.rpath "$out" - cp config.sub "$out" - cp config-ml.in "$out" - cp ltmain.sh "$out" - cp install-sh "$out" - cp mkinstalldirs "$out" + cp config.guess "$out" + cp config.rpath "$out" + cp config.sub "$out" + cp config-ml.in "$out" + cp ltmain.sh "$out" + cp install-sh "$out" + cp mkinstalldirs "$out" - [[ -f MD5SUMS ]]; cp MD5SUMS "$out" - ''; + '' + # `MD5SUMS` exists only in release tarballs, not in a VCS checkout. + + '' + if [[ -f MD5SUMS ]]; then cp MD5SUMS "$out"; fi + '' + ); outputs = [ "out" diff --git a/pkgs/development/compilers/gcc/ng/common/libiberty/default.nix b/pkgs/development/compilers/gcc/ng/common/libiberty/default.nix index a83d9580dabc..28a099b9f9fb 100644 --- a/pkgs/development/compilers/gcc/ng/common/libiberty/default.nix +++ b/pkgs/development/compilers/gcc/ng/common/libiberty/default.nix @@ -11,26 +11,31 @@ stdenv.mkDerivation (finalAttrs: { pname = "libiberty"; inherit version; - src = runCommand "libiberty-src-${version}" { src = monorepoSrc; } '' - runPhase unpackPhase + src = runCommand "libiberty-src-${version}" { src = monorepoSrc; } ( + '' + runPhase unpackPhase - mkdir -p "$out/gcc" - cp gcc/BASE-VER "$out/gcc" - cp gcc/DATESTAMP "$out/gcc" + mkdir -p "$out/gcc" + cp gcc/BASE-VER "$out/gcc" + cp gcc/DATESTAMP "$out/gcc" - cp -r include "$out" - cp -r libiberty "$out" + cp -r include "$out" + cp -r libiberty "$out" - cp config.guess "$out" - cp config.rpath "$out" - cp config.sub "$out" - cp config-ml.in "$out" - cp ltmain.sh "$out" - cp install-sh "$out" - cp mkinstalldirs "$out" + cp config.guess "$out" + cp config.rpath "$out" + cp config.sub "$out" + cp config-ml.in "$out" + cp ltmain.sh "$out" + cp install-sh "$out" + cp mkinstalldirs "$out" - [[ -f MD5SUMS ]]; cp MD5SUMS "$out" - ''; + '' + # `MD5SUMS` exists only in release tarballs, not in a VCS checkout. + + '' + if [[ -f MD5SUMS ]]; then cp MD5SUMS "$out"; fi + '' + ); outputs = [ "out" diff --git a/pkgs/development/compilers/gcc/ng/common/libquadmath/default.nix b/pkgs/development/compilers/gcc/ng/common/libquadmath/default.nix index 982b4177b749..41e309459027 100644 --- a/pkgs/development/compilers/gcc/ng/common/libquadmath/default.nix +++ b/pkgs/development/compilers/gcc/ng/common/libquadmath/default.nix @@ -11,25 +11,30 @@ stdenv.mkDerivation (finalAttrs: { pname = "libquadmath"; inherit version; - src = runCommand "libquadmath-src-${version}" { src = monorepoSrc; } '' - runPhase unpackPhase + src = runCommand "libquadmath-src-${version}" { src = monorepoSrc; } ( + '' + runPhase unpackPhase - mkdir -p "$out/gcc" - cp gcc/BASE-VER "$out/gcc" - cp gcc/DATESTAMP "$out/gcc" + mkdir -p "$out/gcc" + cp gcc/BASE-VER "$out/gcc" + cp gcc/DATESTAMP "$out/gcc" - cp -r libquadmath "$out" + cp -r libquadmath "$out" - cp config.guess "$out" - cp config.rpath "$out" - cp config.sub "$out" - cp config-ml.in "$out" - cp ltmain.sh "$out" - cp install-sh "$out" - cp mkinstalldirs "$out" + cp config.guess "$out" + cp config.rpath "$out" + cp config.sub "$out" + cp config-ml.in "$out" + cp ltmain.sh "$out" + cp install-sh "$out" + cp mkinstalldirs "$out" - [[ -f MD5SUMS ]]; cp MD5SUMS "$out" - ''; + '' + # `MD5SUMS` exists only in release tarballs, not in a VCS checkout. + + '' + if [[ -f MD5SUMS ]]; then cp MD5SUMS "$out"; fi + '' + ); sourceRoot = "${finalAttrs.src.name}/libquadmath"; diff --git a/pkgs/development/compilers/gcc/ng/common/libsanitizer/default.nix b/pkgs/development/compilers/gcc/ng/common/libsanitizer/default.nix index b42736d122bd..c90ed42e5848 100644 --- a/pkgs/development/compilers/gcc/ng/common/libsanitizer/default.nix +++ b/pkgs/development/compilers/gcc/ng/common/libsanitizer/default.nix @@ -12,25 +12,30 @@ stdenv.mkDerivation (finalAttrs: { pname = "libsanitizer"; inherit version; - src = runCommand "libsanitizer-src-${version}" { src = monorepoSrc; } '' - runPhase unpackPhase + src = runCommand "libsanitizer-src-${version}" { src = monorepoSrc; } ( + '' + runPhase unpackPhase - mkdir -p "$out/gcc" - cp gcc/BASE-VER "$out/gcc" - cp gcc/DATESTAMP "$out/gcc" + mkdir -p "$out/gcc" + cp gcc/BASE-VER "$out/gcc" + cp gcc/DATESTAMP "$out/gcc" - cp -r libsanitizer "$out" + cp -r libsanitizer "$out" - cp config.guess "$out" - cp config.rpath "$out" - cp config.sub "$out" - cp config-ml.in "$out" - cp ltmain.sh "$out" - cp install-sh "$out" - cp mkinstalldirs "$out" + cp config.guess "$out" + cp config.rpath "$out" + cp config.sub "$out" + cp config-ml.in "$out" + cp ltmain.sh "$out" + cp install-sh "$out" + cp mkinstalldirs "$out" - [[ -f MD5SUMS ]]; cp MD5SUMS "$out" - ''; + '' + # `MD5SUMS` exists only in release tarballs, not in a VCS checkout. + + '' + if [[ -f MD5SUMS ]]; then cp MD5SUMS "$out"; fi + '' + ); sourceRoot = "${finalAttrs.src.name}/libsanitizer"; diff --git a/pkgs/development/compilers/gcc/ng/common/libssp/default.nix b/pkgs/development/compilers/gcc/ng/common/libssp/default.nix index bcc5d4b9c2e3..45840d75f3bb 100644 --- a/pkgs/development/compilers/gcc/ng/common/libssp/default.nix +++ b/pkgs/development/compilers/gcc/ng/common/libssp/default.nix @@ -14,28 +14,33 @@ stdenv.mkDerivation (finalAttrs: { pname = "libssp"; inherit version; - src = runCommand "libssp-src-${version}" { src = monorepoSrc; } '' - runPhase unpackPhase + src = runCommand "libssp-src-${version}" { src = monorepoSrc; } ( + '' + runPhase unpackPhase - mkdir -p "$out/gcc" - cp gcc/BASE-VER "$out/gcc" - cp gcc/DATESTAMP "$out/gcc" + mkdir -p "$out/gcc" + cp gcc/BASE-VER "$out/gcc" + cp gcc/DATESTAMP "$out/gcc" - cp -r libssp "$out" + cp -r libssp "$out" - cp -r config "$out" - cp -r multilib.am "$out" + cp -r config "$out" + cp -r multilib.am "$out" - cp config.guess "$out" - cp config.rpath "$out" - cp config.sub "$out" - cp config-ml.in "$out" - cp ltmain.sh "$out" - cp install-sh "$out" - cp mkinstalldirs "$out" + cp config.guess "$out" + cp config.rpath "$out" + cp config.sub "$out" + cp config-ml.in "$out" + cp ltmain.sh "$out" + cp install-sh "$out" + cp mkinstalldirs "$out" - [[ -f MD5SUMS ]]; cp MD5SUMS "$out" - ''; + '' + # `MD5SUMS` exists only in release tarballs, not in a VCS checkout. + + '' + if [[ -f MD5SUMS ]]; then cp MD5SUMS "$out"; fi + '' + ); outputs = [ "out" diff --git a/pkgs/development/compilers/gcc/ng/common/libstdcxx/default.nix b/pkgs/development/compilers/gcc/ng/common/libstdcxx/default.nix index 8e43df2ab635..48309b851e68 100644 --- a/pkgs/development/compilers/gcc/ng/common/libstdcxx/default.nix +++ b/pkgs/development/compilers/gcc/ng/common/libstdcxx/default.nix @@ -15,36 +15,41 @@ stdenv.mkDerivation (finalAttrs: { pname = "libstdcxx"; inherit version; - src = runCommand "libstdcxx-src-${version}" { src = monorepoSrc; } '' - runPhase unpackPhase + src = runCommand "libstdcxx-src-${version}" { src = monorepoSrc; } ( + '' + runPhase unpackPhase - mkdir -p "$out/gcc" - cp gcc/BASE-VER "$out/gcc" - cp gcc/DATESTAMP "$out/gcc" + mkdir -p "$out/gcc" + cp gcc/BASE-VER "$out/gcc" + cp gcc/DATESTAMP "$out/gcc" - mkdir -p "$out/libgcc" - cp libgcc/gthr*.h "$out/libgcc" - cp libgcc/unwind-pe.h "$out/libgcc" + mkdir -p "$out/libgcc" + cp libgcc/gthr*.h "$out/libgcc" + cp libgcc/unwind-pe.h "$out/libgcc" - cp -r libstdc++-v3 "$out" + cp -r libstdc++-v3 "$out" - cp -r libiberty "$out" - cp -r include "$out" - cp -r contrib "$out" + cp -r libiberty "$out" + cp -r include "$out" + cp -r contrib "$out" - cp -r config "$out" - cp -r multilib.am "$out" + cp -r config "$out" + cp -r multilib.am "$out" - cp config.guess "$out" - cp config.rpath "$out" - cp config.sub "$out" - cp config-ml.in "$out" - cp ltmain.sh "$out" - cp install-sh "$out" - cp mkinstalldirs "$out" + cp config.guess "$out" + cp config.rpath "$out" + cp config.sub "$out" + cp config-ml.in "$out" + cp ltmain.sh "$out" + cp install-sh "$out" + cp mkinstalldirs "$out" - [[ -f MD5SUMS ]]; cp MD5SUMS "$out" - ''; + '' + # `MD5SUMS` exists only in release tarballs, not in a VCS checkout. + + '' + if [[ -f MD5SUMS ]]; then cp MD5SUMS "$out"; fi + '' + ); outputs = [ "out" From 3f8e1ff98b9089b1197bf5e53f78ea46a86d7822 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Sun, 9 Aug 2026 15:30:22 -0400 Subject: [PATCH 05/11] gcc/ng: add `gccWithLibgcc`, the compiler for the libc bootstrap step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../compilers/gcc/ng/common/default.nix | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pkgs/development/compilers/gcc/ng/common/default.nix b/pkgs/development/compilers/gcc/ng/common/default.nix index 0e040fdb1320..6812f6ffec26 100644 --- a/pkgs/development/compilers/gcc/ng/common/default.nix +++ b/pkgs/development/compilers/gcc/ng/common/default.nix @@ -181,6 +181,27 @@ makeScopeWithSplicing' { stdenv = overrideCC stdenv buildGccPackages.gccNoLibgcc; }; + # The bootstrap step between `gccNoLibgcc` and `gccWithLibc`: libgcc is + # available, libc is not yet. Compiling a libc needs exactly this — libc's + # own sources call into libgcc (integer/floating-point helpers, stack + # unwinding), so `gccNoLibgcc` is not enough, while `gccWithLibc` cannot + # be used to build the very libc it depends on. + # + # `binutilsNoLibc` is what keeps libc out: it supplies the header-only + # `preLibcHeaders` instead of a real libc, so nothing here refers to a + # libc derivation and the cycle stays broken. + gccWithLibgcc = wrapCCWith { + cc = gccPackages.gcc-unwrapped; + libcxx = null; + bintools = binutilsNoLibc; + extraPackages = [ + targetGccPackages.libgcc + ]; + nixSupport.cc-cflags = [ + "-B${targetGccPackages.libgcc}/lib" + ]; + }; + gccWithLibc = wrapCCWith { cc = gccPackages.gcc-unwrapped; libcxx = null; From 83cb487509c83ff6427d2ac738bb1ef37584f474 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Tue, 11 Aug 2026 09:41:28 -0400 Subject: [PATCH 06/11] gcc/ng: build `libgcc` twice, once for the libc and once against it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 `` 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) --- pkgs/by-name/mu/musl/package.nix | 9 ++- .../compilers/gcc/ng/common/default.nix | 44 ++++++++++++++- .../compilers/gcc/ng/common/gcc/default.nix | 9 ++- .../gcc/ng/common/libgcc/default.nix | 55 ++++++++++++++++--- .../gcc/ng/common/libstdcxx/default.nix | 40 +++++++++++++- pkgs/development/libraries/glibc/common.nix | 5 ++ pkgs/os-specific/bsd/netbsd/pkgs/libc.nix | 6 ++ pkgs/top-level/all-packages.nix | 21 +++++-- 8 files changed, 170 insertions(+), 19 deletions(-) diff --git a/pkgs/by-name/mu/musl/package.nix b/pkgs/by-name/mu/musl/package.nix index 3df1b2289934..3cfdae35710a 100644 --- a/pkgs/by-name/mu/musl/package.nix +++ b/pkgs/by-name/mu/musl/package.nix @@ -174,7 +174,14 @@ stdenv.mkDerivation (finalAttrs: { install -D ${tree_h} $dev/include/sys/tree.h ''; - passthru.linuxHeaders = linuxHeaders; + passthru = { + linuxHeaders = linuxHeaders; + + # musl's threads are POSIX threads. `libgcc` and `libstdc++` have to be + # configured for the same threading model as each other, so rather than + # have each guess, they take it from the libc they are built against. + threadModel = "posix"; + }; meta = { description = "Efficient, small, quality libc implementation"; diff --git a/pkgs/development/compilers/gcc/ng/common/default.nix b/pkgs/development/compilers/gcc/ng/common/default.nix index 6812f6ffec26..9ae438ff5e32 100644 --- a/pkgs/development/compilers/gcc/ng/common/default.nix +++ b/pkgs/development/compilers/gcc/ng/common/default.nix @@ -167,6 +167,12 @@ makeScopeWithSplicing' { ]; }; + # `binutilsNoLibc` carries `preLibcHeaders` as its `libc` — the + # header-only stand-in for platforms that have one, and nothing at all + # for platforms that do not. `wrapCCWith` defaults `libc` to + # `bintools.libc`, so this wrapper carries it too, and that is the only + # place the pre-libc stage is written down: everything built with this + # compiler reads `stdenv.cc.libc` and needs no flag of its own. gccNoLibgcc = wrapCCWith { cc = gccPackages.gcc-unwrapped; libcxx = null; @@ -177,10 +183,26 @@ makeScopeWithSplicing' { ]; }; - libgcc = callPackage ./libgcc { + # Built before there is a libc, and good for nothing but getting one + # built: single-threaded, and compiled against `preLibcHeaders` at best. + # Never hand this to users. + # + # Nothing is passed here to say which stage this is; it follows from the + # compiler, exactly as the LLVM set distinguishes `compiler-rt-no-libc` + # from `compiler-rt-libc`. + libgcc-no-libc = callPackage ./libgcc { stdenv = overrideCC stdenv buildGccPackages.gccNoLibgcc; }; + # The real one, built against the finished libc, so it can use that + # libc's threads. This is what everything above the libc gets. + libgcc-libc = callPackage ./libgcc { + stdenv = overrideCC stdenv buildGccPackages.gccWithLibcAndBasicLibgcc; + }; + + libgcc = + if stdenv.hostPlatform.libc == null then gccPackages.libgcc-no-libc else gccPackages.libgcc-libc; + # The bootstrap step between `gccNoLibgcc` and `gccWithLibc`: libgcc is # available, libc is not yet. Compiling a libc needs exactly this — libc's # own sources call into libgcc (integer/floating-point helpers, stack @@ -195,10 +217,26 @@ makeScopeWithSplicing' { libcxx = null; bintools = binutilsNoLibc; extraPackages = [ - targetGccPackages.libgcc + targetGccPackages.libgcc-no-libc ]; nixSupport.cc-cflags = [ - "-B${targetGccPackages.libgcc}/lib" + "-B${targetGccPackages.libgcc-no-libc}/lib" + ]; + }; + + # The rung after `gccWithLibgcc`: the libc it was used to build now + # exists, so this has a real libc, but still only the bootstrap libgcc — + # the finished one is what it is about to build. That is `libgcc-libc`, + # which unlike its predecessor can use the libc's threads. + gccWithLibcAndBasicLibgcc = wrapCCWith { + cc = gccPackages.gcc-unwrapped; + libcxx = null; + bintools = binutils; + extraPackages = [ + targetGccPackages.libgcc-no-libc + ]; + nixSupport.cc-cflags = [ + "-B${targetGccPackages.libgcc-no-libc}/lib" ]; }; diff --git a/pkgs/development/compilers/gcc/ng/common/gcc/default.nix b/pkgs/development/compilers/gcc/ng/common/gcc/default.nix index 48b3401d21c8..d37a89aa83db 100644 --- a/pkgs/development/compilers/gcc/ng/common/gcc/default.nix +++ b/pkgs/development/compilers/gcc/ng/common/gcc/default.nix @@ -38,6 +38,9 @@ targetPackages, libc, bintools, + # Build the shared runtime libraries, and so have the driver's specs emit + # `-lgcc_s`. Derived the way the monolithic build derives it. + enableTargetShared ? stdenv.targetPlatform.hasSharedLibraries, }: let inherit (stdenv) targetPlatform hostPlatform; @@ -260,7 +263,11 @@ stdenv.mkDerivation (finalAttrs: { "--disable-install-libiberty" "--disable-multilib" "--disable-nls" - "--disable-shared" + # Derived rather than forced off: the driver's specs only emit `-lgcc_s` + # for a target that has shared libraries, so hardcoding this leaves every + # throwing C++ program unlinkable even though `libgcc_s.so` is built and + # findable. Same predicate the monolithic build uses. + (lib.enableFeature enableTargetShared "shared") "--enable-default-pie" "--enable-languages=${ lib.concatStrings ( diff --git a/pkgs/development/compilers/gcc/ng/common/libgcc/default.nix b/pkgs/development/compilers/gcc/ng/common/libgcc/default.nix index 9cd1561d76ff..9c5979ef255c 100644 --- a/pkgs/development/compilers/gcc/ng/common/libgcc/default.nix +++ b/pkgs/development/compilers/gcc/ng/common/libgcc/default.nix @@ -12,9 +12,37 @@ buildPackages, which, python3, + # Build `libgcc_s` as well as `libgcc.a`, deriving it the way the monolithic + # build does rather than forcing it off. + enableShared ? stdenv.hostPlatform.hasSharedLibraries, }: +let + # A libc needs libgcc to build, and a libgcc that can use the libc's threads + # needs the libc, so this package is instantiated twice — see + # `libgcc-no-libc` and `libgcc-libc` in the package set, the same split the + # LLVM package set makes with `compiler-rt-no-libc` and `compiler-rt-libc`. + # + # Nothing here says which of the two it is. The difference is entirely in the + # compiler it is handed: the bootstrap wrapper's `libc` is `preLibcHeaders` + # (or nothing at all, on platforms without one), the later wrapper's is the + # finished libc. Everything below reads that one value. + libc = stdenv.cc.libc or null; + + # Which threading model may be used is decided by the libc, so take it from + # there rather than guess — and pass it on in `passthru` so that `libstdcxx`, + # which has to agree, reads the same answer instead of probing for its own. + # + # This is what makes the bootstrap build single-threaded without being told + # to be: a headers-only package declares no `threadModel`, and a real libc + # does. That build exists only to get the libc built and is thrown away + # afterwards, so there is nothing to be gained from threading it, and plenty + # to go wrong: `gthr-posix.h` includes `` unconditionally, which + # at that point is either absent or a stand-in for a libc that does not exist + # yet. + threadModel = if libc == null then "single" else libc.threadModel or "single"; +in stdenv.mkDerivation (finalAttrs: { - pname = "libgcc"; + pname = "libgcc" + lib.optionalString (libc == null) "-no-libc"; inherit version; src = monorepoSrc; @@ -83,9 +111,18 @@ stdenv.mkDerivation (finalAttrs: { buildRoot=$(readlink -e "./build") ''; - postPatch = '' - sourceRoot=$(readlink -e "./libgcc") - ''; + postPatch = + # `SHLIB_LC` defaults to `-lc`, so the `libgcc_s.so` rule cannot link + # before libc exists. The monolithic build clobbers it for exactly this + # reason; do the same rather than giving up on shared libgcc. Runs while + # the working directory is still the monorepo root, before `sourceRoot` + # is repointed below. + lib.optionalString enableShared ( + import ../../../common/libgcc-buildstuff.nix { inherit lib stdenv; } + ) + + '' + sourceRoot=$(readlink -e "./libgcc") + ''; enableParallelBuilding = true; @@ -231,13 +268,14 @@ stdenv.mkDerivation (finalAttrs: { configureFlags = [ "--disable-dependency-tracking" - "gcc_cv_target_thread_file=single" + "gcc_cv_target_thread_file=${threadModel}" # $CC cannot link binaries, let alone run then "cross_compiling=true" - # Do not have dynamic linker without libc "--enable-static" - "--disable-shared" - ]; + ] + # `libgcc_s` needs no libc: it comes out with an empty `DT_NEEDED`, which is + # why the monolithic build ships one even from its nolibc stage. + ++ lib.optional (!enableShared) "--disable-shared"; # Set the variable back the way it was, see corresponding code in # `preConfigure`. @@ -255,6 +293,7 @@ stdenv.mkDerivation (finalAttrs: { passthru = { isGNU = true; + inherit threadModel; }; meta = gcc_meta // { diff --git a/pkgs/development/compilers/gcc/ng/common/libstdcxx/default.nix b/pkgs/development/compilers/gcc/ng/common/libstdcxx/default.nix index 48309b851e68..e40af4021aa4 100644 --- a/pkgs/development/compilers/gcc/ng/common/libstdcxx/default.nix +++ b/pkgs/development/compilers/gcc/ng/common/libstdcxx/default.nix @@ -10,6 +10,7 @@ autoreconfHook269, runCommand, gettext, + libgcc, }: stdenv.mkDerivation (finalAttrs: { pname = "libstdcxx"; @@ -44,6 +45,14 @@ stdenv.mkDerivation (finalAttrs: { cp install-sh "$out" cp mkinstalldirs "$out" + '' + # `src/Makefile` runs this to compute `LTLDFLAGS`, reaching out of the + # subdirectory for it as `$(top_srcdir)/../libtool-ldflags`. Missing, the + # shell says "No such file or directory" and `LTLDFLAGS` silently comes out + # empty, dropping our `LDFLAGS` from every library link. + + '' + cp libtool-ldflags "$out" + '' # `MD5SUMS` exists only in release tarballs, not in a VCS checkout. + '' @@ -90,6 +99,32 @@ stdenv.mkDerivation (finalAttrs: { cd "$buildRoot" configureScript=$sourceRoot/configure chmod +x "$configureScript" + + '' + # Put libgcc's `gthr-default.h` where libstdc++ expects to find it. + # + # It is not a source file. In a monolithic build libgcc's Makefile creates + # it by copying whichever `gthr-.h` matches the target's thread + # model, and libstdc++ -- configured inside that same build tree -- picks + # it up. Standalone there is no such tree, so take the one `libgcc` + # installed. The two implement a single threading model between them, and + # copying libgcc's own answer is what makes them agree by construction + # rather than by two independent guesses that can drift apart. `libgcc` + # decided it from the libc; `gcc_cv_target_thread_file` below repeats the + # same value. + # + # The consequence of getting this wrong is worth spelling out, because the + # installed headers do not show it. `GLIBCXX_CHECK_GTHREADS` compiles + # `#include "gthr.h"` and requires `__GTHREADS_CXX0X`, which + # `gthr-posix.h` defines unconditionally. With no posix `gthr-default.h` + # on the include path that test fails, configure concludes there are no + # gthreads, and `_GLIBCXX_HAS_GTHREADS` is left undefined in + # `c++config.h` -- so `` compiles away and `std::mutex` does not + # exist. Stating the intent with `--enable-threads=posix` does not help: + # the probe overrides intent with an empirical answer, so the header has + # to actually be there. + + '' + cp ${lib.getDev libgcc}/include/gthr-default.h "$sourceRoot/../libgcc/gthr-default.h" ''; configurePlatforms = [ @@ -99,7 +134,10 @@ stdenv.mkDerivation (finalAttrs: { configureFlags = [ "--disable-dependency-tracking" - "gcc_cv_target_thread_file=posix" + # The same answer `libgcc` used, and the model of the `gthr-default.h` + # copied in above. A mismatch here is silent: the unwinder's locks vanish + # while `libstdc++` still hands out `std::thread`. + "gcc_cv_target_thread_file=${libgcc.threadModel}" "cross_compiling=true" "--disable-multilib" diff --git a/pkgs/development/libraries/glibc/common.nix b/pkgs/development/libraries/glibc/common.nix index 5e391970a271..6ce41321494e 100644 --- a/pkgs/development/libraries/glibc/common.nix +++ b/pkgs/development/libraries/glibc/common.nix @@ -278,6 +278,11 @@ stdenv.mkDerivation ( passthru = { inherit version; minorRelease = version; + + # glibc's threads are POSIX threads. `libgcc` and `libstdc++` have to be + # configured for the same threading model as each other, so rather than + # have each guess, they take it from the libc they are built against. + threadModel = "posix"; }; } diff --git a/pkgs/os-specific/bsd/netbsd/pkgs/libc.nix b/pkgs/os-specific/bsd/netbsd/pkgs/libc.nix index e23ba1a4fd36..a67bc775f92e 100644 --- a/pkgs/os-specific/bsd/netbsd/pkgs/libc.nix +++ b/pkgs/os-specific/bsd/netbsd/pkgs/libc.nix @@ -47,5 +47,11 @@ symlinkJoin { fixupPhase ''; + # NetBSD's threads are POSIX threads — `libpthread` is joined in above. + # `libgcc` and `libstdc++` have to be configured for the same threading model + # as each other, so rather than have each guess, they take it from the libc + # they are built against. + passthru.threadModel = "posix"; + meta.platforms = lib.platforms.netbsd; } diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index 3bc94c889614..479ef887d380 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -5631,11 +5631,22 @@ with pkgs; if stdenv.hostPlatform != stdenv.buildPlatform then { stdenv = gccCrossLibcStdenv; # doesn't compile without gcc - libgcc = callPackage ../development/libraries/gcc/libgcc { - gcc = gccCrossLibcStdenv.cc; - glibc = glibc.override { libgcc = null; }; - stdenvNoLibs = gccCrossLibcStdenv; - }; + # glibc `dlopen`s `libgcc_s.so` without consulting the usual search + # path, so it has to name one that already exists. The monolithic + # build gets there by building glibc twice: the inner `libgcc = null` + # one names no libgcc at all, and is used for nothing but building the + # libgcc the real one names. The split package set already has that + # rung as a package of its own — `libgcc-no-libc` is what exists + # before any libc does — so there is nothing left to open-code here. + libgcc = + if stdenv.hostPlatform.useGccNG or false then + gccNGPackages.libgcc-no-libc + else + callPackage ../development/libraries/gcc/libgcc { + gcc = gccCrossLibcStdenv.cc; + glibc = glibc.override { libgcc = null; }; + stdenvNoLibs = gccCrossLibcStdenv; + }; } else { From f39d68cbf9b40e5355af59e14928250d3110bfef Mon Sep 17 00:00:00 2001 From: John Ericson Date: Mon, 10 Aug 2026 18:37:32 -0400 Subject: [PATCH 07/11] gcc/ng: put the target libc's headers where they belong 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 `` 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 `` 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) --- .../compilers/gcc/ng/common/gcc/default.nix | 26 +++++++++++--- .../gcc/ng/common/libgcc/default.nix | 34 +++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/pkgs/development/compilers/gcc/ng/common/gcc/default.nix b/pkgs/development/compilers/gcc/ng/common/gcc/default.nix index d37a89aa83db..fd282acfda67 100644 --- a/pkgs/development/compilers/gcc/ng/common/gcc/default.nix +++ b/pkgs/development/compilers/gcc/ng/common/gcc/default.nix @@ -35,8 +35,6 @@ fromVCS ? false, getVersionFile, buildGccPackages, - targetPackages, - libc, bintools, # Build the shared runtime libraries, and so have the driver's specs emit # `-lgcc_s`. Derived the way the monolithic build derives it. @@ -300,8 +298,14 @@ stdenv.mkDerivation (finalAttrs: { "--with-system-zlib" "--without-included-gettext" "--enable-linker-build-id" - "--with-sysroot=${lib.getDev (targetPackages.libc or libc)}" - "--with-native-system-header-dir=/include" + # Deliberately *no* `--with-sysroot` / `--with-native-system-header-dir` + # pointing at the target libc. Baking a libc store path into the compiler + # makes every libc change rebuild the compiler, which is precisely the + # coupling this split package set exists to remove. cc-wrapper already + # supplies the target libc (`-idirafter /include` and the + # corresponding `-B`/`-L` flags), so the compiler proper does not need to + # know about it -- exactly as in the LLVM package set, where `clang` + # likewise carries no libc reference (`--without-headers` above). ] ++ lib.optionals enablePlugin [ "--enable-plugin" @@ -314,6 +318,20 @@ stdenv.mkDerivation (finalAttrs: { "--with-multilib-list=" ]; + # `LIMITS_H_TEST` decides whether gcc's generated `syslimits.h` chains to the + # target libc's `limits.h` (`#include_next`) or is emitted self-contained. It + # defaults to a `[ -f $(BUILD_SYSTEM_HEADER_DIR)/limits.h ]` probe, which + # necessarily fails here: we deliberately do not point the compiler at a + # sysroot (see `configureFlags`), so there is no libc for it to find at build + # time. + # + # Self-contained is the wrong answer regardless. Every target in this package + # set is hosted, and cc-wrapper always supplies a libc, so the chained header + # is what resolves correctly at *use* time. Without it, anything the libc's + # `limits.h` defines and gcc's does not -- `PATH_MAX` being the common one -- + # goes missing from every libgcc source that needs it. + makeFlags = [ "LIMITS_H_TEST=true" ]; + doCheck = false; postInstall = '' diff --git a/pkgs/development/compilers/gcc/ng/common/libgcc/default.nix b/pkgs/development/compilers/gcc/ng/common/libgcc/default.nix index 9c5979ef255c..29220191a26d 100644 --- a/pkgs/development/compilers/gcc/ng/common/libgcc/default.nix +++ b/pkgs/development/compilers/gcc/ng/common/libgcc/default.nix @@ -120,6 +120,25 @@ stdenv.mkDerivation (finalAttrs: { lib.optionalString enableShared ( import ../../../common/libgcc-buildstuff.nix { inherit lib stdenv; } ) + # gcc's installed `limits.h` chains to the target libc's with + # `#include_next`. Where the compiler has a libc — headers-only or real — + # that resolves, and it has to: some targets build libgcc sources that need + # what only the libc header defines. Where it does not, the chain has + # nowhere to land, and + # even configure's `AC_PROG_CPP` probe fails -- it includes `` + # precisely because that "exists even on freestanding compilers" -- after + # which configure falls back to `/lib/cpp` and reports that as the error. + # + # Only in that case, 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, so nothing is invented here. + # The compiler keeps shipping the chained header either way, which is what + # has to stay correct for everything compiled against a real libc later. + + lib.optionalString (libc == null) '' + mkdir -p "$NIX_BUILD_TOP/freestanding-include" + cp gcc/glimits.h "$NIX_BUILD_TOP/freestanding-include/limits.h" + export NIX_CFLAGS_COMPILE="-isystem $NIX_BUILD_TOP/freestanding-include ''${NIX_CFLAGS_COMPILE-}" + '' + '' sourceRoot=$(readlink -e "./libgcc") ''; @@ -250,6 +269,21 @@ stdenv.mkDerivation (finalAttrs: { "--with-system-zlib" ] + # `gcc/configure` sets `inhibit_libc=true` when host != target and + # `$target_header_dir/stdio.h` does not exist. `inhibit_libc` makes + # `tsystem.h` skip and friends, which is fine for the generic + # sources but breaks the target-specific ones that genuinely need libc + # declarations -- the profiling support files are the usual casualty. + # + # `target_header_dir` is derived from `--with-sysroot`, *not* from + # `--with-headers`, so the sysroot pair is what has to be set. Point it at + # whichever libc the compiler carries, which in the bootstrap build is the + # headers-only one. This affects only the `gcc/configure` run that generates + # libgcc's makefile fragments, not the compiler that gets shipped. + ++ lib.optionals (libc != null) [ + "--with-sysroot=${lib.getDev libc}" + "--with-native-system-header-dir=/include" + ] ++ lib.optional (!stdenv.hostPlatform.isRiscV) # RISC-V does not like it being empty From a1a1eb029ba0963d006caea3e42541b5ee72b1ac Mon Sep 17 00:00:00 2001 From: John Ericson Date: Mon, 10 Aug 2026 18:37:32 -0400 Subject: [PATCH 08/11] gcc/ng: put libstdc++'s library and headers where each belongs 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 `` 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) --- pkgs/build-support/cc-wrapper/default.nix | 10 ++++++++-- .../gcc/ng/15/libstdcxx/force-regular-dirs.patch | 16 ++++++++++++++-- .../compilers/gcc/ng/common/default.nix | 6 ++++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/pkgs/build-support/cc-wrapper/default.nix b/pkgs/build-support/cc-wrapper/default.nix index bf079f1b0e53..d926d85425a3 100644 --- a/pkgs/build-support/cc-wrapper/default.nix +++ b/pkgs/build-support/cc-wrapper/default.nix @@ -804,9 +804,15 @@ stdenvNoCC.mkDerivation { include -cxx-isystem "${getDev libcxx}/include/c++/v1" >> $out/nix-support/libcxx-cxxflags echo "-stdlib=libc++" >> $out/nix-support/libcxx-ldflags '' - # GCC NG friendly libc++ + # This is the GCC NG case, libstdc++ is being built as a separate package. + # + # Point at `include-cxx`, not `include`. `libcxx` is also a propagated + # target-target dep, so the generic setup hook puts its `include` on the *C* + # include path -- and libstdc++ ships headers named after C headers + # (`math.h`, `stdlib.h`, `stdckdint.h`, ...) that are only meant to shadow + # the C ones in C++. A sibling directory the setup hook ignores is enough. + optionalString (libcxx != null && libcxx.isGNU or false) '' - include -isystem "${getDev libcxx}/include" >> $out/nix-support/libcxx-cxxflags + include -isystem "${getDev libcxx}/include-cxx" >> $out/nix-support/libcxx-cxxflags '' ## diff --git a/pkgs/development/compilers/gcc/ng/15/libstdcxx/force-regular-dirs.patch b/pkgs/development/compilers/gcc/ng/15/libstdcxx/force-regular-dirs.patch index e80cffdce1e7..2812ba748e80 100644 --- a/pkgs/development/compilers/gcc/ng/15/libstdcxx/force-regular-dirs.patch +++ b/pkgs/development/compilers/gcc/ng/15/libstdcxx/force-regular-dirs.patch @@ -1,9 +1,21 @@ From db427c55334dd2edc11397d3a92d55dc9c06d1c3 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Sun, 20 Jul 2025 14:20:00 -0400 -Subject: [PATCH] libstdc++: Force regular include/lib dir +Subject: [PATCH] libstdc++: Force regular lib dir, headers in include-cxx Delete a bunch of unneeded logic to do this. + +The library goes straight in $(libdir), with no version-specific or +target-specific subdirectory. + +The headers go in $(includedir)-cxx rather than $(includedir). libstdc++ +intentionally ships headers named after C headers -- `math.h`, `stdlib.h`, +`complex.h`, `stdckdint.h` and friends -- whose whole purpose is to shadow +the C ones when compiling C++. Installed into the ordinary include dir they +shadow them when compiling *C* too, because the generic setup hook puts that +directory on the C include path. A sibling directory keeps them out of C's +way while staying findable for C++; no version or target subdirectory is +needed, since the store path already separates one libstdc++ from another. --- libstdc++-v3/acinclude.m4 | 80 ++------------------------------ libstdc++-v3/include/Makefile.am | 2 +- @@ -21,7 +33,7 @@ index a0094c2dd95..a0718dff394 100644 - glibcxx_toolexeclibdir=no + glibcxx_toolexecdir='$(libdir)' + glibcxx_toolexeclibdir='$(libdir)' -+ gxx_include_dir='$(includedir)' ++ gxx_include_dir='$(includedir)-cxx' glibcxx_prefixdir=$prefix - AC_MSG_CHECKING([for gxx-include-dir]) diff --git a/pkgs/development/compilers/gcc/ng/common/default.nix b/pkgs/development/compilers/gcc/ng/common/default.nix index 9ae438ff5e32..84c31c3bf9d6 100644 --- a/pkgs/development/compilers/gcc/ng/common/default.nix +++ b/pkgs/development/compilers/gcc/ng/common/default.nix @@ -131,6 +131,7 @@ makeScopeWithSplicing' { "-B${targetGccPackages.libssp}/lib" "-B${targetGccPackages.libatomic}/lib" "-B${targetGccPackages.libgomp}/lib" + "-B${targetGccPackages.libstdcxx}/lib" "-B${targetGccPackages.libgfortran}/lib/" ]; }; @@ -163,6 +164,11 @@ makeScopeWithSplicing' { "-B${targetGccPackages.libssp}/lib" "-B${targetGccPackages.libatomic}/lib" "-B${targetGccPackages.libgomp}/lib" + # `libcxx` above tells cc-wrapper where the C++ *headers* are; it does + # not put the library itself on the link path for a GNU compiler. So + # every C++ link failed with `cannot find -lstdc++` until this was + # added, in the same style as the other runtime libraries. + "-B${targetGccPackages.libstdcxx}/lib" "-I${targetGccPackages.libgomp}/lib/gcc/${metadata.release_version}/include" ]; }; From fcde6eb30df0a8233060b89ba4c683e31169dd75 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Mon, 10 Aug 2026 18:37:42 -0400 Subject: [PATCH 09/11] gcc/ng: build libstdc++ with the C driver, and ship `libtool-ldflags` Two things a monolithic build does implicitly that the split set had nobody to do. Linking `libstdc++.so` with `g++` fails, because that driver implies `-lstdc++` and the library being linked is the one that would provide it: ld: cannot find -lstdc++: No such file or directory `libstdc++-v3/src/Makefile.am` names the remedy where it defines `CXXLINK`: use `gcc` as the C++ compilation driver, which the top level arranges via `RAW_CXX_FOR_TARGET` -- `xgcc`, not `xg++`, plus `-shared-libgcc` and `-nostdinc++`. Configured standalone there is no top level to arrange it, so arrange it here. The C driver still compiles `.cc` as C++ by extension; what it drops is the implicit `-lstdc++`. `-shared-libgcc` restores the linkage `g++` would have chosen, and `-nostdinc++` keeps already-installed C++ headers out of the build whose purpose is to produce them. `src/Makefile` also computes `LTLDFLAGS` by shelling out to `$(top_srcdir)/../libtool-ldflags`, reaching out of `libstdc++-v3` into the GCC top level. Our trimmed source tree never copied it, so the shell reported `No such file or directory`, `LTLDFLAGS` came out empty, and `LDFLAGS` was quietly dropped from every library link. Same failure mode as the `gthr-default.h` bug: a file that exists only because a monolithic tree has it, missing without anything treating its absence as an error. Assisted-by: Claude Code (Claude Opus 5) --- .../gcc/ng/common/libstdcxx/default.nix | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/pkgs/development/compilers/gcc/ng/common/libstdcxx/default.nix b/pkgs/development/compilers/gcc/ng/common/libstdcxx/default.nix index e40af4021aa4..654c4992e47b 100644 --- a/pkgs/development/compilers/gcc/ng/common/libstdcxx/default.nix +++ b/pkgs/development/compilers/gcc/ng/common/libstdcxx/default.nix @@ -100,6 +100,30 @@ stdenv.mkDerivation (finalAttrs: { configureScript=$sourceRoot/configure chmod +x "$configureScript" + '' + # Build libstdc++ with the *C* driver, not `g++`. `g++` implies `-lstdc++`, + # which cannot be satisfied while building the very library that provides + # it, and the link fails with `cannot find -lstdc++`. + # + # This is not our invention; it is what a monolithic build does, and + # `libstdc++-v3/src/Makefile.am` says so where it defines `CXXLINK`: + # + # We cannot allow g++ to be used since this would add -lstdc++ to the + # link line which of course is problematic at this point. So, we get + # the top-level directory to configure libstdc++-v3 to use gcc as the + # C++ compilation driver. + # + # The top level does that through `RAW_CXX_FOR_TARGET`, which is `xgcc` + # -- not `xg++` -- plus `-shared-libgcc` and `-nostdinc++`. Building + # standalone there is no top level to arrange it, so arrange it here. The + # C driver still compiles `.cc` as C++ by extension; what it drops is the + # implicit `-lstdc++`. `-shared-libgcc` puts back the linkage `g++` would + # have chosen, which the C driver does not default to, and `-nostdinc++` + # keeps any already-installed C++ headers out of a build whose whole + # purpose is to produce them. + + '' + cxxForLibstdcxx="$CC -shared-libgcc -nostdinc++" + '' # Put libgcc's `gthr-default.h` where libstdc++ expects to find it. # @@ -125,6 +149,9 @@ stdenv.mkDerivation (finalAttrs: { # to actually be there. + '' cp ${lib.getDev libgcc}/include/gthr-default.h "$sourceRoot/../libgcc/gthr-default.h" + + export CXX="$cxxForLibstdcxx" + echo "libstdcxx: building with CXX=$CXX" ''; configurePlatforms = [ From 85edf2c64fc7280d594cc92981d5f23a9e8c2990 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Mon, 10 Aug 2026 14:25:45 -0400 Subject: [PATCH 10/11] gcc/ng: let libstdc++ pick its own locale model `--enable-clocale=gnu` was passed for every target. That selects `config/locale/gnu`, the glibc locale model, whose `ctype_members.cc` reads `__ctype_b` -- a glibc symbol. On any other libc the build fails on the type the table is not: ctype_members.cc:50:51: error: cannot convert 'const short unsigned int*' to 'const std::ctype_base::mask*' {aka 'const unsigned int*'} in assignment libstdc++ works this out for itself from the host triple, as it does for the OS layer next door -- `configure.host:262` maps `linux-musl*` to `os/generic` -- and the monolithic build passes no `--enable-clocale` at all, leaving that to configure. Do the same here. Verified by building the full cross toolchain, `stdenv.cc`, for `aarch64-unknown-linux-musl`, which builds libstdc++ and previously failed on exactly this. Assisted-by: Claude Code (Claude Opus 5) --- .../compilers/gcc/ng/common/libstdcxx/default.nix | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkgs/development/compilers/gcc/ng/common/libstdcxx/default.nix b/pkgs/development/compilers/gcc/ng/common/libstdcxx/default.nix index 654c4992e47b..08865b5ff519 100644 --- a/pkgs/development/compilers/gcc/ng/common/libstdcxx/default.nix +++ b/pkgs/development/compilers/gcc/ng/common/libstdcxx/default.nix @@ -168,7 +168,12 @@ stdenv.mkDerivation (finalAttrs: { "cross_compiling=true" "--disable-multilib" - "--enable-clocale=gnu" + # `gnu` is the glibc locale model: `config/locale/gnu/ctype_members.cc` + # reads `__ctype_b`, which only glibc has. Forcing it everywhere breaks any + # other libc -- on musl the build fails converting `const unsigned short *` + # to `const ctype_base::mask *`. Configure picks the right model from the + # host triple on its own, as it does for the monolithic build, which passes + # no `--enable-clocale` at all. "--disable-libstdcxx-pch" "--disable-vtable-verify" "--enable-libstdcxx-visibility" From a089de30fa56821ae42163f01e6f46b10ba7724b Mon Sep 17 00:00:00 2001 From: John Ericson Date: Tue, 11 Aug 2026 13:41:03 -0400 Subject: [PATCH 11/11] gcc/ng: describe the bootstrap chain in the README 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) --- pkgs/development/compilers/gcc/ng/README.md | 57 ++++++++++++++++++- .../compilers/gcc/ng/common/default.nix | 39 +++++-------- 2 files changed, 69 insertions(+), 27 deletions(-) diff --git a/pkgs/development/compilers/gcc/ng/README.md b/pkgs/development/compilers/gcc/ng/README.md index b40070caebc6..0bbde4ef5cbf 100644 --- a/pkgs/development/compilers/gcc/ng/README.md +++ b/pkgs/development/compilers/gcc/ng/README.md @@ -1,3 +1,58 @@ # GCC Next-Generation -Experimental split GCC package set based on the LLVM package set design. +Experimental split GCC package set, based on the LLVM package set design. + +The monolithic `gcc` derivation builds the compiler and every runtime library in one go, so a change to the target libc rebuilds the compiler too. +This set separates them — `gcc`, `libgcc`, `libstdcxx` and the rest are individual packages — so the compiler stops depending on the libc, and each piece can be rebuilt on its own. +Because GCC is not a multi-target compiler — a single target is baked into the binary with CPP — we still need to rebuild it more than we do LLVM, but someday that should change. + +A platform opts in with `useGccNG`, in the same way it would opt into `useLLVM`. + +## The bootstrap chain + +The libc and libgcc depend on each other: a libc's own sources call into libgcc for integer and floating-point helpers and for stack unwinding, and a libgcc that can use the libc's threads needs the libc. + +The way out we currently use is the same one the LLVM set takes with `compiler-rt-no-libc` and `compiler-rt-libc` — build the runtime twice, either side of the libc. +It is unclear whether this works in general to resolve the circularity, but we shall see. + +That gives four compilers, each one step further along: + +| compiler | libc | libgcc | used to build | +|---|---|---|---| +| `gccNoLibgcc` | headers, or nothing | — | `libgcc-no-libc` | +| `gccWithLibgcc` | headers, or nothing | `libgcc-no-libc` | the libc | +| `gccWithLibcAndBasicLibgcc` | real | `libgcc-no-libc` | `libgcc-libc` | +| `gccWithLibc` / `gcc` | real | `libgcc-libc` | everything else | + +`libgcc` resolves to `libgcc-libc` wherever a libc exists; only those first three stages ever see `libgcc-no-libc`. +The bootstrap one is single-threaded and compiled against the libc's headers at best, so it is not intended for use beyond building the libc. + +Nothing is passed down to say which stage is which. +It follows from the compiler: each package reads `stdenv.cc.libc` and needs no flag of its own. +That is also how the two `libgcc`s differ — same expression, different `stdenv`. + +## Where the pre-libc stage is written down + +`binutilsNoLibc` carries `preLibcHeaders` as its `libc`: the header-only stand-in for platforms that have one, and nothing at all for platforms that do not. +`wrapCCWith` defaults `libc` to `bintools.libc`, so the bootstrap compilers inherit it, and everything built with them reads `stdenv.cc.libc`. + +Setting a sysroot as well is unusual for nixpkgs, since headers normally reach a compiler through the wrapper, as they do here. +It is needed because `gcc/configure` takes `target_header_dir` from `--with-sysroot`, and `target_header_dir` is what decides `inhibit_libc`. +A libgcc built with `inhibit_libc` still compiles, links and installs, just with pieces silently missing. + +Given that, the sysroot is derived from `stdenv.cc.libc` too, so it cannot disagree with the headers. + +## Threading + +Which threading model is available is a property of the libc, so the libc declares it as `passthru.threadModel`; `libgcc` reads it from there, and `libstdcxx` takes both the model and the generated `gthr-default.h` from `libgcc`. + +Reading it from the compiler instead, with `$CC -v | sed -n 's/^Thread model: //p'`, reports the wrong component: in this set the compiler is configured separately from libgcc, so the two can disagree. +A platform whose libc declares nothing gets `single`. + +## Relationship to the monolithic set + +Both are packaged from the same sources and, for now, the same version: `gccNGPackages` tracks `default-gcc-version` with no fallback, so bumping the monolithic default past what is packaged here is an evaluation error rather than a silent version skew. + +Nothing selects GGN NG yet by default. +The plan is for very exotic package sets to switch to this first. +The main tier-1 native Linux package sets cached on `cache.nixos.org` will come later. diff --git a/pkgs/development/compilers/gcc/ng/common/default.nix b/pkgs/development/compilers/gcc/ng/common/default.nix index 84c31c3bf9d6..9929fee1e655 100644 --- a/pkgs/development/compilers/gcc/ng/common/default.nix +++ b/pkgs/development/compilers/gcc/ng/common/default.nix @@ -173,12 +173,11 @@ makeScopeWithSplicing' { ]; }; - # `binutilsNoLibc` carries `preLibcHeaders` as its `libc` — the - # header-only stand-in for platforms that have one, and nothing at all - # for platforms that do not. `wrapCCWith` defaults `libc` to - # `bintools.libc`, so this wrapper carries it too, and that is the only - # place the pre-libc stage is written down: everything built with this - # compiler reads `stdenv.cc.libc` and needs no flag of its own. + # Stage 1 of the bootstrap chain; see ../README.md. + # + # No `libc` is passed: `wrapCCWith` defaults it to `bintools.libc`, and + # `binutilsNoLibc` carries `preLibcHeaders`. That is the only place the + # pre-libc stage is written down. gccNoLibgcc = wrapCCWith { cc = gccPackages.gcc-unwrapped; libcxx = null; @@ -189,13 +188,9 @@ makeScopeWithSplicing' { ]; }; - # Built before there is a libc, and good for nothing but getting one - # built: single-threaded, and compiled against `preLibcHeaders` at best. - # Never hand this to users. - # - # Nothing is passed here to say which stage this is; it follows from the - # compiler, exactly as the LLVM set distinguishes `compiler-rt-no-libc` - # from `compiler-rt-libc`. + # Built before there is a libc, and not intended for use beyond getting + # one built. Note the two differ only by `stdenv`: which stage this is + # follows from the compiler, never from an argument. libgcc-no-libc = callPackage ./libgcc { stdenv = overrideCC stdenv buildGccPackages.gccNoLibgcc; }; @@ -209,15 +204,9 @@ makeScopeWithSplicing' { libgcc = if stdenv.hostPlatform.libc == null then gccPackages.libgcc-no-libc else gccPackages.libgcc-libc; - # The bootstrap step between `gccNoLibgcc` and `gccWithLibc`: libgcc is - # available, libc is not yet. Compiling a libc needs exactly this — libc's - # own sources call into libgcc (integer/floating-point helpers, stack - # unwinding), so `gccNoLibgcc` is not enough, while `gccWithLibc` cannot - # be used to build the very libc it depends on. - # - # `binutilsNoLibc` is what keeps libc out: it supplies the header-only - # `preLibcHeaders` instead of a real libc, so nothing here refers to a - # libc derivation and the cycle stays broken. + # Stage 2: libgcc available, libc not yet — what compiling a libc needs. + # `binutilsNoLibc` is what keeps the libc out, so nothing here refers to + # a libc derivation and the cycle stays broken. gccWithLibgcc = wrapCCWith { cc = gccPackages.gcc-unwrapped; libcxx = null; @@ -230,10 +219,8 @@ makeScopeWithSplicing' { ]; }; - # The rung after `gccWithLibgcc`: the libc it was used to build now - # exists, so this has a real libc, but still only the bootstrap libgcc — - # the finished one is what it is about to build. That is `libgcc-libc`, - # which unlike its predecessor can use the libc's threads. + # Stage 3: real libc, bootstrap libgcc still. The finished libgcc is what + # this is about to build. gccWithLibcAndBasicLibgcc = wrapCCWith { cc = gccPackages.gcc-unwrapped; libcxx = null;