#!/usr/bin/env bash set -euo pipefail cd "$(dirname "$0")" # LinuxCNC source basis: this guard ties the documented wasm-safe shim map to # the linuxcnc_wasm_safe_probe_shims CMake set, the RS274/WASM source manifest, # and the LinuxCNC sources each shim cites. 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: wasm-safe shim probe sources are limited to the' \ list-linuxcnc-wasm-safe-shims.sh >/dev/null grep -F 'set(linuxcnc_wasm_safe_probe_shims' core/CMakeLists.txt >/dev/null grep -F 'LinuxCNC source basis: this guard ties the documented WASM blocker map' \ check-linuxcnc-wasm-blockers-source-map.sh >/dev/null grep -F 'dlopen(NULL, RTLD_GLOBAL);' "$linuxcnc_root/src/emc/rs274ngc/interp_base.cc" >/dev/null grep -F 'interp_lib = dlopen(interp_path, RTLD_NOW);' "$linuxcnc_root/src/emc/rs274ngc/interp_base.cc" >/dev/null grep -F 'Constructor constructor = (Constructor)dlsym(interp_lib, "makeInterp");' \ "$linuxcnc_root/src/emc/rs274ngc/interp_base.cc" >/dev/null grep -F 'dlerror()' "$linuxcnc_root/src/emc/rs274ngc/interp_base.cc" >/dev/null grep -F '#define _(s) gettext(s)' "$linuxcnc_root/src/emc/rs274ngc/interp_internal.hh" >/dev/null grep -F 'int _task = 0;' "$linuxcnc_root/src/emc/rs274ngc/gcodemodule.cc" >/dev/null grep -F 'int _task = 1;' "$linuxcnc_root/src/emc/task/emctaskmain.cc" >/dev/null grep -F 'struct _inittab builtin_modules[]' "$linuxcnc_root/src/emc/task/taskclass.cc" >/dev/null grep -F 'int Interp::fetch_hal_param(' "$linuxcnc_root/src/emc/rs274ngc/interp_namedparams.cc" >/dev/null grep -F 'hal_get_pin_value_by_name(hal_name, &type, &ptr, &conn)' \ "$linuxcnc_root/src/emc/rs274ngc/interp_namedparams.cc" >/dev/null grep -F 'hal_get_signal_value_by_name(hal_name, &type, &ptr, &conn)' \ "$linuxcnc_root/src/emc/rs274ngc/interp_namedparams.cc" >/dev/null grep -F 'hal_get_param_value_by_name(hal_name, &type, &ptr)' \ "$linuxcnc_root/src/emc/rs274ngc/interp_namedparams.cc" >/dev/null grep -F 'HAL_TYPE_UNINITIALIZED = 0,' "$linuxcnc_root/include/hal.h" >/dev/null grep -F 'extern int hal_get_pin_value_by_name(' "$linuxcnc_root/include/hal.h" >/dev/null grep -F 'extern int hal_get_signal_value_by_name(' "$linuxcnc_root/include/hal.h" >/dev/null grep -F 'extern int hal_get_param_value_by_name(' "$linuxcnc_root/include/hal.h" >/dev/null grep -F 'int hal_get_pin_value_by_name(' "$linuxcnc_root/src/hal/hal_lib.c" >/dev/null grep -F 'int hal_get_signal_value_by_name(' "$linuxcnc_root/src/hal/hal_lib.c" >/dev/null grep -F 'int hal_get_param_value_by_name(' "$linuxcnc_root/src/hal/hal_lib.c" >/dev/null grep -F 'wordexp(basename, &exp_result, 0);' "$linuxcnc_root/src/emc/rs274ngc/rs274ngc_pre.cc" >/dev/null grep -F 'EMC_STAT *emcStatus = new EMC_STAT;' "$linuxcnc_root/src/emc/sai/dummyemcstat.cc" >/dev/null grep -F 'NMLmsg::NMLmsg' "$linuxcnc_root/src/libnml/nml/nmlmsg.cc" >/dev/null grep -F 'RCS_STAT_MSG::RCS_STAT_MSG' "$linuxcnc_root/src/libnml/nml/stat_msg.cc" >/dev/null grep -F 'print_rcs_error_new' "$linuxcnc_root/src/libnml/rcs/rcs_print.cc" >/dev/null grep -F 'rtapi_snprintf' "$linuxcnc_root/src/rtapi/uspace_common.h" >/dev/null grep -F 'class PythonPlugin' "$linuxcnc_root/src/emc/pythonplugin/python_plugin.hh" >/dev/null grep -F 'PLUGIN_PYTHON_NOT_INITIALIZED = -12,' "$linuxcnc_root/src/emc/pythonplugin/python_plugin.hh" >/dev/null grep -F 'PLUGIN_NO_CALLABLE = 1,' "$linuxcnc_root/src/emc/pythonplugin/python_plugin.hh" >/dev/null grep -F 'PLUGIN_EXCEPTION = 2' "$linuxcnc_root/src/emc/pythonplugin/python_plugin.hh" >/dev/null grep -F 'PythonPlugin::instantiate' "$linuxcnc_root/src/emc/pythonplugin/python_plugin.cc" >/dev/null grep -F 'if (status < PLUGIN_OK)' "$linuxcnc_root/src/emc/pythonplugin/python_plugin.cc" >/dev/null grep -F 'retval = bp::exec_file(cmd, main_namespace, main_namespace);' \ "$linuxcnc_root/src/emc/pythonplugin/python_plugin.cc" >/dev/null grep -F 'retval = bp::exec(cmd, main_namespace, main_namespace);' \ "$linuxcnc_root/src/emc/pythonplugin/python_plugin.cc" >/dev/null grep -F 'PyObject *rv = PyObject_Call(function.ptr(), tupleargs.ptr(), kwargs.ptr());' \ "$linuxcnc_root/src/emc/pythonplugin/python_plugin.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_nml_register(CANON_TOOL_TABLE *tblptr)' "$linuxcnc_root/src/emc/tooldata/tooldata_nml.cc" >/dev/null grep -F 'int tooldata_db_init(char progname_plus_args[]' "$linuxcnc_root/src/emc/tooldata/tooldata_db.cc" >/dev/null grep -F 'if (!db_live) {return -1;}' "$linuxcnc_root/src/emc/tooldata/tooldata_db.cc" >/dev/null grep -F 'if (!db_live) return 0;' "$linuxcnc_root/src/emc/tooldata/tooldata_db.cc" >/dev/null python3 - "$manifest" <<'PY' from pathlib import Path import re import subprocess import sys manifest = Path(sys.argv[1]) source_map = Path("docs/linuxcnc-wasm-shims-source-map.md").read_text(encoding="utf-8") manifest_text = manifest.read_text(encoding="utf-8") shim_paths = [ line for line in subprocess.check_output(["./list-linuxcnc-wasm-safe-shims.sh"], text=True).splitlines() if line ] if len(shim_paths) != 11: raise SystemExit(f"expected 11 wasm-safe CMake shim sources, found {len(shim_paths)}") if f"| CMake `linuxcnc_wasm_safe_probe_shims` sources | {len(shim_paths)} |" not in source_map: raise SystemExit("docs/linuxcnc-wasm-shims-source-map.md shim count mismatch") expected_shims = { "core/wasm_shims/dlfcn.cc": ["src/emc/rs274ngc/interp_base.cc"], "core/wasm_shims/emc_status_shim.cc": ["src/emc/sai/dummyemcstat.cc"], "core/wasm_shims/gettext_shim.cc": ["src/emc/rs274ngc/interp_internal.hh"], "core/wasm_shims/linuxcnc_runtime_shim.cc": [ "src/emc/rs274ngc/gcodemodule.cc", "src/emc/task/emctaskmain.cc", "src/emc/task/taskclass.cc", "src/emc/rs274ngc/interp_namedparams.cc", "include/hal.h", "src/hal/hal_lib.c", "src/emc/rs274ngc/rs274ngc_pre.cc", ], "core/wasm_shims/nml_status_shim.cc": [ "src/libnml/nml/nmlmsg.cc", "src/libnml/nml/stat_msg.cc", ], "core/wasm_shims/python_c_api_shim.cc": [ "src/emc/rs274ngc/interp_python.cc", "src/emc/rs274ngc/interp_namedparams.cc", "src/emc/rs274ngc/gcodemodule.cc", "src/emc/pythonplugin/python_plugin.cc", ], "core/wasm_shims/rcs_print_shim.cc": ["src/libnml/rcs/rcs_print.cc"], "core/wasm_shims/rtapi_compat.cc": [ "src/rtapi/uspace_common.h", "src/rtapi/rtapi.h", ], "core/wasm_shims/tooldata/tooldata_mmap_backend.cc": [ "src/emc/tooldata/tooldata_mmap.cc", ], "core/wasm_shims/tooldata/tooldata_runtime_stubs.cc": [ "src/emc/tooldata/tooldata_nml.cc", "src/emc/tooldata/tooldata_db.cc", ], "core/wasm_shims/pythonplugin/python_plugin.cc": [ "src/emc/pythonplugin/python_plugin.cc", "src/emc/pythonplugin/python_plugin.hh", ], } if shim_paths != list(expected_shims): raise SystemExit("wasm-safe shim CMake order no longer matches docs/linuxcnc-wasm-shims-source-map.md") for shim_path, linuxcnc_sources in expected_shims.items(): shim_text = Path(shim_path).read_text(encoding="utf-8") if "LinuxCNC source basis:" not in shim_text: raise SystemExit(f"{shim_path} is missing a LinuxCNC source basis comment") if f"| `{shim_path}` |" not in source_map: raise SystemExit(f"docs/linuxcnc-wasm-shims-source-map.md missing shim row: {shim_path}") for source in linuxcnc_sources: if source not in source_map: raise SystemExit(f"docs/linuxcnc-wasm-shims-source-map.md missing source {source} for {shim_path}") if source not in manifest_text: raise SystemExit(f"{source} is not tracked in {manifest}") runtime_shim = Path("core/wasm_shims/linuxcnc_runtime_shim.cc").read_text(encoding="utf-8") runtime_probe = Path("core/wasm_shims/cnc_sim_wasm_runtime_probe_main.cc").read_text(encoding="utf-8") for needle in [ 'extern "C" int hal_get_pin_value_by_name', 'extern "C" int hal_get_signal_value_by_name', 'extern "C" int hal_get_param_value_by_name', "*type = HAL_TYPE_UNINITIALIZED;", "*data = nullptr;", "return -1;", ]: if needle not in runtime_shim: raise SystemExit(f"linuxcnc runtime shim missing HAL unavailable boundary: {needle}") for needle in [ "bool hal_lookup_is_unavailable()", 'hal_get_pin_value_by_name("cnc-sim.missing-pin", &type, &data, &connected) != -1', 'hal_get_signal_value_by_name("cnc-sim.missing-signal", &type, &data, &has_writers) != -1', 'hal_get_param_value_by_name("cnc-sim.missing-param", &type, &data) != -1', "type != HAL_TYPE_UNINITIALIZED", "data != nullptr", ]: if needle not in runtime_probe: raise SystemExit(f"runtime probe missing HAL unavailable assertion: {needle}") python_plugin_shim = Path("core/wasm_shims/pythonplugin/python_plugin.cc").read_text(encoding="utf-8") python_plugin_probe = Path("core/wasm_shims/pythonplugin/python_plugin_probe_main.cc").read_text(encoding="utf-8") python_c_api_shim = Path("core/wasm_shims/python_c_api_shim.cc").read_text(encoding="utf-8") for forbidden in [ "bp::exec(", "bp::exec_file(", "PyObject_Call(", ]: if forbidden in python_plugin_shim: raise SystemExit(f"PythonPlugin wasm shim must not execute Python: {forbidden}") for needle in [ "python disabled in wasm-safe probe", "status = PLUGIN_PYTHON_NOT_INITIALIZED;", "return PLUGIN_NO_CALLABLE;", "status = PLUGIN_EXCEPTION;", "if (status < PLUGIN_OK)", ]: if needle not in python_plugin_shim: raise SystemExit(f"PythonPlugin wasm shim missing disabled-python boundary: {needle}") for needle in [ "PLUGIN_NO_SECTION", "PLUGIN_BAD_INIFILE", "PLUGIN_PYTHON_NOT_INITIALIZED", "PLUGIN_NO_CALLABLE", "PLUGIN_EXCEPTION", "handle_pyerror().empty()", ]: if needle not in python_plugin_probe: raise SystemExit(f"PythonPlugin probe missing disabled-python status assertion: {needle}") for needle in [ "Py_IsInitialized", "PyObject_CallMethod", "Py_BuildValue", "PyModule_Create", "PyType_Ready", ]: if needle not in python_c_api_shim: raise SystemExit(f"Python C API shim missing symbol boundary: {needle}") for phrase in [ "must not add CNC behavior", "must not expand the temporary smoke parser", "CMake `linuxcnc_wasm_safe_probe_shims` set", "RS274/WASM manifest", "browser-safe LinuxCNC adaptations", "dynamic loading unavailable", "unavailable HAL lookup", "must leave HAL values unavailable", "must not switch browser-safe probes into task mode", "must not become a shell expansion feature", "must not become Python execution", "keeping Python execution disabled", "must not execute `bp::exec`, `bp::exec_file`, or", "inactive-DB return values", "negative-status short-circuit behavior", "./check-linuxcnc-wasm-shims-source-map.sh", "does not interpret G-code", ]: if phrase not in source_map: raise SystemExit(f"docs/linuxcnc-wasm-shims-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-shims-source-map.sh" not in text: raise SystemExit(f"{entry} must run ./check-linuxcnc-wasm-shims-source-map.sh") cmake_text = Path("core/CMakeLists.txt").read_text(encoding="utf-8") for shim_path in shim_paths: cmake_suffix = shim_path.removeprefix("core/") if cmake_suffix not in cmake_text: raise SystemExit(f"{shim_path} is not listed in linuxcnc_wasm_safe_probe_shims") blocker_map = Path("docs/linuxcnc-wasm-blockers-source-map.md").read_text(encoding="utf-8") for shim_path in [ "core/wasm_shims/linuxcnc_runtime_shim.cc", "core/wasm_shims/python_c_api_shim.cc", "core/wasm_shims/tooldata/tooldata_mmap_backend.cc", ]: if shim_path not in blocker_map: raise SystemExit(f"docs/linuxcnc-wasm-blockers-source-map.md no longer references {shim_path}") PY