@@ -289,10 +289,70 @@ jobs:
289289 # loads at run time is a question for the descriptor, and answering it
290290 # by deleting files until something breaks is how a payload ends up
291291 # missing one on somebody else's machine.
292- mkdir -p ../out/bin ../out/share/qemu
292+ mkdir -p ../out/bin ../out/share/qemu ../out/lib
293293 cp qemu-system-x86_64 ../out/bin/
294294 cp -r "$SRC/pc-bios/." ../out/share/qemu/
295295
296+ # ⚠️ THE PAYLOAD MUST CARRY WHAT IT LINKS AGAINST, AND MEASURING THAT
297+ # IS WHAT CAUGHT THIS.
298+ #
299+ # An earlier build shipped only the emulator. Its measured closure:
300+ #
301+ # linux libpixman-1.so.0, libglib-2.0.so.0, libgio, libgobject,
302+ # libgmodule, libz.so.1, libzstd.so.1 -- none of them in
303+ # the payload, none of them core glibc
304+ # darwin /opt/homebrew/opt/{pixman,glib,zstd,gnutls,...}/lib/*.dylib
305+ # -- ABSOLUTE paths into the build machine's Homebrew
306+ #
307+ # So that payload installs and then fails on first run for anyone
308+ # without those libraries, and on macOS for anyone whose Homebrew is
309+ # not at the same prefix -- an Intel mac uses /usr/local. The index's
310+ # sibling packages avoid this by bundling: xPack's QEMU carries 51
311+ # shared objects and reaches them through its own relative rpath.
312+ #
313+ # ⚠️ SYSTEM LIBRARIES ARE DELIBERATELY NOT BUNDLED. Core libc, libm
314+ # and the macOS frameworks come from the running system by design;
315+ # carrying a second libc into a process is the two-glibcs shape this
316+ # index's own contract text names as a SIGSEGV source.
317+ if [ "$RUNNER_OS" = "Linux" ]; then
318+ ldd qemu-system-x86_64 \
319+ | awk '/=> \// {print $3}' \
320+ | grep -vE '/(libc|libm|libdl|librt|libpthread|libresolv|ld-linux[^ ]*)\.so' \
321+ | while read -r so; do cp -Ln "$so" ../out/lib/ 2>/dev/null || true; done
322+ # ⚠️ `patchelf` on the EMULATOR, not on a loader. This project's own
323+ # notes forbid patching a loader's own paths; setting a relative
324+ # rpath on an ordinary executable is the opposite case, and it is
325+ # what makes `out/lib` reachable without an environment variable.
326+ sudo apt-get install -y -qq patchelf
327+ patchelf --set-rpath '$ORIGIN/../lib' ../out/bin/qemu-system-x86_64
328+ for so in ../out/lib/*.so*; do patchelf --set-rpath '$ORIGIN' "$so" 2>/dev/null || true; done
329+ elif [ "$RUNNER_OS" = "macOS" ]; then
330+ # Mach-O records each dependency by the install name the LIBRARY
331+ # carries, so copying is not enough: every reference has to be
332+ # rewritten, and so does each bundled library's own id.
333+ otool -L qemu-system-x86_64 | tail -n +2 | awk '{print $1}' \
334+ | grep -E '^/(opt|usr/local)/' \
335+ | while read -r dy; do
336+ cp -Ln "$dy" ../out/lib/ 2>/dev/null || true
337+ install_name_tool -change "$dy" "@loader_path/../lib/$(basename "$dy")" \
338+ ../out/bin/qemu-system-x86_64 2>/dev/null || true
339+ done
340+ for dy in ../out/lib/*.dylib; do
341+ [ -e "$dy" ] || continue
342+ install_name_tool -id "@loader_path/$(basename "$dy")" "$dy" 2>/dev/null || true
343+ otool -L "$dy" | tail -n +2 | awk '{print $1}' | grep -E '^/(opt|usr/local)/' \
344+ | while read -r dep; do
345+ cp -Ln "$dep" ../out/lib/ 2>/dev/null || true
346+ install_name_tool -change "$dep" "@loader_path/$(basename "$dep")" "$dy" 2>/dev/null || true
347+ done
348+ done
349+ # ⚠️ Rewriting invalidates the ad-hoc signature; re-sign, or the
350+ # binary is killed by the kernel rather than merely warned about.
351+ codesign --force -s - ../out/bin/qemu-system-x86_64 2>/dev/null || true
352+ for dy in ../out/lib/*.dylib; do codesign --force -s - "$dy" 2>/dev/null || true; done
353+ fi
354+ rmdir ../out/lib 2>/dev/null || true
355+
296356 - name : Configure and build (Windows / MSYS2)
297357 if : runner.os == 'Windows'
298358 shell : msys2 {0}
@@ -523,6 +583,38 @@ jobs:
523583 # at run time is a question for the descriptor, and answering it by
524584 # deleting directories until something breaks is how a payload ends up
525585 # missing a firmware blob on somebody else's machine.
586+ # ⭐ THE PAYLOAD MUST NOT REACH OUTSIDE ITSELF, ASSERTED RATHER THAN
587+ # ASSUMED. This is the check that would have failed on the first
588+ # build, and it fails loudly instead of at somebody's first run.
589+ echo "── every library the emulator names ──"
590+ case "$RUNNER_OS" in
591+ Linux)
592+ readelf -d out/bin/qemu-system-x86_64 | grep -E 'NEEDED|RUNPATH|RPATH' || true
593+ # Resolve the closure the way the loader will, from the payload.
594+ bad=$(ldd out/bin/qemu-system-x86_64 2>/dev/null \
595+ | awk '/=> \// {print $3}' \
596+ | grep -vE "^$PWD/out/" \
597+ | grep -vE '/(libc|libm|libdl|librt|libpthread|libresolv|ld-linux[^ ]*)\.so' || true)
598+ if [ -n "$bad" ]; then
599+ echo "$bad"
600+ echo "::error::the payload links against libraries it does not carry; installing it would succeed and running it would not"
601+ exit 1
602+ fi
603+ echo "resolved entirely inside the payload, plus core glibc" ;;
604+ macOS)
605+ otool -L out/bin/qemu-system-x86_64 | tail -n +2 | awk '{print $1}'
606+ bad=$(otool -L out/bin/qemu-system-x86_64 | tail -n +2 | awk '{print $1}' \
607+ | grep -E '^/(opt|usr/local)/' || true)
608+ if [ -n "$bad" ]; then
609+ echo "$bad"
610+ echo "::error::the payload names absolute paths into this build machine's package manager; it would fail on any host without the same prefix"
611+ exit 1
612+ fi
613+ echo "no build-machine paths remain" ;;
614+ Windows)
615+ echo "(the DLL closure sits beside the exe; counted above)" ;;
616+ esac
617+
526618 echo "── the ten largest things under the prefix ──"
527619 du -sh out/* 2>/dev/null | sort -rh | head -10
528620 du -sh out/share/* 2>/dev/null | sort -rh | head -10
0 commit comments