3067 lines
122 KiB
Bash
Executable File
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"
|