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) }