Skip to content

docs: ten instances do not accumulate, and correct the wasm size #4

docs: ten instances do not accumulate, and correct the wasm size

docs: ten instances do not accumulate, and correct the wasm size #4

Workflow file for this run

name: WebAssembly
on:
push:
branches: [main]
tags: ['wasm-v*']
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
env:
# Keep in sync with LIBRAW_TOOLS in CMakeLists.txt.
TOOLS: dcraw_emu
# Pinned deliberately. emcc's floating point code generation decides output
# bytes, so an unpinned toolchain makes byte comparisons non-reproducible.
# This is the version the port was validated against.
EMSDK_IMAGE: emscripten/emsdk:6.0.4
jobs:
build:
name: build ${{ matrix.variant }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
# NODERAWFS: real filesystem, same argv and paths as native.
- { variant: node, dir: build-wasm, noderawfs: 'ON' }
# ES module, virtual filesystem, FS/callMain/HEAPU8 exported.
- { variant: browser, dir: build-web, noderawfs: 'OFF' }
steps:
- uses: actions/checkout@v4
- name: Build
run: |
set -euo pipefail
docker run --rm -v "$PWD":/src -w /src "$EMSDK_IMAGE" bash -c "
emcmake cmake -S . -B '${{ matrix.dir }}' \
-DCMAKE_BUILD_TYPE=Release \
-DLIBRAW_WASM_NODERAWFS=${{ matrix.noderawfs }} &&
cmake --build '${{ matrix.dir }}' --target $TOOLS -j \$(nproc)"
- name: Check every tool produced both artifacts
run: |
set -euo pipefail
for t in $TOOLS; do
for ext in js wasm; do
f="${{ matrix.dir }}/$t.$ext"
[ -s "$f" ] || { echo "::error::missing or empty $f"; exit 1; }
done
printf '%-12s %9s bytes of wasm\n' "$t" "$(stat -c%s "${{ matrix.dir }}/$t.wasm")"
done
- uses: actions/upload-artifact@v4
with:
name: libraw-wasm-${{ matrix.variant }}
# one glob per line: upload-artifact does not do brace expansion
path: |
${{ matrix.dir }}/*.js
${{ matrix.dir }}/*.wasm
if-no-files-found: error
# What this catches: build breakage, wrong link flags, a module that aborts on
# startup, and -- specifically -- exception handling being dropped.
#
# What it does NOT catch, and must not be read as covering: numerical parity
# against native. That was measured on the 10-frame reference CR2 bracket (see
# FORK.md) and needs real RAW files, which are ~23 MB each and live outside
# this repository. There is no synthetic substitute: the one place wasm and
# native disagree is AHD's cube-root lookup table, and reaching it takes a
# real Bayer mosaic with enough near-tied interpolation directions.
smoke-node:
name: node smoke test
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: libraw-wasm-node
path: wasm-bin
# -ffp-contract=off is what makes native and wasm comparable at all: x86-64
# and arm64 fuse a*b+c with a single rounding step where wasm MVP rounds
# twice. Not load-bearing for the assertions below, but the recipe belongs
# in CI so it stays correct if real imagery is ever added.
- name: Build native with FP contraction disabled
run: |
set -euo pipefail
cmake -S . -B build-native \
-DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-ffp-contract=off"
cmake --build build-native --target $TOOLS -j "$(nproc)"
# The regression this exists for. LibRaw signals every I/O and format
# error by throwing, and catches in ~78 places; emcc disables exception
# catching by default, so without -fexceptions each of those catches
# becomes `abort()`. The module then dies on a corrupt or non-RAW file
# instead of returning an error, and the app gets an unrecoverable
# RuntimeError rather than a message it can show.
#
# This has a verified negative control: built without -fexceptions, the
# first case below fails with
# RuntimeError: Aborted(undefined) ... at ___cxa_throw
# so the check has teeth rather than merely passing.
#
# A truncated CR2 is used as well as plain junk because it reaches a
# different throw: a real RAW header that then runs out of data.
- name: Non-RAW and truncated input must fail cleanly, not abort
run: |
# GitHub runs steps as `bash -e`, so errexit is already on and
# `set -uo pipefail` does not turn it off. Every command below is
# *expected* to exit nonzero, so it has to go off explicitly or the
# step dies on the first successful rejection.
set +e
set -uo pipefail
printf 'not a raw file at all\n' > junk.bin
head -c 4096 /dev/urandom > trunc.CR2
printf 'II\x2a\x00\x08\x00\x00\x00' >> trunc.CR2
fail=0
for f in junk.bin trunc.CR2; do
for impl in "native build-native/dcraw_emu" "wasm node wasm-bin/dcraw_emu.js"; do
set -- $impl
label=$1; shift
out=$("$@" -T -o 1 -W -j -q 3 -g 2 0 -t 0 -b 1.1 -Z /dev/null "$f" 2>&1)
rc=$?
printf '%-7s %-10s rc=%s %s\n' "$label" "$f" "$rc" "$out"
case "$out" in
*Aborted*|*RuntimeError*)
echo "::error::$label aborted on $f -- exception handling is not linked in"
fail=1 ;;
esac
if [ "$rc" -eq 0 ]; then
echo "::error::$label reported success on $f"
fail=1
fi
case "$out" in
*"Cannot open"*) ;;
*) echo "::error::$label printed no diagnostic for $f"; fail=1 ;;
esac
done
done
[ "$fail" -eq 0 ] || exit 1
echo "both builds reject bad input cleanly"
- name: Usage banner runs and exits
run: |
# As above: dcraw_emu with no arguments prints usage and exits
# nonzero, which is correct and must not fail the step.
set +e
set -uo pipefail
node wasm-bin/dcraw_emu.js > usage.txt 2>&1
grep -q "dcraw emulator" usage.txt || {
echo "::error::the module did not reach main()"; cat usage.txt; exit 1; }
echo "module instantiates and reaches main()"
# The browser build has failure modes no static check catches: INVOKE_RUN left
# on would run main() before JS can stage input, and an unexported FS or
# callMain leaves a filesystem JS cannot reach. Neither shows up until the
# module is actually instantiated in a browser.
#
# No RAW fixture is needed: driving the error path exercises instantiation,
# MEMFS writes, callMain, exit-code propagation and exception catching, which
# is the whole of the embedding contract.
smoke-browser:
name: browser smoke test
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: libraw-wasm-browser
path: web/lib
- uses: actions/setup-node@v4
with: { node-version: '22' }
# `npx playwright install` only downloads browsers. The driver script does
# `import { chromium } from "playwright"`, so the package itself has to be
# installed where node can resolve it.
- name: Install Playwright and Chromium
run: |
set -euo pipefail
npm install --no-save --no-audit --no-fund playwright@1.62.0
npx playwright install --with-deps chromium
- name: Instantiate dcraw_emu in a browser
run: |
set -euo pipefail
cp .github/scripts/browser-smoke.html web/index.html
python3 -m http.server 8731 --bind 127.0.0.1 --directory web &
for _ in $(seq 1 30); do
curl -sf -o /dev/null http://127.0.0.1:8731/index.html && break
sleep 1
done
node .github/scripts/browser-smoke.mjs