Files
wasm-simulator/check-linuxcnc-switchkins-remap-table.sh

3067 lines
122 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
# LinuxCNC source basis: switchkins/remap table validation derives M428/M429/M430
# cases from LinuxCNC configs/sim INI/HAL files, remap_subs/*.ngc #<kinstype>
# assignments, and src/emc/kinematics switchkins sources.
linuxcnc_root=${LINUXCNC_ROOT:-../linuxcnc}
manifest=${1:-linuxcnc-kinematics-source-files.txt}
usage() {
echo "usage: $0 [manifest]" >&2
}
if [[ "${manifest:-}" == "--help" || "${manifest:-}" == "-h" ]]; then
usage
exit 0
fi
if [[ "${manifest:-}" == --* ]]; then
usage
echo "unknown switchkins remap table checker option: $manifest" >&2
exit 1
fi
if [[ -n "${2:-}" ]]; then
usage
echo "too many switchkins remap table checker arguments" >&2
exit 1
fi
if [[ ! -d "$linuxcnc_root" ]]; then
echo "missing LinuxCNC root: $linuxcnc_root" >&2
exit 1
fi
if [[ ! -f "$manifest" ]]; then
echo "missing kinematics manifest: $manifest" >&2
exit 1
fi
LINUXCNC_ROOT="$linuxcnc_root" ./check-linuxcnc-inputs-cached.sh "$manifest" --require-kinematics-complete
validate_manifest_switchkins_complete() {
local manifest_file=$1
if command -v python3 >/dev/null 2>&1; then
if ! python3 - "$linuxcnc_root" "$manifest_file" <<'PY'
import os
import re
import sys
linuxcnc_root, manifest_file = sys.argv[1:]
sim_root = os.path.join(linuxcnc_root, "configs", "sim")
remap_ini_re = re.compile(r"REMAP\s*=\s*M(428|429|430)(?:\D|$)", re.I)
switchkins_remap_names = {"428remap.ngc", "429remap.ngc", "430remap.ngc"}
manifest_config_paths = set()
manifest_remap_paths = set()
with open(manifest_file, encoding="utf-8") as manifest:
for line in manifest:
parts = line.rstrip("\n").split(":", 2)
if len(parts) < 2:
continue
group, source_path = parts[:2]
if group == "config" and source_path.endswith(".ini"):
with open(os.path.join(linuxcnc_root, source_path), encoding="utf-8") as handle:
if remap_ini_re.search(handle.read()):
manifest_config_paths.add(source_path)
elif group == "remap":
manifest_remap_paths.add(source_path)
linuxcnc_config_paths = set()
linuxcnc_remap_paths = set()
for current_root, _dirs, files in os.walk(sim_root):
for filename in files:
source = os.path.join(current_root, filename)
source_path = os.path.relpath(source, linuxcnc_root)
if filename.endswith(".ini"):
with open(source, encoding="utf-8", errors="ignore") as handle:
if remap_ini_re.search(handle.read()):
linuxcnc_config_paths.add(source_path)
elif filename in switchkins_remap_names:
linuxcnc_remap_paths.add(source_path)
missing_configs = sorted(linuxcnc_config_paths - manifest_config_paths)
extra_configs = sorted(manifest_config_paths - linuxcnc_config_paths)
missing_remaps = sorted(linuxcnc_remap_paths - manifest_remap_paths)
extra_remaps = sorted(manifest_remap_paths - linuxcnc_remap_paths)
for source_path in missing_configs:
print(f"missing LinuxCNC switchkins REMAP INI in manifest: {source_path}", file=sys.stderr)
for source_path in extra_configs:
print(f"unexpected LinuxCNC switchkins REMAP INI in manifest: {source_path}", file=sys.stderr)
for source_path in missing_remaps:
print(f"missing LinuxCNC switchkins remap source in manifest: {source_path}", file=sys.stderr)
for source_path in extra_remaps:
print(f"unexpected LinuxCNC switchkins remap source in manifest: {source_path}", file=sys.stderr)
if missing_configs or extra_configs or missing_remaps or extra_remaps:
raise SystemExit(1)
PY
then
exit 1
fi
elif command -v node >/dev/null 2>&1; then
if ! node - "$linuxcnc_root" "$manifest_file" <<'JS'
const fs = require("fs");
const path = require("path");
const [linuxcncRoot, manifestFile] = process.argv.slice(2);
const simRoot = path.join(linuxcncRoot, "configs", "sim");
const remapIniRe = /REMAP\s*=\s*M(428|429|430)(?:\D|$)/i;
const switchkinsRemapNames = new Set(["428remap.ngc", "429remap.ngc", "430remap.ngc"]);
const manifestConfigPaths = new Set();
const manifestRemapPaths = new Set();
for (const line of fs.readFileSync(manifestFile, "utf8").split(/\r?\n/)) {
const parts = line.split(":", 3);
if (parts.length < 2) {
continue;
}
const [group, sourcePath] = parts;
if (group === "config" && sourcePath.endsWith(".ini")) {
const text = fs.readFileSync(path.join(linuxcncRoot, sourcePath), "utf8");
if (remapIniRe.test(text)) {
manifestConfigPaths.add(sourcePath);
}
} else if (group === "remap") {
manifestRemapPaths.add(sourcePath);
}
}
function walkFiles(root) {
const files = [];
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
const fullPath = path.join(root, entry.name);
if (entry.isDirectory()) {
files.push(...walkFiles(fullPath));
} else if (entry.isFile()) {
files.push(fullPath);
}
}
return files;
}
const linuxcncConfigPaths = new Set();
const linuxcncRemapPaths = new Set();
for (const source of walkFiles(simRoot)) {
const sourcePath = path.relative(linuxcncRoot, source);
const basename = path.basename(source);
if (basename.endsWith(".ini")) {
const text = fs.readFileSync(source, "utf8");
if (remapIniRe.test(text)) {
linuxcncConfigPaths.add(sourcePath);
}
} else if (switchkinsRemapNames.has(basename)) {
linuxcncRemapPaths.add(sourcePath);
}
}
const missingConfigs = [...linuxcncConfigPaths].filter((sourcePath) => !manifestConfigPaths.has(sourcePath)).sort();
const extraConfigs = [...manifestConfigPaths].filter((sourcePath) => !linuxcncConfigPaths.has(sourcePath)).sort();
const missingRemaps = [...linuxcncRemapPaths].filter((sourcePath) => !manifestRemapPaths.has(sourcePath)).sort();
const extraRemaps = [...manifestRemapPaths].filter((sourcePath) => !linuxcncRemapPaths.has(sourcePath)).sort();
for (const sourcePath of missingConfigs) {
console.error(`missing LinuxCNC switchkins REMAP INI in manifest: ${sourcePath}`);
}
for (const sourcePath of extraConfigs) {
console.error(`unexpected LinuxCNC switchkins REMAP INI in manifest: ${sourcePath}`);
}
for (const sourcePath of missingRemaps) {
console.error(`missing LinuxCNC switchkins remap source in manifest: ${sourcePath}`);
}
for (const sourcePath of extraRemaps) {
console.error(`unexpected LinuxCNC switchkins remap source in manifest: ${sourcePath}`);
}
if (missingConfigs.length || extraConfigs.length || missingRemaps.length || extraRemaps.length) {
process.exit(1);
}
JS
then
exit 1
fi
else
echo "missing python3 or node for switchkins remap manifest completeness validation" >&2
exit 1
fi
}
validate_manifest_switchkins_complete "$manifest"
validate_manifest_remap_kinstypes() {
local manifest_file=$1
if command -v python3 >/dev/null 2>&1; then
if ! python3 - "$linuxcnc_root" "$manifest_file" <<'PY'
import os
import re
import sys
linuxcnc_root, manifest_file = sys.argv[1:]
remap_re = re.compile(r'^\s*#<kinstype>\s*=\s*(-?\d+)\b')
seen = {}
with open(manifest_file, encoding="utf-8") as manifest:
for line_number, line in enumerate(manifest, 1):
parts = line.rstrip("\n").split(":", 2)
if len(parts) < 2 or parts[0] != "remap":
continue
path = parts[1]
basename = os.path.basename(path)
if basename not in {"428remap.ngc", "429remap.ngc", "430remap.ngc"}:
raise SystemExit(f"unexpected switchkins remap source at {manifest_file}:{line_number}: {path}")
if path in seen:
raise SystemExit(f"duplicate switchkins remap source at {manifest_file}:{line_number}: {path}")
seen[path] = line_number
source = os.path.join(linuxcnc_root, path)
with open(source, encoding="utf-8") as handle:
kinstypes = [int(match.group(1)) for match in map(remap_re.match, handle) if match]
if len(kinstypes) != 1:
raise SystemExit(f"expected exactly one #<kinstype> assignment in {path}, found {len(kinstypes)}")
if kinstypes[0] < 0:
raise SystemExit(f"invalid negative #<kinstype> assignment in {path}: {kinstypes[0]}")
if not seen:
raise SystemExit(f"no switchkins remap sources listed in {manifest_file}")
PY
then
exit 1
fi
elif command -v node >/dev/null 2>&1; then
if ! node - "$linuxcnc_root" "$manifest_file" <<'JS'
const fs = require("fs");
const path = require("path");
const [linuxcncRoot, manifestFile] = process.argv.slice(2);
const remapRe = /^\s*#<kinstype>\s*=\s*(-?\d+)\b/;
const seen = new Map();
for (const [index, line] of fs.readFileSync(manifestFile, "utf8").split(/\r?\n/).entries()) {
const parts = line.split(":", 3);
if (parts.length < 2 || parts[0] !== "remap") {
continue;
}
const sourcePath = parts[1];
const basename = path.basename(sourcePath);
if (!["428remap.ngc", "429remap.ngc", "430remap.ngc"].includes(basename)) {
throw new Error(`unexpected switchkins remap source at ${manifestFile}:${index + 1}: ${sourcePath}`);
}
if (seen.has(sourcePath)) {
throw new Error(`duplicate switchkins remap source at ${manifestFile}:${index + 1}: ${sourcePath}`);
}
seen.set(sourcePath, index + 1);
const kinstypes = fs.readFileSync(path.join(linuxcncRoot, sourcePath), "utf8")
.split(/\r?\n/)
.map((sourceLine) => remapRe.exec(sourceLine))
.filter(Boolean)
.map((match) => Number(match[1]));
if (kinstypes.length !== 1) {
throw new Error(`expected exactly one #<kinstype> assignment in ${sourcePath}, found ${kinstypes.length}`);
}
if (kinstypes[0] < 0) {
throw new Error(`invalid negative #<kinstype> assignment in ${sourcePath}: ${kinstypes[0]}`);
}
}
if (seen.size === 0) {
throw new Error(`no switchkins remap sources listed in ${manifestFile}`);
}
JS
then
exit 1
fi
else
echo "missing python3 or node for switchkins remap manifest validation" >&2
exit 1
fi
}
validate_manifest_remap_kinstypes "$manifest"
validate_manifest_config_remaps() {
local manifest_file=$1
if command -v python3 >/dev/null 2>&1; then
if ! python3 - "$linuxcnc_root" "$manifest_file" <<'PY'
import os
import re
import sys
linuxcnc_root, manifest_file = sys.argv[1:]
remap_re = re.compile(r'REMAP\s*=\s*M(428|429|430)(?:\D|$)', re.I)
seen = {}
with open(manifest_file, encoding="utf-8") as manifest:
for line_number, line in enumerate(manifest, 1):
parts = line.rstrip("\n").split(":", 2)
if len(parts) < 2 or parts[0] != "config" or not parts[1].endswith(".ini"):
continue
source_path = parts[1]
source = os.path.join(linuxcnc_root, source_path)
with open(source, encoding="utf-8") as handle:
mcodes = {int(match.group(1)) for match in remap_re.finditer(handle.read())}
if not mcodes:
continue
seen[source_path] = mcodes
missing = {428, 429} - mcodes
if missing:
raise SystemExit(f"switchkins config {source_path} is missing required M-code remap(s): {sorted(missing)}")
if not seen:
raise SystemExit(f"no switchkins REMAP config sources listed in {manifest_file}")
PY
then
exit 1
fi
elif command -v node >/dev/null 2>&1; then
if ! node - "$linuxcnc_root" "$manifest_file" <<'JS'
const fs = require("fs");
const path = require("path");
const [linuxcncRoot, manifestFile] = process.argv.slice(2);
const remapRe = /REMAP\s*=\s*M(428|429|430)(?:\D|$)/ig;
const seen = new Map();
for (const line of fs.readFileSync(manifestFile, "utf8").split(/\r?\n/)) {
const parts = line.split(":", 3);
if (parts.length < 2 || parts[0] !== "config" || !parts[1].endsWith(".ini")) {
continue;
}
const sourcePath = parts[1];
const text = fs.readFileSync(path.join(linuxcncRoot, sourcePath), "utf8");
const mcodes = new Set();
for (const match of text.matchAll(remapRe)) {
mcodes.add(Number(match[1]));
}
if (mcodes.size === 0) {
continue;
}
seen.set(sourcePath, mcodes);
const missing = [428, 429].filter((mcode) => !mcodes.has(mcode));
if (missing.length > 0) {
throw new Error(`switchkins config ${sourcePath} is missing required M-code remap(s): ${missing.join(",")}`);
}
}
if (seen.size === 0) {
throw new Error(`no switchkins REMAP config sources listed in ${manifestFile}`);
}
JS
then
exit 1
fi
else
echo "missing python3 or node for switchkins remap config validation" >&2
exit 1
fi
}
validate_manifest_config_remaps "$manifest"
validate_generator_cli_contract() {
local manifest_file=$1
LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh --help >/dev/null 2>&1
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh --unknown "$manifest_file" >/dev/null 2>&1; then
echo "switchkins remap generator accepted unknown option" >&2
exit 1
fi
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh --all-output-dir >/dev/null 2>&1; then
echo "switchkins remap generator accepted --all-output-dir without directory" >&2
exit 1
fi
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh "$manifest_file" extra >/dev/null 2>&1; then
echo "switchkins remap generator accepted too many arguments" >&2
exit 1
fi
(
local cli_dir
local cli_manifest
local cli_manifest_with_config_ngc
local cli_manifest_with_config_tbl
local cli_manifest_with_duplicate_ini
local cli_manifest_with_duplicate_remap
local cli_manifest_with_duplicate_tooldata
local cli_manifest_with_unused_hal
local cli_manifest_with_unused_postgui
local cli_manifest_with_unused_remap
local cli_manifest_with_unused_tooldata
local cli_manifest_without_halfile_loadusr_asset
local cli_melfa_manifest
local cli_melfa_without_halcmd_loadusr_asset
local cli_trsrn_manifest
local cli_trsrn_without_g531_asset
local cli_trsrn_without_g69_asset
local cli_trsrn_without_halcmd_asset
local cli_trsrn_without_halcmd_import_asset
local cli_trsrn_without_on_abort_asset
local cli_trsrn_without_python_import_asset
local cli_trsrn_without_python_toplevel_asset
local cli_manifest_without_ini
local cli_manifest_without_hal
local cli_manifest_without_postgui
local cli_manifest_without_remap
local cli_manifest_without_tooldata
local cli_output_root_log
local cli_output_whitespace_log
cli_dir=$(mktemp -d "${TMPDIR:-/tmp}/linuxcnc_switchkins_remap_cli.XXXXXX")
trap 'rm -rf "$cli_dir"' EXIT
cli_manifest="$cli_dir/bridgemill-manifest.txt"
cli_manifest_with_config_ngc="$cli_dir/bridgemill-manifest-with-config-ngc.txt"
cli_manifest_with_config_tbl="$cli_dir/bridgemill-manifest-with-config-tbl.txt"
cli_manifest_with_duplicate_ini="$cli_dir/bridgemill-manifest-with-duplicate-ini.txt"
cli_manifest_with_duplicate_remap="$cli_dir/bridgemill-manifest-with-duplicate-remap.txt"
cli_manifest_with_duplicate_tooldata="$cli_dir/bridgemill-manifest-with-duplicate-tooldata.txt"
cli_manifest_with_unused_hal="$cli_dir/bridgemill-manifest-with-unused-hal.txt"
cli_manifest_with_unused_postgui="$cli_dir/bridgemill-manifest-with-unused-postgui.txt"
cli_manifest_with_unused_remap="$cli_dir/bridgemill-manifest-with-unused-remap.txt"
cli_manifest_with_unused_tooldata="$cli_dir/bridgemill-manifest-with-unused-tooldata.txt"
cli_manifest_without_halfile_loadusr_asset="$cli_dir/bridgemill-manifest-without-halfile-loadusr-asset.txt"
cli_melfa_manifest="$cli_dir/melfa-manifest.txt"
cli_melfa_without_halcmd_loadusr_asset="$cli_dir/melfa-manifest-without-halcmd-loadusr-asset.txt"
cli_trsrn_manifest="$cli_dir/trsrn-manifest.txt"
cli_trsrn_without_g531_asset="$cli_dir/trsrn-manifest-without-g531-asset.txt"
cli_trsrn_without_g69_asset="$cli_dir/trsrn-manifest-without-g69-asset.txt"
cli_trsrn_without_halcmd_asset="$cli_dir/trsrn-manifest-without-halcmd-asset.txt"
cli_trsrn_without_halcmd_import_asset="$cli_dir/trsrn-manifest-without-halcmd-import-asset.txt"
cli_trsrn_without_on_abort_asset="$cli_dir/trsrn-manifest-without-on-abort-asset.txt"
cli_trsrn_without_python_import_asset="$cli_dir/trsrn-manifest-without-python-import-asset.txt"
cli_trsrn_without_python_toplevel_asset="$cli_dir/trsrn-manifest-without-python-toplevel-asset.txt"
cli_manifest_without_ini="$cli_dir/bridgemill-manifest-without-ini.txt"
cli_manifest_without_hal="$cli_dir/bridgemill-manifest-without-hal.txt"
cli_manifest_without_postgui="$cli_dir/bridgemill-manifest-without-postgui.txt"
cli_manifest_without_remap="$cli_dir/bridgemill-manifest-without-remap.txt"
cli_manifest_without_tooldata="$cli_dir/bridgemill-manifest-without-tooldata.txt"
cli_output_root_log="$cli_dir/output-root.log"
cli_output_whitespace_log="$cli_dir/output-whitespace.log"
if ! awk -F: -v manifest="$manifest_file" '
function require(key) {
expected[++expected_count] = key
expected_keys[key] = 1
}
BEGIN {
require("config:configs/sim/axis/vismach/5axis/bridgemill/5axis.ini")
require("config:configs/sim/axis/vismach/5axis/bridgemill/5axisgui.hal")
require("config:configs/sim/axis/vismach/5axis/bridgemill/5axis_postgui.hal")
require("remap:configs/sim/axis/vismach/5axis/bridgemill/remap_subs/428remap.ngc")
require("remap:configs/sim/axis/vismach/5axis/bridgemill/remap_subs/429remap.ngc")
require("remap:configs/sim/axis/vismach/5axis/bridgemill/remap_subs/430remap.ngc")
require("tooldata:configs/sim/axis/vismach/5axis/bridgemill/5axis.tbl")
require("asset:src/hal/user_comps/vismach/5axisgui.py")
}
{
key = $1 ":" $2
if (key in expected_keys) {
if (key in manifest_line) {
print "duplicate switchkins remap generator CLI contract source in " manifest ": " key > "/dev/stderr"
duplicate = 1
}
manifest_line[key] = $0
}
}
END {
missing = 0
for (expected_index = 1; expected_index <= expected_count; expected_index++) {
key = expected[expected_index]
if (!(key in manifest_line)) {
print "switchkins remap generator CLI contract source is not listed in " manifest ": " key > "/dev/stderr"
missing = 1
continue
}
print manifest_line[key]
}
exit missing || duplicate
}
' "$manifest_file" >"$cli_manifest"; then
exit 1
fi
cp "$cli_manifest" "$cli_manifest_with_config_ngc"
grep -F \
'remap:configs/sim/axis/vismach/5axis/bridgemill/remap_subs/428remap.ngc:' \
"$cli_manifest" | sed 's/^remap:/config:/' >>"$cli_manifest_with_config_ngc"
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh \
"$cli_manifest_with_config_ngc" >/dev/null 2>&1; then
echo "switchkins remap generator accepted LinuxCNC remap source listed as config" >&2
exit 1
fi
cp "$cli_manifest" "$cli_manifest_with_config_tbl"
grep -F \
'tooldata:configs/sim/axis/vismach/5axis/bridgemill/5axis.tbl:' \
"$cli_manifest" | sed 's/^tooldata:/config:/' >>"$cli_manifest_with_config_tbl"
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh \
"$cli_manifest_with_config_tbl" >/dev/null 2>&1; then
echo "switchkins remap generator accepted LinuxCNC TOOL_TABLE source listed as config" >&2
exit 1
fi
cp "$cli_manifest" "$cli_manifest_with_duplicate_ini"
grep -F \
'config:configs/sim/axis/vismach/5axis/bridgemill/5axis.ini:' \
"$cli_manifest" >>"$cli_manifest_with_duplicate_ini"
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh \
"$cli_manifest_with_duplicate_ini" >/dev/null 2>&1; then
echo "switchkins remap generator accepted duplicate LinuxCNC INI config source" >&2
exit 1
fi
cp "$cli_manifest" "$cli_manifest_with_duplicate_remap"
grep -F \
'remap:configs/sim/axis/vismach/5axis/bridgemill/remap_subs/428remap.ngc:' \
"$cli_manifest" >>"$cli_manifest_with_duplicate_remap"
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh \
"$cli_manifest_with_duplicate_remap" >/dev/null 2>&1; then
echo "switchkins remap generator accepted duplicate LinuxCNC M428 remap source" >&2
exit 1
fi
cp "$cli_manifest" "$cli_manifest_with_duplicate_tooldata"
grep -F \
'tooldata:configs/sim/axis/vismach/5axis/bridgemill/5axis.tbl:' \
"$cli_manifest" >>"$cli_manifest_with_duplicate_tooldata"
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh \
"$cli_manifest_with_duplicate_tooldata" >/dev/null 2>&1; then
echo "switchkins remap generator accepted duplicate LinuxCNC TOOL_TABLE source" >&2
exit 1
fi
grep -Fv \
'config:configs/sim/axis/vismach/5axis/bridgemill/5axis.ini:' \
"$cli_manifest" >"$cli_manifest_without_ini"
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh \
"$cli_manifest_without_ini" >/dev/null 2>&1; then
echo "switchkins remap generator accepted manifest missing LinuxCNC INI config source" >&2
exit 1
fi
grep -Fv \
'config:configs/sim/axis/vismach/5axis/bridgemill/5axisgui.hal:' \
"$cli_manifest" >"$cli_manifest_without_hal"
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh \
"$cli_manifest_without_hal" >/dev/null 2>&1; then
echo "switchkins remap generator accepted manifest missing LinuxCNC HALFILE source" >&2
exit 1
fi
grep -Fv \
'config:configs/sim/axis/vismach/5axis/bridgemill/5axis_postgui.hal:' \
"$cli_manifest" >"$cli_manifest_without_postgui"
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh \
"$cli_manifest_without_postgui" >/dev/null 2>&1; then
echo "switchkins remap generator accepted manifest missing LinuxCNC POSTGUI_HALFILE source" >&2
exit 1
fi
grep -Fv \
'remap:configs/sim/axis/vismach/5axis/bridgemill/remap_subs/430remap.ngc:' \
"$cli_manifest" >"$cli_manifest_without_remap"
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh \
"$cli_manifest_without_remap" >/dev/null 2>&1; then
echo "switchkins remap generator accepted manifest missing LinuxCNC M430 remap source" >&2
exit 1
fi
grep -Fv \
'tooldata:configs/sim/axis/vismach/5axis/bridgemill/5axis.tbl:' \
"$cli_manifest" >"$cli_manifest_without_tooldata"
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh \
"$cli_manifest_without_tooldata" >/dev/null 2>&1; then
echo "switchkins remap generator accepted manifest missing LinuxCNC TOOL_TABLE source" >&2
exit 1
fi
grep -Fv \
'asset:src/hal/user_comps/vismach/5axisgui.py:' \
"$cli_manifest" >"$cli_manifest_without_halfile_loadusr_asset"
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh \
"$cli_manifest_without_halfile_loadusr_asset" >/dev/null 2>&1; then
echo "switchkins remap generator accepted manifest missing LinuxCNC HALFILE loadusr asset source" >&2
exit 1
fi
if ! awk -F: -v manifest="$manifest_file" '
function require(key) {
expected[++expected_count] = key
expected_keys[key] = 1
}
BEGIN {
require("config:configs/sim/axis/vismach/melfa-sim/melfa.ini")
require("config:configs/sim/axis/vismach/melfa-sim/melfa_dh.hal")
require("config:configs/sim/axis/vismach/melfa-sim/melfa-postgui.hal")
require("remap:configs/sim/axis/vismach/melfa-sim/remap_subs/428remap.ngc")
require("remap:configs/sim/axis/vismach/melfa-sim/remap_subs/429remap.ngc")
require("remap:configs/sim/axis/vismach/melfa-sim/remap_subs/430remap.ngc")
require("tooldata:configs/sim/axis/vismach/melfa-sim/melfa.tbl")
require("asset:src/hal/user_comps/vismach/melfagui.py")
}
{
key = $1 ":" $2
if (key in expected_keys) {
if (key in manifest_line) {
print "duplicate melfa switchkins source in " manifest ": " key > "/dev/stderr"
duplicate = 1
}
manifest_line[key] = $0
}
}
END {
missing = 0
for (expected_index = 1; expected_index <= expected_count; expected_index++) {
key = expected[expected_index]
if (!(key in manifest_line)) {
print "melfa switchkins source is not listed in " manifest ": " key > "/dev/stderr"
missing = 1
continue
}
print manifest_line[key]
}
exit missing || duplicate
}
' "$manifest_file" >"$cli_melfa_manifest"; then
exit 1
fi
grep -Fv \
'asset:src/hal/user_comps/vismach/melfagui.py:' \
"$cli_melfa_manifest" >"$cli_melfa_without_halcmd_loadusr_asset"
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh \
"$cli_melfa_without_halcmd_loadusr_asset" >/dev/null 2>&1; then
echo "switchkins remap generator accepted manifest missing LinuxCNC HALCMD loadusr command asset source" >&2
exit 1
fi
cp "$cli_manifest" "$cli_manifest_with_unused_hal"
grep -F \
'config:configs/sim/axis/vismach/melfa-sim/melfa_dh.hal:' \
"$manifest_file" >>"$cli_manifest_with_unused_hal"
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh \
"$cli_manifest_with_unused_hal" >/dev/null 2>&1; then
echo "switchkins remap generator accepted unused LinuxCNC HALFILE source" >&2
exit 1
fi
cp "$cli_manifest" "$cli_manifest_with_unused_postgui"
grep -F \
'config:configs/sim/axis/vismach/melfa-sim/melfa-postgui.hal:' \
"$manifest_file" >>"$cli_manifest_with_unused_postgui"
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh \
"$cli_manifest_with_unused_postgui" >/dev/null 2>&1; then
echo "switchkins remap generator accepted unused LinuxCNC POSTGUI_HALFILE source" >&2
exit 1
fi
cp "$cli_manifest" "$cli_manifest_with_unused_remap"
grep -F \
'remap:configs/sim/axis/vismach/melfa-sim/remap_subs/428remap.ngc:' \
"$manifest_file" >>"$cli_manifest_with_unused_remap"
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh \
"$cli_manifest_with_unused_remap" >/dev/null 2>&1; then
echo "switchkins remap generator accepted unused LinuxCNC M428 remap source" >&2
exit 1
fi
cp "$cli_manifest" "$cli_manifest_with_unused_tooldata"
grep -F \
'tooldata:configs/sim/axis/vismach/melfa-sim/melfa.tbl:' \
"$manifest_file" >>"$cli_manifest_with_unused_tooldata"
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh \
"$cli_manifest_with_unused_tooldata" >/dev/null 2>&1; then
echo "switchkins remap generator accepted unused LinuxCNC TOOL_TABLE source" >&2
exit 1
fi
if ! awk -F: -v manifest="$manifest_file" '
function require(key) {
expected[++expected_count] = key
expected_keys[key] = 1
}
BEGIN {
require("config:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini")
require("config:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn_postgui.hal")
require("config:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini")
require("config:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn_postgui.hal")
require("remap:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/remap_subs/428remap.ngc")
require("remap:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/remap_subs/429remap.ngc")
require("remap:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/remap_subs/430remap.ngc")
require("tooldata:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.tbl")
require("tooldata:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.tbl")
require("asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/python/remap.py")
require("asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/python/toplevel.py")
require("asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/python/twp-helper-comp.py")
require("asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/python/util.py")
require("asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/remap_subs/g531remap.ngc")
require("asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/remap_subs/g533remap.ngc")
require("asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/remap_subs/g536remap.ngc")
require("asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/remap_subs/g69remap.ngc")
require("asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/remap_subs/on_abort_no_twp_reset.ngc")
require("asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/remap_subs/on_abort_with_twp_reset.ngc")
require("asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/vismach/twp_vismach.py")
require("asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/vismach/xyzacb-trsrn-gui.py")
require("asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/vismach/xyzbca-trsrn-gui.py")
}
{
key = $1 ":" $2
if (key in expected_keys) {
if (key in manifest_line) {
print "duplicate TRSRN switchkins source in " manifest ": " key > "/dev/stderr"
duplicate = 1
}
manifest_line[key] = $0
}
}
END {
missing = 0
for (expected_index = 1; expected_index <= expected_count; expected_index++) {
key = expected[expected_index]
if (!(key in manifest_line)) {
print "TRSRN switchkins source is not listed in " manifest ": " key > "/dev/stderr"
missing = 1
continue
}
print manifest_line[key]
}
exit missing || duplicate
}
' "$manifest_file" >"$cli_trsrn_manifest"; then
exit 1
fi
grep -Fv \
'asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/python/toplevel.py:' \
"$cli_trsrn_manifest" >"$cli_trsrn_without_python_toplevel_asset"
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh \
"$cli_trsrn_without_python_toplevel_asset" >/dev/null 2>&1; then
echo "switchkins remap generator accepted manifest missing LinuxCNC PYTHON TOPLEVEL asset source" >&2
exit 1
fi
grep -Fv \
'asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/python/util.py:' \
"$cli_trsrn_manifest" >"$cli_trsrn_without_python_import_asset"
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh \
"$cli_trsrn_without_python_import_asset" >/dev/null 2>&1; then
echo "switchkins remap generator accepted manifest missing LinuxCNC Python import asset source" >&2
exit 1
fi
grep -Fv \
'asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/python/twp-helper-comp.py:' \
"$cli_trsrn_manifest" >"$cli_trsrn_without_halcmd_asset"
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh \
"$cli_trsrn_without_halcmd_asset" >/dev/null 2>&1; then
echo "switchkins remap generator accepted manifest missing LinuxCNC HALCMD loadusr asset source" >&2
exit 1
fi
grep -Fv \
'asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/vismach/twp_vismach.py:' \
"$cli_trsrn_manifest" >"$cli_trsrn_without_halcmd_import_asset"
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh \
"$cli_trsrn_without_halcmd_import_asset" >/dev/null 2>&1; then
echo "switchkins remap generator accepted manifest missing LinuxCNC HALCMD Python import asset source" >&2
exit 1
fi
grep -Fv \
'asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/remap_subs/g531remap.ngc:' \
"$cli_trsrn_manifest" >"$cli_trsrn_without_g531_asset"
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh \
"$cli_trsrn_without_g531_asset" >/dev/null 2>&1; then
echo "switchkins remap generator accepted manifest missing LinuxCNC G53.1 remap asset source" >&2
exit 1
fi
grep -Fv \
'asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/remap_subs/g69remap.ngc:' \
"$cli_trsrn_manifest" >"$cli_trsrn_without_g69_asset"
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh \
"$cli_trsrn_without_g69_asset" >/dev/null 2>&1; then
echo "switchkins remap generator accepted manifest missing LinuxCNC G69 remap asset source" >&2
exit 1
fi
grep -Fv \
'asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/remap_subs/on_abort_no_twp_reset.ngc:' \
"$cli_trsrn_manifest" >"$cli_trsrn_without_on_abort_asset"
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh \
"$cli_trsrn_without_on_abort_asset" >/dev/null 2>&1; then
echo "switchkins remap generator accepted manifest missing LinuxCNC ON_ABORT_COMMAND asset source" >&2
exit 1
fi
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh \
--all-output-dir "$cli_dir/out with space" "$cli_manifest" >"$cli_output_whitespace_log" 2>&1; then
echo "switchkins remap generator accepted an output directory with whitespace" >&2
exit 1
fi
if ! grep -F "switchkins remap output directory must not contain whitespace: $cli_dir/out with space" \
"$cli_output_whitespace_log" >/dev/null; then
echo "switchkins remap generator did not report whitespace output directories clearly" >&2
sed -n '1,20p' "$cli_output_whitespace_log" >&2
exit 1
fi
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh \
--all-output-dir / "$cli_manifest" >"$cli_output_root_log" 2>&1; then
echo "switchkins remap generator accepted the filesystem root as output directory" >&2
exit 1
fi
if ! grep -F "switchkins remap output directory must not be the filesystem root" \
"$cli_output_root_log" >/dev/null; then
echo "switchkins remap generator did not report filesystem-root output directories clearly" >&2
sed -n '1,20p' "$cli_output_root_log" >&2
exit 1
fi
LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh --all-output-dir "$cli_dir/out" "$cli_manifest"
LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh "$cli_manifest" \
>"$cli_dir/linuxcnc_switchkins_remap_table.stdout.inc"
LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh --config-cases "$cli_manifest" \
>"$cli_dir/linuxcnc_switchkins_remap_config_cases.stdout.inc"
LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh --json-cases "$cli_manifest" \
>"$cli_dir/linuxcnc_switchkins_remap_config_cases.stdout.json"
for generated_file in \
out/linuxcnc_switchkins_remap_table.inc \
out/linuxcnc_switchkins_remap_config_cases.inc \
out/linuxcnc_switchkins_remap_config_cases.json \
linuxcnc_switchkins_remap_table.stdout.inc \
linuxcnc_switchkins_remap_config_cases.stdout.inc \
linuxcnc_switchkins_remap_config_cases.stdout.json
do
if [[ ! -s "$cli_dir/$generated_file" ]]; then
echo "switchkins remap generator CLI contract did not write $generated_file" >&2
exit 1
fi
done
diff -u "$cli_dir/out/linuxcnc_switchkins_remap_table.inc" \
"$cli_dir/linuxcnc_switchkins_remap_table.stdout.inc"
diff -u "$cli_dir/out/linuxcnc_switchkins_remap_config_cases.inc" \
"$cli_dir/linuxcnc_switchkins_remap_config_cases.stdout.inc"
diff -u "$cli_dir/out/linuxcnc_switchkins_remap_config_cases.json" \
"$cli_dir/linuxcnc_switchkins_remap_config_cases.stdout.json"
)
}
validate_generator_cli_contract "$manifest"
generated_dir=$(mktemp -d "${TMPDIR:-/tmp}/linuxcnc_switchkins_remap.XXXXXX")
trap 'rm -rf "$generated_dir"' EXIT
LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh --all-output-dir "$generated_dir" "$manifest"
validate_json() {
local file=$1
local label=$2
if command -v python3 >/dev/null 2>&1; then
if ! python3 - "$file" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as handle:
data = json.load(handle)
if not isinstance(data, list) or not data:
raise SystemExit("expected non-empty JSON array")
seen = set()
seen_values = {}
for index, entry in enumerate(data):
if not isinstance(entry, dict):
raise SystemExit(f"entry {index} is not an object")
if set(entry) != {"field", "value", "m428", "m429", "m430"}:
raise SystemExit(f"entry {index} has unexpected keys")
if not isinstance(entry["field"], str) or not entry["field"]:
raise SystemExit(f"entry {index} has invalid field")
if not isinstance(entry["value"], str) or not entry["value"]:
raise SystemExit(f"entry {index} has invalid value")
for key in ("m428", "m429", "m430"):
if not isinstance(entry[key], int):
raise SystemExit(f"entry {index} has non-integer {key}")
case_key = (entry["field"], entry["value"], entry["m428"], entry["m429"], entry["m430"])
if case_key in seen:
raise SystemExit(f"entry {index} duplicates an earlier case")
seen.add(case_key)
value_key = (entry["field"], entry["value"])
types = (entry["m428"], entry["m429"], entry["m430"])
if value_key in seen_values and seen_values[value_key] != types:
raise SystemExit(f"entry {index} has ambiguous field/value mapping")
seen_values[value_key] = types
PY
then
echo "invalid switchkins remap JSON: $label" >&2
exit 1
fi
elif command -v node >/dev/null 2>&1; then
if ! node - "$file" <<'JS'
const fs = require("fs");
const data = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
if (!Array.isArray(data) || data.length === 0) {
throw new Error("expected non-empty JSON array");
}
const seen = new Set();
const seenValues = new Map();
for (const [index, entry] of data.entries()) {
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
throw new Error(`entry ${index} is not an object`);
}
const keys = Object.keys(entry).sort();
const expected = ["field", "m428", "m429", "m430", "value"];
if (keys.length !== expected.length || keys.some((key, i) => key !== expected[i])) {
throw new Error(`entry ${index} has unexpected keys`);
}
if (typeof entry.field !== "string" || entry.field.length === 0) {
throw new Error(`entry ${index} has invalid field`);
}
if (typeof entry.value !== "string" || entry.value.length === 0) {
throw new Error(`entry ${index} has invalid value`);
}
for (const key of ["m428", "m429", "m430"]) {
if (!Number.isInteger(entry[key])) {
throw new Error(`entry ${index} has non-integer ${key}`);
}
}
const caseKey = JSON.stringify([entry.field, entry.value, entry.m428, entry.m429, entry.m430]);
if (seen.has(caseKey)) {
throw new Error(`entry ${index} duplicates an earlier case`);
}
seen.add(caseKey);
const valueKey = JSON.stringify([entry.field, entry.value]);
const types = JSON.stringify([entry.m428, entry.m429, entry.m430]);
if (seenValues.has(valueKey) && seenValues.get(valueKey) !== types) {
throw new Error(`entry ${index} has ambiguous field/value mapping`);
}
seenValues.set(valueKey, types);
}
JS
then
echo "invalid switchkins remap JSON: $label" >&2
exit 1
fi
else
echo "missing python3 or node for switchkins remap JSON validation" >&2
exit 1
fi
}
validate_generated_header() {
local file=$1
local label=$2
local expected=$3
if [[ "$(head -n 1 "$file")" != "$expected" ]]; then
echo "unexpected generated switchkins remap $label header in $file" >&2
exit 1
fi
}
validate_generated_source_notes() {
local file=$1
local label=$2
shift 2
local expected
for expected in "$@"; do
if ! grep -Fxq "$expected" "$file"; then
echo "missing generated switchkins remap $label source note in $file: $expected" >&2
exit 1
fi
done
}
validate_cpp_table() {
local file=$1
local label=$2
if command -v python3 >/dev/null 2>&1; then
if ! python3 - "$file" "$label" <<'PY'
import json
import re
import sys
file, label = sys.argv[1:]
uppercase_or_space_re = re.compile(r'[A-Z\s]')
row_re = re.compile(
r'^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*'
r'(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$'
)
aliases = {}
row_count = 0
with open(file, encoding="utf-8") as handle:
for line_number, line in enumerate(handle, 1):
match = row_re.match(line)
if not match:
continue
row_count += 1
alias = json.loads(match.group(1))
if not alias:
raise SystemExit(f"{label} table has empty alias at line {line_number}")
if uppercase_or_space_re.search(alias):
raise SystemExit(f"{label} table has unnormalized alias {alias!r} at line {line_number}")
if alias in aliases:
raise SystemExit(
f"{label} table has duplicate alias {alias!r} at lines "
f"{aliases[alias]} and {line_number}"
)
aliases[alias] = line_number
types = (int(match.group(2)), int(match.group(3)), int(match.group(4)))
if types[0] < 0 or types[1] < 0:
raise SystemExit(f"{label} table has missing M428/M429 kinstype at line {line_number}")
for type_value in types:
if type_value < -1:
raise SystemExit(f"{label} table has invalid kinstype {type_value} at line {line_number}")
if row_count == 0:
raise SystemExit(f"{label} table has no rows")
PY
then
exit 1
fi
elif command -v node >/dev/null 2>&1; then
if ! node - "$file" "$label" <<'JS'
const fs = require("fs");
const [file, label] = process.argv.slice(2);
const rowRe = /^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$/;
const uppercaseOrSpaceRe = /[A-Z\s]/;
const aliases = new Map();
let rowCount = 0;
for (const [index, line] of fs.readFileSync(file, "utf8").split(/\r?\n/).entries()) {
const lineNumber = index + 1;
const match = rowRe.exec(line);
if (!match) {
continue;
}
rowCount += 1;
const alias = JSON.parse(match[1]);
if (!alias) {
throw new Error(`${label} table has empty alias at line ${lineNumber}`);
}
if (uppercaseOrSpaceRe.test(alias)) {
throw new Error(`${label} table has unnormalized alias ${JSON.stringify(alias)} at line ${lineNumber}`);
}
if (aliases.has(alias)) {
throw new Error(`${label} table has duplicate alias ${JSON.stringify(alias)} at lines ${aliases.get(alias)} and ${lineNumber}`);
}
aliases.set(alias, lineNumber);
const types = [Number(match[2]), Number(match[3]), Number(match[4])];
if (types[0] < 0 || types[1] < 0) {
throw new Error(`${label} table has missing M428/M429 kinstype at line ${lineNumber}`);
}
for (const typeValue of types) {
if (typeValue < -1) {
throw new Error(`${label} table has invalid kinstype ${typeValue} at line ${lineNumber}`);
}
}
}
if (rowCount === 0) {
throw new Error(`${label} table has no rows`);
}
JS
then
exit 1
fi
else
echo "missing python3 or node for switchkins remap table validation" >&2
exit 1
fi
}
validate_cpp_cases() {
local file=$1
local label=$2
if command -v python3 >/dev/null 2>&1; then
if ! python3 - "$file" "$label" <<'PY'
import json
import re
import sys
file, label = sys.argv[1:]
row_re = re.compile(
r'^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*'
r'("(?:(?:\\.)|[^"\\])*"),\s*'
r'(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$'
)
seen = set()
seen_values = {}
row_count = 0
with open(file, encoding="utf-8") as handle:
for line_number, line in enumerate(handle, 1):
match = row_re.match(line)
if not match:
continue
row_count += 1
field = json.loads(match.group(1))
value = json.loads(match.group(2))
types = (int(match.group(3)), int(match.group(4)), int(match.group(5)))
if types[0] < 0 or types[1] < 0:
raise SystemExit(f"{label} cases have missing M428/M429 kinstype at line {line_number}")
if any(type_value < -1 for type_value in types):
raise SystemExit(f"{label} cases have invalid kinstype at line {line_number}")
if not field or not value:
raise SystemExit(f"{label} cases have empty field/value at line {line_number}")
case_key = (field, value, *types)
if case_key in seen:
raise SystemExit(f"{label} cases duplicate an earlier case at line {line_number}")
seen.add(case_key)
value_key = (field, value)
if value_key in seen_values and seen_values[value_key] != types:
raise SystemExit(f"{label} cases have ambiguous field/value mapping at line {line_number}")
seen_values[value_key] = types
if row_count == 0:
raise SystemExit(f"{label} cases have no rows")
PY
then
exit 1
fi
elif command -v node >/dev/null 2>&1; then
if ! node - "$file" "$label" <<'JS'
const fs = require("fs");
const [file, label] = process.argv.slice(2);
const rowRe = /^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*("(?:(?:\\.)|[^"\\])*"),\s*(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$/;
const seen = new Set();
const seenValues = new Map();
let rowCount = 0;
for (const [index, line] of fs.readFileSync(file, "utf8").split(/\r?\n/).entries()) {
const lineNumber = index + 1;
const match = rowRe.exec(line);
if (!match) {
continue;
}
rowCount += 1;
const field = JSON.parse(match[1]);
const value = JSON.parse(match[2]);
const types = [Number(match[3]), Number(match[4]), Number(match[5])];
if (types[0] < 0 || types[1] < 0) {
throw new Error(`${label} cases have missing M428/M429 kinstype at line ${lineNumber}`);
}
if (types.some((typeValue) => typeValue < -1)) {
throw new Error(`${label} cases have invalid kinstype at line ${lineNumber}`);
}
if (!field || !value) {
throw new Error(`${label} cases have empty field/value at line ${lineNumber}`);
}
const caseKey = JSON.stringify([field, value, ...types]);
if (seen.has(caseKey)) {
throw new Error(`${label} cases duplicate an earlier case at line ${lineNumber}`);
}
seen.add(caseKey);
const valueKey = JSON.stringify([field, value]);
const typeKey = JSON.stringify(types);
if (seenValues.has(valueKey) && seenValues.get(valueKey) !== typeKey) {
throw new Error(`${label} cases have ambiguous field/value mapping at line ${lineNumber}`);
}
seenValues.set(valueKey, typeKey);
}
if (rowCount === 0) {
throw new Error(`${label} cases have no rows`);
}
JS
then
exit 1
fi
else
echo "missing python3 or node for switchkins remap C++ case validation" >&2
exit 1
fi
}
compare_cpp_json_cases() {
local cpp_file=$1
local json_file=$2
local label=$3
if command -v python3 >/dev/null 2>&1; then
if ! python3 - "$cpp_file" "$json_file" "$label" <<'PY'
import json
import re
import sys
cpp_file, json_file, label = sys.argv[1:]
row_re = re.compile(
r'^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*'
r'("(?:(?:\\.)|[^"\\])*"),\s*'
r'(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$'
)
cpp_cases = []
with open(cpp_file, encoding="utf-8") as handle:
for line_number, line in enumerate(handle, 1):
match = row_re.match(line)
if not match:
continue
cpp_cases.append({
"field": json.loads(match.group(1)),
"value": json.loads(match.group(2)),
"m428": int(match.group(3)),
"m429": int(match.group(4)),
"m430": int(match.group(5)),
})
with open(json_file, encoding="utf-8") as handle:
json_cases = json.load(handle)
if cpp_cases != json_cases:
raise SystemExit(f"{label} C++/JSON switchkins remap cases differ")
PY
then
exit 1
fi
elif command -v node >/dev/null 2>&1; then
if ! node - "$cpp_file" "$json_file" "$label" <<'JS'
const fs = require("fs");
const [cppFile, jsonFile, label] = process.argv.slice(2);
const rowRe = /^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*("(?:(?:\\.)|[^"\\])*"),\s*(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$/;
const cppCases = [];
for (const line of fs.readFileSync(cppFile, "utf8").split(/\r?\n/)) {
const match = rowRe.exec(line);
if (!match) {
continue;
}
cppCases.push({
field: JSON.parse(match[1]),
value: JSON.parse(match[2]),
m428: Number(match[3]),
m429: Number(match[4]),
m430: Number(match[5]),
});
}
const jsonCases = JSON.parse(fs.readFileSync(jsonFile, "utf8"));
if (JSON.stringify(cppCases) !== JSON.stringify(jsonCases)) {
throw new Error(`${label} C++/JSON switchkins remap cases differ`);
}
JS
then
exit 1
fi
else
echo "missing python3 or node for switchkins remap C++/JSON comparison" >&2
exit 1
fi
}
extract_api_switchkins_alias_keys() {
local file=$1
if command -v python3 >/dev/null 2>&1; then
python3 - "$file" <<'PY'
import re
import sys
file = sys.argv[1]
pattern = re.compile(r'contains_string_value\(compact,\s*"([^"]+)",\s*alias\)')
function_started = False
brace_depth = 0
keys = []
with open(file, encoding="utf-8") as handle:
for line in handle:
if not function_started:
if line.startswith("bool contains_switchkins_config_alias("):
function_started = True
brace_depth += line.count("{") - line.count("}")
continue
brace_depth += line.count("{") - line.count("}")
keys.extend(pattern.findall(line))
if brace_depth <= 0:
break
if not keys:
raise SystemExit("missing switchkins config alias keys in cnc_sim_api.cpp")
for key in keys:
print(key)
PY
elif command -v node >/dev/null 2>&1; then
node - "$file" <<'JS'
const fs = require("fs");
const file = process.argv[2];
const pattern = /contains_string_value\(compact,\s*"([^"]+)",\s*alias\)/g;
const lines = fs.readFileSync(file, "utf8").split(/\r?\n/);
let started = false;
let braceDepth = 0;
const keys = [];
for (const line of lines) {
if (!started) {
if (line.startsWith("bool contains_switchkins_config_alias(")) {
started = true;
braceDepth += (line.match(/{/g) || []).length - (line.match(/}/g) || []).length;
}
continue;
}
braceDepth += (line.match(/{/g) || []).length - (line.match(/}/g) || []).length;
for (const match of line.matchAll(pattern)) {
keys.push(match[1]);
}
if (braceDepth <= 0) {
break;
}
}
if (keys.length === 0) {
throw new Error("missing switchkins config alias keys in cnc_sim_api.cpp");
}
for (const key of keys) {
console.log(key);
}
JS
else
echo "missing python3 or node for switchkins remap alias extraction" >&2
exit 1
fi
}
validate_switchkins_case_fields_against_api() {
local api_keys_csv=$1
local cpp_file=$2
local json_file=$3
local label=$4
if [[ -z "$api_keys_csv" ]]; then
echo "missing switchkins config alias keys in core/src/cnc_sim_api.cpp" >&2
exit 1
fi
if command -v python3 >/dev/null 2>&1; then
if ! python3 - "$api_keys_csv" "$cpp_file" "$json_file" "$label" <<'PY'
import json
import re
import sys
api_keys_csv, cpp_file, json_file, label = sys.argv[1:]
allowed = {key for key in api_keys_csv.split(",") if key}
if not allowed:
raise SystemExit("missing switchkins config alias keys in cnc_sim_api.cpp")
row_re = re.compile(
r'^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*'
r'("(?:(?:\\.)|[^"\\])*"),\s*'
r'(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$'
)
def load_cases(path):
if path.endswith(".json"):
with open(path, encoding="utf-8") as handle:
return json.load(handle)
with open(path, encoding="utf-8") as handle:
return [
{
"field": json.loads(match.group(1)),
"value": json.loads(match.group(2)),
"m428": int(match.group(3)),
"m429": int(match.group(4)),
"m430": int(match.group(5)),
}
for match in row_re.finditer(handle.read())
]
cases = load_cases(cpp_file) + load_cases(json_file)
covered = set()
for index, entry in enumerate(cases):
field = entry["field"]
if field.lower() not in allowed:
raise SystemExit(f"{label} case {index} uses unsupported switchkins field {field!r}")
covered.add(field.lower())
missing = sorted(allowed - covered)
if missing:
raise SystemExit(f"{label} cases do not cover switchkins API alias field(s): {missing}")
PY
then
exit 1
fi
elif command -v node >/dev/null 2>&1; then
if ! node - "$api_keys_csv" "$cpp_file" "$json_file" "$label" <<'JS'
const fs = require("fs");
const [apiKeysCsv, cppFile, jsonFile, label] = process.argv.slice(2);
const allowed = new Set(apiKeysCsv.split(",").filter(Boolean));
if (allowed.size === 0) {
throw new Error("missing switchkins config alias keys in cnc_sim_api.cpp");
}
const rowRe = /^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*("(?:(?:\\.)|[^"\\])*"),\s*(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$/;
const cases = [];
const covered = new Set();
for (const [path, isJson] of [[cppFile, false], [jsonFile, true]]) {
if (isJson) {
for (const entry of JSON.parse(fs.readFileSync(path, "utf8"))) {
cases.push(entry);
}
continue;
}
for (const line of fs.readFileSync(path, "utf8").split(/\r?\n/)) {
const match = rowRe.exec(line);
if (!match) {
continue;
}
cases.push({
field: JSON.parse(match[1]),
value: JSON.parse(match[2]),
m428: Number(match[3]),
m429: Number(match[4]),
m430: Number(match[5]),
});
}
}
cases.forEach((entry, index) => {
if (!allowed.has(entry.field.toLowerCase())) {
throw new Error(`${label} case ${index} uses unsupported switchkins field ${JSON.stringify(entry.field)}`);
}
covered.add(entry.field.toLowerCase());
});
const missing = [...allowed].filter((key) => !covered.has(key)).sort();
if (missing.length > 0) {
throw new Error(`${label} cases do not cover switchkins API alias field(s): ${missing.join(",")}`);
}
JS
then
exit 1
fi
else
echo "missing python3 or node for switchkins remap API alias validation" >&2
exit 1
fi
}
validate_case_values_against_table() {
local table_file=$1
local cpp_file=$2
local json_file=$3
local label=$4
if command -v python3 >/dev/null 2>&1; then
if ! python3 - "$table_file" "$cpp_file" "$json_file" "$label" <<'PY'
import json
import re
import sys
table_file, cpp_file, json_file, label = sys.argv[1:]
table_row_re = re.compile(
r'^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*'
r'(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$'
)
case_row_re = re.compile(
r'^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*'
r'("(?:(?:\\.)|[^"\\])*"),\s*'
r'(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$'
)
aliases = {}
with open(table_file, encoding="utf-8") as handle:
for line in handle:
match = table_row_re.match(line)
if not match:
continue
aliases[json.loads(match.group(1))] = (
int(match.group(2)),
int(match.group(3)),
int(match.group(4)),
)
if not aliases:
raise SystemExit(f"{label} table has no aliases")
case_entries = []
with open(cpp_file, encoding="utf-8") as handle:
for line_number, line in enumerate(handle, 1):
match = case_row_re.match(line)
if not match:
continue
case_entries.append((
f"{cpp_file}:{line_number}",
json.loads(match.group(1)),
json.loads(match.group(2)),
(int(match.group(3)), int(match.group(4)), int(match.group(5))),
))
with open(json_file, encoding="utf-8") as handle:
for index, entry in enumerate(json.load(handle)):
case_entries.append((
f"{json_file}:{index}",
entry["field"],
entry["value"],
(entry["m428"], entry["m429"], entry["m430"]),
))
for location, field, value, types in case_entries:
normalized = "".join(ch for ch in value.lower() if not ch.isspace())
if normalized not in aliases:
raise SystemExit(f"{label} case value {value!r} at {location} is not covered by the alias table")
if aliases[normalized] != types:
raise SystemExit(
f"{label} case {field}={value!r} at {location} has kinstypes "
f"{types}, but alias table has {aliases[normalized]}"
)
PY
then
exit 1
fi
elif command -v node >/dev/null 2>&1; then
if ! node - "$table_file" "$cpp_file" "$json_file" "$label" <<'JS'
const fs = require("fs");
const [tableFile, cppFile, jsonFile, label] = process.argv.slice(2);
const tableRowRe = /^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$/;
const caseRowRe = /^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*("(?:(?:\\.)|[^"\\])*"),\s*(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$/;
const aliases = new Map();
for (const line of fs.readFileSync(tableFile, "utf8").split(/\r?\n/)) {
const match = tableRowRe.exec(line);
if (match) {
aliases.set(JSON.parse(match[1]), [Number(match[2]), Number(match[3]), Number(match[4])]);
}
}
if (aliases.size === 0) {
throw new Error(`${label} table has no aliases`);
}
const caseEntries = [];
for (const [index, line] of fs.readFileSync(cppFile, "utf8").split(/\r?\n/).entries()) {
const match = caseRowRe.exec(line);
if (!match) {
continue;
}
caseEntries.push({
location: `${cppFile}:${index + 1}`,
field: JSON.parse(match[1]),
value: JSON.parse(match[2]),
types: [Number(match[3]), Number(match[4]), Number(match[5])],
});
}
JSON.parse(fs.readFileSync(jsonFile, "utf8")).forEach((entry, index) => {
caseEntries.push({
location: `${jsonFile}:${index}`,
field: entry.field,
value: entry.value,
types: [entry.m428, entry.m429, entry.m430],
});
});
for (const entry of caseEntries) {
const { location, field, value, types } = entry;
const normalized = value.toLowerCase().replace(/\s+/g, "");
if (!aliases.has(normalized)) {
throw new Error(`${label} case value ${JSON.stringify(value)} at ${location} is not covered by the alias table`);
}
const aliasTypes = aliases.get(normalized);
if (JSON.stringify(aliasTypes) !== JSON.stringify(types)) {
throw new Error(`${label} case ${field}=${JSON.stringify(value)} at ${location} has kinstypes ${JSON.stringify(types)}, but alias table has ${JSON.stringify(aliasTypes)}`);
}
}
JS
then
exit 1
fi
else
echo "missing python3 or node for switchkins remap case/table validation" >&2
exit 1
fi
}
validate_manifest_remap_paths_in_outputs() {
local manifest_file=$1
local table_file=$2
local cpp_file=$3
local json_file=$4
local label=$5
if command -v python3 >/dev/null 2>&1; then
if ! python3 - "$linuxcnc_root" "$manifest_file" "$table_file" "$cpp_file" "$json_file" "$label" <<'PY'
import json
import os
import re
import sys
linuxcnc_root, manifest_file, table_file, cpp_file, json_file, label = sys.argv[1:]
linuxcnc_root = os.path.abspath(linuxcnc_root)
manifest_remaps = []
manifest_inis = []
remap_assignment_re = re.compile(r'^\s*#<kinstype>\s*=\s*(-?\d+)\b')
with open(manifest_file, encoding="utf-8") as handle:
for line in handle:
parts = line.rstrip("\n").split(":", 2)
if len(parts) >= 2 and parts[0] == "remap":
manifest_remaps.append(parts[1])
if len(parts) >= 2 and parts[0] == "config" and parts[1].endswith(".ini"):
manifest_inis.append(parts[1])
if not manifest_remaps:
raise SystemExit(f"{manifest_file} has no LinuxCNC remap sources")
resolved_ini_remaps = set()
remap_line_re = re.compile(r'\bM(428|429|430)\b.*\bngc\s*=\s*([^\s]+)', re.I)
for source_path in manifest_inis:
ini_path = os.path.join(linuxcnc_root, source_path)
with open(ini_path, encoding="utf-8") as handle:
section = ""
subroutine_paths = []
remap_ngc_names = []
for raw_line in handle:
line = re.sub(r'[;#].*$', '', raw_line).strip()
if not line:
continue
if line.startswith("[") and line.endswith("]"):
section = line[1:-1].strip().lower()
continue
if section != "rs274ngc" or "=" not in line:
continue
key, value = line.split("=", 1)
key = key.strip().lower()
value = value.strip()
if key == "subroutine_path":
subroutine_paths.append(value)
elif key == "remap":
match = remap_line_re.search(value)
if match:
ngc_name = match.group(2)
if not ngc_name.endswith(".ngc"):
ngc_name = f"{ngc_name}.ngc"
remap_ngc_names.append(ngc_name)
if not remap_ngc_names:
continue
if not subroutine_paths:
subroutine_paths = ["."]
config_dir = os.path.dirname(ini_path)
for ngc_name in remap_ngc_names:
resolved_source = None
for subroutine_path in subroutine_paths:
for path_part in subroutine_path.split(":"):
if not path_part:
continue
candidate_dir = path_part if os.path.isabs(path_part) else os.path.join(config_dir, path_part)
candidate = os.path.join(candidate_dir, ngc_name)
if os.path.isfile(candidate):
resolved_abs = os.path.abspath(candidate)
rel = os.path.relpath(resolved_abs, linuxcnc_root)
if rel.startswith("..") or os.path.isabs(rel):
raise SystemExit(f"LinuxCNC remap source escapes LinuxCNC root: {resolved_abs}")
resolved_source = rel
break
if resolved_source:
break
if not resolved_source:
raise SystemExit(f"{label} could not resolve LinuxCNC REMAP ngc source {ngc_name} from {source_path}")
resolved_ini_remaps.add(resolved_source)
manifest_remap_set = set(manifest_remaps)
missing_ini_remaps = sorted(resolved_ini_remaps - manifest_remap_set)
unused_manifest_remaps = sorted(manifest_remap_set - resolved_ini_remaps)
if missing_ini_remaps:
raise SystemExit(f"{label} manifest is missing LinuxCNC INI-resolved remap source(s): {missing_ini_remaps}")
if unused_manifest_remaps:
raise SystemExit(f"{label} manifest remap source(s) are not resolved from LinuxCNC INI REMAP/SUBROUTINE_PATH entries: {unused_manifest_remaps}")
remap_group_types = {}
for path in manifest_remaps:
basename = os.path.basename(path)
mcode = int(basename[:3])
with open(os.path.join(linuxcnc_root, path), encoding="utf-8") as handle:
kinstypes = [
int(match.group(1))
for match in map(remap_assignment_re.match, handle)
if match
]
if len(kinstypes) != 1:
raise SystemExit(f"expected exactly one #<kinstype> assignment in {path}, found {len(kinstypes)}")
remap_group_types.setdefault(os.path.dirname(path), {})[mcode] = kinstypes[0]
expected_types = {
path: (
remap_group_types[os.path.dirname(path)].get(428, -1),
remap_group_types[os.path.dirname(path)].get(429, -1),
remap_group_types[os.path.dirname(path)].get(430, -1),
)
for path in manifest_remaps
}
table_row_re = re.compile(
r'^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*'
r'(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$'
)
aliases = {}
with open(table_file, encoding="utf-8") as handle:
for line in handle:
match = table_row_re.match(line)
if match:
aliases[json.loads(match.group(1))] = (
int(match.group(2)),
int(match.group(3)),
int(match.group(4)),
)
case_row_re = re.compile(
r'^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*'
r'("(?:(?:\\.)|[^"\\])*"),\s*'
r'(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$'
)
case_remaps = {}
required_subroutine_case_fields = {
"SUBROUTINE_PATH",
"subroutinePath",
"subroutinepath",
"subroutine_path",
}
case_subroutine_paths = {
field: {}
for field in required_subroutine_case_fields
}
with open(cpp_file, encoding="utf-8") as handle:
for line in handle:
match = case_row_re.match(line)
if not match:
continue
raw_field = json.loads(match.group(1))
field = raw_field.lower()
value = json.loads(match.group(2))
types = (int(match.group(3)), int(match.group(4)), int(match.group(5)))
if field == "remap":
case_remaps[value] = types
if raw_field in required_subroutine_case_fields:
case_subroutine_paths[raw_field][value] = types
with open(json_file, encoding="utf-8") as handle:
for entry in json.load(handle):
raw_field = entry["field"]
field = raw_field.lower()
types = (entry["m428"], entry["m429"], entry["m430"])
if field == "remap":
case_remaps[entry["value"]] = types
if raw_field in required_subroutine_case_fields:
case_subroutine_paths[raw_field][entry["value"]] = types
missing_aliases = [path for path in manifest_remaps if path not in aliases]
missing_cases = [path for path in manifest_remaps if path not in case_remaps]
if missing_aliases:
raise SystemExit(f"{label} table is missing LinuxCNC remap source alias(es): {missing_aliases}")
if missing_cases:
raise SystemExit(f"{label} cases are missing LinuxCNC remap source path(s): {missing_cases}")
expected_subroutine_types = {
os.path.dirname(path): expected_types[path]
for path in manifest_remaps
}
missing_subroutine_cases = [
(field, path)
for field in sorted(required_subroutine_case_fields)
for path in expected_subroutine_types
if path not in case_subroutine_paths[field]
]
if missing_subroutine_cases:
raise SystemExit(
f"{label} cases are missing LinuxCNC SUBROUTINE_PATH remap dir(s): {missing_subroutine_cases}"
)
wrong_alias_types = [
(path, expected_types[path], aliases[path])
for path in manifest_remaps
if path in aliases and aliases[path] != expected_types[path]
]
wrong_case_types = [
(path, expected_types[path], case_remaps[path])
for path in manifest_remaps
if path in case_remaps and case_remaps[path] != expected_types[path]
]
wrong_subroutine_types = [
(field, path, expected_subroutine_types[path], case_subroutine_paths[field][path])
for field in sorted(required_subroutine_case_fields)
for path in expected_subroutine_types
if path in case_subroutine_paths[field] and case_subroutine_paths[field][path] != expected_subroutine_types[path]
]
if wrong_alias_types:
raise SystemExit(f"{label} table has LinuxCNC remap source kinstype mismatch(es): {wrong_alias_types}")
if wrong_case_types:
raise SystemExit(f"{label} cases have LinuxCNC remap source kinstype mismatch(es): {wrong_case_types}")
if wrong_subroutine_types:
raise SystemExit(f"{label} cases have LinuxCNC SUBROUTINE_PATH kinstype mismatch(es): {wrong_subroutine_types}")
PY
then
exit 1
fi
elif command -v node >/dev/null 2>&1; then
if ! node - "$linuxcnc_root" "$manifest_file" "$table_file" "$cpp_file" "$json_file" "$label" <<'JS'
const fs = require("fs");
const path = require("path");
const [linuxcncRoot, manifestFile, tableFile, cppFile, jsonFile, label] = process.argv.slice(2);
const manifestRemaps = fs.readFileSync(manifestFile, "utf8")
.split(/\r?\n/)
.map((line) => line.split(":", 3))
.filter((parts) => parts.length >= 2 && parts[0] === "remap")
.map((parts) => parts[1]);
const manifestInis = fs.readFileSync(manifestFile, "utf8")
.split(/\r?\n/)
.map((line) => line.split(":", 3))
.filter((parts) => parts.length >= 2 && parts[0] === "config" && parts[1].endsWith(".ini"))
.map((parts) => parts[1]);
if (manifestRemaps.length === 0) {
throw new Error(`${manifestFile} has no LinuxCNC remap sources`);
}
const linuxcncRootAbs = path.resolve(linuxcncRoot);
const resolvedIniRemaps = new Set();
const remapLineRe = /\bM(428|429|430)\b.*\bngc\s*=\s*([^\s]+)/i;
for (const sourcePath of manifestInis) {
const iniPath = path.join(linuxcncRootAbs, sourcePath);
let section = "";
const subroutinePaths = [];
const remapNgcNames = [];
for (const rawLine of fs.readFileSync(iniPath, "utf8").split(/\r?\n/)) {
const line = rawLine.replace(/[;#].*$/, "").trim();
if (line.length === 0) {
continue;
}
if (line.startsWith("[") && line.endsWith("]")) {
section = line.slice(1, -1).trim().toLowerCase();
continue;
}
if (section !== "rs274ngc" || !line.includes("=")) {
continue;
}
const equals = line.indexOf("=");
const key = line.slice(0, equals).trim().toLowerCase();
const value = line.slice(equals + 1).trim();
if (key === "subroutine_path") {
subroutinePaths.push(value);
} else if (key === "remap") {
const match = remapLineRe.exec(value);
if (match) {
const ngcName = match[2].endsWith(".ngc") ? match[2] : `${match[2]}.ngc`;
remapNgcNames.push(ngcName);
}
}
}
if (remapNgcNames.length === 0) {
continue;
}
if (subroutinePaths.length === 0) {
subroutinePaths.push(".");
}
const configDir = path.dirname(iniPath);
for (const ngcName of remapNgcNames) {
let resolvedSource = null;
for (const subroutinePath of subroutinePaths) {
for (const pathPart of subroutinePath.split(":")) {
if (!pathPart) {
continue;
}
const candidateDir = path.isAbsolute(pathPart) ? pathPart : path.join(configDir, pathPart);
const candidate = path.join(candidateDir, ngcName);
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
const resolvedAbs = path.resolve(candidate);
const rel = path.relative(linuxcncRootAbs, resolvedAbs);
if (rel.startsWith("..") || path.isAbsolute(rel)) {
throw new Error(`LinuxCNC remap source escapes LinuxCNC root: ${resolvedAbs}`);
}
resolvedSource = rel;
break;
}
}
if (resolvedSource) {
break;
}
}
if (!resolvedSource) {
throw new Error(`${label} could not resolve LinuxCNC REMAP ngc source ${ngcName} from ${sourcePath}`);
}
resolvedIniRemaps.add(resolvedSource);
}
}
const manifestRemapSet = new Set(manifestRemaps);
const missingIniRemaps = [...resolvedIniRemaps].filter((sourcePath) => !manifestRemapSet.has(sourcePath)).sort();
const unusedManifestRemaps = manifestRemaps.filter((sourcePath) => !resolvedIniRemaps.has(sourcePath)).sort();
if (missingIniRemaps.length > 0) {
throw new Error(`${label} manifest is missing LinuxCNC INI-resolved remap source(s): ${missingIniRemaps.join(",")}`);
}
if (unusedManifestRemaps.length > 0) {
throw new Error(`${label} manifest remap source(s) are not resolved from LinuxCNC INI REMAP/SUBROUTINE_PATH entries: ${unusedManifestRemaps.join(",")}`);
}
const remapAssignmentRe = /^\s*#<kinstype>\s*=\s*(-?\d+)\b/;
const remapGroupTypes = new Map();
for (const sourcePath of manifestRemaps) {
const basename = path.basename(sourcePath);
const mcode = Number(basename.slice(0, 3));
const kinstypes = fs.readFileSync(path.join(linuxcncRoot, sourcePath), "utf8")
.split(/\r?\n/)
.map((line) => remapAssignmentRe.exec(line))
.filter(Boolean)
.map((match) => Number(match[1]));
if (kinstypes.length !== 1) {
throw new Error(`expected exactly one #<kinstype> assignment in ${sourcePath}, found ${kinstypes.length}`);
}
const dirname = path.dirname(sourcePath);
if (!remapGroupTypes.has(dirname)) {
remapGroupTypes.set(dirname, new Map());
}
remapGroupTypes.get(dirname).set(mcode, kinstypes[0]);
}
const expectedTypes = new Map();
for (const sourcePath of manifestRemaps) {
const typesByMcode = remapGroupTypes.get(path.dirname(sourcePath));
expectedTypes.set(sourcePath, [
typesByMcode.get(428) ?? -1,
typesByMcode.get(429) ?? -1,
typesByMcode.get(430) ?? -1,
]);
}
const tableRowRe = /^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$/;
const aliases = new Map();
for (const line of fs.readFileSync(tableFile, "utf8").split(/\r?\n/)) {
const match = tableRowRe.exec(line);
if (match) {
aliases.set(JSON.parse(match[1]), [Number(match[2]), Number(match[3]), Number(match[4])]);
}
}
const caseRowRe = /^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*("(?:(?:\\.)|[^"\\])*"),\s*(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$/;
const caseRemaps = new Map();
const requiredSubroutineCaseFields = ["SUBROUTINE_PATH", "subroutinePath", "subroutinepath", "subroutine_path"];
const caseSubroutinePaths = new Map(requiredSubroutineCaseFields.map((field) => [field, new Map()]));
for (const line of fs.readFileSync(cppFile, "utf8").split(/\r?\n/)) {
const match = caseRowRe.exec(line);
if (!match) {
continue;
}
const rawField = JSON.parse(match[1]);
const field = rawField.toLowerCase();
const value = JSON.parse(match[2]);
const types = [Number(match[3]), Number(match[4]), Number(match[5])];
if (field === "remap") {
caseRemaps.set(value, types);
}
if (caseSubroutinePaths.has(rawField)) {
caseSubroutinePaths.get(rawField).set(value, types);
}
}
for (const entry of JSON.parse(fs.readFileSync(jsonFile, "utf8"))) {
const rawField = entry.field;
const field = rawField.toLowerCase();
const types = [entry.m428, entry.m429, entry.m430];
if (field === "remap") {
caseRemaps.set(entry.value, types);
}
if (caseSubroutinePaths.has(rawField)) {
caseSubroutinePaths.get(rawField).set(entry.value, types);
}
}
const missingAliases = manifestRemaps.filter((sourcePath) => !aliases.has(sourcePath));
const missingCases = manifestRemaps.filter((sourcePath) => !caseRemaps.has(sourcePath));
if (missingAliases.length > 0) {
throw new Error(`${label} table is missing LinuxCNC remap source alias(es): ${missingAliases.join(",")}`);
}
if (missingCases.length > 0) {
throw new Error(`${label} cases are missing LinuxCNC remap source path(s): ${missingCases.join(",")}`);
}
const expectedSubroutineTypes = new Map();
for (const sourcePath of manifestRemaps) {
expectedSubroutineTypes.set(path.dirname(sourcePath), expectedTypes.get(sourcePath));
}
const missingSubroutineCases = requiredSubroutineCaseFields.flatMap((field) =>
[...expectedSubroutineTypes.keys()]
.filter((sourcePath) => !caseSubroutinePaths.get(field).has(sourcePath))
.map((sourcePath) => `${field}:${sourcePath}`)
);
if (missingSubroutineCases.length > 0) {
throw new Error(`${label} cases are missing LinuxCNC SUBROUTINE_PATH remap dir(s): ${missingSubroutineCases.join(",")}`);
}
const wrongAliasTypes = manifestRemaps
.filter((sourcePath) => aliases.has(sourcePath))
.filter((sourcePath) => JSON.stringify(aliases.get(sourcePath)) !== JSON.stringify(expectedTypes.get(sourcePath)))
.map((sourcePath) => `${sourcePath}: expected ${JSON.stringify(expectedTypes.get(sourcePath))}, got ${JSON.stringify(aliases.get(sourcePath))}`);
const wrongCaseTypes = manifestRemaps
.filter((sourcePath) => caseRemaps.has(sourcePath))
.filter((sourcePath) => JSON.stringify(caseRemaps.get(sourcePath)) !== JSON.stringify(expectedTypes.get(sourcePath)))
.map((sourcePath) => `${sourcePath}: expected ${JSON.stringify(expectedTypes.get(sourcePath))}, got ${JSON.stringify(caseRemaps.get(sourcePath))}`);
const wrongSubroutineTypes = [...expectedSubroutineTypes.keys()]
.flatMap((sourcePath) =>
requiredSubroutineCaseFields
.filter((field) => caseSubroutinePaths.get(field).has(sourcePath))
.filter((field) => JSON.stringify(caseSubroutinePaths.get(field).get(sourcePath)) !== JSON.stringify(expectedSubroutineTypes.get(sourcePath)))
.map((field) => `${field}:${sourcePath}: expected ${JSON.stringify(expectedSubroutineTypes.get(sourcePath))}, got ${JSON.stringify(caseSubroutinePaths.get(field).get(sourcePath))}`)
);
if (wrongAliasTypes.length > 0) {
throw new Error(`${label} table has LinuxCNC remap source kinstype mismatch(es): ${wrongAliasTypes.join("; ")}`);
}
if (wrongCaseTypes.length > 0) {
throw new Error(`${label} cases have LinuxCNC remap source kinstype mismatch(es): ${wrongCaseTypes.join("; ")}`);
}
if (wrongSubroutineTypes.length > 0) {
throw new Error(`${label} cases have LinuxCNC SUBROUTINE_PATH kinstype mismatch(es): ${wrongSubroutineTypes.join("; ")}`);
}
JS
then
exit 1
fi
else
echo "missing python3 or node for switchkins remap manifest output validation" >&2
exit 1
fi
}
validate_manifest_hal_paths_in_outputs() {
local manifest_file=$1
local table_file=$2
local cpp_file=$3
local json_file=$4
local label=$5
if command -v python3 >/dev/null 2>&1; then
if ! python3 - "$linuxcnc_root" "$manifest_file" "$table_file" "$cpp_file" "$json_file" "$label" <<'PY'
import json
import os
import re
import sys
linuxcnc_root, manifest_file, table_file, cpp_file, json_file, label = sys.argv[1:]
linuxcnc_root = os.path.abspath(linuxcnc_root)
manifest_hals = []
manifest_inis = []
with open(manifest_file, encoding="utf-8") as handle:
for line in handle:
parts = line.rstrip("\n").split(":", 2)
if len(parts) >= 2 and parts[0] == "config" and parts[1].endswith(".hal"):
manifest_hals.append(parts[1])
if len(parts) >= 2 and parts[0] == "config" and parts[1].endswith(".ini"):
manifest_inis.append(parts[1])
if not manifest_hals:
raise SystemExit(f"{manifest_file} has no LinuxCNC HAL config sources")
expected_hal_roles = {}
remap_re = re.compile(r'\bREMAP\s*=\s*M(428|429|430)(?:\D|$)', re.I)
hal_entry_re = re.compile(r'^([^=]+)=(.*)$')
for source_path in manifest_inis:
ini_path = os.path.join(linuxcnc_root, source_path)
with open(ini_path, encoding="utf-8") as handle:
ini_text = handle.read()
if not remap_re.search(ini_text):
continue
section = ""
for raw_line in ini_text.splitlines():
line = re.sub(r'[;#].*$', '', raw_line).strip()
if not line:
continue
if line.startswith("[") and line.endswith("]"):
section = line[1:-1].strip().lower()
continue
if section != "hal":
continue
match = hal_entry_re.match(line)
if not match:
continue
key = match.group(1).strip().lower()
if key not in {"halfile", "postgui_halfile"}:
continue
value = match.group(2).strip()
if not value or value.startswith("LIB:"):
continue
resolved = value if os.path.isabs(value) else os.path.join(os.path.dirname(ini_path), value)
if not os.path.isfile(resolved):
continue
resolved_abs = os.path.abspath(resolved)
rel = os.path.relpath(resolved_abs, linuxcnc_root)
if rel.startswith("..") or os.path.isabs(rel):
raise SystemExit(f"LinuxCNC HAL source escapes LinuxCNC root: {resolved_abs}")
expected_hal_roles.setdefault(rel, set()).add("postgui" if key == "postgui_halfile" else "hal")
unexpected_hals = [path for path in manifest_hals if path not in expected_hal_roles]
if unexpected_hals:
raise SystemExit(f"{label} manifest HAL source(s) are not referenced by LinuxCNC switchkins INI HALFILE/POSTGUI_HALFILE entries: {unexpected_hals}")
table_row_re = re.compile(
r'^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*'
r'(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$'
)
aliases = {}
with open(table_file, encoding="utf-8") as handle:
for line in handle:
match = table_row_re.match(line)
if match:
aliases[json.loads(match.group(1))] = (
int(match.group(2)),
int(match.group(3)),
int(match.group(4)),
)
case_sources = {}
hal_fields = {
"HALFILE",
"halFile",
"halfile",
"hal_file",
}
postgui_hal_fields = {
"POSTGUI_HALFILE",
"postguiHalFile",
"postguihalfile",
"postgui_hal_file",
"postgui_halfile",
}
hal_case_sources = {
field: {}
for field in hal_fields
}
postgui_hal_case_sources = {
field: {}
for field in postgui_hal_fields
}
case_row_re = re.compile(
r'^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*'
r'("(?:(?:\\.)|[^"\\])*"),\s*'
r'(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$'
)
with open(cpp_file, encoding="utf-8") as handle:
for line in handle:
match = case_row_re.match(line)
if not match:
continue
raw_field = json.loads(match.group(1))
value = json.loads(match.group(2))
types = (
int(match.group(3)),
int(match.group(4)),
int(match.group(5)),
)
if raw_field in hal_fields:
case_sources[value] = types
hal_case_sources[raw_field][value] = types
if raw_field in postgui_hal_fields:
case_sources[value] = types
postgui_hal_case_sources[raw_field][value] = types
with open(json_file, encoding="utf-8") as handle:
for entry in json.load(handle):
types = (entry["m428"], entry["m429"], entry["m430"])
if entry["field"] in hal_fields:
case_sources[entry["value"]] = types
hal_case_sources[entry["field"]][entry["value"]] = types
if entry["field"] in postgui_hal_fields:
case_sources[entry["value"]] = types
postgui_hal_case_sources[entry["field"]][entry["value"]] = types
missing_aliases = [path for path in manifest_hals if path not in aliases]
missing_cases = [path for path in manifest_hals if path not in case_sources]
missing_role_cases = [
(field, path, role)
for path in manifest_hals
for role in sorted(expected_hal_roles[path])
for field in sorted(postgui_hal_fields if role == "postgui" else hal_fields)
if path not in (postgui_hal_case_sources[field] if role == "postgui" else hal_case_sources[field])
]
if missing_aliases:
raise SystemExit(f"{label} table is missing LinuxCNC HAL source alias(es): {missing_aliases}")
if missing_cases:
raise SystemExit(f"{label} cases are missing LinuxCNC HAL source path(s): {missing_cases}")
if missing_role_cases:
raise SystemExit(f"{label} cases are missing LinuxCNC HAL source path(s) in the matching HALFILE/POSTGUI_HALFILE field group: {missing_role_cases}")
wrong_types = [
(path, aliases[path], case_sources[path])
for path in manifest_hals
if path in aliases and path in case_sources and aliases[path] != case_sources[path]
]
wrong_role_types = [
(field, path, role, aliases[path], (postgui_hal_case_sources[field] if role == "postgui" else hal_case_sources[field])[path])
for path in manifest_hals
for role in sorted(expected_hal_roles[path])
for field in sorted(postgui_hal_fields if role == "postgui" else hal_fields)
if path in aliases
and path in (postgui_hal_case_sources[field] if role == "postgui" else hal_case_sources[field])
and aliases[path] != (postgui_hal_case_sources[field] if role == "postgui" else hal_case_sources[field])[path]
]
if wrong_types:
raise SystemExit(f"{label} HAL source kinstype mismatch(es): {wrong_types}")
if wrong_role_types:
raise SystemExit(f"{label} HAL source field kinstype mismatch(es): {wrong_role_types}")
PY
then
exit 1
fi
elif command -v node >/dev/null 2>&1; then
if ! node - "$linuxcnc_root" "$manifest_file" "$table_file" "$cpp_file" "$json_file" "$label" <<'JS'
const fs = require("fs");
const path = require("path");
const [linuxcncRootArg, manifestFile, tableFile, cppFile, jsonFile, label] = process.argv.slice(2);
const linuxcncRoot = path.resolve(linuxcncRootArg);
const manifestHals = fs.readFileSync(manifestFile, "utf8")
.split(/\r?\n/)
.map((line) => line.split(":", 3))
.filter((parts) => parts.length >= 2 && parts[0] === "config" && parts[1].endsWith(".hal"))
.map((parts) => parts[1]);
const manifestInis = fs.readFileSync(manifestFile, "utf8")
.split(/\r?\n/)
.map((line) => line.split(":", 3))
.filter((parts) => parts.length >= 2 && parts[0] === "config" && parts[1].endsWith(".ini"))
.map((parts) => parts[1]);
if (manifestHals.length === 0) {
throw new Error(`${manifestFile} has no LinuxCNC HAL config sources`);
}
const expectedHalRoles = new Map();
const remapRe = /\bREMAP\s*=\s*M(428|429|430)(?:\D|$)/i;
for (const sourcePath of manifestInis) {
const iniPath = path.join(linuxcncRoot, sourcePath);
const iniText = fs.readFileSync(iniPath, "utf8");
if (!remapRe.test(iniText)) {
continue;
}
let section = "";
for (const rawLine of iniText.split(/\r?\n/)) {
const line = rawLine.replace(/[;#].*$/, "").trim();
if (line.length === 0) {
continue;
}
if (line.startsWith("[") && line.endsWith("]")) {
section = line.slice(1, -1).trim().toLowerCase();
continue;
}
if (section !== "hal" || !line.includes("=")) {
continue;
}
const equals = line.indexOf("=");
const key = line.slice(0, equals).trim().toLowerCase();
if (key !== "halfile" && key !== "postgui_halfile") {
continue;
}
const value = line.slice(equals + 1).trim();
if (!value || value.startsWith("LIB:")) {
continue;
}
const resolved = path.isAbsolute(value) ? value : path.join(path.dirname(iniPath), value);
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) {
continue;
}
const resolvedAbs = path.resolve(resolved);
const rel = path.relative(linuxcncRoot, resolvedAbs);
if (rel.startsWith("..") || path.isAbsolute(rel)) {
throw new Error(`LinuxCNC HAL source escapes LinuxCNC root: ${resolvedAbs}`);
}
if (!expectedHalRoles.has(rel)) {
expectedHalRoles.set(rel, new Set());
}
expectedHalRoles.get(rel).add(key === "postgui_halfile" ? "postgui" : "hal");
}
}
const unexpectedHals = manifestHals.filter((sourcePath) => !expectedHalRoles.has(sourcePath));
if (unexpectedHals.length > 0) {
throw new Error(`${label} manifest HAL source(s) are not referenced by LinuxCNC switchkins INI HALFILE/POSTGUI_HALFILE entries: ${unexpectedHals.join(",")}`);
}
const tableRowRe = /^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$/;
const aliases = new Map();
for (const line of fs.readFileSync(tableFile, "utf8").split(/\r?\n/)) {
const match = tableRowRe.exec(line);
if (match) {
aliases.set(JSON.parse(match[1]), [Number(match[2]), Number(match[3]), Number(match[4])]);
}
}
const halFields = ["HALFILE", "halFile", "halfile", "hal_file"];
const postguiHalFields = ["POSTGUI_HALFILE", "postguiHalFile", "postguihalfile", "postgui_hal_file", "postgui_halfile"];
const caseRowRe = /^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*("(?:(?:\\.)|[^"\\])*"),\s*(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$/;
const caseSources = new Map();
const halCaseSources = new Map(halFields.map((field) => [field, new Map()]));
const postguiHalCaseSources = new Map(postguiHalFields.map((field) => [field, new Map()]));
for (const line of fs.readFileSync(cppFile, "utf8").split(/\r?\n/)) {
const match = caseRowRe.exec(line);
if (!match) {
continue;
}
const rawField = JSON.parse(match[1]);
const value = JSON.parse(match[2]);
const types = [Number(match[3]), Number(match[4]), Number(match[5])];
if (halCaseSources.has(rawField)) {
caseSources.set(value, types);
halCaseSources.get(rawField).set(value, types);
}
if (postguiHalCaseSources.has(rawField)) {
caseSources.set(value, types);
postguiHalCaseSources.get(rawField).set(value, types);
}
}
for (const entry of JSON.parse(fs.readFileSync(jsonFile, "utf8"))) {
const types = [entry.m428, entry.m429, entry.m430];
if (halCaseSources.has(entry.field)) {
caseSources.set(entry.value, types);
halCaseSources.get(entry.field).set(entry.value, types);
}
if (postguiHalCaseSources.has(entry.field)) {
caseSources.set(entry.value, types);
postguiHalCaseSources.get(entry.field).set(entry.value, types);
}
}
const missingAliases = manifestHals.filter((sourcePath) => !aliases.has(sourcePath));
const missingCases = manifestHals.filter((sourcePath) => !caseSources.has(sourcePath));
const missingRoleCases = manifestHals.flatMap((sourcePath) =>
[...expectedHalRoles.get(sourcePath)].sort()
.flatMap((role) =>
(role === "postgui" ? postguiHalFields : halFields)
.filter((field) => !(role === "postgui" ? postguiHalCaseSources : halCaseSources).get(field).has(sourcePath))
.map((field) => `${field}:${sourcePath} ${role}`)
)
);
if (missingAliases.length > 0) {
throw new Error(`${label} table is missing LinuxCNC HAL source alias(es): ${missingAliases.join(",")}`);
}
if (missingCases.length > 0) {
throw new Error(`${label} cases are missing LinuxCNC HAL source path(s): ${missingCases.join(",")}`);
}
if (missingRoleCases.length > 0) {
throw new Error(`${label} cases are missing LinuxCNC HAL source path(s) in the matching HALFILE/POSTGUI_HALFILE field group: ${missingRoleCases.join(",")}`);
}
const wrongTypes = manifestHals
.filter((sourcePath) => aliases.has(sourcePath) && caseSources.has(sourcePath))
.filter((sourcePath) => JSON.stringify(aliases.get(sourcePath)) !== JSON.stringify(caseSources.get(sourcePath)))
.map((sourcePath) => `${sourcePath}: table ${JSON.stringify(aliases.get(sourcePath))}, case ${JSON.stringify(caseSources.get(sourcePath))}`);
const wrongRoleTypes = manifestHals.flatMap((sourcePath) =>
[...expectedHalRoles.get(sourcePath)].sort()
.flatMap((role) =>
(role === "postgui" ? postguiHalFields : halFields)
.filter((field) => aliases.has(sourcePath))
.filter((field) => (role === "postgui" ? postguiHalCaseSources : halCaseSources).get(field).has(sourcePath))
.filter((field) => JSON.stringify(aliases.get(sourcePath)) !== JSON.stringify((role === "postgui" ? postguiHalCaseSources : halCaseSources).get(field).get(sourcePath)))
.map((field) => `${field}:${sourcePath} ${role}: table ${JSON.stringify(aliases.get(sourcePath))}, case ${JSON.stringify((role === "postgui" ? postguiHalCaseSources : halCaseSources).get(field).get(sourcePath))}`)
)
);
if (wrongTypes.length > 0) {
throw new Error(`${label} HAL source kinstype mismatch(es): ${wrongTypes.join("; ")}`);
}
if (wrongRoleTypes.length > 0) {
throw new Error(`${label} HAL source field kinstype mismatch(es): ${wrongRoleTypes.join("; ")}`);
}
JS
then
exit 1
fi
else
echo "missing python3 or node for switchkins HAL manifest output validation" >&2
exit 1
fi
}
validate_manifest_ini_paths_in_outputs() {
local manifest_file=$1
local table_file=$2
local cpp_file=$3
local json_file=$4
local label=$5
if command -v python3 >/dev/null 2>&1; then
if ! python3 - "$manifest_file" "$table_file" "$cpp_file" "$json_file" "$label" <<'PY'
import json
import re
import sys
manifest_file, table_file, cpp_file, json_file, label = sys.argv[1:]
manifest_inis = []
with open(manifest_file, encoding="utf-8") as handle:
for line in handle:
parts = line.rstrip("\n").split(":", 2)
if len(parts) >= 2 and parts[0] == "config" and parts[1].endswith(".ini"):
manifest_inis.append(parts[1])
if not manifest_inis:
raise SystemExit(f"{manifest_file} has no LinuxCNC INI config sources")
table_row_re = re.compile(
r'^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*'
r'(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$'
)
aliases = {}
with open(table_file, encoding="utf-8") as handle:
for line in handle:
match = table_row_re.match(line)
if match:
aliases[json.loads(match.group(1))] = (
int(match.group(2)),
int(match.group(3)),
int(match.group(4)),
)
ini_fields = {
"INI_FILE_NAME",
"config",
"configPath",
"configpath",
"config_path",
"ini",
"iniFile",
"inifile",
"iniFileName",
"inifilename",
"ini_file",
"ini_file_name",
}
case_sources = {
field: {}
for field in ini_fields
}
case_row_re = re.compile(
r'^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*'
r'("(?:(?:\\.)|[^"\\])*"),\s*'
r'(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$'
)
with open(cpp_file, encoding="utf-8") as handle:
for line in handle:
match = case_row_re.match(line)
if not match:
continue
raw_field = json.loads(match.group(1))
if raw_field in ini_fields:
case_sources[raw_field][json.loads(match.group(2))] = (
int(match.group(3)),
int(match.group(4)),
int(match.group(5)),
)
with open(json_file, encoding="utf-8") as handle:
for entry in json.load(handle):
if entry["field"] in ini_fields:
case_sources[entry["field"]][entry["value"]] = (entry["m428"], entry["m429"], entry["m430"])
missing_aliases = [path for path in manifest_inis if path not in aliases]
missing_cases = [
(field, path)
for field in sorted(ini_fields)
for path in manifest_inis
if path not in case_sources[field]
]
if missing_aliases:
raise SystemExit(f"{label} table is missing LinuxCNC INI source alias(es): {missing_aliases}")
if missing_cases:
raise SystemExit(f"{label} cases are missing LinuxCNC INI source path(s): {missing_cases}")
wrong_types = [
(field, path, aliases[path], case_sources[field][path])
for field in sorted(ini_fields)
for path in manifest_inis
if path in aliases and path in case_sources[field] and aliases[path] != case_sources[field][path]
]
if wrong_types:
raise SystemExit(f"{label} INI source kinstype mismatch(es): {wrong_types}")
PY
then
exit 1
fi
elif command -v node >/dev/null 2>&1; then
if ! node - "$manifest_file" "$table_file" "$cpp_file" "$json_file" "$label" <<'JS'
const fs = require("fs");
const [manifestFile, tableFile, cppFile, jsonFile, label] = process.argv.slice(2);
const manifestInis = fs.readFileSync(manifestFile, "utf8")
.split(/\r?\n/)
.map((line) => line.split(":", 3))
.filter((parts) => parts.length >= 2 && parts[0] === "config" && parts[1].endsWith(".ini"))
.map((parts) => parts[1]);
if (manifestInis.length === 0) {
throw new Error(`${manifestFile} has no LinuxCNC INI config sources`);
}
const tableRowRe = /^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$/;
const aliases = new Map();
for (const line of fs.readFileSync(tableFile, "utf8").split(/\r?\n/)) {
const match = tableRowRe.exec(line);
if (match) {
aliases.set(JSON.parse(match[1]), [Number(match[2]), Number(match[3]), Number(match[4])]);
}
}
const iniFields = ["INI_FILE_NAME", "config", "configPath", "configpath", "config_path", "ini", "iniFile", "inifile", "iniFileName", "inifilename", "ini_file", "ini_file_name"];
const caseRowRe = /^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*("(?:(?:\\.)|[^"\\])*"),\s*(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$/;
const caseSources = new Map(iniFields.map((field) => [field, new Map()]));
for (const line of fs.readFileSync(cppFile, "utf8").split(/\r?\n/)) {
const match = caseRowRe.exec(line);
if (!match) {
continue;
}
const rawField = JSON.parse(match[1]);
if (caseSources.has(rawField)) {
caseSources.get(rawField).set(JSON.parse(match[2]), [Number(match[3]), Number(match[4]), Number(match[5])]);
}
}
for (const entry of JSON.parse(fs.readFileSync(jsonFile, "utf8"))) {
if (caseSources.has(entry.field)) {
caseSources.get(entry.field).set(entry.value, [entry.m428, entry.m429, entry.m430]);
}
}
const missingAliases = manifestInis.filter((sourcePath) => !aliases.has(sourcePath));
const missingCases = iniFields.flatMap((field) =>
manifestInis
.filter((sourcePath) => !caseSources.get(field).has(sourcePath))
.map((sourcePath) => `${field}:${sourcePath}`)
);
if (missingAliases.length > 0) {
throw new Error(`${label} table is missing LinuxCNC INI source alias(es): ${missingAliases.join(",")}`);
}
if (missingCases.length > 0) {
throw new Error(`${label} cases are missing LinuxCNC INI source path(s): ${missingCases.join(",")}`);
}
const wrongTypes = iniFields.flatMap((field) =>
manifestInis
.filter((sourcePath) => aliases.has(sourcePath) && caseSources.get(field).has(sourcePath))
.filter((sourcePath) => JSON.stringify(aliases.get(sourcePath)) !== JSON.stringify(caseSources.get(field).get(sourcePath)))
.map((sourcePath) => `${field}:${sourcePath}: table ${JSON.stringify(aliases.get(sourcePath))}, case ${JSON.stringify(caseSources.get(field).get(sourcePath))}`)
);
if (wrongTypes.length > 0) {
throw new Error(`${label} INI source kinstype mismatch(es): ${wrongTypes.join("; ")}`);
}
JS
then
exit 1
fi
else
echo "missing python3 or node for switchkins INI manifest output validation" >&2
exit 1
fi
}
validate_manifest_ini_identity_in_outputs() {
local manifest_file=$1
local table_file=$2
local cpp_file=$3
local json_file=$4
local label=$5
if command -v python3 >/dev/null 2>&1; then
if ! python3 - "$linuxcnc_root" "$manifest_file" "$table_file" "$cpp_file" "$json_file" "$label" <<'PY'
import json
import os
import re
import sys
linuxcnc_root, manifest_file, table_file, cpp_file, json_file, label = sys.argv[1:]
def normalize_alias(value):
return "".join(ch for ch in value.lower() if not ch.isspace())
def read_ini_identity(path):
values = {}
with open(os.path.join(linuxcnc_root, path), encoding="utf-8") as handle:
for line in handle:
line = re.sub(r'\s*[#;].*$', '', line).strip()
if "=" not in line:
continue
key, value = line.split("=", 1)
key = key.strip().lower()
value = value.strip()
if key in {"machine", "kinematics"} and value and key not in values:
values[key] = value
return values
manifest_inis = []
with open(manifest_file, encoding="utf-8") as handle:
for line in handle:
parts = line.rstrip("\n").split(":", 2)
if len(parts) >= 2 and parts[0] == "config" and parts[1].endswith(".ini"):
manifest_inis.append(parts[1])
if not manifest_inis:
raise SystemExit(f"{manifest_file} has no LinuxCNC INI config sources")
table_row_re = re.compile(
r'^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*'
r'(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$'
)
aliases = {}
with open(table_file, encoding="utf-8") as handle:
for line in handle:
match = table_row_re.match(line)
if match:
aliases[json.loads(match.group(1))] = (
int(match.group(2)),
int(match.group(3)),
int(match.group(4)),
)
case_row_re = re.compile(
r'^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*'
r'("(?:(?:\\.)|[^"\\])*"),\s*'
r'(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$'
)
case_sources = {}
with open(cpp_file, encoding="utf-8") as handle:
for line in handle:
match = case_row_re.match(line)
if match:
case_sources[(json.loads(match.group(1)).lower(), json.loads(match.group(2)))] = (
int(match.group(3)),
int(match.group(4)),
int(match.group(5)),
)
with open(json_file, encoding="utf-8") as handle:
for entry in json.load(handle):
case_sources[(entry["field"].lower(), entry["value"])] = (
entry["m428"],
entry["m429"],
entry["m430"],
)
missing_cases = []
missing_aliases = []
wrong_case_types = []
wrong_alias_types = []
for ini_path in manifest_inis:
if ini_path not in aliases:
continue
expected_types = aliases[ini_path]
for field, value in read_ini_identity(ini_path).items():
case_key = (field, value)
alias = normalize_alias(value)
if case_key not in case_sources:
missing_cases.append((ini_path, field, value))
elif case_sources[case_key] != expected_types:
wrong_case_types.append((ini_path, field, value, expected_types, case_sources[case_key]))
if alias not in aliases:
missing_aliases.append((ini_path, field, value, alias))
elif aliases[alias] != expected_types:
wrong_alias_types.append((ini_path, field, value, expected_types, aliases[alias]))
if missing_cases:
raise SystemExit(f"{label} cases are missing LinuxCNC INI MACHINE/KINEMATICS value(s): {missing_cases}")
if missing_aliases:
raise SystemExit(f"{label} table is missing LinuxCNC INI MACHINE/KINEMATICS alias(es): {missing_aliases}")
if wrong_case_types:
raise SystemExit(f"{label} cases have LinuxCNC INI MACHINE/KINEMATICS kinstype mismatch(es): {wrong_case_types}")
if wrong_alias_types:
raise SystemExit(f"{label} table has LinuxCNC INI MACHINE/KINEMATICS kinstype mismatch(es): {wrong_alias_types}")
PY
then
exit 1
fi
elif command -v node >/dev/null 2>&1; then
if ! node - "$linuxcnc_root" "$manifest_file" "$table_file" "$cpp_file" "$json_file" "$label" <<'JS'
const fs = require("fs");
const path = require("path");
const [linuxcncRoot, manifestFile, tableFile, cppFile, jsonFile, label] = process.argv.slice(2);
function normalizeAlias(value) {
return value.toLowerCase().replace(/\s+/g, "");
}
function readIniIdentity(sourcePath) {
const values = new Map();
for (const sourceLine of fs.readFileSync(path.join(linuxcncRoot, sourcePath), "utf8").split(/\r?\n/)) {
const line = sourceLine.replace(/\s*[#;].*$/, "").trim();
const equals = line.indexOf("=");
if (equals < 0) {
continue;
}
const key = line.slice(0, equals).trim().toLowerCase();
const value = line.slice(equals + 1).trim();
if ((key === "machine" || key === "kinematics") && value && !values.has(key)) {
values.set(key, value);
}
}
return values;
}
const manifestInis = fs.readFileSync(manifestFile, "utf8")
.split(/\r?\n/)
.map((line) => line.split(":", 3))
.filter((parts) => parts.length >= 2 && parts[0] === "config" && parts[1].endsWith(".ini"))
.map((parts) => parts[1]);
if (manifestInis.length === 0) {
throw new Error(`${manifestFile} has no LinuxCNC INI config sources`);
}
const tableRowRe = /^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$/;
const aliases = new Map();
for (const line of fs.readFileSync(tableFile, "utf8").split(/\r?\n/)) {
const match = tableRowRe.exec(line);
if (match) {
aliases.set(JSON.parse(match[1]), [Number(match[2]), Number(match[3]), Number(match[4])]);
}
}
const caseRowRe = /^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*("(?:(?:\\.)|[^"\\])*"),\s*(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$/;
const caseSources = new Map();
for (const line of fs.readFileSync(cppFile, "utf8").split(/\r?\n/)) {
const match = caseRowRe.exec(line);
if (match) {
caseSources.set(JSON.stringify([JSON.parse(match[1]).toLowerCase(), JSON.parse(match[2])]), [Number(match[3]), Number(match[4]), Number(match[5])]);
}
}
for (const entry of JSON.parse(fs.readFileSync(jsonFile, "utf8"))) {
caseSources.set(JSON.stringify([entry.field.toLowerCase(), entry.value]), [entry.m428, entry.m429, entry.m430]);
}
const missingCases = [];
const missingAliases = [];
const wrongCaseTypes = [];
const wrongAliasTypes = [];
for (const iniPath of manifestInis) {
if (!aliases.has(iniPath)) {
continue;
}
const expectedTypes = aliases.get(iniPath);
for (const [field, value] of readIniIdentity(iniPath).entries()) {
const caseKey = JSON.stringify([field, value]);
const alias = normalizeAlias(value);
if (!caseSources.has(caseKey)) {
missingCases.push([iniPath, field, value]);
} else if (JSON.stringify(caseSources.get(caseKey)) !== JSON.stringify(expectedTypes)) {
wrongCaseTypes.push([iniPath, field, value, expectedTypes, caseSources.get(caseKey)]);
}
if (!aliases.has(alias)) {
missingAliases.push([iniPath, field, value, alias]);
} else if (JSON.stringify(aliases.get(alias)) !== JSON.stringify(expectedTypes)) {
wrongAliasTypes.push([iniPath, field, value, expectedTypes, aliases.get(alias)]);
}
}
}
if (missingCases.length > 0) {
throw new Error(`${label} cases are missing LinuxCNC INI MACHINE/KINEMATICS value(s): ${JSON.stringify(missingCases)}`);
}
if (missingAliases.length > 0) {
throw new Error(`${label} table is missing LinuxCNC INI MACHINE/KINEMATICS alias(es): ${JSON.stringify(missingAliases)}`);
}
if (wrongCaseTypes.length > 0) {
throw new Error(`${label} cases have LinuxCNC INI MACHINE/KINEMATICS kinstype mismatch(es): ${JSON.stringify(wrongCaseTypes)}`);
}
if (wrongAliasTypes.length > 0) {
throw new Error(`${label} table has LinuxCNC INI MACHINE/KINEMATICS kinstype mismatch(es): ${JSON.stringify(wrongAliasTypes)}`);
}
JS
then
exit 1
fi
else
echo "missing python3 or node for switchkins INI identity output validation" >&2
exit 1
fi
}
api_keys_csv=$(printf '%s,' $(extract_api_switchkins_alias_keys core/src/cnc_sim_api.cpp))
api_keys_csv=${api_keys_csv%,}
validate_api_switchkins_alias_keys() {
local keys_csv=$1
if command -v python3 >/dev/null 2>&1; then
if ! python3 - "$keys_csv" <<'PY'
import sys
keys = [key for key in sys.argv[1].split(",") if key]
if not keys:
raise SystemExit("missing switchkins config alias keys in cnc_sim_api.cpp")
seen = set()
for key in keys:
if key != key.lower():
raise SystemExit(f"switchkins config alias key is not lowercase: {key}")
if any(ch.isspace() for ch in key):
raise SystemExit(f"switchkins config alias key contains whitespace: {key}")
if key in seen:
raise SystemExit(f"duplicate switchkins config alias key in cnc_sim_api.cpp: {key}")
seen.add(key)
PY
then
exit 1
fi
elif command -v node >/dev/null 2>&1; then
if ! node - "$keys_csv" <<'JS'
const keys = process.argv[2].split(",").filter(Boolean);
if (keys.length === 0) {
throw new Error("missing switchkins config alias keys in cnc_sim_api.cpp");
}
const seen = new Set();
for (const key of keys) {
if (key !== key.toLowerCase()) {
throw new Error(`switchkins config alias key is not lowercase: ${key}`);
}
if (/\s/.test(key)) {
throw new Error(`switchkins config alias key contains whitespace: ${key}`);
}
if (seen.has(key)) {
throw new Error(`duplicate switchkins config alias key in cnc_sim_api.cpp: ${key}`);
}
seen.add(key);
}
JS
then
exit 1
fi
else
echo "missing python3 or node for switchkins remap API key validation" >&2
exit 1
fi
}
validate_api_switchkins_alias_keys "$api_keys_csv"
validate_web_switchkins_case_fields() {
local json_file=$1
local wasm_core_file=$2
local types_file=$3
local label=$4
if command -v python3 >/dev/null 2>&1; then
if ! python3 - "$json_file" "$wasm_core_file" "$types_file" "$label" <<'PY'
import json
import re
import sys
json_file, wasm_core_file, types_file, label = sys.argv[1:]
with open(json_file, encoding="utf-8") as handle:
fields = sorted({entry["field"] for entry in json.load(handle)})
with open(wasm_core_file, encoding="utf-8") as handle:
wasm_core = handle.read()
with open(types_file, encoding="utf-8") as handle:
types = handle.read()
missing_wasm = [
field for field in fields
if not re.search(
rf"\.\.\.\(\s*options\.{re.escape(field)}\s*\?\s*"
rf"\{{\s*{re.escape(field)}\s*:\s*options\.{re.escape(field)}\s*\}}\s*:\s*\{{\}}\s*\)",
wasm_core,
)
]
missing_types = [
field for field in fields
if not re.search(rf"\b{re.escape(field)}\?:\s*string\b", types)
]
if missing_wasm:
raise SystemExit(f"{label} web wasm-core.js does not forward switchkins config field(s): {missing_wasm}")
if missing_types:
raise SystemExit(f"{label} web TypeScript options do not expose switchkins config field(s): {missing_types}")
PY
then
exit 1
fi
elif command -v node >/dev/null 2>&1; then
if ! node - "$json_file" "$wasm_core_file" "$types_file" "$label" <<'JS'
const fs = require("fs");
const [jsonFile, wasmCoreFile, typesFile, label] = process.argv.slice(2);
const fields = [...new Set(JSON.parse(fs.readFileSync(jsonFile, "utf8")).map((entry) => entry.field))].sort();
const wasmCore = fs.readFileSync(wasmCoreFile, "utf8");
const types = fs.readFileSync(typesFile, "utf8");
const missingWasm = fields.filter((field) => {
const escaped = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return !new RegExp(`\\.\\.\\.\\(\\s*options\\.${escaped}\\s*\\?\\s*\\{\\s*${escaped}\\s*:\\s*options\\.${escaped}\\s*\\}\\s*:\\s*\\{\\}\\s*\\)`).test(wasmCore);
});
const missingTypes = fields.filter((field) => !new RegExp(`\\b${field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\?:\\s*string\\b`).test(types));
if (missingWasm.length > 0) {
throw new Error(`${label} web wasm-core.js does not forward switchkins config field(s): ${missingWasm.join(",")}`);
}
if (missingTypes.length > 0) {
throw new Error(`${label} web TypeScript options do not expose switchkins config field(s): ${missingTypes.join(",")}`);
}
JS
then
exit 1
fi
else
echo "missing python3 or node for browser switchkins config field validation" >&2
exit 1
fi
}
validate_tool_table_source_coverage_only() {
local table_file=$1
local cpp_file=$2
local json_file=$3
local api_file=$4
local wasm_core_file=$5
local types_file=$6
local label=$7
if command -v python3 >/dev/null 2>&1; then
if ! python3 - "$table_file" "$cpp_file" "$json_file" "$api_file" "$wasm_core_file" "$types_file" "$label" <<'PY'
import json
import re
import sys
table_file, cpp_file, json_file, api_file, wasm_core_file, types_file, label = sys.argv[1:]
table_row_re = re.compile(
r'^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*'
r'(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$'
)
case_row_re = re.compile(
r'^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*'
r'("(?:(?:\\.)|[^"\\])*"),\s*'
r'(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$'
)
bad_aliases = []
with open(table_file, encoding="utf-8") as handle:
for line_number, line in enumerate(handle, 1):
match = table_row_re.match(line)
if match:
alias = json.loads(match.group(1)).lower()
if alias.endswith(".tbl") or "/tool" in alias or "tool_table" in alias or "tooltable" in alias:
bad_aliases.append((line_number, alias))
bad_cases = []
with open(cpp_file, encoding="utf-8") as handle:
for line_number, line in enumerate(handle, 1):
match = case_row_re.match(line)
if match:
field = json.loads(match.group(1)).lower()
value = json.loads(match.group(2)).lower()
if "tool" in field or value.endswith(".tbl"):
bad_cases.append((line_number, field, value))
with open(json_file, encoding="utf-8") as handle:
for index, entry in enumerate(json.load(handle)):
field = entry["field"].lower()
value = entry["value"].lower()
if "tool" in field or value.endswith(".tbl"):
bad_cases.append((index, field, value))
with open(api_file, encoding="utf-8") as handle:
api_text = handle.read()
with open(wasm_core_file, encoding="utf-8") as handle:
wasm_text = handle.read()
with open(types_file, encoding="utf-8") as handle:
types_text = handle.read()
bad_bridge_tokens = []
for token, text in {
"api": api_text,
"wasm": wasm_text,
"types": types_text,
}.items():
if re.search(r'\btoolTable\b|\btooltable\b|\btool_table\b|\bTOOL_TABLE\b', text):
bad_bridge_tokens.append(token)
if bad_aliases:
raise SystemExit(f"{label} generated TOOL_TABLE alias(es), but TOOL_TABLE is source coverage only: {bad_aliases}")
if bad_cases:
raise SystemExit(f"{label} generated TOOL_TABLE case(s), but TOOL_TABLE is source coverage only: {bad_cases}")
if bad_bridge_tokens:
raise SystemExit(f"{label} exposes TOOL_TABLE through switchkins API/web bridge: {bad_bridge_tokens}")
PY
then
exit 1
fi
elif command -v node >/dev/null 2>&1; then
if ! node - "$table_file" "$cpp_file" "$json_file" "$api_file" "$wasm_core_file" "$types_file" "$label" <<'JS'
const fs = require("fs");
const [tableFile, cppFile, jsonFile, apiFile, wasmCoreFile, typesFile, label] = process.argv.slice(2);
const tableRowRe = /^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$/;
const caseRowRe = /^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*("(?:(?:\\.)|[^"\\])*"),\s*(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$/;
const badAliases = [];
fs.readFileSync(tableFile, "utf8").split(/\r?\n/).forEach((line, index) => {
const match = tableRowRe.exec(line);
if (!match) {
return;
}
const alias = JSON.parse(match[1]).toLowerCase();
if (alias.endsWith(".tbl") || alias.includes("/tool") || alias.includes("tool_table") || alias.includes("tooltable")) {
badAliases.push([index + 1, alias]);
}
});
const badCases = [];
fs.readFileSync(cppFile, "utf8").split(/\r?\n/).forEach((line, index) => {
const match = caseRowRe.exec(line);
if (!match) {
return;
}
const field = JSON.parse(match[1]).toLowerCase();
const value = JSON.parse(match[2]).toLowerCase();
if (field.includes("tool") || value.endsWith(".tbl")) {
badCases.push([index + 1, field, value]);
}
});
JSON.parse(fs.readFileSync(jsonFile, "utf8")).forEach((entry, index) => {
const field = entry.field.toLowerCase();
const value = entry.value.toLowerCase();
if (field.includes("tool") || value.endsWith(".tbl")) {
badCases.push([index, field, value]);
}
});
const bridgeTokenRe = /\btoolTable\b|\btooltable\b|\btool_table\b|\bTOOL_TABLE\b/;
const badBridgeTokens = [
["api", fs.readFileSync(apiFile, "utf8")],
["wasm", fs.readFileSync(wasmCoreFile, "utf8")],
["types", fs.readFileSync(typesFile, "utf8")],
].filter(([_name, text]) => bridgeTokenRe.test(text)).map(([name]) => name);
if (badAliases.length > 0) {
throw new Error(`${label} generated TOOL_TABLE alias(es), but TOOL_TABLE is source coverage only: ${JSON.stringify(badAliases)}`);
}
if (badCases.length > 0) {
throw new Error(`${label} generated TOOL_TABLE case(s), but TOOL_TABLE is source coverage only: ${JSON.stringify(badCases)}`);
}
if (badBridgeTokens.length > 0) {
throw new Error(`${label} exposes TOOL_TABLE through switchkins API/web bridge: ${badBridgeTokens.join(",")}`);
}
JS
then
exit 1
fi
else
echo "missing python3 or node for switchkins TOOL_TABLE source-only validation" >&2
exit 1
fi
}
for generated_file in \
linuxcnc_switchkins_remap_table.inc \
linuxcnc_switchkins_remap_config_cases.inc \
linuxcnc_switchkins_remap_config_cases.json
do
if [[ ! -f "$generated_dir/$generated_file" ]]; then
echo "missing generated switchkins remap output: $generated_file" >&2
exit 1
fi
if [[ ! -s "$generated_dir/$generated_file" ]]; then
echo "empty generated switchkins remap output: $generated_file" >&2
exit 1
fi
done
validate_web_switchkins_case_fields \
"$generated_dir/linuxcnc_switchkins_remap_config_cases.json" \
web/src/wasm-core.js \
web/src/index.ts \
"generated"
validate_tool_table_source_coverage_only \
"$generated_dir/linuxcnc_switchkins_remap_table.inc" \
"$generated_dir/linuxcnc_switchkins_remap_config_cases.inc" \
"$generated_dir/linuxcnc_switchkins_remap_config_cases.json" \
core/src/cnc_sim_api.cpp \
web/src/wasm-core.js \
web/src/index.ts \
"generated"
validate_tool_table_source_coverage_only \
core/src/linuxcnc_switchkins_remap_table.inc \
core/tests/linuxcnc_switchkins_remap_config_cases.inc \
web/public/linuxcnc_switchkins_remap_config_cases.json \
core/src/cnc_sim_api.cpp \
web/src/wasm-core.js \
web/src/index.ts \
"tracked"
validate_generated_header \
"$generated_dir/linuxcnc_switchkins_remap_table.inc" \
"table" \
"// Generated by ./generate-linuxcnc-switchkins-remap-table.sh."
validate_generated_header \
core/src/linuxcnc_switchkins_remap_table.inc \
"tracked table" \
"// Generated by ./generate-linuxcnc-switchkins-remap-table.sh."
validate_generated_header \
"$generated_dir/linuxcnc_switchkins_remap_config_cases.inc" \
"config cases" \
"// Generated by ./generate-linuxcnc-switchkins-remap-table.sh --config-cases."
validate_generated_header \
core/tests/linuxcnc_switchkins_remap_config_cases.inc \
"tracked config cases" \
"// Generated by ./generate-linuxcnc-switchkins-remap-table.sh --config-cases."
validate_generated_source_notes \
"$generated_dir/linuxcnc_switchkins_remap_table.inc" \
"table" \
"// Source: LinuxCNC INI MACHINE/KINEMATICS/HALFILE/POSTGUI_HALFILE/SUBROUTINE_PATH/REMAP entries and" \
"// adjacent remap_subs/{428,429,430}remap.ngc #<kinstype> assignments."
validate_generated_source_notes \
core/src/linuxcnc_switchkins_remap_table.inc \
"tracked table" \
"// Source: LinuxCNC INI MACHINE/KINEMATICS/HALFILE/POSTGUI_HALFILE/SUBROUTINE_PATH/REMAP entries and" \
"// adjacent remap_subs/{428,429,430}remap.ngc #<kinstype> assignments."
validate_generated_source_notes \
"$generated_dir/linuxcnc_switchkins_remap_config_cases.inc" \
"config cases" \
"// Source: LinuxCNC INI config path, MACHINE, KINEMATICS, non-LIB HALFILE/POSTGUI_HALFILE," \
"// SUBROUTINE_PATH-resolved remap_subs entries, and adjacent M428/M429/M430 #<kinstype> assignments."
validate_generated_source_notes \
core/tests/linuxcnc_switchkins_remap_config_cases.inc \
"tracked config cases" \
"// Source: LinuxCNC INI config path, MACHINE, KINEMATICS, non-LIB HALFILE/POSTGUI_HALFILE," \
"// SUBROUTINE_PATH-resolved remap_subs entries, and adjacent M428/M429/M430 #<kinstype> assignments."
validate_json "$generated_dir/linuxcnc_switchkins_remap_config_cases.json" "generated linuxcnc_switchkins_remap_config_cases.json"
validate_json web/public/linuxcnc_switchkins_remap_config_cases.json "web/public/linuxcnc_switchkins_remap_config_cases.json"
validate_cpp_table "$generated_dir/linuxcnc_switchkins_remap_table.inc" "generated"
validate_cpp_table core/src/linuxcnc_switchkins_remap_table.inc "tracked"
validate_cpp_cases "$generated_dir/linuxcnc_switchkins_remap_config_cases.inc" "generated"
validate_cpp_cases core/tests/linuxcnc_switchkins_remap_config_cases.inc "tracked"
compare_cpp_json_cases \
"$generated_dir/linuxcnc_switchkins_remap_config_cases.inc" \
"$generated_dir/linuxcnc_switchkins_remap_config_cases.json" \
"generated"
compare_cpp_json_cases \
core/tests/linuxcnc_switchkins_remap_config_cases.inc \
web/public/linuxcnc_switchkins_remap_config_cases.json \
"tracked"
validate_switchkins_case_fields_against_api \
"$api_keys_csv" \
"$generated_dir/linuxcnc_switchkins_remap_config_cases.inc" \
"$generated_dir/linuxcnc_switchkins_remap_config_cases.json" \
"generated"
validate_case_values_against_table \
"$generated_dir/linuxcnc_switchkins_remap_table.inc" \
"$generated_dir/linuxcnc_switchkins_remap_config_cases.inc" \
"$generated_dir/linuxcnc_switchkins_remap_config_cases.json" \
"generated"
validate_manifest_ini_paths_in_outputs \
"$manifest" \
"$generated_dir/linuxcnc_switchkins_remap_table.inc" \
"$generated_dir/linuxcnc_switchkins_remap_config_cases.inc" \
"$generated_dir/linuxcnc_switchkins_remap_config_cases.json" \
"generated"
validate_manifest_remap_paths_in_outputs \
"$manifest" \
"$generated_dir/linuxcnc_switchkins_remap_table.inc" \
"$generated_dir/linuxcnc_switchkins_remap_config_cases.inc" \
"$generated_dir/linuxcnc_switchkins_remap_config_cases.json" \
"generated"
validate_manifest_hal_paths_in_outputs \
"$manifest" \
"$generated_dir/linuxcnc_switchkins_remap_table.inc" \
"$generated_dir/linuxcnc_switchkins_remap_config_cases.inc" \
"$generated_dir/linuxcnc_switchkins_remap_config_cases.json" \
"generated"
validate_switchkins_case_fields_against_api \
"$api_keys_csv" \
core/tests/linuxcnc_switchkins_remap_config_cases.inc \
web/public/linuxcnc_switchkins_remap_config_cases.json \
"tracked"
validate_case_values_against_table \
core/src/linuxcnc_switchkins_remap_table.inc \
core/tests/linuxcnc_switchkins_remap_config_cases.inc \
web/public/linuxcnc_switchkins_remap_config_cases.json \
"tracked"
validate_manifest_ini_paths_in_outputs \
"$manifest" \
core/src/linuxcnc_switchkins_remap_table.inc \
core/tests/linuxcnc_switchkins_remap_config_cases.inc \
web/public/linuxcnc_switchkins_remap_config_cases.json \
"tracked"
validate_manifest_remap_paths_in_outputs \
"$manifest" \
core/src/linuxcnc_switchkins_remap_table.inc \
core/tests/linuxcnc_switchkins_remap_config_cases.inc \
web/public/linuxcnc_switchkins_remap_config_cases.json \
"tracked"
validate_manifest_hal_paths_in_outputs \
"$manifest" \
core/src/linuxcnc_switchkins_remap_table.inc \
core/tests/linuxcnc_switchkins_remap_config_cases.inc \
web/public/linuxcnc_switchkins_remap_config_cases.json \
"tracked"
diff -u core/src/linuxcnc_switchkins_remap_table.inc "$generated_dir/linuxcnc_switchkins_remap_table.inc"
diff -u core/tests/linuxcnc_switchkins_remap_config_cases.inc "$generated_dir/linuxcnc_switchkins_remap_config_cases.inc"
diff -u web/public/linuxcnc_switchkins_remap_config_cases.json "$generated_dir/linuxcnc_switchkins_remap_config_cases.json"