From 9777fbca6816efd3e3b7dac0c369da35bf0aed67 Mon Sep 17 00:00:00 2001 From: Thomas Czerwonka Date: Sat, 5 Sep 2026 09:06:16 +0200 Subject: [PATCH 1/4] appimage: bundle the ICU libraries lld depends on The lld binary in the llvm.org Linux release tarball is dynamically linked against Ubuntu 22.04's ICU (libicui18n/libicuuc/libicudata .so.70). The tarball does not ship them and neither did the AppImage, so on any distro with a different ICU major (Arch/CachyOS 78, Fedora and Debian 13 76, Ubuntu 24.04 74) `ld.lld` cannot start: ld.lld: error while loading shared libraries: libicui18n.so.70: cannot open shared object file: No such file or directory and CMake's "Check for working C compiler" aborts the whole install. The smoke test in prepare-portable-tools.sh never caught it because it runs on the very Ubuntu 22.04 runner that has ICU 70 installed. lld already carries a $ORIGIN/../lib RUNPATH, so copying the libraries into the toolchain's lib/ is enough - no rpath patching, no LD_LIBRARY_PATH in AppRun. The copy is driven by ldd on the build host rather than a hardcoded list, is applied to every bundled executable, and a new check before the smoke test fails the build if any of them resolves a non-glibc-baseline library from outside the bundle. Co-Authored-By: Claude Fable 5.1 --- Launcher/prepare-portable-tools.sh | 68 +++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/Launcher/prepare-portable-tools.sh b/Launcher/prepare-portable-tools.sh index 5585c2f7c..ee3e83d7c 100755 --- a/Launcher/prepare-portable-tools.sh +++ b/Launcher/prepare-portable-tools.sh @@ -10,7 +10,10 @@ # libc++/libc++abi/libunwind (so the toolchain never has to fall back to the host's system # libstdc++ headers). The raw release is ~1.9 GiB per arch (every LLVM backend, mlir, flang, lldb, # docs, tests); pruned it is ~500 MiB uncompressed / ~100 MiB compressed, verified against a real -# build of this project. +# build of this project. The release binaries are dynamically linked against a few libraries of +# the Ubuntu the LLVM project builds on (ICU, for lld), so those are bundled into lib/ too - see +# bundle_private_deps below - and every executable is verified to resolve nothing but the glibc +# baseline from the host. # # cmake: pruned from the official Kitware GitHub release tarball down to bin/cmake (not # ccmake/cmake-gui/cpack/ctest, which local-build.sh never invokes) plus the Modules/Templates @@ -131,12 +134,49 @@ cp -a "$src/bin/lld" "$work/bin/" strip "$work/bin/lld" ln -s lld "$work/bin/ld.lld" +# The llvm.org release binaries are built on Ubuntu 22.04, and lld is dynamically linked against +# that distro's ICU (libicui18n/libicuuc/libicudata .so.70) - which neither the LLVM tarball nor +# this bundle shipped. Every other distro carries a different ICU major (Arch/CachyOS 78, Fedora +# and Debian 13 76, Ubuntu 24.04 74) and no compat package, so there `ld.lld` could not even +# start ("error while loading shared libraries: libicui18n.so.70") and CMake's "Check for working +# C compiler" aborted the whole install. The smoke test below never caught it because it runs on +# the very Ubuntu 22.04 runner that has ICU 70 in /usr/lib. lld's own RUNPATH is already +# $ORIGIN/../lib, so bundling the libraries there is all it takes: no rpath patching and no +# LD_LIBRARY_PATH juggling in AppRun. +# +# Driven by ldd rather than a hardcoded ICU list so that an LLVM bump that links something else +# (or ICU 74 once the LLVM builders move to 24.04) is picked up, and so that a dependency the +# build host cannot resolve fails here instead of on a user's machine. Applied to every bundled +# executable; the verification at the end then proves each one resolves everything outside the +# glibc baseline from inside the bundle. +baseline_sonames='^(ld-linux-[^ ]*|libc|libm|libdl|libpthread|librt|libgcc_s|libstdc\+\+|libz|liblzma)\.so(\.|$)' +bundle_private_deps() { + # $1 = executable under $work/bin. Each shared library it needs beyond the baseline is copied + # (symlink-dereferenced, under its soname) into $work/lib, where the LLVM binaries' RUNPATH + # looks first. + local exe=$1 soname arrow path rest + while read -r soname arrow path rest; do + [[ "$arrow" == "=>" ]] || continue # vdso / the ELF interpreter line + soname=${soname##*/} # some ldd's print the interpreter as a path + [[ "$soname" =~ $baseline_sonames ]] && continue + if [[ "$path" != /* || ! -f "$path" ]]; then + echo "prepare-portable-tools.sh: $(basename "$exe") needs $soname, which this host cannot resolve" >&2 + exit 1 + fi + [[ -e "$work/lib/$soname" ]] || cp -L "$path" "$work/lib/$soname" + done < <(ldd "$exe") +} +bundle_private_deps "$work/bin/lld" + # llvm-ar/llvm-ranlib: CMake's archiver for the many static libraries this project builds # (aurora, Crypto++, SDL3, Dawn's dependency closure, the translated game shards). cp -a "$src/bin/llvm-ar" "$work/bin/" strip "$work/bin/llvm-ar" ln -s llvm-ar "$work/bin/llvm-ranlib" +bundle_private_deps "$work/bin/clang-23" +bundle_private_deps "$work/bin/llvm-ar" + # Clang's resource directory: builtin headers (stddef.h, immintrin.h, ...) and compiler-rt # (builtins, sanitizer runtimes). `clang -print-resource-dir` must find this at lib/clang//. cp -a "$src/lib/clang" "$work/lib/" @@ -173,6 +213,7 @@ cp -a "$cmake_src/bin/cmake" "$work/bin/" cp -a "$cmake_src/share/cmake-$cmake_share_version/Modules" "$cmake_src/share/cmake-$cmake_share_version/Templates" \ "$work/share/cmake-$cmake_share_version/" rm -rf "$cmake_extract_root" +bundle_private_deps "$work/bin/cmake" # --- ninja, used as-is --- @@ -183,6 +224,7 @@ download_verified "$ninja_archive" \ echo "prepare-portable-tools.sh: staging ninja $ninja_version..." unzip -oq "$ninja_archive" -d "$work/bin" chmod +x "$work/bin/ninja" +bundle_private_deps "$work/bin/ninja" cat > "$work/LICENSE.txt" <" ]] || continue + soname=${soname##*/} + [[ "$soname" =~ $baseline_sonames ]] && continue + case "$path" in + "$work"/*) ;; + *) echo "prepare-portable-tools.sh: $exe resolves $soname from ${path:-nowhere} instead of the bundle - it would break on any host without it" >&2 + exit 1 ;; + esac + done < <(ldd "$work/bin/$exe") +done + echo "prepare-portable-tools.sh: testing the toolchain..." test_dir=$(mktemp -d) trap 'rm -rf "$test_dir"' EXIT From f8646f041dde25d3b85cabbb91f93190b0eec7ad Mon Sep 17 00:00:00 2001 From: Thomas Czerwonka Date: Sat, 5 Sep 2026 09:15:41 +0200 Subject: [PATCH 2/4] appimage: document why libstdc++/libgcc_s/libz/liblzma stay host-provided Review asked for them to be bundled alongside ICU. They are kept as host baseline on purpose: the AppImage excludelist names libstdc++.so.6, libgcc_s.so.1 and libz.so.1 as libraries never to bundle, every glibc distro ships them under these sonames, and the LLVM binaries' floor (GLIBC_2.34 / GLIBCXX_3.4.30) is met by any distro whose glibc can load them at all. ICU is the odd one out because its soname changes on every major release. Comment-only change. Co-Authored-By: Claude Fable 5.1 --- Launcher/prepare-portable-tools.sh | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Launcher/prepare-portable-tools.sh b/Launcher/prepare-portable-tools.sh index ee3e83d7c..c00921b4b 100755 --- a/Launcher/prepare-portable-tools.sh +++ b/Launcher/prepare-portable-tools.sh @@ -148,7 +148,16 @@ ln -s lld "$work/bin/ld.lld" # (or ICU 74 once the LLVM builders move to 24.04) is picked up, and so that a dependency the # build host cannot resolve fails here instead of on a user's machine. Applied to every bundled # executable; the verification at the end then proves each one resolves everything outside the -# glibc baseline from inside the bundle. +# baseline from inside the bundle. +# +# The baseline is what the host is expected to provide: glibc itself, plus libstdc++/libgcc_s/ +# libz/liblzma. Those four are deliberately NOT bundled, matching the AppImage excludelist +# (https://github.com/AppImageCommunity/pkg2appimage/blob/master/excludelist lists libstdc++.so.6, +# libgcc_s.so.1 and libz.so.1 by name) and what this toolchain already did before ICU was bundled: +# they ship with every glibc distro under these exact sonames, and the LLVM binaries' floor +# (GLIBC_2.34 / GLIBCXX_3.4.30, i.e. glibc 2.34+ with GCC 12's libstdc++) is met by every distro +# whose glibc is new enough to load them at all. ICU is different in kind: its soname changes with +# every major and distros carry exactly one, which is why it must travel with the bundle. baseline_sonames='^(ld-linux-[^ ]*|libc|libm|libdl|libpthread|librt|libgcc_s|libstdc\+\+|libz|liblzma)\.so(\.|$)' bundle_private_deps() { # $1 = executable under $work/bin. Each shared library it needs beyond the baseline is copied From 7d90ac3d8a94e21d112762794fbd0f2bc4c3fdc3 Mon Sep 17 00:00:00 2001 From: Thomas Czerwonka Date: Sat, 5 Sep 2026 16:04:09 +0200 Subject: [PATCH 3/4] appimage: bundle lld's libxml2 too, fail hard when ldd cannot run Re-evaluated against LLVM 22.1.8 (main since #141): its lld does not need ICU but libxml2.so.2, which Arch (libxml2 >= 2.14), Fedora and Ubuntu 25.10+ no longer ship - the exact failure in #136. Ubuntu's libxml2 in turn links ICU 70, so the transitive closure has to travel with the bundle as well. - bundle_private_deps now sets a $ORIGIN RUNPATH on every library it copies (patchelf): the loader resolves a library's own dependencies through that library's RUNPATH, not the executable's, so without it libxml2 -> libicuuc would be looked up on the host again. - A missing executable or a failing ldd is an error, never a silent pass, in both the bundling step and the final verification; a static executable ("not a dynamic executable") is tolerated. ldd runs under LC_ALL=C so that text is matched on localized hosts too. - Verification runs ldd with LD_LIBRARY_PATH unset and distinguishes "resolved from the host" from "not found at all". - patchelf added to the AppImage runner's apt-get list; the script refuses to run without it. - LICENSE.txt lists whatever ended up in lib/ plus the libxml2 and ICU licenses. Verified on the toolchains extracted from v0.2.26 (LLVM 22) and v0.2.27 (LLVM 23): with the Ubuntu 22.04 libxml2/libicu70 packages standing in for the runner's /usr/lib, bundling copies exactly {libxml2, libicuuc, libicudata} resp. {libicui18n, libicuuc, libicudata}, the verification passes with LD_LIBRARY_PATH unset, clang -fuse-ld=lld links and runs a program on CachyOS (libxml2.so.16 / ICU 78 host), and removing one bundled library makes the verification fail. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/package.yml | 2 +- Launcher/prepare-portable-tools.sh | 111 +++++++++++++++++++---------- 2 files changed, 73 insertions(+), 40 deletions(-) diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index b97c62a13..920a852d6 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -49,7 +49,7 @@ jobs: - name: Install dependencies (required by appimagetool and SDL3 build) run: | sudo apt-get update - sudo apt-get install -y libfuse2 build-essential git make \ + sudo apt-get install -y libfuse2 build-essential git make patchelf \ pkg-config cmake ninja-build gnome-desktop-testing libasound2-dev libpulse-dev \ libaudio-dev libfribidi-dev libjack-dev libsndio-dev libx11-dev libxext-dev \ libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxtst-dev \ diff --git a/Launcher/prepare-portable-tools.sh b/Launcher/prepare-portable-tools.sh index c00921b4b..20333d9c1 100755 --- a/Launcher/prepare-portable-tools.sh +++ b/Launcher/prepare-portable-tools.sh @@ -11,9 +11,9 @@ # libstdc++ headers). The raw release is ~1.9 GiB per arch (every LLVM backend, mlir, flang, lldb, # docs, tests); pruned it is ~500 MiB uncompressed / ~100 MiB compressed, verified against a real # build of this project. The release binaries are dynamically linked against a few libraries of -# the Ubuntu the LLVM project builds on (ICU, for lld), so those are bundled into lib/ too - see -# bundle_private_deps below - and every executable is verified to resolve nothing but the glibc -# baseline from the host. +# the Ubuntu the LLVM project builds on (libxml2 and/or ICU, for lld), so those are bundled into +# lib/ too - see bundle_private_deps below - and every executable is verified to resolve nothing +# but the glibc baseline from the host. Needs patchelf on the build host. # # cmake: pruned from the official Kitware GitHub release tarball down to bin/cmake (not # ccmake/cmake-gui/cpack/ctest, which local-build.sh never invokes) plus the Modules/Templates @@ -99,6 +99,11 @@ if [[ -x "$toolchain_dir/bin/clang" && -x "$toolchain_dir/bin/ninja" && -x "$too exit 0 fi +command -v patchelf >/dev/null || { + echo "prepare-portable-tools.sh: patchelf is required to bundle the toolchain's shared libraries (apt-get install patchelf)" >&2 + exit 1 +} + work="$destination/.building-toolchain-$arch" rm -rf "$work" mkdir -p "$work/bin" "$work/lib/$target_triple" "$work/include/$target_triple/c++/v1" @@ -134,36 +139,49 @@ cp -a "$src/bin/lld" "$work/bin/" strip "$work/bin/lld" ln -s lld "$work/bin/ld.lld" -# The llvm.org release binaries are built on Ubuntu 22.04, and lld is dynamically linked against -# that distro's ICU (libicui18n/libicuuc/libicudata .so.70) - which neither the LLVM tarball nor -# this bundle shipped. Every other distro carries a different ICU major (Arch/CachyOS 78, Fedora -# and Debian 13 76, Ubuntu 24.04 74) and no compat package, so there `ld.lld` could not even -# start ("error while loading shared libraries: libicui18n.so.70") and CMake's "Check for working -# C compiler" aborted the whole install. The smoke test below never caught it because it runs on -# the very Ubuntu 22.04 runner that has ICU 70 in /usr/lib. lld's own RUNPATH is already -# $ORIGIN/../lib, so bundling the libraries there is all it takes: no rpath patching and no -# LD_LIBRARY_PATH juggling in AppRun. +# The llvm.org release binaries are built on Ubuntu 22.04 and lld is dynamically linked against +# libraries of that distro which neither the LLVM tarball nor this bundle shipped: +# - LLVM 22's lld needs libxml2.so.2. Arch (since libxml2 2.14), Fedora and Ubuntu 25.10+ ship +# libxml2.so.16 only, so there `ld.lld` could not even start (#136), and Ubuntu's libxml2 in +# turn pulls in ICU (libicuuc/libicudata .so.70). +# - LLVM 23's lld needs ICU 70 directly (libicui18n/libicuuc/libicudata; #150, +# llvm/llvm-project#215764). Every other distro carries a different ICU major (Arch 78, +# Fedora and Debian 13 76, Ubuntu 24.04 74) and no compat package. +# Either way the symptom is "error while loading shared libraries: ..." from ld.lld and CMake's +# "Check for working C compiler" aborting the whole install. The smoke test below never caught it +# because it runs on the very Ubuntu 22.04 runner that has all of those in /usr/lib. # -# Driven by ldd rather than a hardcoded ICU list so that an LLVM bump that links something else -# (or ICU 74 once the LLVM builders move to 24.04) is picked up, and so that a dependency the -# build host cannot resolve fails here instead of on a user's machine. Applied to every bundled -# executable; the verification at the end then proves each one resolves everything outside the -# baseline from inside the bundle. +# So every non-baseline library an executable needs is copied next to the toolchain, driven by +# ldd rather than a hardcoded list: ldd prints the whole transitive closure, an LLVM bump that +# links something else is picked up automatically, and a dependency the build host cannot resolve +# fails here instead of on a user's machine. The LLVM executables already carry a $ORIGIN/../lib +# RUNPATH so they find lib/ on their own; each copied library gets a $ORIGIN RUNPATH of its own +# because the loader resolves a library's dependencies through *that library's* RUNPATH, not the +# executable's (Ubuntu's libxml2 -> libicuuc would otherwise be looked up on the host again). +# Applied to every bundled executable; the verification at the end then proves each one resolves +# everything outside the baseline from inside the bundle, with LD_LIBRARY_PATH out of the picture. # # The baseline is what the host is expected to provide: glibc itself, plus libstdc++/libgcc_s/ # libz/liblzma. Those four are deliberately NOT bundled, matching the AppImage excludelist # (https://github.com/AppImageCommunity/pkg2appimage/blob/master/excludelist lists libstdc++.so.6, -# libgcc_s.so.1 and libz.so.1 by name) and what this toolchain already did before ICU was bundled: -# they ship with every glibc distro under these exact sonames, and the LLVM binaries' floor -# (GLIBC_2.34 / GLIBCXX_3.4.30, i.e. glibc 2.34+ with GCC 12's libstdc++) is met by every distro -# whose glibc is new enough to load them at all. ICU is different in kind: its soname changes with -# every major and distros carry exactly one, which is why it must travel with the bundle. +# libgcc_s.so.1 and libz.so.1 by name) and what this toolchain always did: they ship with every +# glibc distro under these exact sonames, and the LLVM binaries' floor (GLIBC_2.34 / +# GLIBCXX_3.4.30, i.e. glibc 2.34+ with GCC 12's libstdc++) is met by every distro whose glibc is +# new enough to load them at all. libxml2 and ICU are different in kind: their sonames change +# across releases and distros carry exactly one, which is why they must travel with the bundle. baseline_sonames='^(ld-linux-[^ ]*|libc|libm|libdl|libpthread|librt|libgcc_s|libstdc\+\+|libz|liblzma)\.so(\.|$)' bundle_private_deps() { # $1 = executable under $work/bin. Each shared library it needs beyond the baseline is copied # (symlink-dereferenced, under its soname) into $work/lib, where the LLVM binaries' RUNPATH - # looks first. - local exe=$1 soname arrow path rest + # looks first, and given a $ORIGIN RUNPATH so its own dependencies resolve there too. + local exe=$1 deps soname arrow path rest + [[ -x "$exe" ]] || { echo "prepare-portable-tools.sh: $exe is missing or not executable" >&2; exit 1; } + if ! deps=$(LC_ALL=C ldd "$exe" 2>&1); then # LC_ALL=C: the "not a dynamic executable" text is matched below + [[ "$deps" == *"not a dynamic executable"* ]] && return 0 # static: nothing to bundle + echo "prepare-portable-tools.sh: ldd failed on $exe:" >&2 + echo "$deps" >&2 + exit 1 + fi while read -r soname arrow path rest; do [[ "$arrow" == "=>" ]] || continue # vdso / the ELF interpreter line soname=${soname##*/} # some ldd's print the interpreter as a path @@ -172,8 +190,10 @@ bundle_private_deps() { echo "prepare-portable-tools.sh: $(basename "$exe") needs $soname, which this host cannot resolve" >&2 exit 1 fi - [[ -e "$work/lib/$soname" ]] || cp -L "$path" "$work/lib/$soname" - done < <(ldd "$exe") + [[ -e "$work/lib/$soname" ]] && continue + cp -L "$path" "$work/lib/$soname" + patchelf --set-rpath '$ORIGIN' "$work/lib/$soname" + done <<< "$deps" } bundle_private_deps "$work/bin/lld" @@ -183,7 +203,7 @@ cp -a "$src/bin/llvm-ar" "$work/bin/" strip "$work/bin/llvm-ar" ln -s llvm-ar "$work/bin/llvm-ranlib" -bundle_private_deps "$work/bin/clang-23" +bundle_private_deps "$work/bin/clang-22" bundle_private_deps "$work/bin/llvm-ar" # Clang's resource directory: builtin headers (stddef.h, immintrin.h, ...) and compiler-rt @@ -243,10 +263,12 @@ clang/lld/llvm-ar $llvm_version (pruned from the official LLVM release for Linux Apache License v2.0 with LLVM Exceptions: https://github.com/llvm/llvm-project/blob/llvmorg-$llvm_version/LICENSE.TXT -ICU (libicui18n, libicuuc, libicudata under lib/), the shared libraries lld from that release -depends on, taken from the Ubuntu 22.04 libicu package that built it - https://icu.unicode.org/ - Unicode License: +Shared libraries under lib/ that the tools above depend on, copied from the Ubuntu 22.04 packages +the LLVM release was built against (bundled because their sonames differ between distributions): +$(cd "$work/lib" && ls -1 *.so.* 2>/dev/null | sed 's/^/ /') + libxml2 - MIT License: + https://gitlab.gnome.org/GNOME/libxml2/-/blob/master/Copyright + ICU - Unicode License: https://github.com/unicode-org/icu/blob/main/LICENSE CMake $cmake_version @@ -259,21 +281,32 @@ Ninja $ninja_version EOF echo "prepare-portable-tools.sh: checking that the toolchain carries its own shared libraries..." -# Every bundled executable must resolve all its non-baseline shared libraries from inside the -# bundle. This host has them all installed, so a "not found" from ldd could never fire here; the -# check is on where each library resolves *from*, not on whether it resolves at all. This is the -# check that would have caught the unbundled ICU 70 that lld shipped with for a while. -for exe in clang-23 lld llvm-ar cmake ninja; do +# Every bundled executable must resolve all its non-baseline shared libraries - transitively, ldd +# prints the whole closure - from inside the bundle, with LD_LIBRARY_PATH out of the picture. This +# host has them all installed, so a "not found" from ldd could never fire here; the check is on +# where each library resolves *from*, not on whether it resolves at all. A missing executable or +# a failing ldd is an error too, never a silent pass. This is the check that would have caught +# the unbundled libxml2 (#136) and ICU (#150) that lld shipped with for a while. +for exe in clang-22 lld llvm-ar cmake ninja; do + [[ -x "$work/bin/$exe" ]] || { echo "prepare-portable-tools.sh: $work/bin/$exe is missing" >&2; exit 1; } + if ! deps=$(env -u LD_LIBRARY_PATH LC_ALL=C ldd "$work/bin/$exe" 2>&1); then + [[ "$deps" == *"not a dynamic executable"* ]] && continue + echo "prepare-portable-tools.sh: ldd failed on $exe:" >&2 + echo "$deps" >&2 + exit 1 + fi while read -r soname arrow path rest; do [[ "$arrow" == "=>" ]] || continue soname=${soname##*/} [[ "$soname" =~ $baseline_sonames ]] && continue case "$path" in "$work"/*) ;; - *) echo "prepare-portable-tools.sh: $exe resolves $soname from ${path:-nowhere} instead of the bundle - it would break on any host without it" >&2 - exit 1 ;; + /*) echo "prepare-portable-tools.sh: $exe resolves $soname from $path instead of the bundle - it would break on any host without it" >&2 + exit 1 ;; + *) echo "prepare-portable-tools.sh: $exe cannot resolve $soname at all ($path $rest)" >&2 + exit 1 ;; esac - done < <(ldd "$work/bin/$exe") + done <<< "$deps" done echo "prepare-portable-tools.sh: testing the toolchain..." From 436fe19395613b54a1ae3757cd07e9f3515a50e0 Mon Sep 17 00:00:00 2001 From: Thomas Czerwonka Date: Sat, 5 Sep 2026 16:18:04 +0200 Subject: [PATCH 4/4] appimage: bundle libstdc++ too, RHEL 9-class hosts have GCC 11's Review pointed out a real hole in the "host baseline" argument: the LLVM binaries want GLIBCXX_3.4.30 (GCC 12) while RHEL 9 / Rocky / Alma / Amazon Linux 2023 ship a glibc new enough for them (2.34) but GCC 11's libstdc++, which stops at GLIBCXX_3.4.29. There the tools would die with "version GLIBCXX_3.4.30 not found". Ubuntu 22.04's libstdc++ (GCC 12.3) needs nothing newer than GLIBC_2.34 itself, so bundling it keeps the glibc floor where it is. The AppImage excludelist's reason for leaving libstdc++ to the host (dlopen'ed host libraries, GPU drivers) does not apply to a compiler and linker that dlopen nothing. - libstdc++ removed from baseline_sonames; libgcc_s/libz/liblzma stay (the symbol versions wanted from them are decades old). - bundle_private_deps now also gives the executable itself a $ORIGIN/../lib RUNPATH when it has non-baseline dependencies and no such entry yet: the LLVM binaries already have it, the ninja release binary does not and would otherwise keep loading the host libstdc++. - LICENSE.txt gains the libstdc++ license. Same harness as before, both LLVM 22 and LLVM 23 toolchains: lib/ now holds libstdc++.so.6 alongside libxml2/ICU, every copy carries $ORIGIN, ninja gets $ORIGIN/../lib, cmake is untouched, verification passes with LD_LIBRARY_PATH unset, link+run works on CachyOS. Co-Authored-By: Claude Fable 5.1 --- Launcher/prepare-portable-tools.sh | 45 ++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/Launcher/prepare-portable-tools.sh b/Launcher/prepare-portable-tools.sh index 20333d9c1..eb6fefd23 100755 --- a/Launcher/prepare-portable-tools.sh +++ b/Launcher/prepare-portable-tools.sh @@ -11,9 +11,9 @@ # libstdc++ headers). The raw release is ~1.9 GiB per arch (every LLVM backend, mlir, flang, lldb, # docs, tests); pruned it is ~500 MiB uncompressed / ~100 MiB compressed, verified against a real # build of this project. The release binaries are dynamically linked against a few libraries of -# the Ubuntu the LLVM project builds on (libxml2 and/or ICU, for lld), so those are bundled into -# lib/ too - see bundle_private_deps below - and every executable is verified to resolve nothing -# but the glibc baseline from the host. Needs patchelf on the build host. +# the Ubuntu the LLVM project builds on (libstdc++, plus libxml2 and/or ICU for lld), so those +# are bundled into lib/ too - see bundle_private_deps below - and every executable is verified to +# resolve nothing but the glibc baseline from the host. Needs patchelf on the build host. # # cmake: pruned from the official Kitware GitHub release tarball down to bin/cmake (not # ccmake/cmake-gui/cpack/ctest, which local-build.sh never invokes) plus the Modules/Templates @@ -161,20 +161,27 @@ ln -s lld "$work/bin/ld.lld" # Applied to every bundled executable; the verification at the end then proves each one resolves # everything outside the baseline from inside the bundle, with LD_LIBRARY_PATH out of the picture. # -# The baseline is what the host is expected to provide: glibc itself, plus libstdc++/libgcc_s/ -# libz/liblzma. Those four are deliberately NOT bundled, matching the AppImage excludelist -# (https://github.com/AppImageCommunity/pkg2appimage/blob/master/excludelist lists libstdc++.so.6, -# libgcc_s.so.1 and libz.so.1 by name) and what this toolchain always did: they ship with every -# glibc distro under these exact sonames, and the LLVM binaries' floor (GLIBC_2.34 / -# GLIBCXX_3.4.30, i.e. glibc 2.34+ with GCC 12's libstdc++) is met by every distro whose glibc is -# new enough to load them at all. libxml2 and ICU are different in kind: their sonames change -# across releases and distros carry exactly one, which is why they must travel with the bundle. -baseline_sonames='^(ld-linux-[^ ]*|libc|libm|libdl|libpthread|librt|libgcc_s|libstdc\+\+|libz|liblzma)\.so(\.|$)' +# The baseline is what the host is expected to provide: glibc itself, plus libgcc_s/libz/liblzma. +# Those are deliberately NOT bundled, matching the AppImage excludelist +# (https://github.com/AppImageCommunity/pkg2appimage/blob/master/excludelist) and what this +# toolchain always did: every glibc distro ships them under these exact sonames and the symbol +# versions the tools want from them (GCC_3.3, ZLIB_1.2.x) are decades old. libstdc++ is NOT +# baseline even though the excludelist has it: the LLVM binaries want GLIBCXX_3.4.30 (GCC 12), +# and there are current distros with a glibc new enough for the tools (GLIBC_2.34) but an older +# libstdc++ - RHEL 9 / Rocky / Alma / Amazon Linux 2023 all ship GCC 11's, which stops at +# GLIBCXX_3.4.29 - where the tools would die with "version GLIBCXX_3.4.30 not found". Ubuntu +# 22.04's libstdc++ (GCC 12.3) needs nothing newer than GLIBC_2.34 itself, so bundling it keeps +# the glibc floor where it is. The excludelist's reason for leaving libstdc++ to the host (an +# older bundled copy breaking host libraries dlopen'ed into the same process, GPU drivers above +# all) does not apply to a compiler and a linker that dlopen nothing. libxml2 and ICU are +# different in kind again: their sonames change across releases and distros carry exactly one. +baseline_sonames='^(ld-linux-[^ ]*|libc|libm|libdl|libpthread|librt|libgcc_s|libz|liblzma)\.so(\.|$)' bundle_private_deps() { # $1 = executable under $work/bin. Each shared library it needs beyond the baseline is copied - # (symlink-dereferenced, under its soname) into $work/lib, where the LLVM binaries' RUNPATH - # looks first, and given a $ORIGIN RUNPATH so its own dependencies resolve there too. - local exe=$1 deps soname arrow path rest + # (symlink-dereferenced, under its soname) into $work/lib and given a $ORIGIN RUNPATH so its + # own dependencies resolve there too. The executable itself gets a $ORIGIN/../lib RUNPATH if + # it does not already have one (the LLVM binaries do; the ninja release binary does not). + local exe=$1 deps soname arrow path rest needs_bundle=0 rpath [[ -x "$exe" ]] || { echo "prepare-portable-tools.sh: $exe is missing or not executable" >&2; exit 1; } if ! deps=$(LC_ALL=C ldd "$exe" 2>&1); then # LC_ALL=C: the "not a dynamic executable" text is matched below [[ "$deps" == *"not a dynamic executable"* ]] && return 0 # static: nothing to bundle @@ -186,6 +193,7 @@ bundle_private_deps() { [[ "$arrow" == "=>" ]] || continue # vdso / the ELF interpreter line soname=${soname##*/} # some ldd's print the interpreter as a path [[ "$soname" =~ $baseline_sonames ]] && continue + needs_bundle=1 if [[ "$path" != /* || ! -f "$path" ]]; then echo "prepare-portable-tools.sh: $(basename "$exe") needs $soname, which this host cannot resolve" >&2 exit 1 @@ -194,6 +202,11 @@ bundle_private_deps() { cp -L "$path" "$work/lib/$soname" patchelf --set-rpath '$ORIGIN' "$work/lib/$soname" done <<< "$deps" + (( needs_bundle )) || return 0 + rpath=$(patchelf --print-rpath "$exe") + if [[ ":$rpath:" != *':$ORIGIN/../lib:'* ]]; then + patchelf --set-rpath "${rpath:+$rpath:}"'$ORIGIN/../lib' "$exe" + fi } bundle_private_deps "$work/bin/lld" @@ -266,6 +279,8 @@ clang/lld/llvm-ar $llvm_version (pruned from the official LLVM release for Linux Shared libraries under lib/ that the tools above depend on, copied from the Ubuntu 22.04 packages the LLVM release was built against (bundled because their sonames differ between distributions): $(cd "$work/lib" && ls -1 *.so.* 2>/dev/null | sed 's/^/ /') + libstdc++ - GPL-3.0 with GCC Runtime Library Exception: + https://gcc.gnu.org/onlinedocs/libstdc++/manual/license.html libxml2 - MIT License: https://gitlab.gnome.org/GNOME/libxml2/-/blob/master/Copyright ICU - Unicode License: