#!/usr/bin/env bash # Hold the crate graph to the shape the rules depend on. # # ./scripts/check-boundaries.sh # # Two boundaries, both of which erode silently if nothing checks them. # # The rules have to run in a browser. Battle value and construction validation # are wanted in two places - in the page while somebody edits a design, and # again on the server that believes the answer - so the crates they are written # against cannot acquire a filesystem, a database or a network. A `use` added # in a hurry is all it takes, and the failure shows up much later as "why can # this not compile to wasm". # # The database must not learn to compute. helm-db takes ComputedStats and is # not allowed to know who produced them, so that swapping the Java bridge for a # native implementation is a change in helm-cli and nowhere else. set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")/.." FAIL=0 note() { printf ' %s\n' "$1"; } fail() { printf 'FAIL: %s\n' "$1" >&2; FAIL=1; } # Crates that must reach wasm, and so must stay clear of these. # # serde_json is on the list for the rules crates but not because it cannot run # in a browser - it compiles to wasm perfectly well. It is forbidden there # because the rules are written against MegaMek's own formats with hand-written # parsers, and a JSON dependency appearing in one of them means a rule has # started reading somebody's serialisation instead. # # helm-query carries serde derives and is still on this list: it describes # helm's own wire shape, which is the one thing in the graph that is allowed to # know it will be serialised. It has no serde_json, because decoding a document # is the job of whoever is holding one. WASM_CRATES=(helm-core helm-facet helm-query helm-force helm-bv helm-cost) FORBIDDEN=(rusqlite zip serde_json libsqlite3-sys) # The boundary crate is the exception: decoding what JavaScript hands over is # the whole of its job. It still may not touch a file or a database. BOUNDARY_CRATES=(helm-wasm) BOUNDARY_FORBIDDEN=(rusqlite zip libsqlite3-sys) # Crates that must not depend on a producer of ComputedStats. CONSUMER_CRATES=(helm-db) PRODUCERS=(helm-bridge helm-bv helm-cost) echo "checking wasm-bound crates carry no I/O dependency" for crate in "${WASM_CRATES[@]}"; do if ! cargo metadata --format-version 1 --no-deps 2>/dev/null | grep -q "\"$crate\""; then note "$crate: not in the workspace yet, skipped" continue fi tree="$(cargo tree -p "$crate" --edges normal 2>/dev/null || true)" for dep in "${FORBIDDEN[@]}"; do if grep -qE "^[^a-z]*\b$dep v" <<<"$tree"; then fail "$crate depends on $dep, which cannot go in a browser." note "It belongs in a crate that depends on $crate, not in $crate." fi done note "$crate: clean" done for crate in "${BOUNDARY_CRATES[@]}"; do if ! cargo metadata --format-version 1 --no-deps 2>/dev/null | grep -q "\"$crate\""; then note "$crate: not in the workspace yet, skipped" continue fi tree="$(cargo tree -p "$crate" --edges normal 2>/dev/null || true)" for dep in "${BOUNDARY_FORBIDDEN[@]}"; do if grep -qE "^[^a-z]*\b$dep v" <<<"$tree"; then fail "$crate depends on $dep, which cannot go in a browser." fi done note "$crate: clean" done echo "checking the database layer cannot compute" for crate in "${CONSUMER_CRATES[@]}"; do tree="$(cargo tree -p "$crate" --edges normal 2>/dev/null || true)" for dep in "${PRODUCERS[@]}"; do if grep -qE "^[^a-z]*\b$dep v" <<<"$tree"; then fail "$crate depends on $dep." note "Computed values reach $crate as helm_core::ComputedStats. Choosing" note "who produces them is helm-cli's job, so that retiring a producer is" note "a change in one crate." fi done note "$crate: clean" done echo "checking the parsers build without their I/O feature" if cargo check -q -p helm-unitfile --no-default-features 2>/dev/null; then note "helm-unitfile --no-default-features: ok" else fail "helm-unitfile does not build without its 'library' feature." note "Parsing one design must not require the zip reader." fi echo "checking the wasm target" if rustup target list --installed 2>/dev/null | grep -q wasm32-unknown-unknown; then for crate in "${WASM_CRATES[@]}"; do if cargo check -q -p "$crate" --target wasm32-unknown-unknown 2>/dev/null; then note "$crate: builds for wasm32-unknown-unknown" else fail "$crate does not build for wasm32-unknown-unknown." fi done for crate in "${BOUNDARY_CRATES[@]}"; do if cargo check -q -p "$crate" --target wasm32-unknown-unknown 2>/dev/null; then note "$crate: builds for wasm32-unknown-unknown" else fail "$crate does not build for wasm32-unknown-unknown." fi done if cargo check -q -p helm-unitfile --no-default-features \ --target wasm32-unknown-unknown 2>/dev/null; then note "helm-unitfile (no default features): builds for wasm32-unknown-unknown" else fail "helm-unitfile does not build for wasm32 without its 'library' feature." fi else # Not installed, and installing a target is not this script's decision to # make. The dependency checks above are the ones that catch the mistake # early anyway; this one only confirms it. note "wasm32-unknown-unknown is not installed, so the build check is skipped." note "Enable it with: rustup target add wasm32-unknown-unknown" fi if [ "$FAIL" -ne 0 ]; then echo echo "crate boundaries are broken; see above." >&2 exit 1 fi echo "crate boundaries hold."