Verify generated Android app contracts
ober
30c88450b7d000e20e041caf729653942195764f
--- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ CLIENT_EXAMPLE ?= examples/client/app.ss SSD_EXAMPLE ?= tests/fixtures/ssd-app.ss ORIGINAL_TACTICS_EXAMPLE ?= tests/fixtures/original-tactics-app.ss ORIGINAL_TACTICS_BUILD_DIR ?= build/original-tactics +SSD_CONTRACT_BUILD_DIR ?= build/ssd-contract JAVA_HOME ?= /opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home ANDROID_HOME ?= $(HOME)/Library/Android/sdk ANDROID_SDK_ROOT ?= $(ANDROID_HOME) @@ -14,7 +15,7 @@ export ANDROID_HOME export ANDROID_SDK_ROOT export PATH := $(JAVA_HOME)/bin:/opt/homebrew/bin:$(PATH) -.PHONY: help clean generate test check-no-raw-kotlin security supply-chain verify ssd-generate ssd-compile original-tactics-generate original-tactics-compile release-evidence apk +.PHONY: help clean generate test check-no-raw-kotlin check-generated-app-contracts security supply-chain verify ssd-generate ssd-compile original-tactics-generate original-tactics-compile release-evidence apk .NOTPARALLEL: help: @@ -25,6 +26,7 @@ help: @echo " verify Run the release-required local verification suite" @echo " ssd-compile Compile the SSD fixture with pinned Gradle/JDK tools" @echo " original-tactics-compile Compile the Original Tactics fixture with pinned Gradle/JDK tools" + @echo " check-generated-app-contracts Verify both generated APK source contracts" @echo " release-evidence Emit SBOM/provenance from a clean committed tree" @echo " clean Remove generated output" @@ -61,12 +63,17 @@ test: check-no-raw-kotlin clean generate $(JERBOA) jandroid.ss generate $(ORIGINAL_TACTICS_EXAMPLE) $(ORIGINAL_TACTICS_BUILD_DIR) test -f $(ORIGINAL_TACTICS_BUILD_DIR)/app/src/main/java/com/jerboa/originaltactics/ShipUiNames.kt grep -q 'label.length <= maxChars' $(ORIGINAL_TACTICS_BUILD_DIR)/app/src/main/java/com/jerboa/originaltactics/ShipUiNames.kt + $(JERBOA) jandroid.ss generate $(SSD_EXAMPLE) $(SSD_CONTRACT_BUILD_DIR) + $(MAKE) check-generated-app-contracts $(MAKE) security $(MAKE) supply-chain check-no-raw-kotlin: scripts/check-no-raw-kotlin.sh +check-generated-app-contracts: + scripts/verify-generated-app-contracts.sh $(ORIGINAL_TACTICS_BUILD_DIR) $(SSD_CONTRACT_BUILD_DIR) + security: JERBOA="$(JERBOA)" tests/security-test.sh JERBOA="$(JERBOA)" tests/ssd-security-test.sh new file mode 100755 --- /dev/null +++ b/scripts/verify-generated-app-contracts.sh @@ -0,0 +1,153 @@ +#!/bin/sh +set -eu + +repo=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd -P) +generated_only=false + +if [ "${1:-}" = "--generated-only" ]; then + generated_only=true + shift +fi + +if [ "$#" -ne 2 ]; then + printf 'usage: %s [--generated-only] ORIGINAL_PROJECT SSD_PROJECT\n' "$0" >&2 + exit 2 +fi + +java_root() { + case "$1" in + */app/src/main/java) printf '%s\n' "$1" ;; + *) printf '%s/app/src/main/java\n' "${1%/}" ;; + esac +} + +original_root=$(java_root "$1") +ssd_root=$(java_root "$2") +failures=0 +missing_files='|' + +fail() { + printf 'app contract failure: %s\n' "$1" >&2 + failures=$((failures + 1)) +} + +require_root() { + if [ ! -d "$1" ]; then + fail "generated Kotlin root is missing: $1" + fi +} + +require_declaration() { + root=$1 + relative=$2 + declaration=$3 + name=$4 + file="$root/$relative" + + if [ ! -f "$file" ]; then + case "$missing_files" in + *"|$file|"*) ;; + *) + fail "generated Kotlin file is missing: $file" + missing_files="${missing_files}${file}|" + ;; + esac + return + fi + + case "$declaration" in + class) + pattern="(^|[[:space:]])class[[:space:]]+$name([[:space:](<:]|$)" + ;; + object) + pattern="(^|[[:space:]])object[[:space:]]+$name([[:space:]{:]|$)" + ;; + fun) + pattern="(^|[[:space:]])fun[[:space:]]+$name[[:space:]]*\\(" + ;; + *) + printf 'internal checker error: unknown declaration kind %s\n' "$declaration" >&2 + exit 2 + ;; + esac + + if ! LC_ALL=C grep -Eq "$pattern" "$file"; then + fail "$relative does not declare $declaration $name" + fi +} + +require_root "$original_root" +require_root "$ssd_root" + +if [ "$generated_only" = false ]; then + "$repo/scripts/check-no-raw-kotlin.sh" +fi + +original_package=com/jerboa/originaltactics +require_declaration "$original_root" "$original_package/MainActivity.kt" class MainActivity +require_declaration "$original_root" "$original_package/MainActivity.kt" fun onCreate +require_declaration "$original_root" "$original_package/RulesClient.kt" object EmbeddedRulesBridge +require_declaration "$original_root" "$original_package/RulesClient.kt" fun handleServiceJson +require_declaration "$original_root" "$original_package/RulesClient.kt" class RulesClient +require_declaration "$original_root" "$original_package/RulesClient.kt" fun post +require_declaration "$original_root" "$original_package/TacticalMapView.kt" class TacticalMapView +require_declaration "$original_root" "$original_package/TacticalMapView.kt" fun setGame +require_declaration "$original_root" "$original_package/TacticalMapView.kt" fun setTacticalStatus +require_declaration "$original_root" "$original_package/TacticalMapView.kt" fun resetView +require_declaration "$original_root" "$original_package/TacticalMapView.kt" fun onTouchEvent +require_declaration "$original_root" "$original_package/TacticalMapView.kt" fun onDraw +require_declaration "$original_root" "$original_package/SsdDamageView.kt" class SsdDamageView +require_declaration "$original_root" "$original_package/SsdDamageView.kt" fun setData +require_declaration "$original_root" "$original_package/SsdDamageView.kt" fun onDraw +require_declaration "$original_root" "$original_package/BattleCommandView.kt" class BattleCommandView +require_declaration "$original_root" "$original_package/BattleCommandView.kt" fun setGame +require_declaration "$original_root" "$original_package/BattleCommandView.kt" fun onDraw +require_declaration "$original_root" "$original_package/BridgeHudView.kt" class BridgeHudView +require_declaration "$original_root" "$original_package/BridgeHudView.kt" fun setData +require_declaration "$original_root" "$original_package/CombatLogView.kt" class CombatLogView +require_declaration "$original_root" "$original_package/CombatLogView.kt" fun setData +require_declaration "$original_root" "$original_package/ShipStatusView.kt" class ShipStatusView +require_declaration "$original_root" "$original_package/ShipStatusView.kt" fun setGame +require_declaration "$original_root" "$original_package/ZoomPanFrameLayout.kt" class ZoomPanFrameLayout +require_declaration "$original_root" "$original_package/ZoomPanFrameLayout.kt" fun onDoubleTap +require_declaration "$original_root" "$original_package/ZoomPanFrameLayout.kt" fun onTouchEvent + +ssd_package=com/sfb/ssdreview +require_declaration "$ssd_root" "$ssd_package/MainActivity.kt" class MainActivity +require_declaration "$ssd_root" "$ssd_package/MainActivity.kt" fun onCreate +require_declaration "$ssd_root" "$ssd_package/MainActivity.kt" fun onResume +require_declaration "$ssd_root" "$ssd_package/MainActivity.kt" fun onPause +require_declaration "$ssd_root" "$ssd_package/MainActivityLifecycle.kt" fun mainActivityOnCreate +require_declaration "$ssd_root" "$ssd_package/MainActivityUi.kt" fun mainActivityBuildUi +require_declaration "$ssd_root" "$ssd_package/MainActivityInteraction.kt" fun mainActivityApplySelectMode +require_declaration "$ssd_root" "$ssd_package/MainActivityInteraction.kt" fun mainActivityFinishSelectInteraction +require_declaration "$ssd_root" "$ssd_package/SsdReviewView.kt" class SsdReviewView +require_declaration "$ssd_root" "$ssd_package/SsdReviewView.kt" fun fitImage +require_declaration "$ssd_root" "$ssd_package/SsdReviewView.kt" fun rotateClockwise +require_declaration "$ssd_root" "$ssd_package/SsdReviewView.kt" fun captureViewport +require_declaration "$ssd_root" "$ssd_package/SsdReviewView.kt" fun restoreViewport +require_declaration "$ssd_root" "$ssd_package/SsdReviewView.kt" fun centerOnGroup +require_declaration "$ssd_root" "$ssd_package/SsdReviewView.kt" fun finishSelection +require_declaration "$ssd_root" "$ssd_package/SsdReviewView.kt" fun onTouchEvent +require_declaration "$ssd_root" "$ssd_package/TruthStore.kt" class TruthStore +require_declaration "$ssd_root" "$ssd_package/TruthStore.kt" fun saveSession +require_declaration "$ssd_root" "$ssd_package/TruthStore.kt" fun applyTruth +require_declaration "$ssd_root" "$ssd_package/TruthStore.kt" fun applyLearnedGuesses +require_declaration "$ssd_root" "$ssd_package/SsdDetector.kt" fun detectCells +require_declaration "$ssd_root" "$ssd_package/SsdOcr.kt" object SsdOcr +require_declaration "$ssd_root" "$ssd_package/SsdOcr.kt" fun recognizeWords +require_declaration "$ssd_root" "$ssd_package/SfbServerClient.kt" class SfbServerClient +require_declaration "$ssd_root" "$ssd_package/SfbServerClient.kt" fun selectFastest +require_declaration "$ssd_root" "$ssd_package/SfbServerClient.kt" fun fetchServerPdfs +require_declaration "$ssd_root" "$ssd_package/SfbServerClient.kt" fun fetchServerPageTruth +require_declaration "$ssd_root" "$ssd_package/SfbServerClient.kt" fun fetchServerRecognition +require_declaration "$ssd_root" "$ssd_package/SfbServerClient.kt" fun fetchServerSnapshot +require_declaration "$ssd_root" "$ssd_package/ClientCrashInstall.kt" fun mainActivityInstallClientCrashLoggerHandler +require_declaration "$ssd_root" "$ssd_package/ClientLogTransport.kt" fun sendClientLogBlocking + +if [ "$failures" -ne 0 ]; then + printf '%s\n' "generated app contract check failed with $failures error(s)" >&2 + exit 1 +fi + +printf '%s\n' 'generated Original Tactics and SSD Kotlin contracts verified'