From 3986de071f59036ab98feb7d6f029c59da64e46b Mon Sep 17 00:00:00 2001 From: tannevaled Date: Fri, 29 May 2026 20:03:40 +0200 Subject: [PATCH 1/7] fix(gnu.org/gcc): add libc-wrapper for end-user pkgx gcc (#8423) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit End-user `pkgx gcc test.c` on a host without distro glibc-devel / libc6-dev couldn't find stdlib.h + crt*.o. Earlier attempts via runtime.env (#13084) and --with-sysroot (#13092) failed because both poisoned gcc's own bootstrap build. This take installs a thin POSIX-sh wrapper at {{prefix}}/bin/ (modeled on the bklibcvenv pattern, brewkit#348) that: 1. resolves the sibling gnu.org/glibc bottle relative to its own install path 2. exec's the real binary (moved to libexec/gcc-wrap/) with -isystem $glibc/include + -L$glibc/lib appended 3. NO-OPs when CPATH is already set (brewkit build-context — the bootstrap gcc loop uses brewkit-composed CPATH, so injecting pkgx-glibc paths twice is unnecessary and could trigger C23 symbol mismatches like __isoc23_strtoul) Pinning gnu.org/glibc <2.38 avoids C23 symbol redirects absent on older CI/host runners; the wrapper's runtime CPATH check guards against build-time poisoning regardless. Wrappers cover gcc, g++, cpp, c++, gfortran. The existing cc -> gcc and gc++ -> c++ symlinks still resolve (now via the wrapper). The wrapper dispatches by $0 basename. Pairs with #13083 (multi-arch triplet symlinks). Together they should fully close #8423. Co-Authored-By: Claude Opus 4.7 --- projects/gnu.org/gcc/package.yml | 56 ++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/projects/gnu.org/gcc/package.yml b/projects/gnu.org/gcc/package.yml index 16c20d91e7..b7a44bee77 100644 --- a/projects/gnu.org/gcc/package.yml +++ b/projects/gnu.org/gcc/package.yml @@ -26,6 +26,16 @@ dependencies: gnu.org/mpfr: ">=2.4.0" gnu.org/mpc: ">=0.8.0" zlib.net: ^1.3 + linux: + # Bottle the libc the compiler can target so end-user invocations + # find stdlib.h + crt*.o + libc.so.6 without distro glibc-devel. + # See #8423. <2.38 avoids C23 symbol redirects that the host's + # older glibc lacks. The thin wrapper in build.script injects + # -isystem/-L at user-invocation time (NOT at gcc's own build), + # so the bootstrap loop is unaffected. The wrapper no-ops when + # CPATH is already set (brewkit build context). + gnu.org/glibc: "<2.38" + kernel.org/linux-headers: "*" darwin/x86-64: # since 15.1.0 libisl.sourceforge.io: ^0 @@ -190,6 +200,52 @@ build: if: linux working-directory: ${{prefix}}/bin + # Linux libc-wrapper: install a thin shim at bin/ that + # injects -isystem $glibc/include + -L$glibc/lib so end-user + # `pkgx gcc test.c` finds stdlib.h + crt*.o + libc.so.6 from the + # pkgx-integrated glibc bottle — without needing distro glibc-devel. + # The wrapper is INERT when CPATH is already populated (brewkit build + # context), so the gcc bootstrap loop is unaffected. See #8423. + - run: | + mkdir -p {{prefix}}/libexec/gcc-wrap + cat > {{prefix}}/libexec/gcc-wrap.sh <<'WRAPPER' + #!/bin/sh + # pkgx libc-wrapper around the real compiler. Resolves the sibling + # gnu.org/glibc bottle relative to its own install path and adds + # -isystem/-L only when the caller hasn't already set up CPATH + # (i.e. only for naked end-user invocations). + self_name=$(basename "$0") + # Strip any Debian multi-arch triplet prefix so the triplet-named + # symlinks from #13083 (x86_64-linux-gnu-gcc -> gcc -> this wrapper) + # resolve to the real compiler moved into libexec/gcc-wrap/. + base=${self_name##*-linux-gnu-} + case "$base" in + cc) target=gcc ;; + gc++) target=c++ ;; + *) target="$base" ;; + esac + bindir=$(cd "$(dirname "$0")" && pwd) + real="$bindir/../libexec/gcc-wrap/$target" + if [ -z "$CPATH" ]; then + for d in "$bindir/../../../glibc/v"*; do + [ -d "$d/include" ] && libc="$d" && break + done + fi + if [ -n "$libc" ]; then + exec "$real" -isystem "$libc/include" -L"$libc/lib" "$@" + else + exec "$real" "$@" + fi + WRAPPER + chmod +x {{prefix}}/libexec/gcc-wrap.sh + cd {{prefix}}/bin + for tool in gcc g++ cpp c++ gfortran; do + [ -f "$tool" ] && [ ! -L "$tool" ] || continue + mv "$tool" ../libexec/gcc-wrap/ + ln -sf ../libexec/gcc-wrap.sh "$tool" + done + if: linux + env: # Branch from the Darwin maintainer of GCC, with a few generic fixes and # Apple Silicon support, located at https://github.com/iains/gcc-12-branch From 6a023fcbcabb510a4c827b1eab7ebe8d3bf671fd Mon Sep 17 00:00:00 2001 From: tannevaled Date: Fri, 29 May 2026 20:11:50 +0200 Subject: [PATCH 2/7] fix(gnu.org/gcc): drop runtime glibc dep, keep opportunistic wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The runtime dep on gnu.org/glibc poisoned gcc's own bootstrap build: brewkit exposes runtime deps via CPATH/LIBRARY_PATH during build too, so configure-time test programs end up compiling against pkgx libc 2.34 but running on the host's ld-linux — and ld-linux finds host libc 2.35 in the default path even with LD_LIBRARY_PATH set, producing `cannot run C++ compiled programs`. The wrapper itself doesn't need the dep declared — it does a runtime sibling lookup (`bindir/../../../glibc/v*`) that no-ops if no pkgx glibc is present. Users who want the bottled libc explicitly add it: pkgx +gnu.org/gcc +gnu.org/glibc gcc test.c This trades automatic resolution for a clean bootstrap. Closing this trade-off the "right" way needs a pkgx feature to separate runtime-only deps from build-time-injected deps. Co-Authored-By: Claude Opus 4.7 --- projects/gnu.org/gcc/package.yml | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/projects/gnu.org/gcc/package.yml b/projects/gnu.org/gcc/package.yml index b7a44bee77..f9e5ce898d 100644 --- a/projects/gnu.org/gcc/package.yml +++ b/projects/gnu.org/gcc/package.yml @@ -26,16 +26,6 @@ dependencies: gnu.org/mpfr: ">=2.4.0" gnu.org/mpc: ">=0.8.0" zlib.net: ^1.3 - linux: - # Bottle the libc the compiler can target so end-user invocations - # find stdlib.h + crt*.o + libc.so.6 without distro glibc-devel. - # See #8423. <2.38 avoids C23 symbol redirects that the host's - # older glibc lacks. The thin wrapper in build.script injects - # -isystem/-L at user-invocation time (NOT at gcc's own build), - # so the bootstrap loop is unaffected. The wrapper no-ops when - # CPATH is already set (brewkit build context). - gnu.org/glibc: "<2.38" - kernel.org/linux-headers: "*" darwin/x86-64: # since 15.1.0 libisl.sourceforge.io: ^0 @@ -202,10 +192,18 @@ build: # Linux libc-wrapper: install a thin shim at bin/ that # injects -isystem $glibc/include + -L$glibc/lib so end-user - # `pkgx gcc test.c` finds stdlib.h + crt*.o + libc.so.6 from the - # pkgx-integrated glibc bottle — without needing distro glibc-devel. - # The wrapper is INERT when CPATH is already populated (brewkit build - # context), so the gcc bootstrap loop is unaffected. See #8423. + # `pkgx +gnu.org/gcc +gnu.org/glibc gcc test.c` finds stdlib.h + + # crt*.o + libc.so.6 from the pkgx-integrated glibc bottle — + # without needing distro glibc-devel. See #8423. + # + # The wrapper is OPPORTUNISTIC: it looks for a sibling pkgx glibc + # bottle and injects only if found AND CPATH is unset. If neither + # condition holds, gcc falls back to the host libc (current + # behaviour). We intentionally do NOT declare gnu.org/glibc as a + # runtime dep here — brewkit would then expose it during gcc's own + # build via CPATH/LIBRARY_PATH, poisoning the bootstrap (test + # programs end up linking pkgx libc but running on host's ld-linux). + # Users opt in explicitly with `pkgx +gnu.org/glibc`. - run: | mkdir -p {{prefix}}/libexec/gcc-wrap cat > {{prefix}}/libexec/gcc-wrap.sh <<'WRAPPER' From e5ace91efc186d4cb828f98857920b856bff8f97 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Fri, 29 May 2026 20:13:55 +0200 Subject: [PATCH 3/7] fix(gnu.org/gcc): wrapper also embeds pkgx ld-linux as PT_INTERP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the wrapper detects a sibling pkgx glibc bottle, it now also passes: -Wl,--dynamic-linker=$glibc/lib/ld-linux-*.so.* -Wl,-rpath,$glibc/lib so the produced binary's ELF interpreter (PT_INTERP) points at pkgx ld-linux. End-to-end consistency: the same libc is used at compile, link, and exec. Without this, the binary's PT_INTERP would be the host's /lib64/ld-linux-x86-64.so.2 and the kernel would load host ld-linux at exec time — even with -L pointing at pkgx libc and rpath set — mixing libc internal layouts across host/pkgx. Co-Authored-By: Claude Opus 4.7 --- projects/gnu.org/gcc/package.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/projects/gnu.org/gcc/package.yml b/projects/gnu.org/gcc/package.yml index f9e5ce898d..9170089a8f 100644 --- a/projects/gnu.org/gcc/package.yml +++ b/projects/gnu.org/gcc/package.yml @@ -230,7 +230,19 @@ build: done fi if [ -n "$libc" ]; then - exec "$real" -isystem "$libc/include" -L"$libc/lib" "$@" + # Pin the pkgx ld-linux as the binary's ELF interpreter and + # rpath the libc dir, so the produced binary is consistent + # end-to-end (compile / link / load all against pkgx glibc). + # Without this, the binary would carry the host's interp path + # in PT_INTERP and the kernel would load host ld-linux at + # exec time, mixing libc versions. + ldso=$(ls "$libc"/lib/ld-linux*.so.* 2>/dev/null | head -n1) + exec "$real" \ + -isystem "$libc/include" \ + -L"$libc/lib" \ + ${ldso:+-Wl,--dynamic-linker="$ldso"} \ + -Wl,-rpath,"$libc/lib" \ + "$@" else exec "$real" "$@" fi From c8cacf487b340ecce7fbf0a444e4efb2c9fc9cfe Mon Sep 17 00:00:00 2001 From: tannevaled Date: Fri, 29 May 2026 20:23:03 +0200 Subject: [PATCH 4/7] fix(gnu.org/gcc): wrapper drops host include path (Nix cc-wrapper style) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a pkgx glibc sibling is found, wrapper now also passes -nostdinc (and -nostdinc++ for g++/c++) to drop /usr/include from gcc's search path, then re-adds: - gcc's own builtin headers (lib/gcc///include[+-fixed]) - libstdc++ headers for C++ tools (include/c++/[/]) - pkgx glibc headers via -isystem Without -nostdinc, gcc still found /usr/include/stdlib.h first because -isystem only appends to the search chain — so pkgx-glibc headers were shadowed by whatever the host had. Modeled on Nix's cc-wrapper.sh (pkgs/build-support/cc-wrapper/). See #8423. The wrapper stays opportunistic (CPATH unset + sibling glibc), so the gcc bootstrap loop is unaffected. Co-Authored-By: Claude Opus 4.7 --- projects/gnu.org/gcc/package.yml | 64 ++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/projects/gnu.org/gcc/package.yml b/projects/gnu.org/gcc/package.yml index 9170089a8f..122256ecf2 100644 --- a/projects/gnu.org/gcc/package.yml +++ b/projects/gnu.org/gcc/package.yml @@ -208,10 +208,18 @@ build: mkdir -p {{prefix}}/libexec/gcc-wrap cat > {{prefix}}/libexec/gcc-wrap.sh <<'WRAPPER' #!/bin/sh - # pkgx libc-wrapper around the real compiler. Resolves the sibling - # gnu.org/glibc bottle relative to its own install path and adds - # -isystem/-L only when the caller hasn't already set up CPATH - # (i.e. only for naked end-user invocations). + # pkgx libc-wrapper modeled on Nix's cc-wrapper. Activates only + # when a sibling pkgx glibc bottle is present and CPATH is unset + # (i.e. naked end-user invocations like `pkgx +gnu.org/gcc + # +gnu.org/glibc gcc test.c`). Otherwise no-ops. + # + # When active: + # -nostdinc drop host /usr/include from search path + # -isystem $gcc/... re-add gcc's own builtin headers + # -isystem $glibc/ add pkgx glibc headers (replaces host) + # -L $glibc/lib link against pkgx libc + crt*.o + # --dynamic-linker bake pkgx ld-linux into PT_INTERP + # -rpath $glibc/lib resolve dyn libs without LD_LIBRARY_PATH self_name=$(basename "$0") # Strip any Debian multi-arch triplet prefix so the triplet-named # symlinks from #13083 (x86_64-linux-gnu-gcc -> gcc -> this wrapper) @@ -223,29 +231,45 @@ build: *) target="$base" ;; esac bindir=$(cd "$(dirname "$0")" && pwd) - real="$bindir/../libexec/gcc-wrap/$target" + gcc_root="$bindir/.." + real="$gcc_root/libexec/gcc-wrap/$target" if [ -z "$CPATH" ]; then for d in "$bindir/../../../glibc/v"*; do [ -d "$d/include" ] && libc="$d" && break done fi - if [ -n "$libc" ]; then - # Pin the pkgx ld-linux as the binary's ELF interpreter and - # rpath the libc dir, so the produced binary is consistent - # end-to-end (compile / link / load all against pkgx glibc). - # Without this, the binary would carry the host's interp path - # in PT_INTERP and the kernel would load host ld-linux at - # exec time, mixing libc versions. - ldso=$(ls "$libc"/lib/ld-linux*.so.* 2>/dev/null | head -n1) - exec "$real" \ - -isystem "$libc/include" \ - -L"$libc/lib" \ - ${ldso:+-Wl,--dynamic-linker="$ldso"} \ - -Wl,-rpath,"$libc/lib" \ - "$@" - else + if [ -z "$libc" ]; then exec "$real" "$@" fi + # Find gcc's own builtin headers (triplet + version glob) + for inc in "$gcc_root"/lib/gcc/*/*/include; do + [ -d "$inc" ] && gcc_inc="$inc" && gcc_inc_fixed="${inc}-fixed" && break + done + # For C++ tools, also re-add libstdc++ headers via -isystem + case "$target" in + g++|c++) + cxx_flag=-nostdinc++ + for cxx_inc in "$gcc_root"/include/c++/*; do + [ -d "$cxx_inc" ] && gcc_cxx_inc="$cxx_inc" && break + done + for cxx_arch_inc in "$gcc_cxx_inc"/*-linux-gnu; do + [ -d "$cxx_arch_inc" ] && gcc_cxx_arch_inc="$cxx_arch_inc" && break + done + ;; + *) cxx_flag= ;; + esac + ldso=$(ls "$libc"/lib/ld-linux*.so.* 2>/dev/null | head -n1) + exec "$real" \ + -nostdinc $cxx_flag \ + ${gcc_inc:+-isystem "$gcc_inc"} \ + ${gcc_inc_fixed:+-isystem "$gcc_inc_fixed"} \ + ${gcc_cxx_inc:+-isystem "$gcc_cxx_inc"} \ + ${gcc_cxx_arch_inc:+-isystem "$gcc_cxx_arch_inc"} \ + -isystem "$libc/include" \ + -L"$libc/lib" \ + ${ldso:+-Wl,--dynamic-linker="$ldso"} \ + -Wl,-rpath,"$libc/lib" \ + "$@" WRAPPER chmod +x {{prefix}}/libexec/gcc-wrap.sh cd {{prefix}}/bin From 763095d108a542f78293182306a2d9ada3652481 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Fri, 29 May 2026 21:20:58 +0200 Subject: [PATCH 5/7] fix(gcc-wrapper): keep real binary in bin/ so cc1 lookup resolves The previous shape moved the real gcc binary to libexec/gcc-wrap/, which broke gcc's relative lookup for cc1 / cc1plus / collect2 (gcc expects them at lib/gcc/// relative to its OWN location). Result: `cannot execute 'cc1': No such file or directory` during fixincludes at make-install time. Keep the real binary at bin/.${tool}-real (hidden by leading dot to avoid cluttering bin/) and have bin/ point at the wrapper. cc1 lookup then resolves the same way it always did. Co-Authored-By: Claude Opus 4.7 --- projects/gnu.org/gcc/package.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/projects/gnu.org/gcc/package.yml b/projects/gnu.org/gcc/package.yml index 122256ecf2..6f5b79a845 100644 --- a/projects/gnu.org/gcc/package.yml +++ b/projects/gnu.org/gcc/package.yml @@ -232,7 +232,11 @@ build: esac bindir=$(cd "$(dirname "$0")" && pwd) gcc_root="$bindir/.." - real="$gcc_root/libexec/gcc-wrap/$target" + # IMPORTANT: keep the real binary in bin/ (as .${target}-real) so + # gcc's internal lookup for cc1 / cc1plus / collect2 (all relative + # to its own dir at lib/gcc///) still resolves. + # Moving it under libexec/ broke that lookup (#13094 v1). + real="$bindir/.${target}-real" if [ -z "$CPATH" ]; then for d in "$bindir/../../../glibc/v"*; do [ -d "$d/include" ] && libc="$d" && break @@ -275,7 +279,7 @@ build: cd {{prefix}}/bin for tool in gcc g++ cpp c++ gfortran; do [ -f "$tool" ] && [ ! -L "$tool" ] || continue - mv "$tool" ../libexec/gcc-wrap/ + mv "$tool" ".${tool}-real" ln -sf ../libexec/gcc-wrap.sh "$tool" done if: linux From 67c16fb11a716591501961fba3f68cb7396707d8 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Mon, 27 Jul 2026 16:30:57 +0200 Subject: [PATCH 6/7] fix(gnu.org/gcc): $0-robust per-tool libc-wrappers + a real test Addresses the review (jhheider): the single shared wrapper muxed on `basename "$0"`, the same argv[0]/self-location fragility that sank the explicit-wrapper approach in brewkit. Rework: - Generate ONE wrapper per tool (gcc/g++/cpp/c++/gfortran) with the target + c++-ness baked in, so it never infers WHICH compiler it is from $0. Locate self by canonicalizing $0 through symlinks (`readlink -f`, the sh analogue of /proc/self/exe). External-symlink and triplet-symlink invocations now resolve correctly. - Fix a latent bug: the pkgx glibc bottle keeps crt*.o / libc.so.6 / ld-linux* in a VERSIONED sub-libdir (lib/glibc-X.Y), not lib/. The old wrapper's `-L$libc/lib` + `ls $libc/lib/ld-linux*` silently found nothing. Locate the sub-libdir via libc.so.6; add `-B $libcdir` (crt) and `-Wl,-rpath,$gcc_root/lib` (so C++/Fortran outputs find libstdc++/libgcc_s). - Add a linux test (jhheider: "ensure we test that we've done what we expected"): with a sibling glibc bottle, gcc/g++ compile+link+RUN and the produced binary's PT_INTERP is asserted to be the bottle's ld.so; plus a negative check that CPATH-set no-ops back to host libc. Validated end-to-end on x86_64 (real hardware): C, C++ (), gfortran, triplet-symlink and external-symlink invocations all produce pkgx-glibc binaries (PT_INTERP under the glibc prefix) that run; CPATH-set and no-glibc-sibling both correctly fall back to the host loader. Co-Authored-By: Claude Opus 4.8 (1M context) --- projects/gnu.org/gcc/package.yml | 227 ++++++++++++++++++++----------- 1 file changed, 150 insertions(+), 77 deletions(-) diff --git a/projects/gnu.org/gcc/package.yml b/projects/gnu.org/gcc/package.yml index 6f5b79a845..65ccedac07 100644 --- a/projects/gnu.org/gcc/package.yml +++ b/projects/gnu.org/gcc/package.yml @@ -206,82 +206,112 @@ build: # Users opt in explicitly with `pkgx +gnu.org/glibc`. - run: | mkdir -p {{prefix}}/libexec/gcc-wrap - cat > {{prefix}}/libexec/gcc-wrap.sh <<'WRAPPER' + cat > {{prefix}}/libexec/gcc-wrap/.template <<'WRAPPER' #!/bin/sh - # pkgx libc-wrapper modeled on Nix's cc-wrapper. Activates only - # when a sibling pkgx glibc bottle is present and CPATH is unset - # (i.e. naked end-user invocations like `pkgx +gnu.org/gcc - # +gnu.org/glibc gcc test.c`). Otherwise no-ops. - # - # When active: - # -nostdinc drop host /usr/include from search path - # -isystem $gcc/... re-add gcc's own builtin headers - # -isystem $glibc/ add pkgx glibc headers (replaces host) - # -L $glibc/lib link against pkgx libc + crt*.o - # --dynamic-linker bake pkgx ld-linux into PT_INTERP - # -rpath $glibc/lib resolve dyn libs without LD_LIBRARY_PATH - self_name=$(basename "$0") - # Strip any Debian multi-arch triplet prefix so the triplet-named - # symlinks from #13083 (x86_64-linux-gnu-gcc -> gcc -> this wrapper) - # resolve to the real compiler moved into libexec/gcc-wrap/. - base=${self_name##*-linux-gnu-} - case "$base" in - cc) target=gcc ;; - gc++) target=c++ ;; - *) target="$base" ;; + # pkgx GCC libc-wrapper (generated per-tool — no $0 basename muxing). + # @TARGET@ / @CXX@ are baked in at install time so this shim never has + # to infer WHICH compiler it stands in for from $0 (jhheider, #13094). + target=@TARGET@ + cxx=@CXX@ + + # Locate ourselves by CANONICALIZING $0 through every symlink — the + # sh-script analogue of /proc/self/exe (which for a #!/bin/sh script + # would be the interpreter, /bin/sh, not us). bin/, the triplet + # symlinks, and even an external symlink into us all resolve back to the + # one real wrapper at /libexec/gcc-wrap/. WHICH compiler we + # are is $target, so $0 is used only to find our location, never the tool. + case "$0" in + */*) invoked=$0 ;; + *) invoked=$(command -v -- "$0" 2>/dev/null || echo "$0") ;; esac - bindir=$(cd "$(dirname "$0")" && pwd) - gcc_root="$bindir/.." - # IMPORTANT: keep the real binary in bin/ (as .${target}-real) so - # gcc's internal lookup for cc1 / cc1plus / collect2 (all relative - # to its own dir at lib/gcc///) still resolves. - # Moving it under libexec/ broke that lookup (#13094 v1). + self=$(readlink -f -- "$invoked" 2>/dev/null) || self= + [ -n "$self" ] || self=$invoked + selfdir=${self%/*} + if [ -d "$selfdir/../../bin" ]; then + bindir=$(CDPATH= cd -- "$selfdir/../../bin" && pwd) + else + bindir=$(CDPATH= cd -- "$selfdir" && pwd) + fi + gcc_root=$(CDPATH= cd -- "$bindir/.." && pwd) + + # Real driver stays in bin/ as .-real so gcc's own relative lookup + # of cc1/cc1plus/collect2 (under libexec/gcc///) keeps working. real="$bindir/.${target}-real" + [ -x "$real" ] || real="$bindir/$target" + + # Opt in only when a sibling pkgx glibc bottle exists and CPATH is unset. + libc= if [ -z "$CPATH" ]; then - for d in "$bindir/../../../glibc/v"*; do - [ -d "$d/include" ] && libc="$d" && break + for cand in "$bindir/../../../glibc/v"*; do + [ -d "$cand/include" ] && libc=$(CDPATH= cd -- "$cand" && pwd) && break done fi - if [ -z "$libc" ]; then - exec "$real" "$@" - fi - # Find gcc's own builtin headers (triplet + version glob) + [ -n "$libc" ] || exec "$real" "$@" + + # glibc bottle keeps crt*.o / libc.so.6 / ld-linux*.so.* in a versioned + # sub-libdir (lib/glibc-X.Y), NOT directly in lib/. Find it via libc.so.6. + libcdir= + for c in "$libc"/lib/glibc-*/libc.so.6 "$libc"/lib/libc.so.6; do + [ -f "$c" ] && libcdir=${c%/libc.so.6} && break + done + [ -n "$libcdir" ] || exec "$real" "$@" + + ldso= + for l in "$libcdir"/ld-linux*.so.* "$libcdir"/ld.so; do + [ -e "$l" ] && ldso=$l && break + done + + # gcc's own builtin headers (dropped by -nostdinc, re-added here). + gcc_inc= gcc_inc_fixed= for inc in "$gcc_root"/lib/gcc/*/*/include; do - [ -d "$inc" ] && gcc_inc="$inc" && gcc_inc_fixed="${inc}-fixed" && break + [ -d "$inc" ] && gcc_inc=$inc && break done - # For C++ tools, also re-add libstdc++ headers via -isystem - case "$target" in - g++|c++) - cxx_flag=-nostdinc++ - for cxx_inc in "$gcc_root"/include/c++/*; do - [ -d "$cxx_inc" ] && gcc_cxx_inc="$cxx_inc" && break - done - for cxx_arch_inc in "$gcc_cxx_inc"/*-linux-gnu; do - [ -d "$cxx_arch_inc" ] && gcc_cxx_arch_inc="$cxx_arch_inc" && break + [ -n "$gcc_inc" ] && [ -d "${gcc_inc}-fixed" ] && gcc_inc_fixed=${gcc_inc}-fixed + + # libstdc++ headers (dropped by -nostdinc++, re-added for C++ drivers). + cxx_flag= cxx_inc= cxx_arch_inc= cxx_back_inc= + if [ "$cxx" = 1 ]; then + cxx_flag=-nostdinc++ + for ci in "$gcc_root"/include/c++/*; do + [ -d "$ci" ] && [ -f "$ci/vector" ] && cxx_inc=$ci && break + done + if [ -n "$cxx_inc" ]; then + for ai in "$cxx_inc"/*-linux-gnu; do + [ -d "$ai" ] && cxx_arch_inc=$ai && break done - ;; - *) cxx_flag= ;; - esac - ldso=$(ls "$libc"/lib/ld-linux*.so.* 2>/dev/null | head -n1) + [ -d "$cxx_inc/backward" ] && cxx_back_inc="$cxx_inc/backward" + fi + fi + exec "$real" \ -nostdinc $cxx_flag \ + ${cxx_inc:+-isystem "$cxx_inc"} \ + ${cxx_arch_inc:+-isystem "$cxx_arch_inc"} \ + ${cxx_back_inc:+-isystem "$cxx_back_inc"} \ ${gcc_inc:+-isystem "$gcc_inc"} \ ${gcc_inc_fixed:+-isystem "$gcc_inc_fixed"} \ - ${gcc_cxx_inc:+-isystem "$gcc_cxx_inc"} \ - ${gcc_cxx_arch_inc:+-isystem "$gcc_cxx_arch_inc"} \ -isystem "$libc/include" \ - -L"$libc/lib" \ + -B "$libcdir" \ + -L "$libcdir" \ ${ldso:+-Wl,--dynamic-linker="$ldso"} \ - -Wl,-rpath,"$libc/lib" \ + -Wl,-rpath,"$libcdir" \ + -Wl,-rpath,"$gcc_root/lib" \ "$@" WRAPPER - chmod +x {{prefix}}/libexec/gcc-wrap.sh + # Generate one wrapper per tool (target + c++-ness baked in), so the + # shim never muxes on $0. bin/ -> ../libexec/gcc-wrap/; + # real driver preserved as bin/.-real. cd {{prefix}}/bin - for tool in gcc g++ cpp c++ gfortran; do + for spec in gcc:0 g++:1 cpp:0 c++:1 gfortran:0; do + tool=${spec%:*}; cxx=${spec#*:} [ -f "$tool" ] && [ ! -L "$tool" ] || continue + sed -e "s/@TARGET@/$tool/g" -e "s/@CXX@/$cxx/g" \ + {{prefix}}/libexec/gcc-wrap/.template > {{prefix}}/libexec/gcc-wrap/$tool + chmod +x {{prefix}}/libexec/gcc-wrap/$tool mv "$tool" ".${tool}-real" - ln -sf ../libexec/gcc-wrap.sh "$tool" + ln -sf ../libexec/gcc-wrap/$tool "$tool" done + rm -f {{prefix}}/libexec/gcc-wrap/.template if: linux env: @@ -361,28 +391,71 @@ build: TRIPLET: aarch64-linux-gnu test: - - gcc --version | grep -q "pkgx GCC {{version}}" - - gcc -print-libgcc-file-name - - gcc -print-multiarch - # some gha mac runners have broken SDKs... - - run: - - if ! test -f /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/AvailabilityInternalLegacy.h || ! test -f ; then - - echo "Missing SDK; skipping remaining tests" - - exit 0 - - fi - if: darwin - - run: - - if test "{{hw.platform}}" = "darwin" && test ! -f /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/_bounds.h ; then - - echo "Missing SDK; skipping remaining tests" - - exit 0 - - fi - if: "^14.2 || >=15.2" - - gcc -o test1 test.c -lgmp - - ./test1 - - g++ -o test2 test.cc - - test "$(./test2)" = "Hello, world!" - - gfortran -o test3 test.f90 - - test "$(./test3)" = "Hello, world!" + # glibc is a linux-only test dep so the wrapper test below has a sibling + # pkgx glibc bottle to inject (the whole point of the feature). + dependencies: + linux: + gnu.org/glibc: '*' + script: + - gcc --version | grep -q "pkgx GCC {{version}}" + - gcc -print-libgcc-file-name + - gcc -print-multiarch + # some gha mac runners have broken SDKs... + - run: + - if ! test -f /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/AvailabilityInternalLegacy.h || ! test -f ; then + - echo "Missing SDK; skipping remaining tests" + - exit 0 + - fi + if: darwin + - run: + - if test "{{hw.platform}}" = "darwin" && test ! -f /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/_bounds.h ; then + - echo "Missing SDK; skipping remaining tests" + - exit 0 + - fi + if: "^14.2 || >=15.2" + - gcc -o test1 test.c -lgmp + - ./test1 + - g++ -o test2 test.cc + - test "$(./test2)" = "Hello, world!" + - gfortran -o test3 test.f90 + - test "$(./test3)" = "Hello, world!" + # libc-wrapper (#13094): with a sibling pkgx glibc bottle and CPATH + # unset, gcc/g++ must compile+link+RUN against that glibc — i.e. the + # produced binary's PT_INTERP is the bottle's ld.so — and must NO-OP + # (fall back to host libc) when CPATH is set. brewkit's own test env + # keeps CPATH populated (dep includes), so the pre-existing steps above + # stay on host libc; we drive the wrapper explicitly with `CPATH=`. + - run: | + set -eu + glibc="{{deps.gnu.org/glibc.prefix}}" + # (1) C: -nostdinc drops host headers; stdio.h must come from the + # injected pkgx glibc, and PT_INTERP must be that bottle's ld.so. + printf '#include \nint main(void){ printf("wrap-ok\\n"); return 0; }\n' > wrap.c + CPATH= gcc wrap.c -o wrap + interp=$(readelf -l wrap | sed -n 's/.*program interpreter: \(.*\)]/\1/p') + case "$interp" in + "$glibc"/*) echo "PASS: C PT_INTERP is pkgx glibc ($interp)" ;; + *) echo "FAIL: expected pkgx glibc ld.so under $glibc, got '$interp'"; exit 1 ;; + esac + test "$(./wrap)" = "wrap-ok" || { echo "FAIL: C binary did not run"; exit 1; } + # (2) C++: -nostdinc++ + libstdc++ re-add + libstdc++/libgcc_s rpath. + printf '#include \n#include \nint main(){ std::vector v; v.push_back(7); printf("%%d\\n", v[0]); return 0; }\n' > wrap.cc + CPATH= g++ wrap.cc -o wrapxx + interpxx=$(readelf -l wrapxx | sed -n 's/.*program interpreter: \(.*\)]/\1/p') + case "$interpxx" in + "$glibc"/*) echo "PASS: C++ PT_INTERP is pkgx glibc ($interpxx)" ;; + *) echo "FAIL: C++ expected pkgx glibc ld.so, got '$interpxx'"; exit 1 ;; + esac + test "$(./wrapxx)" = "7" || { echo "FAIL: C++ binary did not run/print 7"; exit 1; } + # (3) Negative: with CPATH set the wrapper must NO-OP -> host ld.so. + echo 'int main(void){return 0;}' > noop.c + CPATH=1 gcc noop.c -o noop + interp2=$(readelf -l noop | sed -n 's/.*program interpreter: \(.*\)]/\1/p') + case "$interp2" in + "$glibc"/*) echo "FAIL: wrapper should have no-op'd with CPATH set"; exit 1 ;; + *) echo "PASS: CPATH-set no-op falls back to host libc ($interp2)" ;; + esac + if: linux provides: - bin/ar From 6d2ce3f8084c4bd43b11cd033a1be4888fc2139e Mon Sep 17 00:00:00 2001 From: tannevaled Date: Mon, 27 Jul 2026 17:04:28 +0200 Subject: [PATCH 7/7] fix(gnu.org/gcc): resolve glibc bottle physically; robust wrapper test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI (linux-aarch64) caught two bugs the earlier x86_64 scratch validation missed — and this is exactly why the test was worth adding: 1. pkgx installs a literal `v*` convenience symlink next to the real vX.Y.Z bottle dir; the wrapper's `.../glibc/v*` glob matched `v*` first (ASCII '*' sorts before digits) and logical `pwd` kept the literal name, baking `.../glibc/v*/lib/glibc-X.Y/ld-*` (unrunnable) into PT_INTERP. Fix: skip candidates ending in '*' and resolve the chosen dir physically (`cd -P`/`pwd -P`, also applied to bindir/ gcc_root) so no glob/symlink residue survives into $libc. 2. The test asserted against `{{deps.gnu.org/glibc.prefix}}`, but brewkit doesn't template `{{deps.*}}` for TEST deps. Rework the test to capture the host loader from a CPATH-set (no-op) build and assert the wrapped build's PT_INTERP differs from it, matches `*/glibc-*/ld-*`, has no glob residue, exists, and runs — no template needed. The glob-residue guard is a permanent regression check for bug #1. Re-validated against the REAL pkgx layout (all version symlinks incl. `v*`) on x86_64: fully-resolved physical interpreter, C/C++/triplet-/ external-symlink all run, CPATH-set no-ops to host. Co-Authored-By: Claude Opus 4.8 (1M context) --- projects/gnu.org/gcc/package.yml | 77 ++++++++++++++++++++++---------- 1 file changed, 54 insertions(+), 23 deletions(-) diff --git a/projects/gnu.org/gcc/package.yml b/projects/gnu.org/gcc/package.yml index 65ccedac07..2ea56f1e6a 100644 --- a/projects/gnu.org/gcc/package.yml +++ b/projects/gnu.org/gcc/package.yml @@ -227,12 +227,14 @@ build: self=$(readlink -f -- "$invoked" 2>/dev/null) || self= [ -n "$self" ] || self=$invoked selfdir=${self%/*} + # Canonical layout: /libexec/gcc-wrap/ -> bin is ../../bin. + # -P everywhere so we work in PHYSICAL paths, never symlink names. if [ -d "$selfdir/../../bin" ]; then - bindir=$(CDPATH= cd -- "$selfdir/../../bin" && pwd) + bindir=$(CDPATH= cd -P -- "$selfdir/../../bin" && pwd -P) else - bindir=$(CDPATH= cd -- "$selfdir" && pwd) + bindir=$(CDPATH= cd -P -- "$selfdir" && pwd -P) fi - gcc_root=$(CDPATH= cd -- "$bindir/.." && pwd) + gcc_root=$(CDPATH= cd -P -- "$bindir/.." && pwd -P) # Real driver stays in bin/ as .-real so gcc's own relative lookup # of cc1/cc1plus/collect2 (under libexec/gcc///) keeps working. @@ -240,10 +242,18 @@ build: [ -x "$real" ] || real="$bindir/$target" # Opt in only when a sibling pkgx glibc bottle exists and CPATH is unset. + # pkgx installs a literal 'v*' convenience symlink alongside the real + # vX.Y.Z dir (and the glob stays literal if nothing matches at all), so + # SKIP any candidate ending in '*' — otherwise 'v*' sorts first (ASCII + # '*' < digits) and its name leaks unexpanded into PT_INTERP. Resolve + # the chosen dir PHYSICALLY (cd -P / pwd -P) so no symlink name + # (v2 / v2.44 / the sibling glibc symlink) survives into $libc either. libc= if [ -z "$CPATH" ]; then for cand in "$bindir/../../../glibc/v"*; do - [ -d "$cand/include" ] && libc=$(CDPATH= cd -- "$cand" && pwd) && break + case "$cand" in *'*') continue ;; esac + [ -d "$cand/include" ] || continue + libc=$(CDPATH= cd -P -- "$cand" && pwd -P) && break done fi [ -n "$libc" ] || exec "$real" "$@" @@ -427,34 +437,55 @@ test: # stay on host libc; we drive the wrapper explicitly with `CPATH=`. - run: | set -eu - glibc="{{deps.gnu.org/glibc.prefix}}" - # (1) C: -nostdinc drops host headers; stdio.h must come from the - # injected pkgx glibc, and PT_INTERP must be that bottle's ld.so. + interp_of() { readelf -l "$1" | sed -n 's/.*program interpreter: \(.*\)]/\1/p'; } + + # (0) Host / no-op baseline: with CPATH set the wrapper stays dormant, + # so this binary uses the HOST loader. Header-less so it needs no + # glibc headers. We do NOT hardcode the pkgx prefix (brewkit does + # not template {{deps.*.prefix}} for test deps) — we compare the + # wrapped build against this captured host interpreter instead. + echo 'int main(void){return 0;}' > noop.c + CPATH=1 gcc noop.c -o noop + host_interp=$(interp_of noop) + echo "host PT_INTERP: $host_interp" + case "$host_interp" in + */glibc-*/ld-*) echo "FAIL: CPATH-set build should use host libc, got '$host_interp'"; exit 1 ;; + *) echo "PASS: CPATH-set no-op uses host libc" ;; + esac + + # (1) C: -nostdinc drops host headers, so stdio.h must come from the + # injected pkgx glibc; PT_INTERP must switch to the glibc bottle's + # ld.so in its glibc-X.Y sub-libdir — fully resolved (no glob/ + # symlink residue like a literal 'v*') — and the binary must RUN. printf '#include \nint main(void){ printf("wrap-ok\\n"); return 0; }\n' > wrap.c CPATH= gcc wrap.c -o wrap - interp=$(readelf -l wrap | sed -n 's/.*program interpreter: \(.*\)]/\1/p') + interp=$(interp_of wrap) + echo "C PT_INTERP: $interp" + test "$interp" != "$host_interp" || { echo "FAIL: wrapper did not change PT_INTERP"; exit 1; } case "$interp" in - "$glibc"/*) echo "PASS: C PT_INTERP is pkgx glibc ($interp)" ;; - *) echo "FAIL: expected pkgx glibc ld.so under $glibc, got '$interp'"; exit 1 ;; + *'*'*) echo "FAIL: PT_INTERP has an unexpanded glob: '$interp'"; exit 1 ;; esac - test "$(./wrap)" = "wrap-ok" || { echo "FAIL: C binary did not run"; exit 1; } - # (2) C++: -nostdinc++ + libstdc++ re-add + libstdc++/libgcc_s rpath. + case "$interp" in + */glibc-*/ld-*) : ;; + *) echo "FAIL: PT_INTERP is not a pkgx glibc sub-libdir loader: '$interp'"; exit 1 ;; + esac + test -e "$interp" || { echo "FAIL: PT_INTERP path does not exist: '$interp'"; exit 1; } + out=$(./wrap); test "$out" = "wrap-ok" || { echo "FAIL: C binary did not run (got '$out')"; exit 1; } + echo "PASS: C compiled+linked+ran against the pkgx glibc bottle" + + # (2) C++: exercises -nostdinc++ + libstdc++ header re-add + the + # libstdc++/libgcc_s runtime rpath. printf '#include \n#include \nint main(){ std::vector v; v.push_back(7); printf("%%d\\n", v[0]); return 0; }\n' > wrap.cc CPATH= g++ wrap.cc -o wrapxx - interpxx=$(readelf -l wrapxx | sed -n 's/.*program interpreter: \(.*\)]/\1/p') + interpxx=$(interp_of wrapxx) + echo "C++ PT_INTERP: $interpxx" + case "$interpxx" in *'*'*) echo "FAIL: C++ PT_INTERP has glob residue: '$interpxx'"; exit 1 ;; esac case "$interpxx" in - "$glibc"/*) echo "PASS: C++ PT_INTERP is pkgx glibc ($interpxx)" ;; - *) echo "FAIL: C++ expected pkgx glibc ld.so, got '$interpxx'"; exit 1 ;; + */glibc-*/ld-*) : ;; + *) echo "FAIL: C++ PT_INTERP is not the pkgx glibc loader: '$interpxx'"; exit 1 ;; esac test "$(./wrapxx)" = "7" || { echo "FAIL: C++ binary did not run/print 7"; exit 1; } - # (3) Negative: with CPATH set the wrapper must NO-OP -> host ld.so. - echo 'int main(void){return 0;}' > noop.c - CPATH=1 gcc noop.c -o noop - interp2=$(readelf -l noop | sed -n 's/.*program interpreter: \(.*\)]/\1/p') - case "$interp2" in - "$glibc"/*) echo "FAIL: wrapper should have no-op'd with CPATH set"; exit 1 ;; - *) echo "PASS: CPATH-set no-op falls back to host libc ($interp2)" ;; - esac + echo "PASS: C++ compiled+linked+ran against pkgx glibc + libstdc++" if: linux provides: