Skip to content

feat(wasm): bypass NAPI-RS — direct C-ABI over wasm32-wasip1 #352

feat(wasm): bypass NAPI-RS — direct C-ABI over wasm32-wasip1

feat(wasm): bypass NAPI-RS — direct C-ABI over wasm32-wasip1 #352

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
checks: write
contents: read
pull-requests: write
jobs:
changes:
name: Detect changes
runs-on: ubuntu-latest
timeout-minutes: 2
outputs:
code: ${{ steps.filter.outputs.code }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
code:
- 'packages/**'
- 'native/**'
- 'example/**'
- 'tool/**'
- 'pubspec.*'
- '.github/**'
- '!**.md'
ffigen:
name: Generate FFI bindings
needs: [changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: dart-lang/setup-dart@v1
with:
sdk: stable
- name: Install libclang
run: sudo apt-get update && sudo apt-get install -y libclang-dev
- name: Generate bindings
run: bash tool/generate_bindings.sh
- name: Upload bindings
uses: actions/upload-artifact@v4
with:
name: ffi-bindings
path: packages/dart_monty_ffi/lib/src/generated/dart_monty_bindings.dart
retention-days: 1
lint:
name: Lint
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [ffigen]
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- name: Download FFI bindings
uses: actions/download-artifact@v4
with:
name: ffi-bindings
path: packages/dart_monty_ffi/lib/src/generated
- run: flutter pub get
- run: dart format --set-exit-if-changed .
- name: Analyze sub-packages
run: python3 tool/analyze_packages.py
markdown:
name: Markdown lint
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install pymarkdownlnt
- name: pymarkdown scan
run: >-
pymarkdown
--set extensions.front-matter.enabled=\$!True
--disable-rules MD013,MD024,MD033,MD036,MD041,MD060
scan **/*.md
test:
name: Test (${{ matrix.target }})
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [ffigen]
strategy:
matrix:
include:
- target: dart_monty_platform_interface
min-coverage: '70'
- target: dart_monty_ffi
min-coverage: '30'
- target: dart_monty_wasm
min-coverage: '70'
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- name: Download FFI bindings
if: matrix.target == 'dart_monty_ffi'
uses: actions/download-artifact@v4
with:
name: ffi-bindings
path: packages/dart_monty_ffi/lib/src/generated
- name: Install dependencies
run: cd packages/${{ matrix.target }} && dart pub get
- name: Run tests
run: cd packages/${{ matrix.target }} && dart test --coverage=coverage
- name: Enforce coverage
uses: ./.github/actions/enforce-coverage
with:
package-path: packages/${{ matrix.target }}
min-coverage: ${{ matrix.min-coverage }}
needs-format: 'true'
codecov-token: ${{ secrets.CODECOV_TOKEN }}
codecov-flag: ${{ matrix.target }}
- name: Upload lcov artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: lcov-${{ matrix.target }}
path: packages/${{ matrix.target }}/coverage/lcov.info
retention-days: 1
test-native:
name: Test (dart_monty_native)
runs-on: macos-latest
timeout-minutes: 15
needs: [ffigen]
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- name: Download FFI bindings
uses: actions/download-artifact@v4
with:
name: ffi-bindings
path: packages/dart_monty_ffi/lib/src/generated
- name: Install dependencies
run: cd packages/dart_monty_native && flutter pub get
- name: Run tests
run: cd packages/dart_monty_native && flutter test --coverage
- name: Enforce coverage
uses: ./.github/actions/enforce-coverage
with:
package-path: packages/dart_monty_native
min-coverage: '70'
codecov-token: ${{ secrets.CODECOV_TOKEN }}
codecov-flag: dart_monty_native
- name: Upload lcov artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: lcov-dart_monty_native
path: packages/dart_monty_native/coverage/lcov.info
retention-days: 1
test-web:
name: Test (dart_monty_web)
needs: [changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- name: Install dependencies
run: cd packages/dart_monty_web && flutter pub get
- name: Run tests (Chrome)
run: cd packages/dart_monty_web && flutter test --platform chrome
build-js-wrapper:
name: Build JS wrapper
needs: [changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: packages/dart_monty_wasm/js/package-lock.json
- name: Install dependencies
working-directory: packages/dart_monty_wasm/js
run: npm install
- name: Build bridge and worker
working-directory: packages/dart_monty_wasm/js
run: npm run build
- name: Verify assets
run: |
ls -la packages/dart_monty_wasm/assets/
test -f packages/dart_monty_wasm/assets/dart_monty_bridge.js
test -f packages/dart_monty_wasm/assets/dart_monty_worker.js
test -f packages/dart_monty_wasm/assets/dart_monty_native.wasm
wasm-ladder:
name: WASM ladder integration
needs: [changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: packages/dart_monty_wasm/js/package-lock.json
- uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- name: Build JS bridge
working-directory: packages/dart_monty_wasm/js
run: npm install && npm run build
- name: Prepare integration test
run: |
PKG=packages/dart_monty_wasm
INTEG_WEB="$PKG/test/integration/web"
cp "$PKG/assets/dart_monty_bridge.js" "$INTEG_WEB/"
cp "$PKG/assets/dart_monty_worker.js" "$INTEG_WEB/"
cp "$PKG/assets/"*.wasm "$INTEG_WEB/"
cd "$PKG"
dart pub get
dart compile js test/integration/python_ladder_test.dart \
-o test/integration/web/ladder_runner.dart.js
mkdir -p test/integration/web/fixtures
cp ../../test/fixtures/python_ladder/tier_*.json test/integration/web/fixtures/
- name: Run WASM ladder in headless Chrome
run: |
INTEG_WEB="packages/dart_monty_wasm/test/integration/web"
SERVE_PORT=8099
python3 << 'PYEOF' &
import http.server, functools, os
integ = os.environ.get("INTEG_WEB", "packages/dart_monty_wasm/test/integration/web")
port = int(os.environ.get("SERVE_PORT", "8099"))
class H(http.server.SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header("Cross-Origin-Opener-Policy", "same-origin")
self.send_header("Cross-Origin-Embedder-Policy", "require-corp")
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Cache-Control", "no-store")
super().end_headers()
def guess_type(self, path):
if path.endswith(".mjs"): return "application/javascript"
if path.endswith(".wasm"): return "application/wasm"
return super().guess_type(path)
def log_message(self, fmt, *args): pass
handler = functools.partial(H, directory=integ)
http.server.HTTPServer(("127.0.0.1", port), handler).serve_forever()
PYEOF
sleep 2
CHROME="google-chrome-stable"
LADDER_LOG=$(mktemp)
timeout 120 "$CHROME" \
--headless \
--disable-gpu \
--no-sandbox \
--disable-dev-shm-usage \
--enable-logging=stderr \
--v=0 \
"http://127.0.0.1:$SERVE_PORT/ladder.html" \
2>"$LADDER_LOG" || true
WEB_RESULTS=$(grep -o 'LADDER_RESULT:{.*}' "$LADDER_LOG" 2>/dev/null || true)
LADDER_DONE=$(grep -c 'LADDER_DONE' "$LADDER_LOG" 2>/dev/null || echo "0")
if [ -z "$WEB_RESULTS" ]; then
echo "ERROR: No LADDER_RESULT lines captured from Chrome."
grep -i "CONSOLE" "$LADDER_LOG" | head -20 || true
rm -f "$LADDER_LOG"
exit 1
fi
echo "$WEB_RESULTS" | while IFS= read -r line; do
echo " $line"
done
TOTAL=$(echo "$WEB_RESULTS" | wc -l | tr -d ' ')
FAILURES=$(echo "$WEB_RESULTS" | grep -c '"ok":false' || echo "0")
rm -f "$LADDER_LOG"
echo ""
echo "Results: $TOTAL fixtures, $FAILURES failures"
if [ "$FAILURES" -gt 0 ]; then
echo "FAIL: WASM ladder had $FAILURES bridge failures"
exit 1
fi
if [ "$LADDER_DONE" -eq 0 ]; then
echo "WARN: LADDER_DONE signal not found"
fi
echo "WASM ladder: PASSED ($TOTAL fixtures)"
env:
INTEG_WEB: packages/dart_monty_wasm/test/integration/web
SERVE_PORT: "8099"
build-smoke:
name: Build smoke (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 20
needs: [ffigen]
strategy:
matrix:
include:
- os: macos-latest
target: macos
- os: ubuntu-latest
target: linux
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: native
- name: Install Linux desktop dependencies
if: matrix.os == 'ubuntu-latest'
run: >-
sudo apt-get update && sudo apt-get install -y
clang cmake ninja-build pkg-config libgtk-3-dev
- name: Download FFI bindings
uses: actions/download-artifact@v4
with:
name: ffi-bindings
path: packages/dart_monty_ffi/lib/src/generated
- name: Build native library and copy to plugin
run: bash tool/build_native.sh
- name: Install dependencies
run: cd example/desktop && flutter pub get
- name: Build
run: cd example/desktop && flutter build ${{ matrix.target }}
rust:
name: Rust (fmt + clippy + test + coverage)
needs: [changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
with:
workspaces: native
- name: cargo fmt
working-directory: native
run: cargo fmt --check
- name: cargo clippy
working-directory: native
run: cargo clippy -- -D warnings
- name: Install cargo-tarpaulin
uses: taiki-e/install-action@cargo-tarpaulin
- name: Run tests with coverage
working-directory: native
run: |
OUTPUT=$(cargo tarpaulin --out Xml --output-dir target/tarpaulin 2>&1)
echo "$OUTPUT"
PCT=$(echo "$OUTPUT" | grep -oP '\d+\.\d+% coverage' | grep -oP '\d+\.\d+' | tail -1 || echo "0")
echo "Coverage: ${PCT}%"
WHOLE=${PCT%%.*}
if [ "${WHOLE:-0}" -lt 70 ]; then
echo "FAIL: Coverage ${PCT}% < 70% minimum."
exit 1
fi
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: native/target/tarpaulin/cobertura.xml
flags: native
build-wasm:
name: Build WASM
runs-on: ubuntu-latest
timeout-minutes: 20
needs: [rust]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-wasip1
- uses: Swatinem/rust-cache@v2
with:
workspaces: native
- name: Build WASM
working-directory: native
run: cargo build --release --target wasm32-wasip1
patch-coverage:
name: Patch coverage gate (90%)
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [test, test-native]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install diff-cover
run: pip install diff-cover
- name: Download all lcov artifacts
uses: actions/download-artifact@v4
with:
pattern: lcov-*
path: coverage-artifacts
- name: Merge lcov files
run: |
sudo apt-get update && sudo apt-get install -y lcov
LCOV_FILES=""
for f in coverage-artifacts/*/lcov.info; do
if [ -f "$f" ]; then
echo "Found: $f"
LCOV_FILES="$LCOV_FILES -a $f"
fi
done
if [ -z "$LCOV_FILES" ]; then
echo "ERROR: No lcov files found"
exit 1
fi
lcov $LCOV_FILES -o merged-coverage.info
echo "Merged coverage written to merged-coverage.info"
- name: Check patch coverage
run: |
diff-cover merged-coverage.info \
--compare-branch=origin/${{ github.base_ref }} \
--fail-under=90 \
--diff-range-notation='..'