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
55 changes: 55 additions & 0 deletions .github/workflows/build-engine.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: build-engine

# Builds a relocatable WebKitGTK engine from the runner's apt WebKitGTK and
# proves it loads from the store with its dependency closure resolved via the
# engine's own $ORIGIN rpaths. This is the start of the hosted-engine pipeline;
# it runs on demand and on the engine branch, not on every push.
on:
workflow_dispatch:
push:
branches: [feat/relocatable-engine]

jobs:
build:
name: relocate WebKitGTK (ubuntu)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install WebKitGTK 6.0 + GTK 4 + patchelf
run: |
sudo apt-get update
sudo apt-get install -y libwebkitgtk-6.0-4 libgtk-4-1 patchelf

- uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- run: bun install --frozen-lockfile

- name: Build the relocatable engine
run: |
chmod +x tools/engine/build-webkitgtk-linux.sh
WK_VER=$(dpkg-query -W -f='${Version}' libwebkitgtk-6.0-4 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
ENGINE_ID="webkitgtk-6.0-${WK_VER:-0.0.0}-built1-linux-x64"
echo "ENGINE_ID=$ENGINE_ID" >> "$GITHUB_ENV"
echo "Building engine id: $ENGINE_ID"
mkdir -p "$RUNNER_TEMP/store"
tools/engine/build-webkitgtk-linux.sh "$RUNNER_TEMP/store" "$ENGINE_ID"
touch "$RUNNER_TEMP/store/$ENGINE_ID/INSTALLATION_COMPLETE"
echo "--- engine tree ---"
find "$RUNNER_TEMP/store/$ENGINE_ID" -maxdepth 2 | head -40

- name: Prove it loads from the store via $ORIGIN (no LD_LIBRARY_PATH)
run: |
set -o pipefail
OUT=$(BUNMASKA_ENGINES_PATH="$RUNNER_TEMP/store" \
BUNMASKA_WEBKIT_ID="$ENGINE_ID" \
bun tools/engine/engine-load-probe.ts 2>&1)
echo "probe: $OUT"
N=$(printf '%s\n' "$OUT" | sed -n 's/^STORE_LIBS=//p')
if [ -z "$N" ] || [ "$N" -lt 5 ]; then
echo "FAIL: ${N:-0} libraries loaded from the relocated engine (expected the closure via \$ORIGIN)"
exit 1
fi
echo "PASS: $N libraries loaded from the relocated engine via \$ORIGIN"
11 changes: 9 additions & 2 deletions src/main/engine/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,12 +152,15 @@ export const engineLibPath = (resolution: EngineResolution, soname: string): str
/**
* The environment overrides needed for a pinned engine: prepend its `lib/` to
* `LD_LIBRARY_PATH` so its bundled GTK/libsoup/ICU/GStreamer win over the
* distro's, and point `GIO_EXTRA_MODULES` at its gio modules. Empty in system mode.
* distro's, point `GIO_EXTRA_MODULES` at its gio modules, and point
* `WEBKIT_EXEC_PATH` at its `libexec/` so WebKit spawns the engine's OWN helper
* processes (WebKitNetworkProcess/WebProcess/GPUProcess) rather than the system's.
* Empty in system mode.
*/
export const engineEnv = (
resolution: EngineResolution,
env: StoreEnv,
): { LD_LIBRARY_PATH?: string; GIO_EXTRA_MODULES?: string } => {
): { LD_LIBRARY_PATH?: string; GIO_EXTRA_MODULES?: string; WEBKIT_EXEC_PATH?: string } => {
if (resolution.mode !== 'pinned' || resolution.libDir === undefined) {
return {};
}
Expand All @@ -166,6 +169,7 @@ export const engineEnv = (
LD_LIBRARY_PATH:
prior !== undefined && prior.length > 0 ? `${resolution.libDir}:${prior}` : resolution.libDir,
GIO_EXTRA_MODULES: join(resolution.libDir, 'gio', 'modules'),
WEBKIT_EXEC_PATH: join(resolution.libDir, '..', 'libexec'),
};
};

Expand Down Expand Up @@ -208,6 +212,9 @@ export const prepareEngineForLoad = (
if (env.GIO_EXTRA_MODULES !== undefined) {
target['GIO_EXTRA_MODULES'] = env.GIO_EXTRA_MODULES;
}
if (env.WEBKIT_EXEC_PATH !== undefined) {
target['WEBKIT_EXEC_PATH'] = env.WEBKIT_EXEC_PATH;
}
// Auto-link only a STORE pin (it has an id + root); an explicit-dir pin and
// system mode have nothing to refcount.
if (
Expand Down
3 changes: 2 additions & 1 deletion tests/unit/main/engine-resolve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,12 @@ describe('engineLibPath', () => {
});

describe('engineEnv', () => {
test('pinned -> prepends the lib dir to LD_LIBRARY_PATH and sets GIO_EXTRA_MODULES', () => {
test('pinned -> sets LD_LIBRARY_PATH, GIO_EXTRA_MODULES, and WEBKIT_EXEC_PATH', () => {
const r = resolve({ env: {}, readBakedId: () => ID });
const env = engineEnv(r, { LD_LIBRARY_PATH: '/usr/lib' });
expect(env.LD_LIBRARY_PATH).toBe(`${ROOT}/${ID}/lib:/usr/lib`);
expect(env.GIO_EXTRA_MODULES).toBe(`${ROOT}/${ID}/lib/gio/modules`);
expect(env.WEBKIT_EXEC_PATH).toBe(`${ROOT}/${ID}/libexec`);
});

test('pinned with no prior LD_LIBRARY_PATH -> just the lib dir', () => {
Expand Down
103 changes: 103 additions & 0 deletions tools/engine/build-webkitgtk-linux.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
#
# Build a RELOCATABLE WebKitGTK 6.0 engine directory for the Bunmaska engine
# store. It relocates the system WebKitGTK + GTK + their shared-object closure
# into a self-contained tree whose libraries find each other via `$ORIGIN`, so
# the engine can be `dlopen`ed from `~/.bunmaska/webkit/<id>/` independent of the
# distro's own WebKitGTK.
#
# This is the apt-relocate path (proves the mechanism + produces a usable engine
# on a matching/newer glibc). A truly cross-distro build compiles on an old-glibc
# base; that is a later refinement — the structure here is the same.
#
# Usage: build-webkitgtk-linux.sh <out-dir> <engine-id>
# Produces: <out-dir>/<engine-id>/{lib/,libexec/,engine.json}
#
set -euo pipefail

OUT_DIR="${1:?usage: build-webkitgtk-linux.sh <out-dir> <engine-id>}"
ENGINE_ID="${2:?usage: build-webkitgtk-linux.sh <out-dir> <engine-id>}"
SONAME="libwebkitgtk-6.0.so.4"
GTK_SONAME="libgtk-4.so.1"

ENGINE_DIR="${OUT_DIR}/${ENGINE_ID}"
LIB_DIR="${ENGINE_DIR}/lib"
LIBEXEC_DIR="${ENGINE_DIR}/libexec"

# Core glibc/loader libraries that must stay the system's — bundling them causes
# loader/symbol conflicts. Everything else in the closure gets bundled.
KEEP_SYSTEM="ld-linux-x86-64.so.2 ld-linux-aarch64.so.1 libc.so.6 libm.so.6 libpthread.so.0 libdl.so.2 librt.so.1 libresolv.so.2 libgcc_s.so.1"

log() { printf ' • %s\n' "$*"; }

command -v patchelf >/dev/null || { echo "patchelf is required (apt install patchelf)"; exit 1; }

# Resolve a soname to its absolute path via ldconfig.
resolve_soname() {
ldconfig -p | grep -F "$1" | head -1 | sed -E 's/.*=>\s*//'
}

WEBKIT_PATH="$(resolve_soname "$SONAME")"
GTK_PATH="$(resolve_soname "$GTK_SONAME")"
[ -n "$WEBKIT_PATH" ] || { echo "system $SONAME not found"; exit 1; }
[ -n "$GTK_PATH" ] || { echo "system $GTK_SONAME not found"; exit 1; }
log "WebKitGTK: $WEBKIT_PATH"
log "GTK: $GTK_PATH"

mkdir -p "$LIB_DIR" "$LIBEXEC_DIR"

is_kept() { case " $KEEP_SYSTEM " in *" $1 "*) return 0;; *) return 1;; esac; }

# Collect the full transitive .so closure of both roots (ldd is transitive).
collect_closure() {
ldd "$1" 2>/dev/null | awk '{ for (i=1;i<=NF;i++) if ($i ~ /^\//) print $i }'
}

copy_lib() {
local src="$1" name
name="$(basename "$src")"
is_kept "$name" && return 0
[ -f "$LIB_DIR/$name" ] && return 0
cp -L "$src" "$LIB_DIR/$name"
chmod u+w "$LIB_DIR/$name"
}

log "Bundling the shared-object closure…"
# Copy the two roots first (preserve their sonames), then the closure.
cp -L "$WEBKIT_PATH" "$LIB_DIR/$SONAME"; chmod u+w "$LIB_DIR/$SONAME"
cp -L "$GTK_PATH" "$LIB_DIR/$GTK_SONAME"; chmod u+w "$LIB_DIR/$GTK_SONAME"
{ collect_closure "$WEBKIT_PATH"; collect_closure "$GTK_PATH"; } | sort -u | while IFS= read -r so; do
[ -f "$so" ] && copy_lib "$so"
done

COUNT="$(find "$LIB_DIR" -name '*.so*' | wc -l | tr -d ' ')"
log "Bundled ${COUNT} libraries"

log "Rewriting RPATHs to \$ORIGIN…"
find "$LIB_DIR" -name '*.so*' -type f | while IFS= read -r so; do
patchelf --set-rpath '$ORIGIN' "$so" 2>/dev/null || true
done

# WebKit spawns helper processes; copy them best-effort so render works later.
log "Copying WebKit helper processes (best-effort)…"
HELPER_SRC="$(dirname "$WEBKIT_PATH")/webkitgtk-6.0"
for helper in WebKitNetworkProcess WebKitWebProcess WebKitGPUProcess; do
if [ -f "$HELPER_SRC/$helper" ]; then
cp -L "$HELPER_SRC/$helper" "$LIBEXEC_DIR/$helper"
chmod u+w "$LIBEXEC_DIR/$helper"
patchelf --set-rpath '$ORIGIN/../lib' "$LIBEXEC_DIR/$helper" 2>/dev/null || true
log " + $helper"
else
log " ! $helper not found at $HELPER_SRC (render may need it)"
fi
done

cat > "${ENGINE_DIR}/engine.json" <<JSON
{
"id": "${ENGINE_ID}",
"soname": "${SONAME}",
"note": "relocatable WebKitGTK 6.0, apt-relocated; libs find each other via \$ORIGIN"
}
JSON

log "Engine built at ${ENGINE_DIR} (${COUNT} libs)"
23 changes: 23 additions & 0 deletions tools/engine/engine-load-probe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Build-engine probe: load WebKitGTK through the real loader from a relocated
* engine in the store, then count how many shared objects resolved FROM the
* engine dir. A high count proves the `$ORIGIN` rpaths make the whole dependency
* closure self-contained (not just `libwebkitgtk` itself). Run with
* `BUNMASKA_ENGINES_PATH` + `BUNMASKA_WEBKIT_ID` set, and deliberately WITHOUT
* `LD_LIBRARY_PATH`, so the deps must resolve via the engine's own rpaths.
*/

import { readFileSync } from 'node:fs';
import { loadWebKitGtkFFI } from '../../src/main/platform/linux/webkitgtk-ffi';

loadWebKitGtkFFI(); // dlopen the relocated engine; its NEEDED libs resolve via $ORIGIN

const maps = readFileSync('/proc/self/maps', 'utf8');
const store = process.env['BUNMASKA_ENGINES_PATH'] ?? '/nonexistent-store';
const fromEngine = new Set(
maps
.split('\n')
.map((line) => line.trim().split(/\s+/).pop() ?? '')
.filter((path) => path.startsWith(store) && path.includes('.so')),
);
process.stdout.write(`STORE_LIBS=${fromEngine.size}\n`);
Loading