ci: turn errexit off in the steps whose commands are meant to fail #3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |