Files
nixpkgs/pkgs/development/compilers/gcc/ng/common/libgcc/default.nix
John Ericson 6b6fa6b5ce gccNGPackages.libgcc: keep the ELF-only bootstrap trick off PE/COFF
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)
2026-08-13 16:25:23 -04:00

417 lines
16 KiB
Nix

{
lib,
stdenv,
gcc_meta,
release_version,
version,
getVersionFile,
monorepoSrc ? null,
fetchpatch,
autoreconfHook269,
buildGccPackages,
buildPackages,
which,
python3,
# Build `libgcc_s` as well as `libgcc.a`. Only ever worth turning *off*: the
# assertion below refuses to force it on where the default says it cannot
# work, since what follows from that is a link failure much further away.
enableShared ? __defaultEnableShared,
# The following are arguments rather than a `let` bindings only so
# that it is in scope for the default definition above.
# Whether a shared libgcc can be built at all here, derived the way the
# monolithic build derives it rather than forced off.
#
# Shared needs something to link against. Normally that is the libc, on any
# format -- PE/COFF included -- which is why every stage after the bootstrap
# builds it. ELF additionally allows it *before* the libc exists, because a
# shared object may keep undefined symbols; `libgcc_s.so` comes out with an
# empty `DT_NEEDED`, which is why the monolithic build ships one even from
# its nolibc stage. A PE/COFF DLL must resolve everything at link time, so
# there the bootstrap yields only `libgcc.a` -- all it is used for anyway.
__defaultEnableShared ?
stdenv.hostPlatform.hasSharedLibraries && (__haveLinkableLibc || stdenv.hostPlatform.isElf),
# Whether the compiler has a libc that can actually be linked against, as
# opposed to nothing at all or a headers-only stand-in.
__haveLinkableLibc ? (stdenv.cc.libc or null) != null && !(stdenv.cc.libc.headersOnly or false),
}:
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 `<pthread.h>` 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
assert enableShared -> __defaultEnableShared;
stdenv.mkDerivation (finalAttrs: {
pname = "libgcc" + lib.optionalString (libc == null) "-no-libc";
inherit version;
src = monorepoSrc;
outputs = [
"out"
"dev"
];
strictDeps = true;
depsBuildBuild = [
buildPackages.stdenv.cc
buildGccPackages.libiberty
];
nativeBuildInputs = [
autoreconfHook269
which
python3
];
patches = [
(fetchpatch {
name = "delete-MACHMODE_H.patch";
url = "https://github.com/gcc-mirror/gcc/commit/493aae4b034d62054d5e7e54dc06cd9a8be54e29.diff";
hash = "sha256-oEk0lnI96RlpALWpb7J+GnrtgQsFVqDO57I/zjiqqTk=";
})
(fetchpatch {
name = "custom-threading-model.patch";
url = "https://github.com/gcc-mirror/gcc/commit/e5d853bbe9b05d6a00d98ad236f01937303e40c4.diff";
hash = "sha256-92LIttIXdh12/lRhivb2JTPpqUmGBRn+uKmR5pzuveo=";
includes = [
"config/*"
"libgcc/configure.ac"
];
})
(fetchpatch {
name = "no-pie-cflags.patch";
url = "https://github.com/gcc-mirror/gcc/commit/77144dd3b6736e0166156bb509590d924375a4f1.diff";
hash = "sha256-QlxlTkWAK1dB7JiU5wz2iOW24gj3bFaeBpwb90oWwns=";
includes = [
"gcc/Makefile.in"
"gcc/configure.ac"
"libgcc/Makefile.in"
"libgcc/configure.ac"
];
})
(fetchpatch {
name = "no-target-system-root.patch";
url = "https://github.com/gcc-mirror/gcc/commit/9947930b7ae923010c5061fd8fa6b1ec4f22f161.diff";
hash = "sha256-BZmpHpJuuyDmQMwpQhSgCZO0Rg7kXt8rTiJAT+e0sUw=";
})
(fetchpatch {
name = "regular-libdir-includedir.patch";
url = "https://inbox.sourceware.org/gcc-patches/20250717174911.1536129-1-git@JohnEricson.me/raw";
hash = "sha256-Cn7rvg1FI7H/26GzSe4pv5VW/gvwbwGqivAqEeawkwk=";
})
(getVersionFile "libgcc/force-regular-dirs.patch")
];
autoreconfFlags = "--install --force --verbose . libgcc";
postUnpack = ''
mkdir -p ./build
buildRoot=$(readlink -e "./build")
'';
postPatch =
# Both halves of this are ELF-specific, so it is applied only there.
# `crti.o`/`crtn.o` are an ELF convention; forcing the rule on a PE/COFF
# target makes the build assemble the generic ELF `crti.S` with a PE
# assembler, which fails outright. And on those targets `SHLIB_LC` is not
# `-lc` but the list of system import libraries the DLL needs, so blanking
# it would leave the shared libgcc missing its real dependencies.
#
# Trick to build a gcc that is capable of emitting shared libraries *without* having the
# hostPlatform libc available beforehand. Taken from:
# https://web.archive.org/web/20170222224855/http://frank.harvard.edu/~coldwell/toolchain/
# https://web.archive.org/web/20170224235700/http://frank.harvard.edu/~coldwell/toolchain/t-linux.diff
lib.optionalString (enableShared && stdenv.hostPlatform.isElf) (
let
# crt{i,n}.o are the first and last (respectively) object file
# linked when producing an executable. Traditionally these
# files are delivered as part of the C library, but on GNU
# systems they are in fact built by GCC. Since libgcc needs to
# build before glibc, we can't wait for them to be copied by
# glibc. At this early pre-glibc stage these files sometimes
# have different names.
crtstuff-ofiles =
if stdenv.hostPlatform.isPower64 then "ecrti.o ecrtn.o ncrti.o ncrtn.o" else "crti.o crtn.o";
# Normally, `SHLIB_LC` is set to `-lc`, which means that
# `libgcc_s.so` cannot be built until `libc.so` is available.
# The assignment below clobbers this variable, removing the
# `-lc`.
#
# On PowerPC we add `-mnewlib`, which means "libc has not been
# built yet". This causes libgcc's Makefile to use the
# gcc-built `{e,n}crt{n,i}.o` instead of failing to find the
# versions which have been repackaged in libc as `crt{n,i}.o`
#
SHLIB_LC = lib.optionalString stdenv.hostPlatform.isPower64 "-mnewlib";
in
''
echo 'libgcc.a: ${crtstuff-ofiles}' >> libgcc/Makefile.in
echo 'SHLIB_LC=${SHLIB_LC}' >> libgcc/Makefile.in
''
# Meanwhile, crt{i,n}.S are not present on certain platforms
# (e.g. LoongArch64), resulting in the following error:
#
# No rule to make target '../../../gcc-xx.x.x/libgcc/config/loongarch/crti.S', needed by 'crti.o'. Stop.
#
# For LoongArch64 and S390, a hacky workaround is to simply touch them,
# as the platform forces .init_array support.
#
# https://www.openwall.com/lists/musl/2022/11/09/3
#
# 'parsed.cpu.family' won't be correct for every platform.
+ (lib.optionalString
(stdenv.hostPlatform.isLoongArch64 || stdenv.hostPlatform.isS390 || stdenv.hostPlatform.isAlpha)
''
touch libgcc/config/${stdenv.hostPlatform.parsed.cpu.family}/crt{i,n}.S
''
)
+ lib.optionalString (stdenv.hostPlatform.isPower && !stdenv.hostPlatform.isPower64) ''
touch libgcc/config/rs6000/crt{i,n}.S
''
)
# 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 `<limits.h>`
# 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")
'';
enableParallelBuilding = true;
preConfigure = ''
cd "$buildRoot"
mkdir -p build-${stdenv.buildPlatform.config}/libiberty/
cd build-${stdenv.buildPlatform.config}/libiberty/
ln -s ${buildGccPackages.libiberty}/lib/libiberty.a ./
mkdir -p "$buildRoot/gcc"
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)"}
export CXX_FOR_BUILD=${lib.getExe' buildPackages.stdenv.cc "$(basename $CXX_FOR_BUILD)"}
export LD_FOR_BUILD=${lib.getExe' buildPackages.stdenv.cc.bintools "$(basename $LD_FOR_BUILD)"}
export AS=$AS_FOR_BUILD
export CC=$CC_FOR_BUILD
export CPP=$CPP_FOR_BUILD
export CXX=$CXX_FOR_BUILD
export LD=$LD_FOR_BUILD
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'
"$sourceRoot/../gcc/configure" $topLevelConfigureFlags
sed -e 's,libgcc.mvars:.*$,libgcc.mvars:,' -i Makefile
make \
config.h \
libgcc.mvars \
tconfig.h \
tm.h \
options.h \
insn-constants.h \
version.h
)
mkdir -p "$buildRoot/gcc/include"
mkdir -p "$buildRoot/gcc/${stdenv.hostPlatform.config}/libgcc"
cd "$buildRoot/gcc/${stdenv.hostPlatform.config}/libgcc"
configureScript=$sourceRoot/configure
chmod +x "$configureScript"
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)"}
export CXX_FOR_BUILD=${lib.getExe' buildPackages.stdenv.cc "$(basename $CXX_FOR_BUILD)"}
export LD_FOR_BUILD=${lib.getExe' buildPackages.stdenv.cc.bintools "$(basename $LD_FOR_BUILD)"}
export AS=${lib.getExe' stdenv.cc "$(basename $AS)"}
export CC=${lib.getExe' stdenv.cc "$(basename $CC)"}
export CPP=${lib.getExe' stdenv.cc "$(basename $CPP)"}
export CXX=${lib.getExe' stdenv.cc "$(basename $CXX)"}
export LD=${lib.getExe' stdenv.cc.bintools "$(basename $LD)"}
export AS_FOR_TARGET=${lib.getExe' stdenv.cc "$(basename $AS_FOR_TARGET)"}
export CC_FOR_TARGET=${lib.getExe' stdenv.cc "$(basename $CC_FOR_TARGET)"}
export CPP_FOR_TARGET=${lib.getExe' stdenv.cc "$(basename $CPP_FOR_TARGET)"}
export LD_FOR_TARGET=${lib.getExe' stdenv.cc.bintools "$(basename $LD_FOR_TARGET)"}
''
+ lib.optionalString stdenv.hostPlatform.isMusl ''
NIX_CFLAGS_COMPILE_OLD=$NIX_CFLAGS_COMPILE
NIX_CFLAGS_COMPILE+=' -isystem ${stdenv.cc.cc}/lib/gcc/${stdenv.hostPlatform.config}/${version}/include-fixed'
'';
topLevelConfigureFlags = [
"--build=${stdenv.buildPlatform.config}"
"--host=${stdenv.buildPlatform.config}"
"--target=${stdenv.hostPlatform.config}"
"--disable-bootstrap"
"--disable-multilib"
"--enable-languages=c"
"--disable-fixincludes"
"--disable-intl"
"--disable-lto"
"--disable-libatomic"
"--disable-libbacktrace"
"--disable-libcpp"
"--disable-libssp"
"--disable-libquadmath"
"--disable-libgomp"
"--disable-libvtv"
"--disable-vtable-verify"
"--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 <unistd.h> 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
"--with-multilib-list="
++
lib.optional (stdenv.hostPlatform.libc == "glibc")
# Cheat and use previous stage's glibc to avoid infinite recursion. As
# of GCC 11, libgcc only cares if the version is greater than 2.19,
# which is quite ancient, so this little lie should be fine.
"--with-glibc-version=${buildPackages.glibc.version}";
configurePlatforms = [
"build"
"host"
];
configureFlags = [
"--disable-dependency-tracking"
"gcc_cv_target_thread_file=${threadModel}"
# $CC cannot link binaries, let alone run then
"cross_compiling=true"
"--enable-static"
(lib.enableFeature enableShared "shared")
];
# Set the variable back the way it was, see corresponding code in
# `preConfigure`.
postConfigure = lib.optionalString stdenv.hostPlatform.isMusl ''
NIX_CFLAGS_COMPILE=$NIX_CFLAGS_COMPILE_OLD
'';
makeFlags = [ "MULTIBUILDTOP:=../" ];
postInstall = ''
install -c -m 644 gthr-default.h "$dev/include"
'';
doCheck = true;
passthru = {
isGNU = true;
inherit threadModel;
};
meta = gcc_meta // {
homepage = "https://gcc.gnu.org/";
};
})