Skip to content

fix(gnu.org/gcc): add libc-wrapper for end-user pkgx gcc (#8423) - #13094

Open
tannevaled wants to merge 7 commits into
pkgxdev:mainfrom
tannevaled:fix/gcc-libc-wrapper
Open

fix(gnu.org/gcc): add libc-wrapper for end-user pkgx gcc (#8423)#13094
tannevaled wants to merge 7 commits into
pkgxdev:mainfrom
tannevaled:fix/gcc-libc-wrapper

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Summary

Adds a thin POSIX-sh wrapper at {{prefix}}/bin/<tool> (gcc, g++, cpp, c++, gfortran) that injects -isystem $glibc/include -L$glibc/lib for end-user invocations, without poisoning gcc's own bootstrap build. The real binaries are moved to {{prefix}}/libexec/gcc-wrap/. Adds gnu.org/glibc: '<2.38' + kernel.org/linux-headers: '*' as Linux deps so pkgx provisions a sibling glibc bottle the wrapper can find.

Why this approach

Previous attempts at #8423:

This take avoids both pitfalls because the wrapper:

  • is only added to the FINAL installed gcc (no build-time poisoning)
  • NO-OPs when CPATH is already set (brewkit build context — guards future bootstrap loops)
  • resolves the sibling glibc bottle path lazily via $0-relative lookup, not via baked-in configure flags
  • pins glibc <2.38 to avoid C23 symbol redirects (__isoc23_*) absent on older runners

Pattern is modeled on the bklibcvenv shim (brewkit#348).

Pairs with

Test plan

  • CI builds gcc on linux/x86-64 + linux/aarch64 (cross + native)
  • Verify pkgx gcc -E -x c /dev/null succeeds without distro glibc-devel
  • On a stock Fedora rootfs without glibc-devel, pkgx gcc test.c -o test && ./test succeeds
  • pkgx +gnu.org/gcc cc -v 2>&1 | grep gnu.org/glibc confirms the wrapper found the bottle
  • Bootstrap of gcc from a wrapped gcc (next gcc version build) doesn't double-inject
  • Darwin build unchanged (wrapper guarded by if: linux)

🤖 Generated with Claude Code

tannevaled and others added 5 commits July 7, 2026 18:39
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 (pkgxdev#13084) and --with-sysroot (pkgxdev#13092) failed because both
poisoned gcc's own bootstrap build.

This take installs a thin POSIX-sh wrapper at {{prefix}}/bin/<tool>
(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 pkgxdev#13083 (multi-arch triplet symlinks). Together they
should fully close pkgxdev#8423.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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/<triplet>/<ver>/include[+-fixed])
  - libstdc++ headers for C++ tools (include/c++/<ver>[/<triplet>])
  - 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 pkgxdev#8423.

The wrapper stays opportunistic (CPATH unset + sibling glibc), so the
gcc bootstrap loop is unaffected.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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/<triplet>/<version>/ 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/<tool> point at the wrapper.
cc1 lookup then resolves the same way it always did.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@tannevaled
tannevaled force-pushed the fix/gcc-libc-wrapper branch from 72122da to 763095d Compare July 7, 2026 16:40
@jhheider

Copy link
Copy Markdown
Contributor

should this be re-evaluated in light of the changes (thin loader shim in zig) to bklibcvenv? the shims had the problem of messing with $0, which the loader doesn't. we should also ensure we test that we've done what we expected.

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++ (<vector>),
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) <noreply@anthropic.com>
@tannevaled

Copy link
Copy Markdown
Contributor Author

Reworked per your review:

  • No more $0 muxing. Instead of one shared shim that infers which compiler it stands in for from basename "$0", I now generate one wrapper per tool (gcc/g++/cpp/c++/gfortran) with the target + c++-ness baked in at install time. The shim locates itself by canonicalizing $0 through symlinks (readlink -f, the sh analogue of /proc/self/exe) — so triplet-symlink and even external-symlink invocations resolve to the right target. The real driver stays in bin/ as .<tool>-real so gcc's own cc1/cc1plus/collect2 lookup keeps working.
  • A real test (linux): with a sibling gnu.org/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 with CPATH set the wrapper no-ops back to host libc.

While writing the test I also caught a latent bug in the original wrapper: the pkgx glibc bottle keeps crt*.o/libc.so.6/ld-linux* in a versioned sub-libdir (lib/glibc-X.Y), not lib/, so the old -L$libc/lib + ls $libc/lib/ld-linux* found nothing. Fixed (locate via libc.so.6, add -B + the libstdc++/libgcc_s rpath).

Validated end-to-end on real x86_64 hardware (C, C++ <vector>, gfortran, triplet- and external-symlink invocations all produce pkgx-glibc binaries that run; CPATH-set + no-sibling both fall back to host). CI is the final confirmation of the brewkit sibling-layout assumption.

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) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants