feat(wasm): bypass NAPI-RS — direct C-ABI over wasm32-wasip1 #352
Workflow file for this run
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: 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='..' |