Skip to content

build-and-release

build-and-release #21

Workflow file for this run

name: build-and-release
# Builds the full Gecko->wasm engine and the gecko.js package, then attaches the
# packaged library (dist/gecko.js + dist/gecko.wasm + the wasm/ engine artifacts)
# to a GitHub Release. (embed-xul/ + embed-chrome/ are legacy stubs and are NOT built
# here -- the maintained package is gecko.js/.)
#
# !!! HEAVY BUILD β€” READ FIRST !!!
# A full Gecko->wasm build is enormous: ~26 GB objdir, a ~3.4 GB (relocatable) libxul.so,
# a ~245 MB gecko.wasm, and 25-50 min on 12 cores. On a standard hosted runner
# (4 vCPU / 16 GB RAM / ~14 GB free disk) this is marginal on ALL THREE axes:
# - disk: we free ~30 GB below; still tight. Prefer a larger/self-hosted runner.
# - ram: the final emcc relink (wasm-opt over the 245 MB module) spikes hard, so we
# add a best-effort swapfile below. Engine LTO is OFF by default: the libxul
# LTO link needs >54 GiB (it OOM-killed an 8-core/31 GiB runner even with
# ThinLTO + a --thinlto-jobs cap + 23 GiB swap). Re-enable only on a ~64 GiB+
# host with `make ... LTO=1` (see mozconfig + Makefile LTO knob).
# - time: on 4 vCPU expect ~2-4 h (job cap is 6 h).
# If it doesn't fit, switch `runs-on` to a larger runner (e.g. a self-hosted one
# or GitHub larger runners) β€” the steps are otherwise unchanged.
#
# This workflow is a STARTING POINT; the local upgrade to emscripten 6.0.1 was
# verified with a DEBUG build. The RELEASE path here runs optimized + wasm-opt but
# WITHOUT engine LTO (LTO doesn't fit this runner β€” see above).
on:
push:
tags: ['v*']
workflow_dispatch:
permissions:
contents: write # create releases / upload assets
env:
EMSDK_VERSION: '6.0.1' # emcc 6.0.1 / clang 23 (bundles binaryen v130)
RUST_VERSION: '1.95.0'
PNPM_VERSION: '9.12.0' # matches root package.json "packageManager"
MOZBUILD_STATE_PATH: ${{ github.workspace }}/.mozbuild
MACH_NO_TERMINAL_FOOTER: '1'
jobs:
build:
runs-on: firefox
timeout-minutes: 350
steps:
- name: Checkout the embedder + build harness (this repo)
uses: actions/checkout@v4
# - name: Free up disk space (Gecko objdir is ~26 GB)
# uses: jlumbroso/free-disk-space@main
# with:
# tool-cache: true
# android: true
# dotnet: true
# haskell: true
# large-packages: true
# swap-storage: true
- name: Install build prerequisites (clang/libclang for bindgen, etc.)
run: |
sudo apt-get update
# Host toolchain + misc build deps. `clang`/`clang++` here only serve
# mozconfig's HOST_CC/HOST_CXX (host-side codegen tools) -- their version
# is irrelevant. The bindgen libclang is installed separately below.
sudo apt-get install -y --no-install-recommends \
clang llvm-dev libpulse-dev python3 mercurial unzip zstd rsync \
wget lsb-release gnupg software-properties-common
# bindgen (gecko-profiler + other rust crates) parses emscripten 6.0.1's
# libc++ headers, which are LLVM 23 and use clang>=19 builtins: __is_convertible
# (older clang errors "'_Tp' does not refer to a value") and __builtin_ctzg/
# __builtin_clzg ("use of undeclared identifier"). Ubuntu's stock libclang is
# 18 -- too old -- so bindgen fails with "Unable to generate bindings" and the
# whole engine build dies at force-cargo-library-build long before libxul links.
# Install a matching-era libclang (21, same as mach-bootstrap's clang, which is
# what works locally) from apt.llvm.org and point bindgen at it via LIBCLANG_PATH.
LLVM_VER=21
wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh
chmod +x /tmp/llvm.sh
sudo /tmp/llvm.sh ${LLVM_VER}
sudo apt-get install -y --no-install-recommends libclang-${LLVM_VER}-dev
# mozconfig.full.emscripten reads $LIBCLANG_PATH for --with-libclang-path.
echo "LIBCLANG_PATH=/usr/lib/llvm-${LLVM_VER}/lib" >> "$GITHUB_ENV"
# Install emscripten from a FRESH emsdk clone (its bundled tags know 6.0.x; an
# older/cached emsdk may not). em_config reads $EMSDK and resolves binaryen at
# $EMSDK/upstream -- emsdk 6.0.1 bundles binaryen v130, which accepts the wasm
# features rust/LLVM emit, so NO separate binaryen install is needed anymore.
- name: Set up emscripten ${{ env.EMSDK_VERSION }}
run: |
git clone --depth 1 https://github.com/emscripten-core/emsdk.git "$HOME/emsdk"
"$HOME/emsdk/emsdk" install "${EMSDK_VERSION}"
"$HOME/emsdk/emsdk" activate "${EMSDK_VERSION}"
echo "EMSDK=$HOME/emsdk" >> "$GITHUB_ENV"
echo "$HOME/emsdk/upstream/emscripten" >> "$GITHUB_PATH"
echo "$HOME/emsdk" >> "$GITHUB_PATH"
"$HOME/emsdk/upstream/emscripten/emcc" --version | head -1
"$HOME/emsdk/upstream/bin/wasm-opt" --version | head -1
- name: Set up Rust ${{ env.RUST_VERSION }} (+ rust-src, wasm target)
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ env.RUST_VERSION }}
components: rust-src
targets: wasm32-unknown-emscripten
# gecko.js's package build (rspack bundle) runs under pnpm; the repo pins
# pnpm via package.json "packageManager", so let corepack provide it.
- name: Set up Node + pnpm ${{ env.PNPM_VERSION }}
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Enable corepack/pnpm
run: |
corepack enable
corepack prepare "pnpm@${PNPM_VERSION}" --activate
pnpm --version
# em_config is committed and portable (reads $EMSDK; binaryen defaults to the
# emsdk's bundled $EMSDK/upstream), so no shim or config generation is needed.
- name: Clone the pinned Gecko engine fork (depth 1)
run: make firefox
# Memory headroom safety net. Engine LTO is OFF by default (it needs >54 GiB and
# OOM-killed this 8-core/~31 GiB runner), so the big remaining spike is wasm-opt
# -O3 over the ~250 MB module at the final relink. Adding swap lets a peak above
# RAM spill instead of getting OOM-killed (SIGTERM / exit 143). Fully best-effort
# -- never fails the job (a runner may already have swap, lack disk, or forbid swapon).
- name: Add swap headroom (best-effort)
run: |
if swapon --show 2>/dev/null | grep -q .; then
echo ">> swap already present:"; swapon --show; free -h; exit 0
fi
SW=/swapfile
( sudo fallocate -l 24G "$SW" || sudo dd if=/dev/zero of="$SW" bs=1M count=24576 ) 2>/dev/null \
&& sudo chmod 600 "$SW" && sudo mkswap "$SW" >/dev/null 2>&1 && sudo swapon "$SW" 2>/dev/null \
&& echo ">> added 24G swap at $SW" || echo "!! could not add swap (continuing without it)"
free -h || true
# No path rewriting needed: mozconfig.full.emscripten derives its objdir and
# FreeType include from $topsrcdir, and libclang from $LIBCLANG_PATH (set above).
- name: "Build the engine + gecko.js (release: zstd + wasm-opt, no engine LTO) β€” long"
run: |
# wasm-opt's recursive passes overflow the default 8MB stack on the
# ~250MB module and die with SIGSEGV. Raise the stack limit for the whole
# make -> mach -> em++ -> wasm-opt subprocess tree (ulimit is inherited).
# Also needed for the libxul.so relink: emscripten 6.0.1's wasm-ld SIGSEGVs
# on the `-shared` link, so `make build` relinks the engine libs as `-r`
# relocatable objects (gecko.js/build/relink-engine-r.sh, run between two
# `mach build` passes) -- handled inside `make`, no extra step here.
# Engine LTO is intentionally OFF (LTO=1 would need a ~64 GiB+ runner; this one
# OOM-kills the LTO link). RELEASE still gives -O3 + wasm-opt over the module.
ulimit -s unlimited || ulimit -s 524288 || true
make libxul RELEASE=1 # firefox cloned; vendor -> mach build (+relink) -> build-lib.sh -> rspack
- name: Show link errors on failure
if: failure()
run: |
echo "=== gecko.js/build/link.err (tail) ==="
tail -n 60 gecko.js/build/link.err 2>/dev/null || echo "(no link.err)"
- name: Package the gecko.js library (the runnable ESM bundle)
run: |
PKG=stage/gecko.js
mkdir -p "$PKG"
# The runnable/publishable artifact is dist/ (the ESM bundle + the single
# served gecko.wasm[.zst]) + package.json. The wasm/ dir is build intermediates
# -- gecko.js/worker.js/gecko.data are INLINED into dist/gecko.js, so only
# dist/gecko.wasm[.zst] is served separately; no need to ship wasm/.
cp -r gecko.js/dist gecko.js/package.json "$PKG/"
cp gecko.js/README.md "$PKG/" 2>/dev/null || true
cat > "$PKG/RELEASE.txt" <<'TXT'
gecko.js β€” embeddable Gecko (the Firefox engine) compiled to WebAssembly (emscripten 6.0.1).
The ESM bundle (dist/gecko.js) inlines the glue + gecko.data; consumers serve only the wasm
(dist/gecko.wasm or, in this release build, dist/gecko.wasm.zst β€” decompressed in-browser).
Must be served cross-origin-isolated (COOP: same-origin + COEP: require-corp) for SharedArrayBuffer.
See README / the embed-demo for usage: `new Gecko({ canvas, wispUrl }); await g.init(); g.load(url)`.
TXT
tar -C stage -czf gecko.js-${{ github.ref_name }}.tar.gz gecko.js
ls -la gecko.js-*.tar.gz
- name: Upload build artifact (always)
uses: actions/upload-artifact@v4
with:
name: gecko.js-${{ github.ref_name }}
path: gecko.js-*.tar.gz
if-no-files-found: error
# Build the chrome-demo as a static Vite site (the full Firefox front-end demo).
# Runs AFTER the gecko.js artifact upload so that primary artifact is published
# even if the demo build trips. The demo's vite config stages the GRE resource set
# from the engine objdir via rsync into public/chrome-assets.tar.zst and emits the
# served gecko.wasm[.zst]; GECKO_OBJDIR points it at the RELEASE objdir (its default
# is the local debug objdir). gecko.js (workspace dep) + node_modules already exist
# from `make libxul` above. NOTE: chrome-demo/dist is a static site that still must
# be served cross-origin-isolated (COOP/COEP) WITH a WISP proxy at /wisp (the demo's
# dev/preview server provides both; a plain static host does not).
- name: Build the chrome-demo (static Vite build)
run: |
export GECKO_OBJDIR="${{ github.workspace }}/obj-full-emscripten-release"
pnpm --filter chrome-demo build
tar -C chrome-demo -czf chrome-demo-${{ github.ref_name }}.tar.gz dist
ls -la chrome-demo-*.tar.gz
- name: Upload chrome-demo artifact (always)
uses: actions/upload-artifact@v4
with:
name: chrome-demo-${{ github.ref_name }}
path: chrome-demo-*.tar.gz
if-no-files-found: error
- name: Publish GitHub Release (tag pushes only)
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v2
with:
files: |
gecko.js-*.tar.gz
chrome-demo-*.tar.gz
generate_release_notes: true