|
| 1 | +name: WebAssembly |
| 2 | + |
| 3 | +on: |
| 4 | + push: |
| 5 | + branches: [main] |
| 6 | + tags: ['wasm-v*'] |
| 7 | + pull_request: |
| 8 | + branches: [main] |
| 9 | + workflow_dispatch: |
| 10 | + |
| 11 | +permissions: |
| 12 | + contents: read |
| 13 | + |
| 14 | +env: |
| 15 | + # Keep in sync with LIBRAW_TOOLS in CMakeLists.txt. |
| 16 | + TOOLS: dcraw_emu |
| 17 | + # Pinned deliberately. emcc's floating point code generation decides output |
| 18 | + # bytes, so an unpinned toolchain makes byte comparisons non-reproducible. |
| 19 | + # This is the version the port was validated against. |
| 20 | + EMSDK_IMAGE: emscripten/emsdk:6.0.4 |
| 21 | + |
| 22 | +jobs: |
| 23 | + build: |
| 24 | + name: build ${{ matrix.variant }} |
| 25 | + runs-on: ubuntu-latest |
| 26 | + strategy: |
| 27 | + fail-fast: false |
| 28 | + matrix: |
| 29 | + include: |
| 30 | + # NODERAWFS: real filesystem, same argv and paths as native. |
| 31 | + - { variant: node, dir: build-wasm, noderawfs: 'ON' } |
| 32 | + # ES module, virtual filesystem, FS/callMain/HEAPU8 exported. |
| 33 | + - { variant: browser, dir: build-web, noderawfs: 'OFF' } |
| 34 | + |
| 35 | + steps: |
| 36 | + - uses: actions/checkout@v4 |
| 37 | + |
| 38 | + - name: Build |
| 39 | + run: | |
| 40 | + set -euo pipefail |
| 41 | + docker run --rm -v "$PWD":/src -w /src "$EMSDK_IMAGE" bash -c " |
| 42 | + emcmake cmake -S . -B '${{ matrix.dir }}' \ |
| 43 | + -DCMAKE_BUILD_TYPE=Release \ |
| 44 | + -DLIBRAW_WASM_NODERAWFS=${{ matrix.noderawfs }} && |
| 45 | + cmake --build '${{ matrix.dir }}' --target $TOOLS -j \$(nproc)" |
| 46 | +
|
| 47 | + - name: Check every tool produced both artifacts |
| 48 | + run: | |
| 49 | + set -euo pipefail |
| 50 | + for t in $TOOLS; do |
| 51 | + for ext in js wasm; do |
| 52 | + f="${{ matrix.dir }}/$t.$ext" |
| 53 | + [ -s "$f" ] || { echo "::error::missing or empty $f"; exit 1; } |
| 54 | + done |
| 55 | + printf '%-12s %9s bytes of wasm\n' "$t" "$(stat -c%s "${{ matrix.dir }}/$t.wasm")" |
| 56 | + done |
| 57 | +
|
| 58 | + - uses: actions/upload-artifact@v4 |
| 59 | + with: |
| 60 | + name: libraw-wasm-${{ matrix.variant }} |
| 61 | + # one glob per line: upload-artifact does not do brace expansion |
| 62 | + path: | |
| 63 | + ${{ matrix.dir }}/*.js |
| 64 | + ${{ matrix.dir }}/*.wasm |
| 65 | + if-no-files-found: error |
| 66 | + |
| 67 | + # What this catches: build breakage, wrong link flags, a module that aborts on |
| 68 | + # startup, and -- specifically -- exception handling being dropped. |
| 69 | + # |
| 70 | + # What it does NOT catch, and must not be read as covering: numerical parity |
| 71 | + # against native. That was measured on the 10-frame reference CR2 bracket (see |
| 72 | + # FORK.md) and needs real RAW files, which are ~23 MB each and live outside |
| 73 | + # this repository. There is no synthetic substitute: the one place wasm and |
| 74 | + # native disagree is AHD's cube-root lookup table, and reaching it takes a |
| 75 | + # real Bayer mosaic with enough near-tied interpolation directions. |
| 76 | + smoke-node: |
| 77 | + name: node smoke test |
| 78 | + needs: build |
| 79 | + runs-on: ubuntu-latest |
| 80 | + steps: |
| 81 | + - uses: actions/checkout@v4 |
| 82 | + |
| 83 | + - uses: actions/download-artifact@v4 |
| 84 | + with: |
| 85 | + name: libraw-wasm-node |
| 86 | + path: wasm-bin |
| 87 | + |
| 88 | + # -ffp-contract=off is what makes native and wasm comparable at all: x86-64 |
| 89 | + # and arm64 fuse a*b+c with a single rounding step where wasm MVP rounds |
| 90 | + # twice. Not load-bearing for the assertions below, but the recipe belongs |
| 91 | + # in CI so it stays correct if real imagery is ever added. |
| 92 | + - name: Build native with FP contraction disabled |
| 93 | + run: | |
| 94 | + set -euo pipefail |
| 95 | + cmake -S . -B build-native \ |
| 96 | + -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-ffp-contract=off" |
| 97 | + cmake --build build-native --target $TOOLS -j "$(nproc)" |
| 98 | +
|
| 99 | + # The regression this exists for. LibRaw signals every I/O and format |
| 100 | + # error by throwing, and catches in ~78 places; emcc disables exception |
| 101 | + # catching by default, so without -fexceptions each of those catches |
| 102 | + # becomes `abort()`. The module then dies on a corrupt or non-RAW file |
| 103 | + # instead of returning an error, and the app gets an unrecoverable |
| 104 | + # RuntimeError rather than a message it can show. |
| 105 | + # |
| 106 | + # This has a verified negative control: built without -fexceptions, the |
| 107 | + # first case below fails with |
| 108 | + # RuntimeError: Aborted(undefined) ... at ___cxa_throw |
| 109 | + # so the check has teeth rather than merely passing. |
| 110 | + # |
| 111 | + # A truncated CR2 is used as well as plain junk because it reaches a |
| 112 | + # different throw: a real RAW header that then runs out of data. |
| 113 | + - name: Non-RAW and truncated input must fail cleanly, not abort |
| 114 | + run: | |
| 115 | + set -uo pipefail |
| 116 | + printf 'not a raw file at all\n' > junk.bin |
| 117 | + head -c 4096 /dev/urandom > trunc.CR2 |
| 118 | + printf 'II\x2a\x00\x08\x00\x00\x00' >> trunc.CR2 |
| 119 | +
|
| 120 | + fail=0 |
| 121 | + for f in junk.bin trunc.CR2; do |
| 122 | + for impl in "native build-native/dcraw_emu" "wasm node wasm-bin/dcraw_emu.js"; do |
| 123 | + set -- $impl |
| 124 | + label=$1; shift |
| 125 | + out=$("$@" -T -o 1 -W -j -q 3 -g 2 0 -t 0 -b 1.1 -Z /dev/null "$f" 2>&1) |
| 126 | + rc=$? |
| 127 | + printf '%-7s %-10s rc=%s %s\n' "$label" "$f" "$rc" "$out" |
| 128 | + case "$out" in |
| 129 | + *Aborted*|*RuntimeError*) |
| 130 | + echo "::error::$label aborted on $f -- exception handling is not linked in" |
| 131 | + fail=1 ;; |
| 132 | + esac |
| 133 | + if [ "$rc" -eq 0 ]; then |
| 134 | + echo "::error::$label reported success on $f" |
| 135 | + fail=1 |
| 136 | + fi |
| 137 | + case "$out" in |
| 138 | + *"Cannot open"*) ;; |
| 139 | + *) echo "::error::$label printed no diagnostic for $f"; fail=1 ;; |
| 140 | + esac |
| 141 | + done |
| 142 | + done |
| 143 | + [ "$fail" -eq 0 ] || exit 1 |
| 144 | + echo "both builds reject bad input cleanly" |
| 145 | +
|
| 146 | + - name: Usage banner runs and exits |
| 147 | + run: | |
| 148 | + set -uo pipefail |
| 149 | + node wasm-bin/dcraw_emu.js > usage.txt 2>&1 |
| 150 | + grep -q "dcraw emulator" usage.txt || { |
| 151 | + echo "::error::the module did not reach main()"; cat usage.txt; exit 1; } |
| 152 | + echo "module instantiates and reaches main()" |
| 153 | +
|
| 154 | + # The browser build has failure modes no static check catches: INVOKE_RUN left |
| 155 | + # on would run main() before JS can stage input, and an unexported FS or |
| 156 | + # callMain leaves a filesystem JS cannot reach. Neither shows up until the |
| 157 | + # module is actually instantiated in a browser. |
| 158 | + # |
| 159 | + # No RAW fixture is needed: driving the error path exercises instantiation, |
| 160 | + # MEMFS writes, callMain, exit-code propagation and exception catching, which |
| 161 | + # is the whole of the embedding contract. |
| 162 | + smoke-browser: |
| 163 | + name: browser smoke test |
| 164 | + needs: build |
| 165 | + runs-on: ubuntu-latest |
| 166 | + steps: |
| 167 | + - uses: actions/checkout@v4 |
| 168 | + |
| 169 | + - uses: actions/download-artifact@v4 |
| 170 | + with: |
| 171 | + name: libraw-wasm-browser |
| 172 | + path: web/lib |
| 173 | + |
| 174 | + - uses: actions/setup-node@v4 |
| 175 | + with: { node-version: '22' } |
| 176 | + |
| 177 | + # `npx playwright install` only downloads browsers. The driver script does |
| 178 | + # `import { chromium } from "playwright"`, so the package itself has to be |
| 179 | + # installed where node can resolve it. |
| 180 | + - name: Install Playwright and Chromium |
| 181 | + run: | |
| 182 | + set -euo pipefail |
| 183 | + npm install --no-save --no-audit --no-fund playwright@1.62.0 |
| 184 | + npx playwright install --with-deps chromium |
| 185 | +
|
| 186 | + - name: Instantiate dcraw_emu in a browser |
| 187 | + run: | |
| 188 | + set -euo pipefail |
| 189 | + cp .github/scripts/browser-smoke.html web/index.html |
| 190 | + python3 -m http.server 8731 --bind 127.0.0.1 --directory web & |
| 191 | + for _ in $(seq 1 30); do |
| 192 | + curl -sf -o /dev/null http://127.0.0.1:8731/index.html && break |
| 193 | + sleep 1 |
| 194 | + done |
| 195 | + node .github/scripts/browser-smoke.mjs |
0 commit comments