From 0cff64a790bf604b3c5fce65ac951fa5a917764c Mon Sep 17 00:00:00 2001 From: tannevaled Date: Wed, 20 May 2026 16:04:54 +0200 Subject: [PATCH] useShellEnv: don't auto-export lib/ for libc-style bottles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- fixtures/projects/curl.se/package.yml | 2 ++ fixtures/projects/gnu.org/glibc/package.yml | 4 +++ src/hooks/useShellEnv.test.ts | 36 ++++++++++++++++++++- src/hooks/useShellEnv.ts | 24 +++++++++++++- 4 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 fixtures/projects/curl.se/package.yml create mode 100644 fixtures/projects/gnu.org/glibc/package.yml diff --git a/fixtures/projects/curl.se/package.yml b/fixtures/projects/curl.se/package.yml new file mode 100644 index 0000000..6b849d1 --- /dev/null +++ b/fixtures/projects/curl.se/package.yml @@ -0,0 +1,2 @@ +provides: + - bin/curl diff --git a/fixtures/projects/gnu.org/glibc/package.yml b/fixtures/projects/gnu.org/glibc/package.yml new file mode 100644 index 0000000..22021be --- /dev/null +++ b/fixtures/projects/gnu.org/glibc/package.yml @@ -0,0 +1,4 @@ +provides: + - bin/getconf +platforms: + - linux diff --git a/src/hooks/useShellEnv.test.ts b/src/hooks/useShellEnv.test.ts index e55245f..c10f3ac 100644 --- a/src/hooks/useShellEnv.test.ts +++ b/src/hooks/useShellEnv.test.ts @@ -1,4 +1,4 @@ -import { assertEquals } from "@std/assert" +import { assertEquals, assert } from "@std/assert" import { useTestConfig } from "./useTestConfig.ts" import useShellEnv from "./useShellEnv.ts" import SemVer from "../utils/semver.ts" @@ -24,3 +24,37 @@ Deno.test("useShellEnv", async () => { assertEquals(env.PATH, `${installations[0].path.join("bin")}${SEP}${installations[1].path.join("bin")}`) }) + +Deno.test("useShellEnv: gnu.org/glibc does not export lib/ to LIBRARY_PATH", async () => { + // Regression test for libc-style bottles polluting consumers' + // LD_LIBRARY_PATH and breaking the host's coreutils. + const { map } = useShellEnv() + const { prefix } = useTestConfig() + + const glibc_install = { + pkg: { project: 'gnu.org/glibc', version: new SemVer('2.43.0') }, + path: prefix.join("gnu.org/glibc/v2.43.0"), + } + const curl_install = { + pkg: { project: 'curl.se', version: new SemVer('8.13.0') }, + path: prefix.join("curl.se/v8.13.0"), + } + + // Both bottles have a populated lib/ directory. + glibc_install.path.join("lib").mkdir('p') + curl_install.path.join("lib").mkdir('p') + + const vars = await map({ installations: [glibc_install, curl_install] }) + + // curl's lib/ is still exposed (existing behaviour). + const lib_paths = vars.LIBRARY_PATH ?? [] + assert( + lib_paths.some(p => p.endsWith("curl.se/v8.13.0/lib")), + `expected curl's lib/ in LIBRARY_PATH, got: ${lib_paths.join(", ")}`, + ) + // glibc's lib/ is NOT — that's the new opt-out behaviour. + assert( + !lib_paths.some(p => p.endsWith("gnu.org/glibc/v2.43.0/lib")), + `glibc's lib/ should not be in LIBRARY_PATH, got: ${lib_paths.join(", ")}`, + ) +}) diff --git a/src/hooks/useShellEnv.ts b/src/hooks/useShellEnv.ts index ab885e5..efc5ba3 100644 --- a/src/hooks/useShellEnv.ts +++ b/src/hooks/useShellEnv.ts @@ -19,6 +19,21 @@ export const EnvKeys = [ ] as const export type EnvKey = typeof EnvKeys[number] +/// Projects whose `lib/` MUST NOT be auto-added to LIBRARY_PATH / +/// LD_LIBRARY_PATH for consumers, because their `lib/` *is* the libc +/// (libc.so.6, libm.so.6, ld-linux*.so) — adding it to consumers' +/// dynamic-linker search path forces every executable in the same +/// pkgx env to load this bottle's libc, which fails the moment the +/// host's own ld-linux is older than what the bottle's libc requires. +/// +/// This set lists projects that legitimately ship libc itself. It is +/// intentionally small and hardcoded; long-term the per-package opt +/// should be expressed in the bottle's own metadata so the pantry +/// doesn't need a coordinated libpkgx release for new entries. +const NO_LIB_EXPORT: ReadonlySet = new Set([ + 'gnu.org/glibc', +]) + interface Options { installations: Installation[] } @@ -55,7 +70,14 @@ async function map({installations}: Options): Promise> } } - if (archaic) { + // libc-style projects (see NO_LIB_EXPORT) MUST NOT have their + // lib/ added to LIBRARY_PATH / LD_LIBRARY_PATH: adding a glibc + // bottle's lib/ to LD_LIBRARY_PATH would force every executable + // in the same pkgx env to load THIS bottle's libc.so.6, which + // breaks on hosts whose own ld-linux is older than the bottle's + // libc requires. Also skip the CPATH/include auto-add for the + // same reason (compile-time vs runtime libc skew). + if (archaic && !NO_LIB_EXPORT.has(installation.pkg.project)) { vars.LIBRARY_PATH = compact_add(vars.LIBRARY_PATH, installation.path.join("lib").chuzzle()?.string) vars.CPATH = compact_add(vars.CPATH, installation.path.join("include").chuzzle()?.string) }