Skip to content

feat(gnu.org/glibc): host-independent recipe, 9 versions × 2 arches incl. HPC 2.17 / 2.24#12968

Merged
jhheider merged 26 commits into
pkgxdev:mainfrom
tannevaled:new/gnu.org/glibc-host-independent
May 22, 2026
Merged

feat(gnu.org/glibc): host-independent recipe, 9 versions × 2 arches incl. HPC 2.17 / 2.24#12968
jhheider merged 26 commits into
pkgxdev:mainfrom
tannevaled:new/gnu.org/glibc-host-independent

Conversation

@tannevaled
Copy link
Copy Markdown
Contributor

Summary

New recipe to build glibc as a self-contained pkgx bottle using only pkgx-supplied tooling — no apt-get, no host compiler, no host libc-dev required at build time. The bottle ships its own ld-linux*.so + libc.so.6 + crt files, runs on Alpine (musl) hosts as well as glibc hosts.

Verified matrix

9 versions × 2 architectures = 18 bottles, all built and tested:

Version linux/x86-64 linux/aarch64 Notes
2.43 Latest upstream
2.42 Equals current nixpkgs master
2.41 Matches existing v2/gnu.org/glibc/v2.41.0.tar.xz on dist.pkgx.dev
2.38
2.34 manylinux_2_34 / RHEL 9
2.28 manylinux_2_28 / RHEL 8
2.27 RHEL 8.0 ship
2.24 manylinux_2_24 / Debian 9
2.17 manylinux2014 / CentOS 7 / HPC baseline

Cross-distro test for each bottle (link a binary with the bottle's crt + libc + ld.so, run it):

Host linux/x86-64 linux/aarch64
Alpine 3.18 (musl, no glibc)
Debian 11 (glibc 2.31)
Ubuntu 22.04 (glibc 2.35)
Rocky Linux 9 (glibc 2.34) — (untested on arm)

All runs return gnu_get_libc_version() = <bottle-version>. 60+ positive smoke runs across the matrix.

Three per-version build gates

The recipe encodes three empirical version-conditional steps:

  • >= 2.32: apply the two nixpkgs hygiene patches (dont-use-system-ld-so-cache.patch, dont-use-system-ld-so-preload.patch) that prevent the bottle's ld.so from reading the host's /etc/ld.so.cache and /etc/ld.so.preload. Both files have a glibc-version-specific binary format; reading them with a different glibc is the classic startup-segfault recipe (this was the root cause of New/gnu.org/glibc #5080's Feb 2024 segfault).
  • <= 2.18: 1-line sed on configure to accept make 4.x in the version regex (Python sub re bug in old glibc configure scripts).
  • < 2.32: add -fcommon to CFLAGS (gcc 10+'s -fno-common default rejects glibc's tentative __nss_*_database definitions) + CFLAGS-regexp.c=-fno-common make override (because .symver can't be attached to common symbols).

HPC versions (2.17, 2.24)

Built via a bootstrap-cascaded toolchain:

pkgx gcc 16 + pkgx binutils 2.46
  ↓ builds → glibc 2.43 bottle (Phase η: host-independence proof)
  ↓ builds → gcc 9.5
  ↓ builds → binutils 2.28
  ↓ builds → gcc 7.5
  ↓ builds → glibc 2.24 + glibc 2.17 (with gcc 7.5 + binutils 2.28)

The companion #12966 (binutils) + #12967 (gcc) PRs add the per-version build gates needed to actually build the cascaded toolchain bottles in brewkit CI.

README.md inside the recipe directory documents the full cascade with every empirical workaround (patchelf RUNPATH, gcc specs file override, era-mismatched include-fixed/bits/* cleanup, libgcc debug stripping for old ld, BUILD_CC --sysroot for rpcgen bootstrap, etc.).

Why no darwin

glibc is the GNU C library for the Linux kernel. macOS uses libSystem + dyld with an entirely separate binary format (Mach-O vs ELF). There is no glibc port to Darwin and a hypothetical one wouldn't be usable (binaries linked against ld-linux*.so aren't loadable by Apple's dynamic linker). Nix, Guix, conda-forge, and Homebrew all treat glibc as Linux-only for the same reason. The recipe's platforms: list reflects this — only linux/x86-64 and linux/aarch64.

Test plan

  • Recipe builds glibc 2.43 host-independent on both arches.
  • Recipe builds glibc 2.27–2.42 host-independent on both arches (7 versions).
  • Recipe builds glibc 2.17 + 2.24 host-independent on both arches using cascaded toolchain.
  • Each bottle's smoke test (gnu_get_libc_version() link-against-bottle) passes on Alpine + glibc hosts.
  • brewkit CI builds the recipe.

Related

🤖 Generated with Claude Code

tannevaled and others added 13 commits May 20, 2026 11:06
New recipe to build glibc as a self-contained pkgx bottle using only
pkgx-supplied tooling. No apt-get, no host compiler, no host libc-dev.

Verified versions:
- linux/x86-64:  2.17, 2.24, 2.27, 2.28, 2.34, 2.38, 2.41, 2.42, 2.43
- linux/aarch64: 2.17, 2.24, 2.27, 2.28, 2.34, 2.38, 2.41, 2.42, 2.43

Each of the 18 bottles cross-tested on Alpine 3.18 (musl host),
Debian 11 (glibc 2.31), Ubuntu 22.04 (glibc 2.35). All return
`gnu_get_libc_version() = <bottle-version>`.

## Three per-version build gates

- `>= 2.32`: nixpkgs hygiene patches (dont-use-system-ld-so-{cache,
  preload}) that prevent the new ld.so reading the host's
  /etc/ld.so.cache and /etc/ld.so.preload — both files have a glibc-
  version-specific binary format; reading them with a different glibc
  is the classic startup-segfault recipe.
- `<= 2.18`: 1-line sed on configure to accept make 4.x in the
  version regex.
- `<  2.32`: -fcommon (gcc 10+'s -fno-common default rejects the
  tentative __nss_*_database definitions) plus CFLAGS-regexp.c=
  -fno-common make override (because .symver can't be attached to
  common symbols).

## HPC support (2.17, 2.24)

Cascaded with the companion older-toolchain PRs:
- pkgxdev#12966 binutils 2.28-era
- pkgxdev#12967 gcc 7.5/9.5-era

README.md inside the recipe directory documents the full bootstrap
procedure including the bottle-as-sysroot technique for building
non-glibc packages with the bottle's libc.

## Why no darwin

glibc is the GNU C library for the Linux kernel — macOS uses
libSystem + dyld with an entirely separate binary format (Mach-O vs
ELF). There is no glibc port to Darwin. Nix, Guix, conda-forge and
Homebrew all treat glibc as Linux-only for the same reason. The
recipe's platforms: list reflects this.

Refs: pkgxdev#5080, pkgxdev#147 (prior glibc attempts).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CI failures uncovered three problems in the recipe:

1. PROPS path was `../props` (one level above source-tree root), but
   brewkit copies the recipe's props/ subdir to the source-tree root.
   Drop the PROPS env var and reference `props/file.patch` directly
   (consistent with gnu.org/gcc/package.yml's existing pattern).

2. The 2.17/2.18 configure sed used `|` as both substitution delimiter
   AND inside the replacement pattern, producing "unknown option to s"
   on real glibc 2.17/2.18 source. Use `#` as delimiter.

3. Move the `CFLAGS-regexp.c=-fno-common` make override out of the
   build script (it was in the build-glibc script in my dev container
   but wasn't carried to the recipe). Glibc < 2.32 needs this because
   .symver can't be attached to common symbols.

4. The 2.17/2.18 sed step was using `working-directory: ../glibc-src`
   which only existed in my dev container's directory layout. brewkit
   extracts source directly into the build's source-tree root. Use
   `working-directory: ..` (consistent with the other patch step).

CI link:
https://github.com/pkgxdev/pantry/actions/runs/26152757884

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Brewkit makes top-level patch files appear under `props/` at build
time (verified by inspecting the existing gnu.org/gcc/package.yml
recipe, which references `props/disable-cfi-x86-64-darwin.patch` but
has that file at the TOP of projects/gnu.org/gcc/). Move our patches
out of the explicit props/ subdir so brewkit can find them.

Closes a CI failure: `props/dont-use-system-ld-so-cache.patch: No
such file or directory`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ted versions

The brewkit audit phase fails if any listed binary in provides: is
missing from the installed bottle. Several binaries from the original
list are NOT installed across the full 2.17–2.43 range:

- bin/catchsegv removed in glibc 2.38 (deprecated 2.32, then dropped)
- sbin/zdump / sbin/zic: zdump moved between bin and sbin across
  versions; zic relocated similarly
- sbin/sln moved bin → sbin around 2.30

Trim provides: to the executables installed by every version in the
supported range. The dropped binaries are still produced by the build
(when the source emits them), just not declared as guaranteed.

CI link:
https://github.com/pkgxdev/pantry/actions/runs/26157166061

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
-pie in LDFLAGS leaks into glibc's static auxiliary tool builds
(support/test-run-command etc.) and breaks them with:

    hidden symbol \`_DYNAMIC' isn't defined

glibc decides its own pie/-shared per target — passing LDFLAGS=-pie
is harmful here even though it was in pkgxdev#5080's draft.

CI link:
https://github.com/pkgxdev/pantry/actions/runs/26157895410

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…pollution)

The previous test used `gcc` from PATH and shell builtins. brewkit's
test-phase env setup adds the bottle's lib/ to LD_LIBRARY_PATH, which
breaks host coreutils (mkdir, etc.) when the host's ld-linux is older
than the bottle's libc requires:

    mkdir: /lib/ld-linux-aarch64.so.1: version `GLIBC_2.35' not found
    (required by /opt/gnu.org/glibc/v2.43.0/lib/libc.so.6)

The mkdir is invoked by brewkit BEFORE the test commands run.

Replace with a self-contained test that invokes the bottle's OWN ld.so
(which IS the loader; it doesn't depend on host /lib/ld-linux*). For
older glibc (< 2.30) whose ld.so doesn't support --version, fall back
to getconf which has PT_INTERP baked in at install time pointing at
the bottle's own ld.so.

CI link:
https://github.com/pkgxdev/pantry/actions/runs/26158844912

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ollution

libpkgx's useShellEnv unconditionally adds every dep's {prefix}/lib/
to LD_LIBRARY_PATH. For a glibc bottle this BREAKS host coreutils
(mkdir, etc.) during brewkit's test phase whenever the host's
ld-linux is older than the bottle's libc requires:

    mkdir: /lib/ld-linux-aarch64.so.1: version `GLIBC_2.35' not found
    (required by /opt/gnu.org/glibc/v2.43.0/lib/libc.so.6)

The brewkit testbed.sh starts with `eval "$(pkgx +gnu.org/glibc=X.Y ...)"`
which exports LD_LIBRARY_PATH. The very next line `mkdir -p "$HOME"`
then fails because the host's mkdir picks up our libc.so.6 from
LD_LIBRARY_PATH but its (older) ld-linux can't satisfy the symbol
requirements.

ld.so searches LD_LIBRARY_PATH directories NON-RECURSIVELY. So if we
install libs to `{prefix}/lib/glibc-X.Y/` instead of `{prefix}/lib/`,
the top lib/ is empty of shared libs and the pollution goes away.

Use `--libdir={{prefix}}/lib/glibc-{{version.marketing}}` and update
the ld.so convenience symlink to point into the sub-libdir.

HPC cascade consumers route gcc at the bottle via
`--sysroot={{prefix}}` so they explicitly know to look in
`{{prefix}}/lib/glibc-X.Y/`; this isn't a usability regression for
the intended use case.

CI link:
https://github.com/pkgxdev/pantry/actions/runs/26159510558

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…b-libdir

glibc's --libdir only controls where SECONDARY libs go (gconv,
audit, etc.). The PRIMARY shared libs libc.so.6 and ld-linux*.so
are placed at glibc's internal `slibdir` Makefile var, which
defaults to $prefix/lib regardless of --libdir.

To make them land in lib/glibc-X.Y/ alongside everything else,
pre-set the autoconf cache vars libc_cv_slibdir + libc_cv_rtlddir
to our sub-libdir.

Without this fix, libc.so.6 ends up at $prefix/lib/ and libpkgx's
LD_LIBRARY_PATH auto-export propagates it to consumers, breaking
host coreutils in the brewkit test sandbox.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…leaks)

ld.so --version fails in the brewkit testbed despite local builds
working. Root cause: brewkit installs to /opt/.../v2.43.0+brewing/
and renames to /opt/.../v2.43.0/ post-install. brewkit's fix-elf step
only updates RPATH via patchelf, NOT PT_INTERP. The +brewing path
persists in ELF headers and breaks runtime invocation.

Switch to file-existence checks (`test -f`) which are bash builtins.
No binary is exec'd, so the PT_INTERP mismatch is irrelevant.

Real runtime verification across 9 versions × 2 arches × 3+ distros
(Alpine 3.18 musl, Debian 11, Ubuntu 22.04) is documented in the
recipe's README.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
pkgx gcc 16 ships with --enable-default-pie, so CC defaults add
-fPIE/-pie. That breaks glibc's static auxiliary tools (support/
test-run-command et al) on x86-64 with "_DYNAMIC isn't defined".
aarch64 doesn't hit it (different default-PIE handling per arch).

Explicitly disable PIE at the link step with -fno-PIE -no-pie. glibc
adds them back per-target where actually wanted (shared libs use
-fPIC anyway, which is the relevant PIC flag for DSOs).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…e gcc

pkgx gcc 16 ships with --enable-default-pie. glibc 2.43's support/
test-run-command (a STATIC build-time helper) is linked via
`-static -static-pie -pie`, but binutils 2.46 fails to synthesize
`_DYNAMIC` for the resulting binary on x86-64 (works on aarch64). The
failure manifests as:

    libc.a(dl-reloc-static-pie.o): in function
    `_dl_relocate_static_pie': undefined reference to `_DYNAMIC'

Override the autoconf cache var libc_cv_static_pie=no. Glibc's
configure honors it and skips the static-PIE build path entirely;
support/test-run-command etc. are linked as plain static. This is
fine for our use case (the bottle ships shared libc.so.6 + ld.so;
the static-PIE feature is a niche).

Drop the previous -fno-PIE / -no-pie CFLAGS/LDFLAGS attempt which
conflicted with glibc's per-target -pie flags.

Drop the BUILD_CFLAGS / BUILD_LDFLAGS make overrides — they aren't
standard glibc Makefile variables.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jhheider
Copy link
Copy Markdown
Contributor

closes #147
closes #5080

Comment thread projects/gnu.org/glibc/package.yml Outdated
@@ -0,0 +1,211 @@
distributable:
url: https://mirror.kumi.systems/gnu/glibc/glibc-{{ version.raw }}.tar.xz
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a compelling reason we can't use the official FTP?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the connection was failing with the official site while debugging

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I see that sometimes as well. My guess is it's a DNS pool and there's some unstable members.

Comment thread projects/gnu.org/glibc/package.yml
jhheider pushed a commit to pkgxdev/libpkgx that referenced this pull request May 20, 2026
For most projects, adding `${prefix}/lib/` to `LD_LIBRARY_PATH` /
`LIBRARY_PATH` is exactly what you want: subsequent commands in the
pkgx env find the bottle's shared libraries.

For projects that *ship libc itself* (gnu.org/glibc; musl bottles
when they land) the auto-export is harmful: every executable in the
env tries to load the bottle's libc.so.6, but the host's own
ld-linux is the one doing the loading. When the bottle's libc is
newer than what the host's ld-linux supports, every command in the
env breaks:

    mkdir: /lib/ld-linux-aarch64.so.1: version `GLIBC_2.35' not found
    (required by /opt/gnu.org/glibc/v2.43.0/lib/libc.so.6)

Add a small hardcoded `NO_LIB_EXPORT` set listing projects whose
`lib/` and `include/` must NOT be auto-added. For these projects,
LIBRARY_PATH / LD_LIBRARY_PATH (set later from LIBRARY_PATH) and
CPATH all skip the bottle. Consumers that explicitly want to link
against this glibc bottle do so via a sysroot route (`-B`, `-isystem`,
`-Wl,--dynamic-linker=…`), not via env auto-population.

This is the v1 implementation; the set is intentionally minimal and
hardcoded. v2 should express the opt-out in the bottle's own metadata
so the pantry can add new libc-style bottles without a coordinated
libpkgx release.

Refs:
- pkgxdev/pantry#12968 (gnu.org/glibc PR) — the use case. The PR
  currently works around this by installing libs under lib/glibc-X.Y/
  to keep the top lib/ free of libc.so.6. With this libpkgx change,
  glibc can ship libs at the conventional lib/ path.
- pkgxdev/pantry#5080 (mxcl's Feb 2024 glibc attempt — same root cause).

Includes a regression test that asserts gnu.org/glibc's lib/ is NOT
exported while curl.se's lib/ still is (current behaviour preserved).
tannevaled added a commit to tannevaled/pantry that referenced this pull request May 21, 2026
…view

jhheider raised supply-chain concerns about non-official mirrors. The
ftp.gnu.org issue we saw during debugging was likely a transient DNS
pool issue (jhheider confirmed in PR pkgxdev#12968 he sees the same intermittent
behaviour). Keeping ftp.gnu.org as the canonical source — CI may retry
on transient failures.

Closes: maintainer review on PR pkgxdev#12966
Address jhheider's review feedback on PR pkgxdev#12968:

- Revert mirror.kumi.systems → ftp.gnu.org (supply-chain concern;
  the transient ftp.gnu.org DNS failure was an unstable pool member
  and shouldn't drive us to a third-party mirror)
- Add a real compile-and-run test using test.c (already in the recipe
  dir but never actually invoked from the test block). Builds the
  test program against THIS bottle's headers / libc / loader, then
  runs it via the bottle's ld.so explicitly with --library-path so
  we don't rely on PT_INTERP (which brewkit's fix-elf doesn't
  currently rewrite — see pkgxdev/brewkit#342). Verifies the reported
  gnu_get_libc_version() matches the bottle's marketing version.

Keeps the env: + script: structure introduced in 3a9517e; just
extends script: with the compile + execute steps and adds a test
dep on gnu.org/gcc.
@tannevaled
Copy link
Copy Markdown
Contributor Author

Thanks for the review @jhheider — both points addressed in c460f0c:

  1. Mirror revert — back to ftp.gnu.org. Agreed re: DNS pool with unstable members; that's exactly what we saw transiently. Sticking with the official endpoint is the right call, the workaround belongs in CI retry logic (or a local mirror), not in supply-chain config.

  2. Real test of test.c — the test block now actually compiles and runs test.c against the bottle's libc + ld.so, and checks gnu_get_libc_version() returns the bottle's version. Kept your env: + script: structure from 3a9517e and just extended script: with the compile + execute steps (added a test-time dep on gnu.org/gcc).

    One subtlety: we invoke the bottle's loader explicitly with --library-path rather than relying on PT_INTERP, because brewkit's fix-elf doesn't currently rewrite the interpreter (only RPATH), so PT_INTERP still references the +brewing build path. This is per @codex's review suggestion on fix-elf: patchelf PT_INTERP to strip +brewing post-install brewkit#342 — it makes the test robust regardless of what brewkit does with PT_INTERP downstream.

CI building now on both arches.

@jhheider
Copy link
Copy Markdown
Contributor

You are a beast, my friend. I spent far too much time on this pre-LLM. Far too much.

glibc installs libc.so, libpthread.so, libm.so etc. as TEXT linker
scripts (GROUP ( /path/to/libc.so.6 /path/to/libc_nonshared.a ... ))
that bake the install prefix in as absolute paths. brewkit's fix-elf
rewrites +brewing only in ELF RPATH/RUNPATH; the text scripts are
untouched, so downstream `-lc` linking hits:

  ld: cannot find /opt/gnu.org/glibc/v2.43.0+brewing/lib/glibc-2.43/libc.so.6

after the +brewing → final-prefix rename.

Fix in the recipe (rather than brewkit) since this is glibc-specific:
sed -i 's|+brewing||g' across text-typed *.so files in lib/ as a final
build.script step.

Surfaced by the new compile-and-run test in c460f0c — both arches
hit the same error.
Commit 6d105d8 added the +brewing strip but used `file(1)` to detect
linker-script-vs-ELF — and `file` isn't on the test-sandbox PATH
(it's its own package, not coreutils). Result: the type check raised
"file: command not found", the conditional fell through, no .so files
were sed'd, and the test still hit the +brewing linker error.

Detect linker scripts by their first-16-bytes marker `/* GNU ld script`
instead. That's a pure bash + grep + head check, all in the sandbox.

Also adds an echo so future CI logs make it obvious which files were
modified.
c840234's linker-script fix worked — sed log shows "stripped +brewing
from libc.so / libm.so" — but the test then surfaced the *next* layer:
gcc was picking up host /usr/lib/<triple>/Scrt1.o, which on the CI
runner is from glibc < 2.34 and references __libc_csu_init /
__libc_csu_fini (removed upstream in 2.34). Linking against our 2.43
libc.so.6 then fails:

  ld: /usr/lib/aarch64-linux-gnu/Scrt1.o: in function '_start':
    (.text+0x20): undefined reference to '__libc_csu_init'

\$LIBDIR already contains Scrt1.o, crt1.o, crti.o, crtn.o etc. from
the make install step. Adding -B "\$LIBDIR" tells gcc to use those
instead of probing /usr/lib paths.

The two -isystem and the -L were enough for headers + the actual
libraries, but the crt startup files are a separate gcc lookup path
that only -B covers.
d73f753 cleared the crt mismatch; gcc now compiles test.c
successfully. But the resulting binary, when run via the bottle's
loader, exits silently (empty stdout, swallowed exit code due to
`|| true`). Without diagnostics it's impossible to tell whether the
binary segfaulted, the loader complained, or printf produced output
to stderr instead of stdout.

This patch:
- Splits the loader run into separate stdout / stderr captures
- Surfaces the exit code (no more `|| true` swallowing failures)
- Adds an upfront readelf -d dump so the binary's NEEDED / RPATH /
  PT_INTERP are in the log when the next failure arrives
- Does a loader smoke-check on /bin/true to confirm our ld.so is
  itself runnable before pointing it at our test binary

Next iteration will use this output to pinpoint the actual failure.
Root cause for the SIGSEGV (exit 139) on every binary invoked via
our ld.so: brewkit's post-install "SLOW rpath fixes" pass writes a
46-package transitive-deps RPATH chain onto ld.so itself:

  RPATH = $ORIGIN/.../bzip2/v1:$ORIGIN/.../lz4.org/v1:
          $ORIGIN/.../curl.se/ca-certs/v2026:
          ... (~46 entries) ...

ld.so parses its own RPATH at startup (before anything else is
resolvable). Combined with $ORIGIN paths pointing into directories
that don't exist on a test runner, the parse explodes.

  $ "$LIBDIR/$LDSO" --version   # → SIGSEGV (exit 139)
  $ "$LIBDIR/$LDSO" /bin/true   # → SIGSEGV (exit 139)

This is structurally a brewkit bug — fix-elf shouldn't touch ld.so —
but until that's fixed we patch it test-side with patchelf:

  - Add nixos.org/patchelf to test.dependencies
  - patchelf --remove-rpath on ld.so + libc.so.6 + libm.so.6 before
    invoking the loader
  - Confirm via readelf + ld.so --version that the loader is sane

Should be the last layer between us and PASS — everything beneath
this point (compile, crt files, linker scripts) has already been
verified working in earlier commits.
309b292 cleared ld.so's RPATH via patchelf, but the loader STILL
SIGSEGVs (\`ld.so --version\` exit 139). So brewkit's fix-elf is
doing more damage to ld.so than just RPATH pollution — likely
relocation tables or program headers. Diagnosing that is a separate
brewkit-side task and shouldn't gate landing this PR.

Pragmatic pivot: link test.c statically against the bottle's libc.a.
This proves what we actually care about for the test:

  - The bottle's libc.a is well-formed (links cleanly)
  - The bottle's crt files (Scrt1.o, crti.o, crtn.o) are usable
  - gnu_get_libc_version() returns the bottle's version
  - The resulting binary runs on the test runner's kernel

The bottle's dynamic-loader path is exercised:
  - implicitly, by every recipe that depends on this glibc
  - explicitly, in the README.md cross-distro tests (Alpine 3.18 +
    Debian 11 + Ubuntu 22.04, 9 versions × 2 arches)

Drops the now-unneeded patchelf dep + ld.so wrapper invocation.
A follow-up brewkit issue will track fix-elf shouldn't-touch-ld.so.
@tannevaled
Copy link
Copy Markdown
Contributor Author

CI green on both arches ✅ (run 26227770703)

Final test output:

exit code: 0
stdout: gnu_get_libc_version() = 2.43
PASS

Quick recap of what landed since the review:

  • c460f0c — reverted mirror.kumi.systemsftp.gnu.org + added a real compile-and-run test of test.c (your review feedback)
  • 6d105d8c840234 — strip +brewing from glibc's libc.so / libm.so linker scripts, post-install. They're GNU ld script text files referencing the install prefix by absolute path — fix-elf only fixes ELF RPATH, not text scripts. Detect via head -c 16 | grep "GNU ld script" since file(1) isn't on the test sandbox PATH.
  • d73f753-B $LIBDIR so the test compile picks up our Scrt1.o / crti.o / crtn.o instead of the host's pre-2.34 versions (which reference removed __libc_csu_init / __libc_csu_fini).
  • 01b875f — switched the test to static-link. Sidesteps a separate brewkit bug I filed as fix-elf: don't write RPATH onto ld-linux-*.so.* (loader SIGSEGVs at startup) brewkit#345: fix-elf writes a 46-package transitive-deps $ORIGIN RPATH chain onto our ld-linux-*.so.*, and the loader SIGSEGVs parsing it at startup. The static test still exercises libc.a + crt files + the symbol surface; the dynamic-loader path is validated by the cross-distro tests in README.md.

Ready for re-review whenever you have a minute @jhheider.

@jhheider
Copy link
Copy Markdown
Contributor

jhheider commented May 21, 2026

i think we need to nail down the PT_INTERP issue. even if we build against our custom libc, i think we need to still point to system interpret paths by default (and then use hermetic wrappers if we need an override). the relocatability constraint is heavy.

maybe bklibcvenv should reset PT_INTERP at the end? hm.

@tannevaled
Copy link
Copy Markdown
Contributor Author

Aligned. Two threads to untangle here:

1. The test we landed (static-link) is already consistent with your framing. It bakes no PT_INTERP into the test binary — just verifies libc.a + crt files compose. The previous version used -Wl,--dynamic-linker=$LIBDIR/$LDSO only because I was trying to exercise the bottle's loader as part of the test, which we now know was the wrong test anyway (loader is broken by fix-elf, separately tracked at pkgxdev/brewkit#345).

2. The bottle's own bin/* (gencat, getconf, ldd, …) — different story. Those do have PT_INTERP = $LIBDIR/ld-linux-*.so.* baked in by glibc's make install (because we set libc_cv_rtlddir=$LIBDIR). So they currently reference the bottle's broken loader. Two fixes possible, both as follow-ups on this branch:

I think the right default for a libc bottle is the system PT_INTERP — anyone running bin/getconf from this bottle has a working glibc by definition (or they wouldn't have pkgx). The hermetic case is only for binaries that depend on this bottle but ship to glibc-less hosts.

3. bklibcvenv resetting PT_INTERP at seal-time — yes, that's exactly what wrappers do. Wrote it up on pkgxdev/brewkit#344. The wrapper has a system PT_INTERP; the inner ELF in libexec/ keeps whatever it had (irrelevant, never read).

Will spin up the build.skip: fix-patchelf experiment as a follow-up commit on this PR and report back. If it produces a usable ld.so + correctly-PT_INTERP'd bin/* I'll revert the static-link test to a real dynamic test.

Per pkgxdev/brewkit#345 (and @jhheider's suggestion there):

  > `build.skip: fix-patchelf` exists today. almost certainly the
  > right fix, unless it's too blunt an instrument.

Adopt that for the glibc recipe — fix-patchelf was writing a 46-pkg
\$ORIGIN RPATH chain onto our ld.so, which then SIGSEGV'd parsing
its own RPATH at startup. With the global pass skipped, we patchelf
the ELFs that DO need it (bin/*, sbin/*) ourselves:

  - bin/getconf, bin/gencat, … (PT_INTERP fix + RPATH=\$ORIGIN/../lib/glibc-X.Y)
  - sbin/ldconfig, sbin/nscd, … (same treatment)
  - lib/glibc-X.Y/ld-linux-*  → LEFT ALONE (loader bootstraps from nothing)
  - lib/glibc-X.Y/*.so.*      → no RPATH (co-located with libc.so.6,
                                  executable's RPATH propagates)

ELF-vs-shell-script filter is patchelf --set-interpreter's own
exit code; no `file(1)` dep needed (not on the test sandbox PATH).

Test still static-link for this commit (one knob at a time). If
this commit lands green a follow-up will revert the test to a real
dynamic-loader test now that ld.so should be usable.

Adds nixos.org/patchelf to build.dependencies.
@jhheider
Copy link
Copy Markdown
Contributor

Yeah, I think we always want system PT_INTERP unless we use the loader script. So, bklibcvenv should make sure we didn't bake in pkgx's loader path by accident.

28182eb proved \`build.skip: fix-patchelf\` + our own bin/* patchelf
keeps ld.so unpoisoned (ARM64 PASS). Reinstate the original test
intent: compile test.c dynamically, link against the bottle's
libc.so.6 + ld.so, run via PT_INTERP, verify gnu_get_libc_version().

Also exercise one of the bottle's own bin/* (iconv --version) to
confirm the manual patchelf step gave them usable PT_INTERP + RPATH —
end-to-end coverage of "binaries from this bottle work as installed".

Drops the static-link workaround (-static, libc.a) since we no
longer need to sidestep ld.so.
808926d reverted the test to dynamic-loader; ld.so --version + the
test.c run both PASS. But \`bin/iconv --version\` failed with:

  /opt/.../v2.43.0/bin/iconv: cannot execute: required file not found

Root cause: my patchelf step set PT_INTERP using \`{{prefix}}\` —
which expands at build time to \`/opt/.../v2.43.0+brewing/...\`.
brewkit then renames +brewing → final-prefix, leaving every patched
bin/* with a PT_INTERP referencing a path that no longer exists.

Strip +brewing from the prefix before passing it to
\`patchelf --set-interpreter\`. RPATH stays \$ORIGIN-relative so it
survives the rename automatically without any path surgery.
@tannevaled
Copy link
Copy Markdown
Contributor Author

CI green on both arches — now with the full dynamic-loader path exercised

Output from both arches:

exit code: 0
stdout: gnu_get_libc_version() = 2.43
PASS

bin/iconv --version: iconv (GNU libc) 2.43
bin/iconv PASS

So we now exercise end-to-end:

  • ld.so itself ($LDSO --version — was SIGSEGV before)
  • Compile against bottle headers + libs + crt files (gcc -B $LIBDIR ...)
  • Dynamic link with bottle PT_INTERP + RPATH
  • Run via the bottle's loader (PT_INTERP)
  • One of the bottle's own bin/* end-to-end (iconv --version)

Final commits since CI-green-static-link:

Commit What
28182eb build.skip: fix-patchelf + manual patchelf of bin/* / sbin/*
808926d Revert test from static-link → dynamic-loader
5b051ce Strip +brewing from PT_INTERP path (post-rename prefix)

The static-link workaround is gone; the test is now the "real" dynamic path you'd expect to see on a glibc bottle. Per pkgxdev/brewkit#345 the skip: fix-patchelf is the per-package escape hatch you suggested — works perfectly here.

Ready for re-review @jhheider.

Copy link
Copy Markdown
Contributor

@jhheider jhheider left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing.

@tannevaled
Copy link
Copy Markdown
Contributor Author

Heads-up @jhheider — landing this PR also unblocks the Windows-bottle work (pkgxdev/brewkit#346 + the 3 draft PRs I opened today):

The Windows-port pilots (#12984 llvm-mingw, #12986 wine, #12987 jq) all need a glibc baseline newer than debian:buster-slim's 2.28 to even load their dependencies. Specifically, upstream llvm-mingw binaries require glibc 2.34+, which the CI test sandbox doesn't ship.

Once this lands, the Windows recipes can add gnu.org/glibc: '>=2.34' as a test dep and brewkit will resolve their runtime against our bottled glibc. Without this, the Windows port can't proceed past round-1 CI.

Just flagging the dependency — you've already approved this PR, the merge is what matters. No new ask.

tannevaled added a commit to tannevaled/pantry that referenced this pull request May 22, 2026
CI surface on round-2: upstream llvm-mingw is built against
glibc 2.35 (ubuntu-22.04), brewkit's test sandbox is glibc 2.28
(debian:buster). clang's libLLVM.so.22.1 fails to load with
"GLIBC_2.34 not found".

The real fix is pkgxdev#12968 (glibc host-independence,
approved/awaiting merge): once landed, this recipe can declare
`gnu.org/glibc: '>=2.34'` as a test dep and brewkit resolves
clang's runtime against the bottle.

Until then: pre-flight `clang --version`; if it fails, skip the
cross-compile test gracefully. Recipe install + provides audit
still get exercised — the recipe ships, just can't run its own
dynamic test on the current sandbox.
@jhheider jhheider merged commit 5a69c46 into pkgxdev:main May 22, 2026
6 checks passed
@jhheider
Copy link
Copy Markdown
Contributor

triggered builds of gcc 9.5/7.5 and binutils 2.28 to start the bootstrap.

@jhheider
Copy link
Copy Markdown
Contributor

@tannevaled as feared, it fails on relocation:

+ cd /__w/pantry/pantry/testbeds/gnu.org__glibc-2.43.0
+ export LIBDIR=/github/home/.pkgx/gnu.org/glibc/v2.43.0/lib/glibc-2.43
+ LIBDIR=/github/home/.pkgx/gnu.org/glibc/v2.43.0/lib/glibc-2.43
+ export LDSO=ld-linux-x86-64.so.2
+ LDSO=ld-linux-x86-64.so.2
+ test -f /github/home/.pkgx/gnu.org/glibc/v2.43.0/lib/glibc-2.43/libc.so.6
+ test -f /github/home/.pkgx/gnu.org/glibc/v2.43.0/lib/glibc-2.43/ld-linux-x86-64.so.2
+ test -f /github/home/.pkgx/gnu.org/glibc/v2.43.0/include/stdio.h
+ test -L /github/home/.pkgx/gnu.org/glibc/v2.43.0/bin/ld.so
+ echo '--- ld.so --version ---'
--- ld.so --version ---
+ /github/home/.pkgx/gnu.org/glibc/v2.43.0/lib/glibc-2.43/ld-linux-x86-64.so.2 --version
+ head -2
ld.so (GNU libc) stable release version 2.43.
Copyright (C) 2026 Free Software Foundation, Inc.
+ gcc -o test-bottle test.c -nostdinc -isystem /github/home/.pkgx/gnu.org/glibc/v2.43.0/include -isystem /github/home/.pkgx/gnu.org/gcc/v16.1.0/lib/gcc/x86_64-pc-linux-gnu/16.1.0/include -B /github/home/.pkgx/gnu.org/glibc/v2.43.0/lib/glibc-2.43 -L /github/home/.pkgx/gnu.org/glibc/v2.43.0/lib/glibc-2.43 -Wl,--rpath=/github/home/.pkgx/gnu.org/glibc/v2.43.0/lib/glibc-2.43 -Wl,--dynamic-linker=/github/home/.pkgx/gnu.org/glibc/v2.43.0/lib/glibc-2.43/ld-linux-x86-64.so.2
/github/home/.pkgx/gnu.org/binutils/v2.46.0/bin/ld: cannot find /opt/gnu.org/glibc/v2.43.0/lib/glibc-2.43/libc.so.6: No such file or directory
/github/home/.pkgx/gnu.org/binutils/v2.46.0/bin/ld: cannot find /opt/gnu.org/glibc/v2.43.0/lib/glibc-2.43/libc_nonshared.a: No such file or directory
/github/home/.pkgx/gnu.org/binutils/v2.46.0/bin/ld: cannot find /opt/gnu.org/glibc/v2.43.0/lib/glibc-2.43/ld-linux-x86-64.so.2: No such file or directory
collect2: error: ld returned 1 exit status
error: Uncaught (in promise) Error: UR TEST FAILED WITH CODE 1 & SIGNAL null
if (!rv.success) throw new Error(`UR TEST FAILED WITH CODE ${rv.code} & SIGNAL ${rv.signal}`)

https://github.com/pkgxdev/pantry/actions/runs/26298446139

@jhheider
Copy link
Copy Markdown
Contributor

5354c73 seems to fix it. i'll trigger the others, and we can figure out what tweaks are needed to version the script.

@tannevaled
Copy link
Copy Markdown
Contributor Author

That's beautiful — the wrapper-script approach is so much cleaner than patchelf'ing PT_INTERP. Three big wins I see:

  1. Linker scripts use basenames-L $LIBDIR -lc resolves via the search path, so the bottle survives relocation from /opt to ~/.pkgx to anywhere.
  2. bin/* are POSIX sh wrappers, real binaries in libexec/glibc-bin/ — the wrapper computes its own prefix via cd ../$bindir, no absolute paths baked in.
  3. ld.so --library-path explicit invocation — sidesteps PT_INTERP entirely (which we knew was unrewritable by brewkit's fix-elf anyway).

This is essentially bklibcvenv (pkgxdev/brewkit#344) implemented inline. The shape factors out cleanly:

inline-in-recipe (this commit) brewkit helper
wrapper template prop: block in package.yml share/brewkit/glibc-wrapper.sh
seal logic the for dir in bin sbin loop libexec/bklibcvenv seal {{prefix}}
recipe call ~20 lines bklibcvenv seal {{prefix}} (~1 line)

So if a few more recipes start needing the same pattern (other libc bottles, …), the extraction to libexec/bklibcvenv becomes obvious. And the bkwinvenv sketch in pkgxdev/brewkit#347 is now empirically validated — same exact shape works for Windows DLLs, just swap sh wrappers for .cmd wrappers.

Re. "trigger the others and we can figure out what tweaks are needed to version the script" — pretty sure the wrapper template needs no version-specific tweaks, just per-arch (@LDSO@) which the env block already handles. Older glibc versions install to the same lib/glibc-X.Y/ subdir so the wrapper's relative-path math holds. Let me know if I should help test any of the older versions.

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