| # Synapse Agriculture β WASM build pipeline | |
| # Run from workspace root inside `nix develop` | |
| # | |
| # This script enforces the correct build ordering: | |
| # 1. Native tests (fastest feedback loop) | |
| # 2. WASM compilation (catches target-specific issues) | |
| # 3. Size profiling (validates MCU memory budget) | |
| # 4. Runtime validation (wasm3 / wasmtime) | |
| # | |
| # Each step gates the next β fail fast, don't waste time. | |
| set -euo pipefail | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| CYAN='\033[0;36m' | |
| NC='\033[0m' | |
| WASM_TARGET="wasm32-unknown-unknown" | |
| WASI_TARGET="wasm32-wasip1" | |
| SENSOR_WASM="target/${WASM_TARGET}/release/synapse_sensor.wasm" | |
| OPTIMIZED_WASM="target/wasm-opt/synapse_sensor.wasm" | |
| # MCU memory budget (RP2350: 520KB total SRAM) | |
| # wasm3 runtime: ~64KB, LoRa + HAL: ~48KB, heap: ~100KB | |
| # Leaves roughly 300KB for the WASM module binary | |
| MAX_WASM_SIZE_KB=300 | |
| step() { echo -e "\n${CYAN}βββ $1 βββ${NC}\n"; } | |
| pass() { echo -e "${GREEN} β $1${NC}"; } | |
| fail() { echo -e "${RED} β $1${NC}"; exit 1; } | |
| warn() { echo -e "${YELLOW} β $1${NC}"; } | |
| # ββ Step 1: Native tests ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Run all tests on the host architecture (x86_64). | |
| # This validates shared types, calibration math, CBOR serialization, | |
| # and ASCII parsing WITHOUT any WASM complexity. | |
| # This is your fastest iteration loop β stay here until green. | |
| step "Step 1: Native tests (all crates)" | |
| cargo test --workspace --quiet 2>&1 | |
| pass "All native tests passed" | |
| # ββ Step 2: Compile sensor module to WASM βββββββββββββββββββββββββββββββββ | |
| # Target: wasm32-unknown-unknown (bare WASM, no WASI) | |
| # This is what runs on the MCU via wasm3. | |
| # cdylib crate-type in synapse-sensor produces the .wasm file. | |
| # release profile uses opt-level=z, LTO, panic=abort, strip=true | |
| # for minimum binary size. | |
| step "Step 2: Compile synapse-sensor β WASM (MCU target)" | |
| cargo build \ | |
| --package synapse-sensor \ | |
| --target "${WASM_TARGET}" \ | |
| --release \ | |
| --quiet 2>&1 | |
| if [ ! -f "${SENSOR_WASM}" ]; then | |
| fail "WASM binary not found at ${SENSOR_WASM}" | |
| fi | |
| RAW_SIZE=$(wc -c < "${SENSOR_WASM}") | |
| RAW_SIZE_KB=$((RAW_SIZE / 1024)) | |
| pass "Raw WASM binary: ${RAW_SIZE_KB}KB (${RAW_SIZE} bytes)" | |
| # ββ Step 3: Optimize for size βββββββββββββββββββββββββββββββββββββββββββββ | |
| # wasm-opt from Binaryen does aggressive dead code elimination, | |
| # constant folding, and code deduplication. The -Oz flag optimizes | |
| # purely for size (vs -O4 which optimizes for speed). | |
| # This typically cuts Rust WASM binaries by 40-60%. | |
| step "Step 3: Size optimization (wasm-opt -Oz)" | |
| mkdir -p "$(dirname ${OPTIMIZED_WASM})" | |
| wasm-opt -Oz \ | |
| --strip-debug \ | |
| --strip-producers \ | |
| -o "${OPTIMIZED_WASM}" \ | |
| "${SENSOR_WASM}" 2>&1 | |
| OPT_SIZE=$(wc -c < "${OPTIMIZED_WASM}") | |
| OPT_SIZE_KB=$((OPT_SIZE / 1024)) | |
| SAVINGS=$(( (RAW_SIZE - OPT_SIZE) * 100 / RAW_SIZE )) | |
| pass "Optimized WASM: ${OPT_SIZE_KB}KB (${OPT_SIZE} bytes, ${SAVINGS}% reduction)" | |
| # ββ Step 4: MCU memory budget check ββββββββββββββββββββββββββββββββββββββ | |
| # Hard gate: if the module exceeds the RP2350 memory budget, stop. | |
| # Better to catch this now than when flashing hardware in the field. | |
| step "Step 4: MCU memory budget check (max ${MAX_WASM_SIZE_KB}KB)" | |
| if [ "${OPT_SIZE_KB}" -gt "${MAX_WASM_SIZE_KB}" ]; then | |
| fail "WASM module (${OPT_SIZE_KB}KB) exceeds MCU budget (${MAX_WASM_SIZE_KB}KB)" | |
| echo " Run: twiggy top ${OPTIMIZED_WASM}" | |
| echo " to find what's taking space" | |
| exit 1 | |
| fi | |
| pass "Module fits in MCU budget: ${OPT_SIZE_KB}KB / ${MAX_WASM_SIZE_KB}KB" | |
| # ββ Step 5: Size profiling ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Even if we're under budget, know where the bytes are going. | |
| # twiggy shows the top functions by size β if serde or fmt | |
| # machinery snuck in, you'll see it here. | |
| step "Step 5: Size profile (top 15 largest items)" | |
| if command -v twiggy &>/dev/null; then | |
| twiggy top "${OPTIMIZED_WASM}" -n 15 2>&1 || warn "twiggy failed (non-fatal)" | |
| else | |
| warn "twiggy not found β skip size profiling" | |
| fi | |
| # ββ Step 6: WASM validation ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # wasm-tools validate checks the module against the WASM spec. | |
| # Catches issues like invalid opcodes, type mismatches, or | |
| # features the MCU runtime doesn't support. | |
| step "Step 6: WASM module validation" | |
| if command -v wasm-tools &>/dev/null; then | |
| wasm-tools validate "${OPTIMIZED_WASM}" 2>&1 | |
| pass "Module passes WASM spec validation" | |
| else | |
| warn "wasm-tools not found β skip validation" | |
| fi | |
| # ββ Step 7: Export inspection βββββββββββββββββββββββββββββββββββββββββββββ | |
| # Verify the module exports the expected guest functions. | |
| # The wasm3 runtime on the MCU will look for these by name. | |
| step "Step 7: Export verification" | |
| if command -v wasm-tools &>/dev/null; then | |
| EXPORTS=$(wasm-tools dump "${OPTIMIZED_WASM}" 2>/dev/null | grep -c "export" || echo "0") | |
| echo " Module has ${EXPORTS} exports" | |
| # Check for our required guest functions | |
| for func in guest_init guest_sample guest_reconfigure; do | |
| if wasm2wat "${OPTIMIZED_WASM}" 2>/dev/null | grep -q "\"${func}\""; then | |
| pass "Found export: ${func}" | |
| else | |
| fail "Missing required export: ${func}" | |
| fi | |
| done | |
| else | |
| warn "wasm-tools not found β skip export check" | |
| fi | |
| # ββ Step 8: Import verification βββββββββββββββββββββββββββββββββββββββββββ | |
| # Verify the module imports match what the MCU host firmware provides. | |
| # Any import the host doesn't implement = crash at instantiation. | |
| step "Step 8: Import verification (host function dependencies)" | |
| if command -v wabt &>/dev/null && command -v wasm2wat &>/dev/null; then | |
| echo " Required host functions:" | |
| wasm2wat "${OPTIMIZED_WASM}" 2>/dev/null \ | |
| | grep '(import' \ | |
| | sed 's/.*"\([^"]*\)".*/ β \1/' \ | |
| || warn "Could not extract imports" | |
| else | |
| warn "wabt not found β skip import check" | |
| fi | |
| # ββ Summary βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| step "BUILD COMPLETE" | |
| echo -e " ${GREEN}Native tests: PASS${NC}" | |
| echo -e " ${GREEN}WASM compile: PASS${NC}" | |
| echo -e " ${GREEN}Size budget: ${OPT_SIZE_KB}KB / ${MAX_WASM_SIZE_KB}KB${NC}" | |
| echo -e " ${GREEN}Spec valid: PASS${NC}" | |
| echo "" | |
| echo " Artifacts:" | |
| echo " Raw: ${SENSOR_WASM}" | |
| echo " Optimized: ${OPTIMIZED_WASM}" | |
| echo "" | |
| echo " Next steps:" | |
| echo " wasmtime (WASI): needs wasm32-wasip1 target build" | |
| echo " wasm3 (MCU sim): wasm3 ${OPTIMIZED_WASM} --func guest_sample" | |
| echo " Browser: trunk serve crates/synapse-web" | |
| echo "" | |