Notcurses-Native.git | scripts/ci/ | bundle-elf.sh


#!/usr/bin/env bash # Stage notcurses .so libs into bundle/, recursively walk ldd to copy # ffmpeg sibling shared libs into the same directory, set DT_RUNPATH # to $ORIGIN on every shipped .so so they find each other at load # time, and strip. # # Honours $EXTRA_SKIP_LIBS (space-separated glob patterns) — alpine # musl callers pass 'ld-musl-*.so.1 libc.musl-*.so.1' so we don't # bundle musl's loader / libc (alpine systems already have them; # bundling would produce a 2-libc conflict). # # Reads the notcurses build dir from $NOTCURSES_SRC_DIR/build — # caller (build-linux-*.sh) sets this after fetch-notcurses-source.sh # resolves the pinned SHA. # # Designed to run INSIDE a build container (manylinux2014 for glibc, # alpine:3.20 for musl) — needs ldd, patchelf, strip on PATH. The # extra LD_LIBRARY_PATH the caller may have exported (to point at # workspace-cached ffmpeg) is preserved through to ldd via the env. set -euxo pipefail : "${NOTCURSES_SRC_DIR:?must be set — call scripts/ci/fetch-notcurses-source.sh first}" # Capture the workspace root BEFORE we cd into the build dir, so # absolute-path copies don't depend on `cd ../../..` relative # math (the build dir lives under _ci-cache/notcurses-source// # which isn't a fixed depth from the workspace). workspace="$PWD" mkdir -p "$workspace/bundle" ( cd "$NOTCURSES_SRC_DIR/build" for lib in libnotcurses libnotcurses-core libnotcurses-ffi; do found=$(find . -name "${lib}.so.*" -type f | sort | tail -1) [[ -z "$found" ]] && found=$(find . -name "${lib}.so" -type f | head -1) cp "$found" "$workspace/bundle/${lib}.so" done ) # Skiplist: stay-dynamic system libraries. Bundling these would # produce crt clashes / loader conflicts. is_system_lib() { local name="$1" case "$name" in libc.so.*|libm.so.*|libpthread.so.*|libdl.so.*| \ librt.so.*|libstdc++.so.*|ld-linux*|libgcc_s.so.*| \ libresolv.so.*|libnsl.so.*|linux-vdso.so.*| \ libutil.so.*|libcrypt.so.*) return 0 ;; esac # Caller-supplied extra patterns (alpine musl loader names, etc.). if [[ -n "${EXTRA_SKIP_LIBS:-}" ]]; then local pat for pat in $EXTRA_SKIP_LIBS; do # shellcheck disable=SC2053 [[ "$name" == $pat ]] && return 0 done fi return 1 } process_deps() { local lib="$1" # Strip any `(...)` trailing marker from ldd output and skip # non-absolute paths (catches `not found`, bare filenames, etc.). ldd "$lib" 2>/dev/null \ | sed -n 's/ (.*)$//; s/^[[:space:]]*\([^ ]\+\) => \(.\+\)$/\1\t\2/p' \ | while IFS=$'\t' read -r name path; do is_system_lib "$name" && continue [[ -z "$path" ]] && continue [[ "$path" != /* && "$path" != ?:[/\\]* ]] && continue if [[ ! -f "bundle/$name" ]]; then cp "$path" "bundle/$name" echo "Bundled: $name (from $path)" process_deps "bundle/$name" # recurse fi done } for lib in bundle/libnotcurses.so bundle/libnotcurses-core.so bundle/libnotcurses-ffi.so; do process_deps "$lib" done # Set rpath to $ORIGIN on every shipped .so so they find siblings # in their own directory at load time. NO `|| true` here — silent # patchelf failures would leave dylibs falling back to system paths # at user-runtime; we fail loud instead. find bundle -name '*.so*' -type f | while read f; do patchelf --set-rpath '$ORIGIN' "$f" done find bundle -name '*.so*' -type f -exec strip --strip-unneeded {} \; 2>/dev/null || true # Hard audit: every non-symlink .so in bundle/ must now report # RUNPATH=$ORIGIN. patchelf above can't set it if a binary has no # PT_DYNAMIC, or already has DT_RPATH instead of DT_RUNPATH, or # similar quirk — and the per-file failures get masked by the # pipeline. Verify and fail loudly here. echo "--- RUNPATH audit ---" fail=0 for f in $(find bundle -name '*.so*' -type f ! -type l); do rpath=$(patchelf --print-rpath "$f" 2>/dev/null) if [[ "$rpath" != '$ORIGIN' ]]; then echo "::error file=$f::RUNPATH is '$rpath', expected '\$ORIGIN'" fail=1 else echo "ok: $f RUNPATH=\$ORIGIN" fi done if (( fail != 0 )); then echo "❌ One or more bundled .so files don't have RUNPATH=\$ORIGIN." echo " They'll fall back to /lib64/ etc. at user-runtime instead" echo " of finding bundle siblings. Investigate the patchelf step." exit 1 fi echo "--- bundle contents ---" ls -la bundle/ # Diagnostic ldd in CLEAN env: the build set LD_LIBRARY_PATH + # CMAKE_PREFIX_PATH etc. pointing at $CACHE/lib so cmake/configure # could find source-built deps. Those env vars are still set when # bundle-elf.sh runs, so a plain `ldd bundle/libnotcurses.so` would # resolve via LD_LIBRARY_PATH instead of the binary's $ORIGIN # RUNPATH, painting a misleading picture (deps showing as resolving # to $CACHE/lib that won't exist on user machines). `env -i` wipes # all env vars except PATH, mimicking the user-runtime ld.so view. echo "--- ldd in clean env (user-runtime view) ---" LDD_OUT=$(env -i PATH=/usr/bin:/bin ldd bundle/libnotcurses.so 2>&1 || true) echo "$LDD_OUT" # Hard audit: every "lib.so.N => path" line must have its path # inside bundle/ UNLESS the lib is a system library we deliberately # don't bundle (libc, libm, libpthread, libdl, librt, libgcc_s, # linux-vdso, libstdc++, libresolv, libnsl, libutil, libcrypt, # libcrypto for Linux; ld-linux* / ld-musl* loaders). echo "--- non-system dep resolution audit ---" fail=0 while IFS= read -r line; do # Skip lines that aren't "X => Y" form (linux-vdso prints just # "linux-vdso.so.1 (0x...)" without an arrow — those are system). if ! [[ "$line" =~ \=\> ]]; then continue fi # " libfoo.so.N => /some/path (0x...)" — strip leading # whitespace, split on ' => '. libname="${line#"${line%%[![:space:]]*}"}" # ltrim libname="${libname%% =>*}" rest="${line#*=> }" libpath="${rest% \(*}" case "$libname" in libc.so.*|libc.musl-*.so.*|\ libm.so.*|libpthread.so.*|libdl.so.*|\ librt.so.*|libstdc++.so.*|libgcc_s.so.*|libresolv.so.*|\ libnsl.so.*|libutil.so.*|libcrypt.so.*|libcrypto.so.*|\ libssl.so.*|ld-linux*|ld-musl*|linux-vdso.so.*) # musl's libc is `libc.musl-.so.1` (and confusingly # ldd reports the same loader at both `libc.musl-.so.1` # AND `ld-musl-.so.1` — they're literally the same # binary; the first is libc, the second is the dynamic # loader, and musl ships them as one file via symlink). # Both names are system libs that must stay dynamic. continue ;; esac case "$libpath" in not\ found) echo "::error::Non-system lib '$libname' is NOT FOUND in clean env" fail=1 ;; bundle/*|*/bundle/*) # Inside bundle/ — matches both relative (`bundle/foo`, # which is what ldd emits when ld.so resolves via # $ORIGIN from the binary's own dir) and absolute # (`/work/bundle/foo`) forms. : ;; *) echo "::error::Non-system lib '$libname' resolves to '$libpath' (outside bundle/)" fail=1 ;; esac done <<< "$LDD_OUT" if (( fail != 0 )); then echo "❌ Bundle audit failed: one or more non-system deps don't" echo " resolve to bundle/ at user-runtime. The shipped tarball" echo " would fall back to system paths (different libs / missing" echo " libs) on the user's machine." exit 1 fi echo "✅ Bundle audit passed: every non-system dep resolves inside bundle/"