diff --git a/scripts/inject-patched-wrapper.sh b/scripts/inject-patched-wrapper.sh index f4954fa5..c1f3c754 100755 --- a/scripts/inject-patched-wrapper.sh +++ b/scripts/inject-patched-wrapper.sh @@ -1,12 +1,15 @@ #!/bin/sh -# Inject the patched libNativeWrapper.so over the stock one that -# `electrobun build` downloads. The patched wrapper fixes a 100%-CPU busy-loop -# in CEF's browser process — see apps/loadout-overlay/vendor/README.md for the -# root cause and how the binary is produced. +# Post-build fixes injected over the artifacts `electrobun build` downloads. # -# This is the single source of truth for the swap, called from every build +# 1. Patched libNativeWrapper.so — fixes a 100%-CPU busy-loop in CEF's +# browser process; see apps/loadout-overlay/vendor/README.md for the +# root cause and how the binary is produced. +# 2. libstdc++ preload shim around the bun binary — see the comment at +# that step below. +# +# This is the single source of truth for both, called from every build # entry point (scripts/build.sh and apps/loadout-overlay's package.json build -# scripts) so no path can silently ship the stock (spinning) wrapper. Run it +# scripts) so no path can silently ship the stock artifacts. Run it # AFTER `electrobun build`. POSIX-sh, guarded, and idempotent: a missing vendor # file just warns and leaves the stock wrapper in place. # @@ -22,6 +25,59 @@ else ELECTROBUN_DIR="$SCRIPT_DIR/../apps/loadout-overlay" fi +# --- libstdc++ preload shim ------------------------------------------------- +# libNativeWrapper.so links webkit2gtk even in CEF mode, which pulls +# libjavascriptcoregtk into the overlay process ahead of libstdc++. Arch's +# webkit2gtk 2.52.5-1 regressed to EXPORTING libstdc++'s std::call_once +# internals (__once_proxy / __once_callable / __once_call) from JSC, so +# libstdc++'s own __once_proxy reads JSC's TLS slot while a deepbind-scoped +# writer (mesa's libLLVM, during radeonsi shader-compiler init) writes +# libstdc++'s — the callback reads back NULL and the process jumps to 0x0. +# Symptom: the overlay segfaults ~200ms in during CEF GL init on AMD. +# +# Preloading the real libstdc++ puts it first in the global lookup scope, so +# every object binds the once-machinery to the same copy and the split brain +# is gone. Bare soname so the loader resolves the right path per distro +# (/usr/lib on Arch, /usr/lib64 on Fedora/Bazzite). Harmless where the bug +# is absent: libstdc++ is in the process either way. +# +# The launcher REPLACES LD_PRELOAD for its bun child (upstream electrobun +# issue), so a unit-level Environment= can't reach the process that matters — +# hence this shim wrapping the bun binary itself. The real fix is rebuilding +# the wrapper without the webkit link; patchelf --remove-needed does NOT work +# (Zig emits eager GLOB_DAT relocs for address-taken webkit symbols). +# This step runs before the wrapper guard below so it applies even when the +# vendored wrapper is absent. +# The CEF tree contains space-laden names ("bun Helper (Renderer)", …), so +# iterate the find output line-by-line rather than word-splitting it. +shimmed=0 +while IFS= read -r bun; do + [ -n "$bun" ] || continue + # Idempotent: skip if this bun is already our shim script. + if head -c2 "$bun" 2>/dev/null | grep -q '#!'; then + continue + fi + mv "$bun" "$bun.real" + cat > "$bun" <<'SHIM' +#!/bin/sh +# libstdc++ must win symbol interposition over webkit2gtk's JSC, which +# (as of Arch webkit2gtk 2.52.5-1) leaks libstdc++'s std::call_once +# internals and splits its TLS state — crashing mesa's LLVM init on +# radeonsi. Preloading the real libstdc++ restores consistent bindings. +# See scripts/inject-patched-wrapper.sh for the full story. +export LD_PRELOAD="libstdc++.so.6${LD_PRELOAD:+:$LD_PRELOAD}" +exec "$(dirname "$0")/bun.real" "$@" +SHIM + chmod +x "$bun" + shimmed=$((shimmed + 1)) +done </dev/null) +EOF + +if [ "$shimmed" -gt 0 ]; then + echo "[inject-wrapper] installed libstdc++ preload shim over $shimmed bun binary(ies) (webkit2gtk JSC __once_proxy interposition fix)." +fi + PATCHED="$ELECTROBUN_DIR/vendor/libNativeWrapper.so" if [ ! -f "$PATCHED" ]; then echo "[inject-wrapper] WARNING: no vendored wrapper at $PATCHED — using stock wrapper (overlay will spin at 100% CPU)." >&2 @@ -31,12 +87,15 @@ fi # The build emits the wrapper under build//loadout-overlay-*/bin/. # Replace every libNativeWrapper*.so the build produced (the runtime dlopen's # libNativeWrapper.so; the _cef.so variant, if present, is harmless to match). -# Build paths contain no spaces, so an unquoted find expansion is safe here. +# Line-by-line for the same space-safety reason as the shim loop above. injected=0 -for so in $(find "$ELECTROBUN_DIR/build" -type f -name 'libNativeWrapper*.so' 2>/dev/null); do +while IFS= read -r so; do + [ -n "$so" ] || continue cp -f "$PATCHED" "$so" injected=$((injected + 1)) -done +done </dev/null) +EOF if [ "$injected" -gt 0 ]; then echo "[inject-wrapper] injected patched libNativeWrapper.so into $injected build artifact(s) (CEF CPU-spin fix)." diff --git a/scripts/install-pr.sh b/scripts/install-pr.sh new file mode 100755 index 00000000..8314d580 --- /dev/null +++ b/scripts/install-pr.sh @@ -0,0 +1,92 @@ +#!/bin/sh +# Build and install Loadout from a pull request, for testing a fix before +# it merges. Fetches the PR head via GitHub's refs/pull/N/head (works for +# same-repo and fork PRs alike), builds from source, and installs exactly +# like `bun run build-and-install`. +# +# Usage: +# sh scripts/install-pr.sh # e.g. sh scripts/install-pr.sh 214 +# sh scripts/install-pr.sh # e.g. sh scripts/install-pr.sh fix/my-branch +# +# Environment: +# LOADOUT_REPO git URL to fetch from (default: the public https URL, which +# needs no SSH keys; set to git@github.com:srsholmes/loadout.git +# or an https URL with a token if you need authenticated access) +# +# The build happens in a fresh shallow clone under ~/.cache/loadout-pr-test/, +# so an existing checkout (and any local changes in it) is never touched. +# (Deliberately NOT inside ~/.cache/loadout — the backend runs as root and +# owns that directory, so a user-run script can't create anything there.) +# Reverting to the official build afterwards is one command — the README's +# install one-liner reinstalls the current release over this. +# +# Requires: git, bun (https://bun.sh), and the build deps a normal +# `bun run build-and-install` needs. Prompts for sudo once, at install time, +# same as the regular installer. +set -eu + +REPO="${LOADOUT_REPO:-https://github.com/srsholmes/loadout.git}" +CACHE_ROOT="${XDG_CACHE_HOME:-$HOME/.cache}/loadout-pr-test" + +if [ -t 1 ]; then + RED='\033[0;31m'; GREEN='\033[0;32m'; BLUE='\033[0;34m'; NC='\033[0m' +else + RED=''; GREEN=''; BLUE=''; NC='' +fi +info() { printf "${BLUE}[INFO]${NC} %s\n" "$1"; } +ok() { printf "${GREEN}[OK]${NC} %s\n" "$1"; } +fail() { printf "${RED}[ERROR]${NC} %s\n" "$1" >&2; exit 1; } + +REF_ARG="${1:-}" +[ -n "$REF_ARG" ] || fail "usage: install-pr.sh " + +# A numeric argument is a PR number; anything else is a branch name. +# refs/pull/N/head is GitHub's read-only ref for a PR's current head — it +# exists for every open PR, including ones from forks, without needing to +# know the contributor's branch name. +case "$REF_ARG" in + *[!0-9]*) REF="refs/heads/$REF_ARG"; NAME="$REF_ARG" ;; + *) REF="refs/pull/$REF_ARG/head"; NAME="pr-$REF_ARG" ;; +esac + +command -v git >/dev/null 2>&1 || fail "git is required" + +# bun might not be on PATH in a fresh shell even when installed (its +# installer puts it in ~/.bun/bin and only edits interactive-shell rc files). +if ! command -v bun >/dev/null 2>&1; then + if [ -x "$HOME/.bun/bin/bun" ]; then + PATH="$HOME/.bun/bin:$PATH" + export PATH + else + fail "bun is required to build from source — install it first: curl -fsSL https://bun.sh/install | bash" + fi +fi + +WORKDIR="$CACHE_ROOT/$(printf '%s' "$NAME" | tr '/' '-')" +info "Fetching $REF into $WORKDIR..." +mkdir -p "$WORKDIR" +cd "$WORKDIR" +if [ ! -d .git ]; then + git init -q + git remote add origin "$REPO" +else + # A cached workdir may carry an origin from an earlier run with a + # different LOADOUT_REPO — keep it in sync with this run's choice. + git remote set-url origin "$REPO" +fi +# --depth 1 keeps the clone small; re-running the script re-fetches the +# PR's current head, so an updated PR just needs the same command again. +git fetch --depth 1 origin "$REF" || fail "could not fetch $REF from $REPO — check the PR number / branch name and your access to the repo" +git checkout -q --detach FETCH_HEAD +ok "Checked out $(git rev-parse --short HEAD) ($REF)" + +info "Installing dependencies..." +bun install --frozen-lockfile + +info "Building + installing (this replaces your current Loadout install; sudo will prompt once)..." +bun run build-and-install + +echo "" +ok "Installed Loadout from $NAME ($(git rev-parse --short HEAD))." +info "Verify: systemctl --user is-active loadout-overlay && journalctl --user -u loadout-overlay --since '-2min' | grep -c panic" +info "Revert to the official release at any time with the README install one-liner."