Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 68 additions & 9 deletions scripts/inject-patched-wrapper.sh
Original file line number Diff line number Diff line change
@@ -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.
#
Expand All @@ -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 <<EOF
$(find "$ELECTROBUN_DIR/build" -type f -name bun -path '*/bin/*' 2>/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
Expand All @@ -31,12 +87,15 @@ fi
# The build emits the wrapper under build/<variant>/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 <<EOF
$(find "$ELECTROBUN_DIR/build" -type f -name 'libNativeWrapper*.so' 2>/dev/null)
EOF

if [ "$injected" -gt 0 ]; then
echo "[inject-wrapper] injected patched libNativeWrapper.so into $injected build artifact(s) (CEF CPU-spin fix)."
Expand Down
92 changes: 92 additions & 0 deletions scripts/install-pr.sh
Original file line number Diff line number Diff line change
@@ -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 <pr-number> # e.g. sh scripts/install-pr.sh 214
# sh scripts/install-pr.sh <branch-name> # 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 <pr-number | branch-name>"

# 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."
Loading