Files
jasp-desktop-bin/build/bundle-libs.sh
T
agw e009dc82d3 Retry the module build, count the bundles, and stop bundling the loader
The later bump worked - the build reached staging. Three new problems:

- renv restores concurrently and not always in dependency order: forecast
  was lazy-loaded while vctrs was still compiling ('there is no package
  called vctrs', six lines before 'vctrs 0.7.3 built from source in 52s').
  That aborts the whole sapply() loop, so one race cost 20 modules. Retry
  the pass up to three times; renv skips what is installed, so each pass
  converges.

- The guard only required a non-empty Modules/local, so a build with 2 of
  22 modules passed it and went on to stage and package. It now requires
  the full expected count and lists what it did get.

- bundle-libs.sh crashed on the dynamic loader: ldd prints it as
  '/lib64/ld-linux-x86-64.so.2 => ...', so the first column is a path and
  the ld-linux* denylist entry never matched. Basename the soname before
  the check.
2026-09-02 20:45:00 +02:00

110 lines
4.3 KiB
Bash
Executable File

#!/bin/bash
# Make the staged JASP tree independent of Arch's rolling libraries.
#
# Copies every shared library JASP and its bundled R depend on into
# $PREFIX/lib, except a denylist of genuinely stable system components, then
# rewrites the rpath of every ELF object in the tree to look there first.
#
# The alternative — declaring versioned `depends` on icu, jsoncpp and friends —
# is worse on a rolling distro: when Arch ships the next ICU, pacman would
# refuse to upgrade it while JASP is installed, blocking the user's whole
# system upgrade. Bundling means we never constrain the user's system.
#
# $1 staged install tree (contains .$PREFIX)
# $2 install prefix, e.g. /opt/jasp-desktop
set -euo pipefail
STAGE_DIR="${1:?staged tree is required}"
PREFIX="${2:-/opt/jasp-desktop}"
LIBDIR="${STAGE_DIR}${PREFIX}/lib"
# Libraries that must keep coming from the system. Bundling any of these either
# breaks (glibc must match the running kernel/loader) or causes symbol clashes
# with the system Qt6 that JASP still links against.
#
# Qt6 itself is deliberately here: variant B keeps Qt as a system dependency and
# relies on Qt's binary-compatibility guarantee within Qt 6.
is_system_lib() {
case "$1" in
ld-linux*|libc.so*|libm.so*|libdl.so*|libpthread.so*|librt.so*|\
libresolv.so*|libnsl.so*|libutil.so*|libcrypt.so*|libanl.so*|\
libstdc++.so*|libgcc_s.so*|libatomic.so*) return 0 ;;
libQt6*) return 0 ;;
libGL*.so*|libEGL*.so*|libGLX*.so*|libOpenGL.so*|libGLdispatch.so*|\
libgbm.so*|libdrm*.so*|libvulkan.so*) return 0 ;;
libX11*.so*|libXext.so*|libXrender.so*|libXrandr.so*|libXi.so*|\
libXfixes.so*|libXcursor.so*|libXinerama.so*|libXcomposite.so*|\
libXdamage.so*|libXtst.so*|libXau.so*|libXdmcp.so*|libXss.so*|\
libxcb*.so*|libwayland*.so*|libxkbcommon*.so*) return 0 ;;
libdbus-1.so*|libsystemd.so*|libudev.so*) return 0 ;;
libasound.so*|libpulse*.so*) return 0 ;;
libfontconfig.so*|libfreetype.so*) return 0 ;;
libz.so*) return 0 ;;
*) return 1 ;;
esac
}
mkdir -p "$LIBDIR"
# Every ELF object in the tree: the JASP binary, libR, and the compiled .so of
# each of the several hundred R module packages.
mapfile -t elf_files < <(
find "$STAGE_DIR" -type f \( -name '*.so' -o -name '*.so.*' -o -perm -u+x \) \
-exec sh -c 'file -b "$1" | grep -q "^ELF" && echo "$1"' _ {} \;
)
echo ":: found ${#elf_files[@]} ELF objects in the staged tree"
# Resolving deps is iterative: a bundled library pulls in its own dependencies.
copied_any=1
round=0
while [[ "$copied_any" -eq 1 ]]; do
copied_any=0
round=$((round + 1))
mapfile -t scan < <(
printf '%s\n' "${elf_files[@]}"
find "$LIBDIR" -type f -name '*.so*' 2>/dev/null || true
)
for obj in "${scan[@]}"; do
[[ -f "$obj" ]] || continue
# ldd resolves against the build container, which is exactly the Arch
# userland we want to capture.
while read -r soname path; do
[[ -n "$path" && -f "$path" ]] || continue
# ldd reports some entries with an absolute path in the first column —
# the dynamic loader shows up as "/lib64/ld-linux-x86-64.so.2 => ...".
# Without this the denylist misses it and the copy below tries to create
# $LIBDIR/lib64/ld-linux-x86-64.so.2 in a directory that does not exist.
soname="${soname##*/}"
is_system_lib "$soname" && continue
[[ -f "$LIBDIR/$soname" ]] && continue
# Never bundle something that already lives inside our own tree.
case "$path" in "$STAGE_DIR"*|"$PREFIX"*) continue ;; esac
cp -L "$path" "$LIBDIR/$soname"
echo ":: bundled $soname"
copied_any=1
done < <(ldd "$obj" 2>/dev/null | awk '$2=="=>" && $3 ~ /^\// {print $1, $3}')
done
done
echo ":: bundling converged after $round round(s)"
chmod u+w "$LIBDIR"/*.so* 2>/dev/null || true
# An absolute rpath is safe here because the tarball always unpacks to $PREFIX;
# it avoids $ORIGIN arithmetic across bin/, R/lib/R/lib/ and the per-package
# libs/ directories, which all sit at different depths.
echo ":: setting rpath on ELF objects"
patched=0
for obj in "${elf_files[@]}" "$LIBDIR"/*.so*; do
[[ -f "$obj" ]] || continue
if patchelf --set-rpath "${PREFIX}/lib" "$obj" 2>/dev/null; then
patched=$((patched + 1))
fi
done
echo ":: rpath set on $patched objects"
echo ":: bundled library count: $(find "$LIBDIR" -name '*.so*' | wc -l)"
du -sh "$LIBDIR"