收紧TRSRN资产清单校验

This commit is contained in:
cnc
2026-06-03 16:44:33 +08:00
parent 702f94cc52
commit 180c0f6dbe
2 changed files with 339 additions and 0 deletions

View File

@@ -363,6 +363,14 @@ validate_generator_cli_contract() {
local cli_manifest_with_unused_postgui local cli_manifest_with_unused_postgui
local cli_manifest_with_unused_remap local cli_manifest_with_unused_remap
local cli_manifest_with_unused_tooldata local cli_manifest_with_unused_tooldata
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_ini
local cli_manifest_without_hal local cli_manifest_without_hal
local cli_manifest_without_postgui local cli_manifest_without_postgui
@@ -380,6 +388,14 @@ validate_generator_cli_contract() {
cli_manifest_with_unused_postgui="$cli_dir/bridgemill-manifest-with-unused-postgui.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_remap="$cli_dir/bridgemill-manifest-with-unused-remap.txt"
cli_manifest_with_unused_tooldata="$cli_dir/bridgemill-manifest-with-unused-tooldata.txt" cli_manifest_with_unused_tooldata="$cli_dir/bridgemill-manifest-with-unused-tooldata.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_ini="$cli_dir/bridgemill-manifest-without-ini.txt"
cli_manifest_without_hal="$cli_dir/bridgemill-manifest-without-hal.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_postgui="$cli_dir/bridgemill-manifest-without-postgui.txt"
@@ -549,6 +565,118 @@ validate_generator_cli_contract() {
exit 1 exit 1
fi 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
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 --all-output-dir "$cli_dir/out" "$cli_manifest"
LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh "$cli_manifest" \ LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh "$cli_manifest" \
>"$cli_dir/linuxcnc_switchkins_remap_table.stdout.inc" >"$cli_dir/linuxcnc_switchkins_remap_table.stdout.inc"

View File

@@ -100,10 +100,26 @@ read_ini_switchkins_config() {
print "halfile\t" value print "halfile\t" value
} else if (section == "hal" && key == "postgui_halfile") { } else if (section == "hal" && key == "postgui_halfile") {
print "postgui_halfile\t" value print "postgui_halfile\t" value
} else if (section == "hal" && key == "halcmd") {
lower = tolower(value)
if (lower ~ /(^|[[:space:]])loadusr([[:space:]]|$)/ &&
match(lower, /[^[:space:]]+\.py([[:space:]]|$)/)) {
py = substr(value, RSTART, RLENGTH)
sub(/[[:space:]]*$/, "", py)
print "halcmd_loadusr_py\t" py
}
} else if (section == "emcio" && key == "tool_table") { } else if (section == "emcio" && key == "tool_table") {
print "tool_table\t" value print "tool_table\t" value
} else if (section == "rs274ngc" && key == "subroutine_path") { } else if (section == "rs274ngc" && key == "subroutine_path") {
print "subroutine_path\t" value print "subroutine_path\t" value
} else if (section == "rs274ngc" && key == "on_abort_command") {
lower = tolower(value)
if (match(lower, /o[[:space:]]*<[^>]+>[[:space:]]*call/)) {
ngc = substr(value, RSTART, RLENGTH)
sub(/^[Oo][[:space:]]*</, "", ngc)
sub(/>[[:space:]]*[Cc][Aa][Ll][Ll].*$/, "", ngc)
print "on_abort_ngc\t" ngc
}
} else if (section == "rs274ngc" && key == "remap") { } else if (section == "rs274ngc" && key == "remap") {
lower = tolower(value) lower = tolower(value)
for (mcode = 428; mcode <= 430; mcode++) { for (mcode = 428; mcode <= 430; mcode++) {
@@ -114,6 +130,20 @@ read_ini_switchkins_config() {
print "remap_" mcode "\t" ngc print "remap_" mcode "\t" ngc
} }
} }
if (match(lower, /ngc[[:space:]]*=[[:space:]]*[^[:space:]]+/)) {
ngc = substr(value, RSTART, RLENGTH)
sub(/^[Nn][Gg][Cc][[:space:]]*=[[:space:]]*/, "", ngc)
print "remap_ngc\t" ngc
}
if (match(lower, /python[[:space:]]*=[[:space:]]*[^[:space:]]+/)) {
python = substr(value, RSTART, RLENGTH)
sub(/^[Pp][Yy][Tt][Hh][Oo][Nn][[:space:]]*=[[:space:]]*/, "", python)
print "python_remap\t" python
}
} else if (section == "python" && key == "path_append") {
print "python_path_append\t" value
} else if (section == "python" && key == "toplevel") {
print "python_toplevel\t" value
} }
} }
' "$file" ' "$file"
@@ -156,9 +186,12 @@ declare -A manifest_remap_sources=()
declare -A used_remap_sources=() declare -A used_remap_sources=()
declare -A manifest_tooldata_sources=() declare -A manifest_tooldata_sources=()
declare -A used_tooldata_sources=() declare -A used_tooldata_sources=()
declare -A manifest_asset_sources=()
declare -A used_asset_sources=()
manifest_config_count=0 manifest_config_count=0
manifest_remap_count=0 manifest_remap_count=0
manifest_tooldata_count=0 manifest_tooldata_count=0
manifest_asset_count=0
validate_manifest_source_path() { validate_manifest_source_path() {
local group=$1 local group=$1
@@ -226,6 +259,15 @@ while IFS=: read -r manifest_group manifest_path _manifest_note; do
manifest_tooldata_sources[$manifest_path]=1 manifest_tooldata_sources[$manifest_path]=1
manifest_tooldata_count=$((manifest_tooldata_count + 1)) manifest_tooldata_count=$((manifest_tooldata_count + 1))
;; ;;
asset)
validate_manifest_source_path "$manifest_group" "$manifest_path"
if [[ -n "${manifest_asset_sources[$manifest_path]:-}" ]]; then
echo "duplicate LinuxCNC asset source in $manifest: $manifest_path" >&2
exit 1
fi
manifest_asset_sources[$manifest_path]=1
manifest_asset_count=$((manifest_asset_count + 1))
;;
esac esac
done <"$manifest" done <"$manifest"
@@ -322,6 +364,108 @@ tooldata_source_for_tool_table() {
resolved_tooldata_source=$resolved_linuxcnc_path resolved_tooldata_source=$resolved_linuxcnc_path
} }
resolved_asset_source=
asset_source_for_path() {
local config=$1
local path=$2
resolved_asset_source=
[[ -n "$path" ]] || return 1
local config_dir
config_dir_for "$config"
config_dir=$resolved_config_dir
local resolved
resolve_config_path "$config_dir" "$path"
resolved=$resolved_config_path
[[ -f "$resolved" ]] || return 1
relative_linuxcnc_path "$resolved"
resolved_asset_source=$resolved_linuxcnc_path
}
resolved_python_module_source=
python_module_source_for_name() {
local config=$1
local module=$2
local python_paths=${3:-}
resolved_python_module_source=
[[ -n "$module" ]] || return 1
[[ "$module" == *.* ]] && return 1
local module_path=${module//.//}
if [[ -z "$python_paths" ]]; then
python_paths="."
fi
local config_dir
config_dir_for "$config"
config_dir=$resolved_config_dir
local python_path
while IFS= read -r python_path; do
[[ -n "$python_path" ]] || continue
local path_part
IFS=: read -ra path_parts <<<"$python_path"
for path_part in "${path_parts[@]}"; do
[[ -n "$path_part" ]] || continue
local candidate_dir
resolve_config_path "$config_dir" "$path_part"
candidate_dir=$resolved_config_path
if [[ -f "$candidate_dir/$module_path.py" ]]; then
relative_linuxcnc_path "$candidate_dir/$module_path.py"
resolved_python_module_source=$resolved_linuxcnc_path
return 0
fi
done
done <<<"$python_paths"
return 1
}
record_asset_source() {
local config_path=$1
local source=$2
local label=$3
if [[ -z "${manifest_asset_sources[$source]:-}" ]]; then
echo "LinuxCNC $label asset source is not listed as asset in $manifest: $source ($config_path)" >&2
exit 1
fi
used_asset_sources[$source]=1
}
declare -A walked_python_import_assets=()
record_python_import_assets() {
local config_path=$1
local source=$2
local walk_key="$config_path|$source"
if [[ -n "${walked_python_import_assets[$walk_key]:-}" ]]; then
return 0
fi
walked_python_import_assets[$walk_key]=1
local source_dir=${source%/*}
local imported
while IFS= read -r imported; do
[[ -n "$imported" ]] || continue
local import_source="$source_dir/$imported.py"
if [[ -f "$linuxcnc_root/$import_source" ]]; then
record_asset_source "$config_path" "$import_source" "Python import"
record_python_import_assets "$config_path" "$import_source"
fi
done < <(awk '
/^[[:space:]]*import[[:space:]]+[A-Za-z_][A-Za-z0-9_]*([[:space:]]|$)/ {
value = $0
sub(/^[[:space:]]*import[[:space:]]+/, "", value)
sub(/[[:space:],].*$/, "", value)
print value
}
/^[[:space:]]*from[[:space:]]+[A-Za-z_][A-Za-z0-9_]*[[:space:]]+import[[:space:]]+/ {
value = $0
sub(/^[[:space:]]*from[[:space:]]+/, "", value)
sub(/[[:space:]].*$/, "", value)
print value
}
' "$linuxcnc_root/$source")
}
resolved_kinstype= resolved_kinstype=
declare -A kinstype_cache=() declare -A kinstype_cache=()
kinstype_for_remap_source() { kinstype_for_remap_source() {
@@ -664,6 +808,12 @@ while IFS=: read -r group path _note; do
postgui_halfiles= postgui_halfiles=
tool_tables= tool_tables=
subroutine_paths= subroutine_paths=
remap_ngcs=
on_abort_ngcs=
python_paths=
python_toplevels=
python_remaps=
halcmd_loadusr_pys=
remap_ngc_428= remap_ngc_428=
remap_ngc_429= remap_ngc_429=
remap_ngc_430= remap_ngc_430=
@@ -687,6 +837,24 @@ while IFS=: read -r group path _note; do
subroutine_path) subroutine_path)
subroutine_paths+="${subroutine_paths:+$'\n'}$ini_value" subroutine_paths+="${subroutine_paths:+$'\n'}$ini_value"
;; ;;
remap_ngc)
remap_ngcs+="${remap_ngcs:+$'\n'}$ini_value"
;;
on_abort_ngc)
on_abort_ngcs+="${on_abort_ngcs:+$'\n'}$ini_value"
;;
python_path_append)
python_paths+="${python_paths:+$'\n'}$ini_value"
;;
python_toplevel)
python_toplevels+="${python_toplevels:+$'\n'}$ini_value"
;;
python_remap)
python_remaps+="${python_remaps:+$'\n'}$ini_value"
;;
halcmd_loadusr_py)
halcmd_loadusr_pys+="${halcmd_loadusr_pys:+$'\n'}$ini_value"
;;
remap_428) remap_428)
[[ -n "$remap_ngc_428" ]] || remap_ngc_428=$ini_value [[ -n "$remap_ngc_428" ]] || remap_ngc_428=$ini_value
;; ;;
@@ -799,6 +967,44 @@ while IFS=: read -r group path _note; do
record_tooldata_source "$path" "$resolved_tooldata_source" record_tooldata_source "$path" "$resolved_tooldata_source"
fi fi
done <<<"$tool_tables" done <<<"$tool_tables"
while IFS= read -r ngc_name; do
[[ -n "$ngc_name" ]] || continue
if remap_source_for_ngc "$config" "$ngc_name" "$subroutine_paths"; then
if [[ -z "${manifest_remap_sources[$resolved_remap_source]:-}" ]]; then
record_asset_source "$path" "$resolved_remap_source" "RS274NGC REMAP ngc"
fi
fi
done <<<"$remap_ngcs"
while IFS= read -r ngc_name; do
[[ -n "$ngc_name" ]] || continue
if remap_source_for_ngc "$config" "$ngc_name" "$subroutine_paths"; then
record_asset_source "$path" "$resolved_remap_source" "ON_ABORT_COMMAND"
fi
done <<<"$on_abort_ngcs"
while IFS= read -r toplevel; do
[[ -n "$toplevel" ]] || continue
if asset_source_for_path "$config" "$toplevel"; then
python_toplevel_source=$resolved_asset_source
record_asset_source "$path" "$python_toplevel_source" "PYTHON TOPLEVEL"
record_python_import_assets "$path" "$python_toplevel_source"
fi
done <<<"$python_toplevels"
while IFS= read -r python_remap; do
[[ -n "$python_remap" ]] || continue
if python_module_source_for_name "$config" "$python_remap" "$python_paths"; then
python_remap_source=$resolved_python_module_source
record_asset_source "$path" "$python_remap_source" "Python REMAP module"
record_python_import_assets "$path" "$python_remap_source"
fi
done <<<"$python_remaps"
while IFS= read -r loadusr_py; do
[[ -n "$loadusr_py" ]] || continue
if asset_source_for_path "$config" "$loadusr_py"; then
halcmd_loadusr_source=$resolved_asset_source
record_asset_source "$path" "$halcmd_loadusr_source" "HALCMD loadusr"
record_python_import_assets "$path" "$halcmd_loadusr_source"
fi
done <<<"$halcmd_loadusr_pys"
for remap_source in "$remap_source_428" "$remap_source_429" "$remap_source_430"; do for remap_source in "$remap_source_428" "$remap_source_429" "$remap_source_430"; do
if [[ -n "$remap_source" ]]; then if [[ -n "$remap_source" ]]; then
@@ -821,6 +1027,7 @@ done <"$manifest"
unique_config_source_count=${#used_config_sources[@]} unique_config_source_count=${#used_config_sources[@]}
unique_resolved_remap_count=${#used_remap_sources[@]} unique_resolved_remap_count=${#used_remap_sources[@]}
unique_tooldata_source_count=${#used_tooldata_sources[@]} unique_tooldata_source_count=${#used_tooldata_sources[@]}
unique_asset_source_count=${#used_asset_sources[@]}
validate_generated_content() { validate_generated_content() {
if ((manifest_config_count == 0)); then if ((manifest_config_count == 0)); then
@@ -871,6 +1078,10 @@ validate_generated_content() {
done done
exit 1 exit 1
fi fi
if ((unique_asset_source_count > manifest_asset_count)); then
echo "LinuxCNC asset manifest coverage mismatch: used $unique_asset_source_count of $manifest_asset_count asset sources" >&2
exit 1
fi
if ((unique_resolved_remap_count != manifest_remap_count)); then if ((unique_resolved_remap_count != manifest_remap_count)); then
echo "LinuxCNC remap manifest coverage mismatch: resolved $unique_resolved_remap_count of $manifest_remap_count remap sources" >&2 echo "LinuxCNC remap manifest coverage mismatch: resolved $unique_resolved_remap_count of $manifest_remap_count remap sources" >&2
local remap_source local remap_source