#!/usr/bin/env bash set -euo pipefail cd "$(dirname "$0")" # LinuxCNC source basis: this guard ties the documented WASM blocker map to # LinuxCNC RS274/Python/remap/tooldata sources, the rs274 WASM manifest, and # wasm-safe shim replacement sources. It validates source coverage only; it # does not add CNC behavior, expand smoke parsing, or interpret G-code. linuxcnc_root=${LINUXCNC_ROOT:-../linuxcnc} manifest=${1:-linuxcnc-rs274-wasm-source-files.txt} LINUXCNC_ROOT="$linuxcnc_root" ./check-linuxcnc-inputs-cached.sh "$manifest" linuxcnc_root=$(cd "$linuxcnc_root" && pwd) grep -F 'LinuxCNC source basis: this blocker scan is constrained to the RS274/WASM' \ analyze-linuxcnc-wasm-blockers.sh >/dev/null grep -F 'blocked replacement is not in wasm shim build set' \ analyze-linuxcnc-wasm-blockers.sh >/dev/null grep -F 'shim_source_output=$(./list-linuxcnc-wasm-safe-shims.sh)' \ analyze-linuxcnc-wasm-blockers.sh >/dev/null grep -F 'LinuxCNC source basis: this guardrail locks the blocker categories reported' \ test-linuxcnc-wasm-blockers.sh >/dev/null grep -F 'Cache wasm blocker analyzer output' docs/linuxcnc-source-policy.md >/dev/null grep -F '#include ' "$linuxcnc_root/src/emc/rs274ngc/gcodemodule.cc" >/dev/null grep -F 'PyInit_gcode' "$linuxcnc_root/src/emc/rs274ngc/gcodemodule.cc" >/dev/null grep -F '#include ' "$linuxcnc_root/src/emc/rs274ngc/interpmodule.cc" >/dev/null grep -F '#include "pythonplugin/python_plugin.hh"' "$linuxcnc_root/src/emc/rs274ngc/interp_python.cc" >/dev/null grep -F 'dlopen' "$linuxcnc_root/src/emc/rs274ngc/interp_base.cc" >/dev/null grep -F 'opendir(direct)' "$linuxcnc_root/src/emc/rs274ngc/interp_o_word.cc" >/dev/null grep -F 'readdir(aDir)' "$linuxcnc_root/src/emc/rs274ngc/interp_o_word.cc" >/dev/null grep -F 'std::string tempfile = std::string(filename) + ".new";' \ "$linuxcnc_root/src/emc/rs274ngc/rs274ngc_pre.cc" >/dev/null grep -F '+ RS274NGC_PARAMETER_FILE_BACKUP_SUFFIX;' \ "$linuxcnc_root/src/emc/rs274ngc/rs274ngc_pre.cc" >/dev/null grep -F 'unlink(bakfile.c_str());' \ "$linuxcnc_root/src/emc/rs274ngc/rs274ngc_pre.cc" >/dev/null grep -F 'if(link(filename, bakfile.c_str()) < 0)' \ "$linuxcnc_root/src/emc/rs274ngc/rs274ngc_pre.cc" >/dev/null grep -F 'if(rename(tempfile.c_str(), filename) < 0)' \ "$linuxcnc_root/src/emc/rs274ngc/rs274ngc_pre.cc" >/dev/null grep -F '#define TOOL_MMAP_FILENAME ".tool.mmap"' "$linuxcnc_root/src/emc/tooldata/tooldata_mmap.cc" >/dev/null grep -F 'int tool_mmap_creator(' "$linuxcnc_root/src/emc/tooldata/tooldata_mmap.cc" >/dev/null python3 - "$linuxcnc_root" "$manifest" <<'PY' from pathlib import Path import re import subprocess import sys linuxcnc_root = Path(sys.argv[1]) manifest = Path(sys.argv[2]) source_map = Path("docs/linuxcnc-wasm-blockers-source-map.md").read_text(encoding="utf-8") manifest_lines = [ line for line in manifest.read_text(encoding="utf-8").splitlines() if line and not line.startswith("#") ] manifest_counts = {"core": 0, "blocked": 0, "header": 0, "blocked-header": 0, "metadata": 0} for line in manifest_lines: group = line.split(":", 1)[0] if group not in manifest_counts: raise SystemExit(f"unexpected WASM manifest group in blocker source map guard: {group}") manifest_counts[group] += 1 for group, count in manifest_counts.items(): if f"| `{group}` | {count} |" not in source_map: raise SystemExit(f"docs/linuxcnc-wasm-blockers-source-map.md count mismatch for {group}") output = subprocess.check_output( ["./analyze-linuxcnc-wasm-blockers.sh", str(manifest)], text=True, env={"LINUXCNC_ROOT": str(linuxcnc_root)}, ) sections = {} current = None for line in output.splitlines(): if line.startswith("[") and line.endswith("]"): current = line[1:-1] sections[current] = [] elif current and line: sections[current].append(line) expected_counts = { "manifest-core": "core=26", "manifest-blocked": "blocked=9", "python": 8, "python-compile-covered": 8, "tooldata": 1, "native-backend": 1, "native-fs": 1, "blocked-replacements": 9, "core-python-shimmed": 6, "core-dlopen-shimmed": 1, "core-tooldata-shimmed": 1, "core-tooldata-users-shimmed": 2, "core-native-fs-shimmed": 16, } manifest_section = sections.get("manifest", []) if expected_counts["manifest-core"] not in manifest_section: raise SystemExit("WASM blocker analyzer core count changed") if expected_counts["manifest-blocked"] not in manifest_section: raise SystemExit("WASM blocker analyzer blocked count changed") for section, count in expected_counts.items(): if section.startswith("manifest-"): continue entries = [entry for entry in sections.get(section, []) if entry != "(none)"] if len(entries) != count: raise SystemExit(f"WASM blocker analyzer section {section} count changed: {len(entries)}") if f"| `{section}` | {count} |" not in source_map: raise SystemExit(f"docs/linuxcnc-wasm-blockers-source-map.md analyzer count mismatch for {section}") if sections.get("dlopen") != ["(none)"]: raise SystemExit("blocked dlopen section is no longer empty") if sections.get("tooldata-users") != ["(none)"]: raise SystemExit("blocked tooldata-users section is no longer empty") if sections.get("python") != sections.get("python-compile-covered"): raise SystemExit("blocked Python sources are no longer fully compile-covered by the inert Python C API probe") shim_paths = set(Path(line).as_posix() for line in subprocess.check_output( ["./list-linuxcnc-wasm-safe-shims.sh"], text=True, ).splitlines() if line) required_tracked_entries = [ "src/emc/rs274ngc/interp_base.cc -> core/wasm_shims/dlfcn.cc", "src/emc/rs274ngc/gcodemodule.cc", "src/emc/rs274ngc/interpmodule.cc", "src/emc/tooldata/tooldata_mmap.cc", "src/emc/rs274ngc/interp_base.cc", "src/emc/rs274ngc/interp_o_word.cc", "src/emc/rs274ngc/rs274ngc_pre.cc", ] required_replacements = [ "src/emc/rs274ngc/canonmodule.cc -> core/wasm_shims/linuxcnc_runtime_shim.cc", "src/emc/rs274ngc/gcodemodule.cc -> core/wasm_shims/linuxcnc_runtime_shim.cc", "src/emc/rs274ngc/interpmodule.cc -> core/wasm_shims/linuxcnc_runtime_shim.cc", "src/emc/rs274ngc/pyarrays.cc -> core/wasm_shims/python_c_api_shim.cc", "src/emc/rs274ngc/pyblock.cc -> core/wasm_shims/python_c_api_shim.cc", "src/emc/rs274ngc/pyemctypes.cc -> core/wasm_shims/python_c_api_shim.cc", "src/emc/rs274ngc/pyinterp1.cc -> core/wasm_shims/python_c_api_shim.cc", "src/emc/rs274ngc/pyparamclass.cc -> core/wasm_shims/python_c_api_shim.cc", "src/emc/tooldata/tooldata_mmap.cc -> core/wasm_shims/tooldata/tooldata_mmap_backend.cc", ] for entry in required_tracked_entries: if " -> " in entry: source, shim = entry.split(" -> ", 1) if source not in sections.get("core-dlopen-shimmed", []): raise SystemExit(f"WASM blocker analyzer output missing core dlopen shimmed source: {source}") if shim not in shim_paths: raise SystemExit(f"WASM blocker analyzer core dlopen shim is absent from wasm-safe shim set: {shim}") elif entry not in output: raise SystemExit(f"WASM blocker analyzer output missing required entry: {entry}") if entry not in source_map: raise SystemExit(f"docs/linuxcnc-wasm-blockers-source-map.md missing required entry: {entry}") replacement_entries = [entry for entry in sections.get("blocked-replacements", []) if entry != "(none)"] if replacement_entries != required_replacements: raise SystemExit(f"WASM blocker replacement set changed: {replacement_entries}") if "Required replacement set:" not in source_map: raise SystemExit("docs/linuxcnc-wasm-blockers-source-map.md must document the complete replacement set") for entry in required_replacements: if entry not in source_map: raise SystemExit(f"docs/linuxcnc-wasm-blockers-source-map.md missing required replacement: {entry}") for replacement in re.findall(r"-> (core/wasm_shims/[^\s]+)", output): if replacement not in shim_paths: raise SystemExit(f"blocked replacement is absent from wasm-safe shim set: {replacement}") for phrase in [ "does not add CNC behavior", "must not become smoke parser behavior", "blocked `dlopen` and blocked tooldata-users remain empty", 'parameter-file replacement through `filename + ".new"`', "O-word lookup and parameter-file replacement", "Python/Boost.Python remap paths remain tracked blockers", "Python/Boost.Python blockers are compile-covered by the inert Python C API probe", "./check-linuxcnc-wasm-blockers-source-map.sh", "does not interpret G-code", ]: if phrase not in source_map: raise SystemExit(f"docs/linuxcnc-wasm-blockers-source-map.md missing boundary phrase: {phrase}") for entry in [ "test-native.sh", "test-linuxcnc-source-link.sh", "build-wasm.sh", ]: text = Path(entry).read_text(encoding="utf-8") if "./check-linuxcnc-wasm-blockers-source-map.sh" not in text: raise SystemExit(f"{entry} must run ./check-linuxcnc-wasm-blockers-source-map.sh") for entry in [ "test-native.sh", "test-linuxcnc-source-link.sh", ]: text = Path(entry).read_text(encoding="utf-8") if "CNC_SIM_WASM_BLOCKERS_SELF_CHECKS=1" not in text: raise SystemExit(f"{entry} must run wasm blocker analyzer self-checks") PY