|
| 1 | +#!/usr/bin/env bash |
| 2 | +# audit-tinygo-stdlib.sh — Audit Go stdlib package compatibility with TinyGo wasip2 |
| 3 | +# |
| 4 | +# US-302: Audit Go stdlib compatibility for wasip2 target |
| 5 | +# |
| 6 | +# Usage: |
| 7 | +# scripts/audit-tinygo-stdlib.sh [--build|--table|--json|--test|--all] |
| 8 | +# |
| 9 | +# Modes: |
| 10 | +# --build Run TinyGo wasip2 builds for all 20 stdlib packages |
| 11 | +# --table Generate human-readable markdown compatibility table |
| 12 | +# --json Print compatibility results as JSON |
| 13 | +# --test Run Go test suite (validates file structure + JSON schema) |
| 14 | +# --all Run build + table + test (default) |
| 15 | +# |
| 16 | +# Exit codes: |
| 17 | +# 0 — Success (individual package failures are expected and recorded) |
| 18 | +# 1 — Script error or prerequisites missing |
| 19 | +# 2 — Go tests failed |
| 20 | + |
| 21 | +set -euo pipefail |
| 22 | + |
| 23 | +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 24 | +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" |
| 25 | +FIXTURE_DIR="${PROJECT_ROOT}/tests/fixtures/go-stdlib-compat" |
| 26 | +COMPAT_DB="${PROJECT_ROOT}/compat-db" |
| 27 | +JSON_OUTPUT="${COMPAT_DB}/tinygo-stdlib.json" |
| 28 | +TABLE_OUTPUT="${COMPAT_DB}/tinygo-stdlib-compat-table.md" |
| 29 | +TINYGO_BIN="${PROJECT_ROOT}/build/tinygo/bin/tinygo" |
| 30 | +TMP_DIR="/tmp/tinygo-stdlib-audit" |
| 31 | + |
| 32 | +MODE="${1:---all}" |
| 33 | + |
| 34 | +log() { echo "==> $*" >&2; } |
| 35 | +err() { echo "ERROR: $*" >&2; } |
| 36 | + |
| 37 | +# Map directory name to Go import path |
| 38 | +dir_to_import_path() { |
| 39 | + local dir="$1" |
| 40 | + case "${dir}" in |
| 41 | + pkg_fmt) echo "fmt" ;; |
| 42 | + pkg_strings) echo "strings" ;; |
| 43 | + pkg_strconv) echo "strconv" ;; |
| 44 | + pkg_encoding_json) echo "encoding/json" ;; |
| 45 | + pkg_encoding_base64) echo "encoding/base64" ;; |
| 46 | + pkg_crypto_sha256) echo "crypto/sha256" ;; |
| 47 | + pkg_crypto_tls) echo "crypto/tls" ;; |
| 48 | + pkg_math) echo "math" ;; |
| 49 | + pkg_sort) echo "sort" ;; |
| 50 | + pkg_bytes) echo "bytes" ;; |
| 51 | + pkg_io) echo "io" ;; |
| 52 | + pkg_os) echo "os" ;; |
| 53 | + pkg_net) echo "net" ;; |
| 54 | + pkg_net_http) echo "net/http" ;; |
| 55 | + pkg_database_sql) echo "database/sql" ;; |
| 56 | + pkg_context) echo "context" ;; |
| 57 | + pkg_sync) echo "sync" ;; |
| 58 | + pkg_time) echo "time" ;; |
| 59 | + pkg_regexp) echo "regexp" ;; |
| 60 | + pkg_log) echo "log" ;; |
| 61 | + *) echo "${dir}" ;; |
| 62 | + esac |
| 63 | +} |
| 64 | + |
| 65 | +# Map directory name to human-friendly package name |
| 66 | +dir_to_name() { |
| 67 | + local dir="$1" |
| 68 | + dir_to_import_path "${dir}" |
| 69 | +} |
| 70 | + |
| 71 | +find_tinygo() { |
| 72 | + if [ -x "${TINYGO_BIN}" ]; then |
| 73 | + echo "${TINYGO_BIN}" |
| 74 | + elif command -v tinygo &>/dev/null; then |
| 75 | + command -v tinygo |
| 76 | + else |
| 77 | + echo "" |
| 78 | + fi |
| 79 | +} |
| 80 | + |
| 81 | +get_tinygo_version() { |
| 82 | + local bin="$1" |
| 83 | + "${bin}" version 2>&1 | head -1 || echo "unknown" |
| 84 | +} |
| 85 | + |
| 86 | +# Escape a string for safe JSON embedding |
| 87 | +json_escape() { |
| 88 | + local s="$1" |
| 89 | + # Use python if available, otherwise sed |
| 90 | + if command -v python3 &>/dev/null; then |
| 91 | + python3 -c "import json,sys; print(json.dumps(sys.stdin.read()), end='')" <<< "${s}" |
| 92 | + else |
| 93 | + # Basic escaping: backslash, quotes, newlines |
| 94 | + echo -n "\"$(echo "${s}" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' '|' | sed 's/|/\\n/g')\"" |
| 95 | + fi |
| 96 | +} |
| 97 | + |
| 98 | +run_build() { |
| 99 | + local tinygo_bin |
| 100 | + tinygo_bin="$(find_tinygo)" |
| 101 | + |
| 102 | + if [ -z "${tinygo_bin}" ]; then |
| 103 | + err "TinyGo not found. Install via: scripts/build-tinygo.sh" |
| 104 | + err "Looked in: ${TINYGO_BIN} and system PATH" |
| 105 | + exit 1 |
| 106 | + fi |
| 107 | + |
| 108 | + local tinygo_version |
| 109 | + tinygo_version="$(get_tinygo_version "${tinygo_bin}")" |
| 110 | + local go_version |
| 111 | + go_version="$(go version 2>&1 | sed 's/go version go//' | cut -d' ' -f1)" |
| 112 | + local tested_at |
| 113 | + tested_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" |
| 114 | + |
| 115 | + log "TinyGo: ${tinygo_version}" |
| 116 | + log "Go: ${go_version}" |
| 117 | + log "Target: wasip2" |
| 118 | + log "Fixtures: ${FIXTURE_DIR}" |
| 119 | + echo "" |
| 120 | + |
| 121 | + mkdir -p "${TMP_DIR}" "${COMPAT_DB}" |
| 122 | + |
| 123 | + # Collect results as JSON array entries |
| 124 | + local packages_json="" |
| 125 | + local pkg_count=0 |
| 126 | + local pass_count=0 |
| 127 | + local fail_count=0 |
| 128 | + local partial_count=0 |
| 129 | + |
| 130 | + # Sort directories for deterministic output |
| 131 | + for pkg_dir in $(find "${FIXTURE_DIR}" -maxdepth 1 -type d -name 'pkg_*' | sort); do |
| 132 | + local dir_name |
| 133 | + dir_name="$(basename "${pkg_dir}")" |
| 134 | + local import_path |
| 135 | + import_path="$(dir_to_import_path "${dir_name}")" |
| 136 | + local pkg_name |
| 137 | + pkg_name="$(dir_to_name "${dir_name}")" |
| 138 | + |
| 139 | + pkg_count=$((pkg_count + 1)) |
| 140 | + local wasm_out="${TMP_DIR}/${dir_name}.wasm" |
| 141 | + |
| 142 | + printf " [%2d/20] %-20s " "${pkg_count}" "${pkg_name}" |
| 143 | + |
| 144 | + # Run TinyGo build, capture stderr |
| 145 | + local compile_output="" |
| 146 | + local compile_status="" |
| 147 | + local exit_code=0 |
| 148 | + |
| 149 | + compile_output=$("${tinygo_bin}" build -target=wasip2 -o "${wasm_out}" "${pkg_dir}/main.go" 2>&1) || exit_code=$? |
| 150 | + |
| 151 | + if [ ${exit_code} -eq 0 ]; then |
| 152 | + compile_status="pass" |
| 153 | + pass_count=$((pass_count + 1)) |
| 154 | + echo "PASS" |
| 155 | + else |
| 156 | + # Check if it's a partial failure (compiled but with warnings) |
| 157 | + if [ -f "${wasm_out}" ]; then |
| 158 | + compile_status="partial" |
| 159 | + partial_count=$((partial_count + 1)) |
| 160 | + echo "PARTIAL" |
| 161 | + else |
| 162 | + compile_status="fail" |
| 163 | + fail_count=$((fail_count + 1)) |
| 164 | + echo "FAIL" |
| 165 | + fi |
| 166 | + fi |
| 167 | + |
| 168 | + # Extract error details |
| 169 | + local errors_json="[]" |
| 170 | + local error_count=0 |
| 171 | + local missing_symbols_json="[]" |
| 172 | + local notes="" |
| 173 | + |
| 174 | + if [ "${compile_status}" != "pass" ] && [ -n "${compile_output}" ]; then |
| 175 | + # Count error lines |
| 176 | + error_count=$(echo "${compile_output}" | grep -c 'error:\|Error\|cannot\|undefined\|not declared\|missing' 2>/dev/null || echo "0") |
| 177 | + |
| 178 | + # Collect individual error messages (first 20 lines) |
| 179 | + local error_lines="" |
| 180 | + while IFS= read -r line; do |
| 181 | + if [ -n "${line}" ]; then |
| 182 | + local escaped |
| 183 | + escaped=$(echo "${line}" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/ /g') |
| 184 | + if [ -n "${error_lines}" ]; then |
| 185 | + error_lines="${error_lines}, " |
| 186 | + fi |
| 187 | + error_lines="${error_lines}\"${escaped}\"" |
| 188 | + fi |
| 189 | + done < <(echo "${compile_output}" | head -20) |
| 190 | + if [ -n "${error_lines}" ]; then |
| 191 | + errors_json="[${error_lines}]" |
| 192 | + fi |
| 193 | + |
| 194 | + # Extract missing/undefined symbols |
| 195 | + local symbols="" |
| 196 | + while IFS= read -r sym; do |
| 197 | + if [ -n "${sym}" ]; then |
| 198 | + local escaped_sym |
| 199 | + escaped_sym=$(echo "${sym}" | sed 's/\\/\\\\/g; s/"/\\"/g') |
| 200 | + if [ -n "${symbols}" ]; then |
| 201 | + symbols="${symbols}, " |
| 202 | + fi |
| 203 | + symbols="${symbols}\"${escaped_sym}\"" |
| 204 | + fi |
| 205 | + done < <(echo "${compile_output}" | grep -oP 'undefined: \K[a-zA-Z0-9_.]+' 2>/dev/null || true) |
| 206 | + if [ -n "${symbols}" ]; then |
| 207 | + missing_symbols_json="[${symbols}]" |
| 208 | + fi |
| 209 | + |
| 210 | + notes="TinyGo wasip2 compilation failed with ${error_count} error(s)" |
| 211 | + else |
| 212 | + notes="Compiles successfully with TinyGo wasip2" |
| 213 | + fi |
| 214 | + |
| 215 | + # Build JSON entry |
| 216 | + local entry |
| 217 | + entry=$(cat <<ENTRYEOF |
| 218 | + { |
| 219 | + "name": "${pkg_name}", |
| 220 | + "importPath": "${import_path}", |
| 221 | + "compileStatus": "${compile_status}", |
| 222 | + "errors": ${errors_json}, |
| 223 | + "errorCount": ${error_count}, |
| 224 | + "missingSymbols": ${missing_symbols_json}, |
| 225 | + "notes": "${notes}" |
| 226 | + } |
| 227 | +ENTRYEOF |
| 228 | + ) |
| 229 | + |
| 230 | + if [ -n "${packages_json}" ]; then |
| 231 | + packages_json="${packages_json}, |
| 232 | +${entry}" |
| 233 | + else |
| 234 | + packages_json="${entry}" |
| 235 | + fi |
| 236 | + done |
| 237 | + |
| 238 | + # Extract compiler version number |
| 239 | + local compiler_version |
| 240 | + compiler_version=$(echo "${tinygo_version}" | grep -oP '\d+\.\d+\.\d+' | head -1 || echo "unknown") |
| 241 | + |
| 242 | + # Write final JSON |
| 243 | + cat > "${JSON_OUTPUT}" <<JSONEOF |
| 244 | +{ |
| 245 | + "compiler": "tinygo", |
| 246 | + "compilerVersion": "${compiler_version}", |
| 247 | + "target": "wasip2", |
| 248 | + "testedAt": "${tested_at}", |
| 249 | + "goVersion": "${go_version}", |
| 250 | + "userStory": "US-302", |
| 251 | + "packageCount": ${pkg_count}, |
| 252 | + "packages": [ |
| 253 | +${packages_json} |
| 254 | + ] |
| 255 | +} |
| 256 | +JSONEOF |
| 257 | + |
| 258 | + echo "" |
| 259 | + log "Results written to ${JSON_OUTPUT}" |
| 260 | + log "Summary: ${pass_count} pass, ${fail_count} fail, ${partial_count} partial (${pkg_count} total)" |
| 261 | + |
| 262 | + # Validate count |
| 263 | + if [ "${pkg_count}" -ne 20 ]; then |
| 264 | + err "Expected 20 packages, found ${pkg_count}" |
| 265 | + exit 1 |
| 266 | + fi |
| 267 | + |
| 268 | + # Clean up temp files |
| 269 | + rm -rf "${TMP_DIR}" |
| 270 | +} |
| 271 | + |
| 272 | +generate_table() { |
| 273 | + if [ ! -f "${JSON_OUTPUT}" ]; then |
| 274 | + err "JSON results not found: ${JSON_OUTPUT}" |
| 275 | + err "Run with --build first to generate results." |
| 276 | + exit 1 |
| 277 | + fi |
| 278 | + |
| 279 | + log "Generating compatibility table..." |
| 280 | + |
| 281 | + # Use jq to generate the markdown table |
| 282 | + if ! command -v jq &>/dev/null; then |
| 283 | + err "jq not found. Install jq to generate markdown tables." |
| 284 | + exit 1 |
| 285 | + fi |
| 286 | + |
| 287 | + local compiler_version target tested_at go_version pkg_count |
| 288 | + compiler_version=$(jq -r '.compilerVersion' "${JSON_OUTPUT}") |
| 289 | + target=$(jq -r '.target' "${JSON_OUTPUT}") |
| 290 | + tested_at=$(jq -r '.testedAt' "${JSON_OUTPUT}") |
| 291 | + go_version=$(jq -r '.goVersion' "${JSON_OUTPUT}") |
| 292 | + pkg_count=$(jq -r '.packageCount' "${JSON_OUTPUT}") |
| 293 | + |
| 294 | + local pass_count fail_count partial_count |
| 295 | + pass_count=$(jq '[.packages[] | select(.compileStatus == "pass")] | length' "${JSON_OUTPUT}") |
| 296 | + fail_count=$(jq '[.packages[] | select(.compileStatus == "fail")] | length' "${JSON_OUTPUT}") |
| 297 | + partial_count=$(jq '[.packages[] | select(.compileStatus == "partial")] | length' "${JSON_OUTPUT}") |
| 298 | + |
| 299 | + { |
| 300 | + echo "# TinyGo wasip2 Standard Library Compatibility" |
| 301 | + echo "" |
| 302 | + echo "**Compiler**: TinyGo ${compiler_version}" |
| 303 | + echo "**Target**: ${target}" |
| 304 | + echo "**Go Version**: ${go_version}" |
| 305 | + echo "**Tested**: ${tested_at}" |
| 306 | + echo "**User Story**: US-302" |
| 307 | + echo "" |
| 308 | + echo "## Summary" |
| 309 | + echo "" |
| 310 | + echo "- **Pass**: ${pass_count}/${pkg_count} packages compile successfully" |
| 311 | + echo "- **Fail**: ${fail_count}/${pkg_count} packages fail to compile" |
| 312 | + echo "- **Partial**: ${partial_count}/${pkg_count} packages compile with warnings" |
| 313 | + echo "" |
| 314 | + echo "## Compatibility Table" |
| 315 | + echo "" |
| 316 | + echo "| Package | Import Path | Status | Errors | Notes |" |
| 317 | + echo "|---------|-------------|--------|--------|-------|" |
| 318 | + |
| 319 | + # Generate table rows using jq |
| 320 | + jq -r '.packages[] | "| \(.name) | \(.importPath) | \(.compileStatus) | \(.errorCount) | \(.notes) |"' "${JSON_OUTPUT}" |
| 321 | + |
| 322 | + echo "" |
| 323 | + echo "---" |
| 324 | + echo "" |
| 325 | + echo "*Generated by \`scripts/audit-tinygo-stdlib.sh --table\`*" |
| 326 | + } > "${TABLE_OUTPUT}" |
| 327 | + |
| 328 | + log "Table written to ${TABLE_OUTPUT}" |
| 329 | +} |
| 330 | + |
| 331 | +show_json() { |
| 332 | + if [ ! -f "${JSON_OUTPUT}" ]; then |
| 333 | + err "JSON results not found: ${JSON_OUTPUT}" |
| 334 | + err "Run with --build first to generate results." |
| 335 | + exit 1 |
| 336 | + fi |
| 337 | + |
| 338 | + if command -v jq &>/dev/null; then |
| 339 | + jq '.' "${JSON_OUTPUT}" |
| 340 | + else |
| 341 | + cat "${JSON_OUTPUT}" |
| 342 | + fi |
| 343 | +} |
| 344 | + |
| 345 | +run_tests() { |
| 346 | + log "Running Go test suite..." |
| 347 | + |
| 348 | + cd "${FIXTURE_DIR}" |
| 349 | + |
| 350 | + if ! go vet ./... 2>&1; then |
| 351 | + err "go vet failed" |
| 352 | + exit 2 |
| 353 | + fi |
| 354 | + |
| 355 | + if ! go test -v -count=1 -timeout=120s . 2>&1; then |
| 356 | + err "Go tests failed" |
| 357 | + exit 2 |
| 358 | + fi |
| 359 | + |
| 360 | + log "All Go tests passed." |
| 361 | +} |
| 362 | + |
| 363 | +case "${MODE}" in |
| 364 | + --build) run_build ;; |
| 365 | + --table) generate_table ;; |
| 366 | + --json) show_json ;; |
| 367 | + --test) run_tests ;; |
| 368 | + --all) run_build; generate_table; run_tests ;; |
| 369 | + --help|-h) |
| 370 | + echo "Usage: $0 [--build|--table|--json|--test|--all]" |
| 371 | + echo "" |
| 372 | + echo "Modes:" |
| 373 | + echo " --build Run TinyGo wasip2 builds for all 20 stdlib packages" |
| 374 | + echo " --table Generate human-readable markdown compatibility table" |
| 375 | + echo " --json Print compatibility results as JSON" |
| 376 | + echo " --test Run Go test suite (validates file structure + JSON schema)" |
| 377 | + echo " --all Run build + table + test (default)" |
| 378 | + echo "" |
| 379 | + echo "Prerequisites:" |
| 380 | + echo " - Go 1.22+ (go command in PATH)" |
| 381 | + echo " - TinyGo 0.40+ (build/tinygo/bin/tinygo or system PATH)" |
| 382 | + echo " - jq (for --table mode)" |
| 383 | + ;; |
| 384 | + *) |
| 385 | + echo "Unknown option: ${MODE}" |
| 386 | + echo "Run $0 --help for usage" |
| 387 | + exit 1 |
| 388 | + ;; |
| 389 | +esac |
0 commit comments