From 4c55ab3435ccfdab0510925ba3f1bb8ed17bb5b0 Mon Sep 17 00:00:00 2001 From: cnc Date: Mon, 1 Jun 2026 06:11:08 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AC=AC=206=20=E7=BB=84=EF=BC=9A=E6=BA=90?= =?UTF-8?q?=E7=A0=81=E9=93=BE=E6=8E=A5=E3=80=81=E6=9E=84=E5=BB=BA=E5=92=8C?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E7=BA=A6=E6=9D=9F=E9=97=AD=E7=8E=AF=E5=AE=8C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build-wasm.sh | 9 +- check-linuxcnc-inputs.sh | 922 ++++++++++++++ check-linuxcnc-switchkins-remap-table.sh | 1078 ++++++++++++++++- core/CMakeLists.txt | 3 +- core/src/cnc_sim_api.cpp | 10 +- core/src/linuxcnc_canon_bridge.cpp | 18 + core/src/linuxcnc_switchkins_remap_table.inc | 24 +- .../cnc_sim_api_linuxcnc_rs274_smoke.cpp | 128 ++ ...linuxcnc_switchkins_remap_config_cases.inc | 321 ++++- docs/architecture.md | 16 + docs/linuxcnc-porting.md | 9 +- docs/linuxcnc-source-policy.md | 91 +- generate-linuxcnc-switchkins-remap-table.sh | 648 ++++++++-- linuxcnc-kinematics-source-files.txt | 118 ++ list-linuxcnc-kinematics-manifest-sources.sh | 2 +- test-all-native.sh | 309 ++++- test-linuxcnc-source-link.sh | 123 +- test-linuxcnc-source-syntax.sh | 101 +- test-linuxcnc-wasm-cmake-safe-probe.sh | 174 ++- ...inuxcnc-wasm-rs274ngc-pre-link-blockers.sh | 2 +- test-native.sh | 379 +++++- test-web-wasm-browser-smoke.sh | 453 ++++++- test-web-wasm-node-smoke.cjs | 646 +++++----- test-web-wasm-node-smoke.sh | 114 +- web/index.html | 2 +- web/package-lock.json | 994 +++++++++++++++ web/public/cnc_sim.js | 2 +- ...inuxcnc_switchkins_remap_config_cases.json | 319 ++++- web/src/app.js | 225 +++- web/src/index.ts | 11 + web/src/wasm-core.d.ts | 52 + web/src/wasm-core.js | 574 ++++++++- web/test-browser-wasm-smoke-app-sections.js | 66 + web/test-browser-wasm-smoke-helpers.js | 176 +++ ...st-browser-wasm-smoke-linuxcnc-sections.js | 72 ++ ...-browser-wasm-smoke-opfs-basic-sections.js | 114 ++ ...wser-wasm-smoke-opfs-directory-sections.js | 174 +++ ...browser-wasm-smoke-opfs-mirror-sections.js | 76 ++ ...wser-wasm-smoke-opfs-parameter-sections.js | 255 ++++ ...browser-wasm-smoke-opfs-policy-sections.js | 49 + ...wser-wasm-smoke-opfs-workspace-sections.js | 14 + web/test-browser-wasm-smoke-sections.js | 99 ++ web/test-browser-wasm-smoke.html | 78 +- 43 files changed, 8414 insertions(+), 636 deletions(-) create mode 100644 web/package-lock.json create mode 100644 web/test-browser-wasm-smoke-app-sections.js create mode 100644 web/test-browser-wasm-smoke-helpers.js create mode 100644 web/test-browser-wasm-smoke-linuxcnc-sections.js create mode 100644 web/test-browser-wasm-smoke-opfs-basic-sections.js create mode 100644 web/test-browser-wasm-smoke-opfs-directory-sections.js create mode 100644 web/test-browser-wasm-smoke-opfs-mirror-sections.js create mode 100644 web/test-browser-wasm-smoke-opfs-parameter-sections.js create mode 100644 web/test-browser-wasm-smoke-opfs-policy-sections.js create mode 100644 web/test-browser-wasm-smoke-opfs-workspace-sections.js create mode 100644 web/test-browser-wasm-smoke-sections.js diff --git a/build-wasm.sh b/build-wasm.sh index ebd311c..bf83724 100755 --- a/build-wasm.sh +++ b/build-wasm.sh @@ -5,6 +5,11 @@ cd "$(dirname "$0")" linuxcnc_root=${LINUXCNC_ROOT:-../linuxcnc} manifest=${1:-linuxcnc-rs274-wasm-source-files.txt} +build_jobs=${CNC_SIM_BUILD_JOBS:-8} +if ! [[ "$build_jobs" =~ ^[1-9][0-9]*$ ]]; then + echo "CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs" >&2 + exit 1 +fi LINUXCNC_ROOT="$linuxcnc_root" ./check-linuxcnc-inputs.sh "$manifest" manifest=$(cd "$(dirname "$manifest")" && pwd)/$(basename "$manifest") @@ -113,8 +118,8 @@ emcmake cmake -S core -B build/wasm \ -DCNC_SIM_LINUXCNC_ROOT="$linuxcnc_root" \ -DCNC_SIM_LINUXCNC_WASM_SOURCE_MANIFEST="$manifest" \ -DCNC_SIM_ENABLE_LINUXCNC_WASM_SAFE_PROBE=ON -cmake --build build/wasm --target cnc_sim_wasm_runtime_probe -cmake --build build/wasm --target cnc_sim_wasm +cmake --build build/wasm --target cnc_sim_wasm_runtime_probe --parallel "$build_jobs" +cmake --build build/wasm --target cnc_sim_wasm --parallel "$build_jobs" mkdir -p web/public cp build/wasm/cnc_sim.js web/public/cnc_sim.js cp build/wasm/cnc_sim.wasm web/public/cnc_sim.wasm diff --git a/check-linuxcnc-inputs.sh b/check-linuxcnc-inputs.sh index e56ac96..6443bbd 100755 --- a/check-linuxcnc-inputs.sh +++ b/check-linuxcnc-inputs.sh @@ -6,6 +6,29 @@ cd "$(dirname "$0")" linuxcnc_root=${LINUXCNC_ROOT:-../linuxcnc} manifest=${1:-} require_complete=${2:-} +allowed_manifest_groups=( + asset + binding + blocked + blocked-header + component + config + core + generated + header + metadata + remap + tooldata +) + +contains_manifest_group() { + local candidate=$1 + local group + for group in "${allowed_manifest_groups[@]}"; do + [[ "$group" == "$candidate" ]] && return 0 + done + return 1 +} if [[ "$manifest" == "--root-only" ]]; then manifest= @@ -32,6 +55,43 @@ if [[ ! -d "$linuxcnc_root" ]]; then echo "missing LinuxCNC root: $linuxcnc_root" >&2 exit 1 fi +linuxcnc_root=$(cd "$linuxcnc_root" && pwd) + +if [[ -n "$manifest" ]]; then + seen_manifest_sources=() + manifest_line_number=0 + while IFS= read -r manifest_line || [[ -n "$manifest_line" ]]; do + manifest_line_number=$((manifest_line_number + 1)) + case "$manifest_line" in + ""|\#*) + continue + ;; + esac + + IFS=: read -r group path note extra <<<"$manifest_line" + if [[ -z "$group" || -z "$path" || -z "$note" || -n "${extra:-}" ]]; then + echo "bad manifest line $manifest_line_number: $manifest_line" >&2 + exit 1 + fi + if ! contains_manifest_group "$group"; then + echo "unknown manifest group: $group" >&2 + exit 1 + fi + if [[ "$path" = /* || "$path" == .. || "$path" == ../* || "$path" == */.. || "$path" == */../* ]]; then + echo "manifest source must be LinuxCNC-root relative: $path" >&2 + exit 1 + fi + if printf '%s\n' "${seen_manifest_sources[@]}" | grep -Fx -- "$path" >/dev/null; then + echo "duplicate manifest source: $path" >&2 + exit 1 + fi + if [[ ! -f "$linuxcnc_root/$path" ]]; then + echo "missing manifest source: $path" >&2 + exit 1 + fi + seen_manifest_sources+=("$path") + done < "$manifest" +fi if [[ -n "$manifest" && "$require_complete" == "--require-rs274-complete" ]]; then rs274_dir="$linuxcnc_root/src/emc/rs274ngc" @@ -96,6 +156,148 @@ if [[ -n "$manifest" && "$require_complete" == "--require-kinematics-complete" ] exit 1 fi + mapfile -t kinematics_header_sources < <( + find "$kinematics_dir" -maxdepth 1 -type f -name '*.h' \ + | sed "s#^$linuxcnc_root/##" \ + | sort + ) + mapfile -t manifest_kinematics_header_sources < <( + awk -F: ' + $1 !~ /^($|#)/ && $1 == "header" && $2 ~ /^src\/emc\/kinematics\/.*\.h$/ { print $2 } + ' "$manifest" | sort -u + ) + mapfile -t kinematics_metadata_sources < <( + find "$kinematics_dir" -maxdepth 1 -type f \ + \( -name 'Submakefile' -o -name 'meson.build' \) \ + | sed "s#^$linuxcnc_root/##" \ + | sort + ) + mapfile -t manifest_kinematics_metadata_sources < <( + awk -F: ' + $1 !~ /^($|#)/ && $1 == "metadata" && $2 ~ /^src\/emc\/kinematics\/(Submakefile|meson\.build)$/ { print $2 } + ' "$manifest" | sort -u + ) + mapfile -t kinematics_asset_sources < <( + find "$kinematics_dir" -maxdepth 1 -type f -name '*.fig' \ + | sed "s#^$linuxcnc_root/##" \ + | sort + ) + mapfile -t manifest_kinematics_asset_sources < <( + awk -F: ' + $1 !~ /^($|#)/ && $1 == "asset" && $2 ~ /^src\/emc\/kinematics\/.*\.fig$/ { print $2 } + ' "$manifest" | sort -u + ) + + missing_kinematics_header_sources=() + for source in "${kinematics_header_sources[@]}"; do + if ! printf '%s\n' "${manifest_kinematics_header_sources[@]}" | grep -Fx -- "$source" >/dev/null; then + missing_kinematics_header_sources+=("$source") + fi + done + if ((${#missing_kinematics_header_sources[@]} > 0)); then + echo "manifest does not group all LinuxCNC kinematics headers as header sources: $manifest" >&2 + printf 'missing kinematics header source: %s\n' "${missing_kinematics_header_sources[@]}" >&2 + exit 1 + fi + + missing_kinematics_metadata_sources=() + for source in "${kinematics_metadata_sources[@]}"; do + if ! printf '%s\n' "${manifest_kinematics_metadata_sources[@]}" | grep -Fx -- "$source" >/dev/null; then + missing_kinematics_metadata_sources+=("$source") + fi + done + if ((${#missing_kinematics_metadata_sources[@]} > 0)); then + echo "manifest does not group all LinuxCNC kinematics build metadata as metadata sources: $manifest" >&2 + printf 'missing kinematics metadata source: %s\n' "${missing_kinematics_metadata_sources[@]}" >&2 + exit 1 + fi + + missing_kinematics_asset_sources=() + for source in "${kinematics_asset_sources[@]}"; do + if ! printf '%s\n' "${manifest_kinematics_asset_sources[@]}" | grep -Fx -- "$source" >/dev/null; then + missing_kinematics_asset_sources+=("$source") + fi + done + if ((${#missing_kinematics_asset_sources[@]} > 0)); then + echo "manifest does not group all LinuxCNC kinematics reference assets as asset sources: $manifest" >&2 + printf 'missing kinematics asset source: %s\n' "${missing_kinematics_asset_sources[@]}" >&2 + exit 1 + fi + + posemath_dir="$linuxcnc_root/src/libnml/posemath" + if [[ ! -d "$posemath_dir" ]]; then + echo "missing LinuxCNC posemath source directory: $posemath_dir" >&2 + exit 1 + fi + mapfile -t posemath_core_sources < <( + find "$posemath_dir" -maxdepth 1 -type f \ + \( -name '*.c' -o -name '*.cc' \) \ + | sed "s#^$linuxcnc_root/##" \ + | sort + ) + mapfile -t manifest_posemath_core_sources < <( + awk -F: ' + $1 !~ /^($|#)/ && $1 == "core" && $2 ~ /^src\/libnml\/posemath\/.*\.(c|cc)$/ { print $2 } + ' "$manifest" | sort -u + ) + mapfile -t posemath_header_sources < <( + find "$posemath_dir" -maxdepth 1 -type f -name '*.h' \ + | sed "s#^$linuxcnc_root/##" \ + | sort + ) + mapfile -t manifest_posemath_header_sources < <( + awk -F: ' + $1 !~ /^($|#)/ && $1 == "header" && $2 ~ /^src\/libnml\/posemath\/.*\.h$/ { print $2 } + ' "$manifest" | sort -u + ) + mapfile -t posemath_metadata_sources < <( + find "$posemath_dir" -maxdepth 1 -type f \ + \( -name 'Submakefile' -o -name 'meson.build' \) \ + | sed "s#^$linuxcnc_root/##" \ + | sort + ) + mapfile -t manifest_posemath_metadata_sources < <( + awk -F: ' + $1 !~ /^($|#)/ && $1 == "metadata" && $2 ~ /^src\/libnml\/posemath\/(Submakefile|meson\.build)$/ { print $2 } + ' "$manifest" | sort -u + ) + + missing_posemath_core_sources=() + for source in "${posemath_core_sources[@]}"; do + if ! printf '%s\n' "${manifest_posemath_core_sources[@]}" | grep -Fx -- "$source" >/dev/null; then + missing_posemath_core_sources+=("$source") + fi + done + if ((${#missing_posemath_core_sources[@]} > 0)); then + echo "manifest does not group all LinuxCNC posemath C/C++ sources as core sources: $manifest" >&2 + printf 'missing posemath core source: %s\n' "${missing_posemath_core_sources[@]}" >&2 + exit 1 + fi + + missing_posemath_header_sources=() + for source in "${posemath_header_sources[@]}"; do + if ! printf '%s\n' "${manifest_posemath_header_sources[@]}" | grep -Fx -- "$source" >/dev/null; then + missing_posemath_header_sources+=("$source") + fi + done + if ((${#missing_posemath_header_sources[@]} > 0)); then + echo "manifest does not group all LinuxCNC posemath headers as header sources: $manifest" >&2 + printf 'missing posemath header source: %s\n' "${missing_posemath_header_sources[@]}" >&2 + exit 1 + fi + + missing_posemath_metadata_sources=() + for source in "${posemath_metadata_sources[@]}"; do + if ! printf '%s\n' "${manifest_posemath_metadata_sources[@]}" | grep -Fx -- "$source" >/dev/null; then + missing_posemath_metadata_sources+=("$source") + fi + done + if ((${#missing_posemath_metadata_sources[@]} > 0)); then + echo "manifest does not group all LinuxCNC posemath build metadata as metadata sources: $manifest" >&2 + printf 'missing posemath metadata source: %s\n' "${missing_posemath_metadata_sources[@]}" >&2 + exit 1 + fi + mapfile -t m428_m430_remap_sources < <( find "$linuxcnc_root/configs" -path '*/remap_subs/*' -type f \ \( -name '428remap.ngc' -o -name '429remap.ngc' -o -name '430remap.ngc' \) \ @@ -165,4 +367,724 @@ if [[ -n "$manifest" && "$require_complete" == "--require-kinematics-complete" ] printf 'missing M428/M429/M430 INI source: %s\n' "${missing_m428_m430_ini_sources[@]}" >&2 exit 1 fi + + mapfile -t manifest_component_sources < <( + awk -F: ' + $1 !~ /^($|#)/ && $1 == "component" { print $2 } + ' "$manifest" | sort -u + ) + mapfile -t manifest_generated_sources < <( + awk -F: ' + $1 !~ /^($|#)/ && $1 == "generated" { print $2 } + ' "$manifest" | sort -u + ) + mapfile -t manifest_metadata_sources < <( + awk -F: ' + $1 !~ /^($|#)/ && $1 == "metadata" { print $2 } + ' "$manifest" | sort -u + ) + m428_m430_kinematics_component_sources=() + m428_m430_kinematics_generated_sources=() + for source in "${manifest_m428_m430_ini_sources[@]}"; do + while IFS= read -r kinematics_name; do + [[ -n "$kinematics_name" ]] || continue + component_source="src/hal/components/$kinematics_name.comp" + generated_source="src/objects/hal/components/$kinematics_name.c" + if [[ -f "$linuxcnc_root/$component_source" ]]; then + m428_m430_kinematics_component_sources+=("$component_source") + fi + if [[ -f "$linuxcnc_root/$generated_source" ]]; then + m428_m430_kinematics_generated_sources+=("$generated_source") + fi + done < <( + sed -n 's/^[[:space:]]*KINEMATICS[[:space:]]*=[[:space:]]*\([^[:space:]#;]*\).*/\1/Ip' "$linuxcnc_root/$source" + ) + done + mapfile -t m428_m430_kinematics_component_sources < <( + printf '%s\n' "${m428_m430_kinematics_component_sources[@]}" | sed '/^$/d' | sort -u + ) + mapfile -t m428_m430_kinematics_generated_sources < <( + printf '%s\n' "${m428_m430_kinematics_generated_sources[@]}" | sed '/^$/d' | sort -u + ) + + missing_m428_m430_kinematics_component_sources=() + for source in "${m428_m430_kinematics_component_sources[@]}"; do + if ! printf '%s\n' "${manifest_component_sources[@]}" | grep -Fx -- "$source" >/dev/null; then + missing_m428_m430_kinematics_component_sources+=("$source") + fi + done + if ((${#missing_m428_m430_kinematics_component_sources[@]} > 0)); then + echo "manifest does not cover all LinuxCNC component sources referenced by M428/M429/M430 INI KINEMATICS entries: $manifest" >&2 + printf 'missing M428/M429/M430 KINEMATICS component source: %s\n' "${missing_m428_m430_kinematics_component_sources[@]}" >&2 + exit 1 + fi + + missing_m428_m430_kinematics_generated_sources=() + for source in "${m428_m430_kinematics_generated_sources[@]}"; do + if ! printf '%s\n' "${manifest_generated_sources[@]}" | grep -Fx -- "$source" >/dev/null; then + missing_m428_m430_kinematics_generated_sources+=("$source") + fi + done + if ((${#missing_m428_m430_kinematics_generated_sources[@]} > 0)); then + echo "manifest does not cover all LinuxCNC generated component sources referenced by M428/M429/M430 INI KINEMATICS entries: $manifest" >&2 + printf 'missing M428/M429/M430 KINEMATICS generated source: %s\n' "${missing_m428_m430_kinematics_generated_sources[@]}" >&2 + exit 1 + fi + + generated_component_metadata_sources=() + for source in "${manifest_generated_sources[@]}"; do + case "$source" in + src/objects/hal/components/*.c) + metadata_source=${source%.c}.mak + if [[ -f "$linuxcnc_root/$metadata_source" ]]; then + generated_component_metadata_sources+=("$metadata_source") + fi + ;; + esac + done + mapfile -t generated_component_metadata_sources < <( + printf '%s\n' "${generated_component_metadata_sources[@]}" | sed '/^$/d' | sort -u + ) + + missing_generated_component_metadata_sources=() + for source in "${generated_component_metadata_sources[@]}"; do + if ! printf '%s\n' "${manifest_metadata_sources[@]}" | grep -Fx -- "$source" >/dev/null; then + missing_generated_component_metadata_sources+=("$source") + fi + done + if ((${#missing_generated_component_metadata_sources[@]} > 0)); then + echo "manifest does not cover all LinuxCNC generated component make metadata adjacent to generated kinematics sources: $manifest" >&2 + printf 'missing generated component make metadata source: %s\n' "${missing_generated_component_metadata_sources[@]}" >&2 + exit 1 + fi + + missing_m428_m430_ini_remap_sources=() + unresolved_m428_m430_ini_remaps=() + for source in "${manifest_m428_m430_ini_sources[@]}"; do + config="$linuxcnc_root/$source" + config_dir=$(cd "$(dirname "$config")" && pwd) + mapfile -t subroutine_paths < <( + sed -n 's/^[[:space:]]*SUBROUTINE_PATH[[:space:]]*=[[:space:]]*//Ip' "$config" | + sed 's/[[:space:]]*[#;].*$//; s/^[[:space:]]*//; s/[[:space:]]*$//' + ) + if ((${#subroutine_paths[@]} == 0)); then + subroutine_paths=(".") + fi + + for mcode in 428 429 430; do + ngc_name=$(sed -n "s/.*REMAP[[:space:]]*=[[:space:]]*M$mcode[[:space:]][^#;]*ngc[[:space:]]*=[[:space:]]*\\([^[:space:]#;]*\\).*/\\1/Ip" "$config" | + head -n 1) + [[ -n "$ngc_name" ]] || continue + [[ "$ngc_name" == *.ngc ]] || ngc_name="$ngc_name.ngc" + + resolved_source= + for subroutine_path in "${subroutine_paths[@]}"; do + [[ -n "$subroutine_path" ]] || continue + IFS=: read -ra path_parts <<<"$subroutine_path" + for path_part in "${path_parts[@]}"; do + [[ -n "$path_part" ]] || continue + if [[ "$path_part" = /* ]]; then + candidate_dir=$path_part + else + candidate_dir="$config_dir/$path_part" + fi + if [[ -f "$candidate_dir/$ngc_name" ]]; then + resolved_abs=$(cd "$(dirname "$candidate_dir/$ngc_name")" && pwd)/$(basename "$candidate_dir/$ngc_name") + resolved_source=${resolved_abs#"$linuxcnc_root/"} + break 2 + fi + done + done + + if [[ -z "$resolved_source" ]]; then + unresolved_m428_m430_ini_remaps+=("$source M$mcode ngc=$ngc_name") + elif ! printf '%s\n' "${manifest_m428_m430_remap_sources[@]}" | grep -Fx -- "$resolved_source" >/dev/null; then + missing_m428_m430_ini_remap_sources+=("$resolved_source") + fi + done + done + + if ((${#unresolved_m428_m430_ini_remaps[@]} > 0)); then + echo "LinuxCNC M428/M429/M430 INI remap sources could not be resolved through SUBROUTINE_PATH: $manifest" >&2 + printf 'unresolved M428/M429/M430 INI remap: %s\n' "${unresolved_m428_m430_ini_remaps[@]}" >&2 + exit 1 + fi + + if ((${#missing_m428_m430_ini_remap_sources[@]} > 0)); then + echo "manifest does not cover all LinuxCNC M428/M429/M430 remap sources referenced by INI REMAP entries: $manifest" >&2 + printf 'missing INI-referenced M428/M429/M430 remap source: %s\n' "${missing_m428_m430_ini_remap_sources[@]}" >&2 + exit 1 + fi + + mapfile -t manifest_config_hal_sources < <( + awk -F: ' + $1 !~ /^($|#)/ && $1 == "config" && $2 ~ /\.hal$/ { print $2 } + ' "$manifest" | sort -u + ) + unresolved_m428_m430_config_hal_sources=() + mapfile -t m428_m430_config_hal_sources < <( + for source in "${manifest_m428_m430_ini_sources[@]}"; do + config_dir=$(cd "$(dirname "$linuxcnc_root/$source")" && pwd) + sed -n 's/^[[:space:]]*\(HALFILE\|POSTGUI_HALFILE\)[[:space:]]*=[[:space:]]*//Ip' "$linuxcnc_root/$source" | + sed 's/[[:space:]]*[#;].*$//; s/^[[:space:]]*//; s/[[:space:]]*$//' | + while IFS= read -r hal_source; do + [[ -n "$hal_source" && "$hal_source" != LIB:* ]] || continue + if [[ "$hal_source" = /* ]]; then + candidate=$hal_source + else + candidate="$config_dir/$hal_source" + fi + if [[ -f "$candidate" ]]; then + candidate_abs=$(cd "$(dirname "$candidate")" && pwd)/$(basename "$candidate") + printf '%s\n' "${candidate_abs#"$linuxcnc_root/"}" + else + printf '%s HALFILE=%s\n' "$source" "$hal_source" >&2 + fi + done + done | sort -u + ) + mapfile -t unresolved_m428_m430_config_hal_sources < <( + for source in "${manifest_m428_m430_ini_sources[@]}"; do + config_dir=$(cd "$(dirname "$linuxcnc_root/$source")" && pwd) + sed -n 's/^[[:space:]]*\(HALFILE\|POSTGUI_HALFILE\)[[:space:]]*=[[:space:]]*//Ip' "$linuxcnc_root/$source" | + sed 's/[[:space:]]*[#;].*$//; s/^[[:space:]]*//; s/[[:space:]]*$//' | + while IFS= read -r hal_source; do + [[ -n "$hal_source" && "$hal_source" != LIB:* ]] || continue + if [[ "$hal_source" = /* ]]; then + candidate=$hal_source + else + candidate="$config_dir/$hal_source" + fi + [[ -f "$candidate" ]] || printf '%s HALFILE=%s\n' "$source" "$hal_source" + done + done | sort -u + ) + + if ((${#unresolved_m428_m430_config_hal_sources[@]} > 0)); then + echo "LinuxCNC M428/M429/M430 INI HALFILE/POSTGUI_HALFILE sources could not be resolved: $manifest" >&2 + printf 'unresolved M428/M429/M430 HAL source: %s\n' "${unresolved_m428_m430_config_hal_sources[@]}" >&2 + exit 1 + fi + + missing_m428_m430_config_hal_sources=() + for source in "${m428_m430_config_hal_sources[@]}"; do + if ! printf '%s\n' "${manifest_config_hal_sources[@]}" | grep -Fx -- "$source" >/dev/null; then + missing_m428_m430_config_hal_sources+=("$source") + fi + done + + if ((${#missing_m428_m430_config_hal_sources[@]} > 0)); then + echo "manifest does not cover all LinuxCNC HALFILE/POSTGUI_HALFILE sources referenced by M428/M429/M430 INI files: $manifest" >&2 + printf 'missing M428/M429/M430 HAL source: %s\n' "${missing_m428_m430_config_hal_sources[@]}" >&2 + exit 1 + fi + + mapfile -t manifest_asset_sources < <( + awk -F: ' + $1 !~ /^($|#)/ && $1 == "asset" { print $2 } + ' "$manifest" | sort -u + ) + mapfile -t manifest_tooldata_sources < <( + awk -F: ' + $1 !~ /^($|#)/ && $1 == "tooldata" { print $2 } + ' "$manifest" | sort -u + ) + + resolve_vismach_loadusr_source() { + local command_name=$1 + case "$command_name" in + */*|*\\*|*.py) + return 0 + ;; + esac + local vismach_source="src/hal/user_comps/vismach/$command_name.py" + if [[ -f "$linuxcnc_root/$vismach_source" ]]; then + printf '%s\n' "$vismach_source" + fi + } + + m428_m430_vismach_loadusr_sources=() + for source in "${manifest_m428_m430_ini_sources[@]}"; do + while IFS= read -r command_name; do + [[ -n "$command_name" ]] || continue + while IFS= read -r vismach_source; do + [[ -n "$vismach_source" ]] && m428_m430_vismach_loadusr_sources+=("$vismach_source") + done < <(resolve_vismach_loadusr_source "$command_name") + done < <( + awk ' + BEGIN { IGNORECASE = 1 } + /^[[:space:]]*HALCMD[[:space:]]*=/ && /loadusr[[:space:]]/ { + for (i = 1; i <= NF; i++) { + if ($i == "-W" && (i + 1) <= NF) { + value = $(i + 1) + sub(/[[:space:]#;].*/, "", value) + if (value != "") print value + } + } + } + ' "$linuxcnc_root/$source" + ) + done + for source in "${m428_m430_config_hal_sources[@]}"; do + while IFS= read -r command_name; do + [[ -n "$command_name" ]] || continue + while IFS= read -r vismach_source; do + [[ -n "$vismach_source" ]] && m428_m430_vismach_loadusr_sources+=("$vismach_source") + done < <(resolve_vismach_loadusr_source "$command_name") + done < <( + awk ' + /^[[:space:]]*loadusr[[:space:]]/ { + for (i = 1; i <= NF; i++) { + if ($i == "-W" && (i + 1) <= NF) { + value = $(i + 1) + sub(/[[:space:]#;].*/, "", value) + if (value != "") print value + } + } + } + ' "$linuxcnc_root/$source" + ) + done + mapfile -t m428_m430_vismach_loadusr_sources < <( + printf '%s\n' "${m428_m430_vismach_loadusr_sources[@]}" | sed '/^$/d' | sort -u + ) + + missing_m428_m430_vismach_loadusr_sources=() + for source in "${m428_m430_vismach_loadusr_sources[@]}"; do + if ! printf '%s\n' "${manifest_asset_sources[@]}" | grep -Fx -- "$source" >/dev/null; then + missing_m428_m430_vismach_loadusr_sources+=("$source") + fi + done + if ((${#missing_m428_m430_vismach_loadusr_sources[@]} > 0)); then + echo "manifest does not cover all LinuxCNC vismach user component sources referenced by M428/M429/M430 INI/HAL loadusr commands as asset sources: $manifest" >&2 + printf 'missing M428/M429/M430 vismach loadusr source: %s\n' "${missing_m428_m430_vismach_loadusr_sources[@]}" >&2 + exit 1 + fi + + if ((${#m428_m430_vismach_loadusr_sources[@]} > 0)); then + vismach_submakefile_source=src/hal/user_comps/vismach/Submakefile + if [[ -f "$linuxcnc_root/$vismach_submakefile_source" ]] && + ! printf '%s\n' "${manifest_metadata_sources[@]}" | grep -Fx -- "$vismach_submakefile_source" >/dev/null; then + echo "manifest does not cover LinuxCNC vismach user component build metadata as metadata sources: $manifest" >&2 + printf 'missing vismach user component metadata source: %s\n' "$vismach_submakefile_source" >&2 + exit 1 + fi + fi + + m428_m430_vismach_man_sources=() + for source in "${m428_m430_vismach_loadusr_sources[@]}"; do + command_name=$(basename "$source" .py) + man_source="docs/src/man/man1/$command_name.1.adoc" + if [[ -f "$linuxcnc_root/$man_source" ]]; then + m428_m430_vismach_man_sources+=("$man_source") + fi + done + mapfile -t m428_m430_vismach_man_sources < <( + printf '%s\n' "${m428_m430_vismach_man_sources[@]}" | sed '/^$/d' | sort -u + ) + missing_m428_m430_vismach_man_sources=() + for source in "${m428_m430_vismach_man_sources[@]}"; do + if ! printf '%s\n' "${manifest_metadata_sources[@]}" | grep -Fx -- "$source" >/dev/null; then + missing_m428_m430_vismach_man_sources+=("$source") + fi + done + if ((${#missing_m428_m430_vismach_man_sources[@]} > 0)); then + echo "manifest does not cover LinuxCNC vismach user component manpage sources as metadata sources: $manifest" >&2 + printf 'missing vismach user component manpage source: %s\n' "${missing_m428_m430_vismach_man_sources[@]}" >&2 + exit 1 + fi + + qtvcp_panel_sources=() + for source in "${manifest_m428_m430_ini_sources[@]}"; do + if grep -Eiq '^[[:space:]]*EMBED_TAB_COMMAND[[:space:]]*=[[:space:]]*qtvcp[[:space:]]+vismach_scara([[:space:]#;]|$)' "$linuxcnc_root/$source"; then + qtvcp_panel_sources+=( + share/qtvcp/panels/vismach_scara/vismach_scara.ui + share/qtvcp/panels/vismach_scara/vismach_scara_handler.py + lib/python/qtvcp/lib/qt_vismach/README.txt + lib/python/qtvcp/lib/qt_vismach/__init__.py + lib/python/qtvcp/lib/qt_vismach/primitives.py + lib/python/qtvcp/lib/qt_vismach/qt_vismach.py + lib/python/qtvcp/lib/qt_vismach/scara.py + ) + fi + done + mapfile -t qtvcp_panel_sources < <( + printf '%s\n' "${qtvcp_panel_sources[@]}" | sed '/^$/d' | sort -u + ) + missing_qtvcp_panel_sources=() + for source in "${qtvcp_panel_sources[@]}"; do + if [[ ! -f "$linuxcnc_root/$source" ]]; then + missing_qtvcp_panel_sources+=("$source") + elif ! printf '%s\n' "${manifest_asset_sources[@]}" | grep -Fx -- "$source" >/dev/null; then + missing_qtvcp_panel_sources+=("$source") + fi + done + if ((${#missing_qtvcp_panel_sources[@]} > 0)); then + echo "manifest does not cover all LinuxCNC QtVCP vismach_scara sources referenced by M428/M429/M430 INI EMBED_TAB_COMMAND entries as asset sources: $manifest" >&2 + printf 'missing M428/M429/M430 QtVCP panel source: %s\n' "${missing_qtvcp_panel_sources[@]}" >&2 + exit 1 + fi + + m428_m430_config_asset_sources=() + unresolved_m428_m430_config_asset_sources=() + for source in "${manifest_m428_m430_ini_sources[@]}"; do + config_dir=$(cd "$(dirname "$linuxcnc_root/$source")" && pwd) + while IFS= read -r config_asset; do + key=${config_asset%%=*} + asset_source=${config_asset#*=} + [[ -n "$asset_source" ]] || continue + if [[ "$asset_source" = /* ]]; then + candidate=$asset_source + else + candidate="$config_dir/$asset_source" + fi + if [[ -f "$candidate" ]]; then + candidate_abs=$(cd "$(dirname "$candidate")" && pwd)/$(basename "$candidate") + case "$candidate_abs" in + "$linuxcnc_root"/*) + m428_m430_config_asset_sources+=("${candidate_abs#"$linuxcnc_root/"}") + ;; + *) + unresolved_m428_m430_config_asset_sources+=("$source $key=$asset_source") + ;; + esac + else + unresolved_m428_m430_config_asset_sources+=("$source $key=$asset_source") + fi + done < <( + awk -F= ' + BEGIN { IGNORECASE = 1 } + /^[[:space:]]*(PYVCP|OPEN_FILE)[[:space:]]*=/ { + key = $1 + value = $0 + sub(/^[^=]*=/, "", value) + sub(/[[:space:]]*[#;].*$/, "", value) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", key) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) + gsub(/^"|"$/, "", value) + if (value != "") { + print toupper(key) "=" value + } + } + ' "$linuxcnc_root/$source" + ) + done + mapfile -t m428_m430_config_asset_sources < <( + printf '%s\n' "${m428_m430_config_asset_sources[@]}" | sed '/^$/d' | sort -u + ) + mapfile -t unresolved_m428_m430_config_asset_sources < <( + printf '%s\n' "${unresolved_m428_m430_config_asset_sources[@]}" | sed '/^$/d' | sort -u + ) + + if ((${#unresolved_m428_m430_config_asset_sources[@]} > 0)); then + echo "LinuxCNC M428/M429/M430 INI PYVCP/OPEN_FILE assets could not be resolved: $manifest" >&2 + printf 'unresolved M428/M429/M430 config asset: %s\n' "${unresolved_m428_m430_config_asset_sources[@]}" >&2 + exit 1 + fi + + missing_m428_m430_config_asset_sources=() + for source in "${m428_m430_config_asset_sources[@]}"; do + if ! printf '%s\n' "${manifest_asset_sources[@]}" | grep -Fx -- "$source" >/dev/null; then + missing_m428_m430_config_asset_sources+=("$source") + fi + done + + if ((${#missing_m428_m430_config_asset_sources[@]} > 0)); then + echo "manifest does not cover all LinuxCNC PYVCP/OPEN_FILE assets referenced by M428/M429/M430 INI files as asset sources: $manifest" >&2 + printf 'missing M428/M429/M430 config asset source: %s\n' "${missing_m428_m430_config_asset_sources[@]}" >&2 + exit 1 + fi + + m428_m430_config_readme_sources=() + for source in "${manifest_m428_m430_ini_sources[@]}"; do + config_dir=$(cd "$(dirname "$linuxcnc_root/$source")" && pwd) + readme_source="$config_dir/README" + if [[ -f "$readme_source" ]]; then + readme_abs=$(cd "$(dirname "$readme_source")" && pwd)/$(basename "$readme_source") + m428_m430_config_readme_sources+=("${readme_abs#"$linuxcnc_root/"}") + fi + done + mapfile -t m428_m430_config_readme_sources < <( + printf '%s\n' "${m428_m430_config_readme_sources[@]}" | sed '/^$/d' | sort -u + ) + + missing_m428_m430_config_readme_sources=() + for source in "${m428_m430_config_readme_sources[@]}"; do + if ! printf '%s\n' "${manifest_asset_sources[@]}" | grep -Fx -- "$source" >/dev/null; then + missing_m428_m430_config_readme_sources+=("$source") + fi + done + + if ((${#missing_m428_m430_config_readme_sources[@]} > 0)); then + echo "manifest does not cover LinuxCNC README sources adjacent to M428/M429/M430 INI files as asset sources: $manifest" >&2 + printf 'missing M428/M429/M430 README source: %s\n' "${missing_m428_m430_config_readme_sources[@]}" >&2 + exit 1 + fi + + m428_m430_config_tooldata_sources=() + unresolved_m428_m430_config_tooldata_sources=() + for source in "${manifest_m428_m430_ini_sources[@]}"; do + config_dir=$(cd "$(dirname "$linuxcnc_root/$source")" && pwd) + while IFS= read -r tool_table; do + [[ -n "$tool_table" ]] || continue + if [[ "$tool_table" = /* ]]; then + candidate=$tool_table + else + candidate="$config_dir/$tool_table" + fi + if [[ -f "$candidate" ]]; then + candidate_abs=$(cd "$(dirname "$candidate")" && pwd)/$(basename "$candidate") + case "$candidate_abs" in + "$linuxcnc_root"/*) + m428_m430_config_tooldata_sources+=("${candidate_abs#"$linuxcnc_root/"}") + ;; + *) + unresolved_m428_m430_config_tooldata_sources+=("$source TOOL_TABLE=$tool_table") + ;; + esac + else + unresolved_m428_m430_config_tooldata_sources+=("$source TOOL_TABLE=$tool_table") + fi + done < <( + awk -F= ' + BEGIN { IGNORECASE = 1 } + /^[[:space:]]*TOOL_TABLE[[:space:]]*=/ { + value = $0 + sub(/^[^=]*=/, "", value) + sub(/[[:space:]]*[#;].*$/, "", value) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) + gsub(/^"|"$/, "", value) + if (value != "") print value + } + ' "$linuxcnc_root/$source" + ) + done + mapfile -t m428_m430_config_tooldata_sources < <( + printf '%s\n' "${m428_m430_config_tooldata_sources[@]}" | sed '/^$/d' | sort -u + ) + mapfile -t unresolved_m428_m430_config_tooldata_sources < <( + printf '%s\n' "${unresolved_m428_m430_config_tooldata_sources[@]}" | sed '/^$/d' | sort -u + ) + + missing_m428_m430_config_tooldata_sources=() + for source in "${m428_m430_config_tooldata_sources[@]}"; do + if ! printf '%s\n' "${manifest_tooldata_sources[@]}" | grep -Fx -- "$source" >/dev/null; then + missing_m428_m430_config_tooldata_sources+=("$source") + fi + done + + if ((${#missing_m428_m430_config_tooldata_sources[@]} > 0)); then + echo "manifest does not cover all resolvable LinuxCNC TOOL_TABLE files referenced by M428/M429/M430 INI files as tooldata sources: $manifest" >&2 + printf 'missing M428/M429/M430 tool table source: %s\n' "${missing_m428_m430_config_tooldata_sources[@]}" >&2 + exit 1 + fi + + mapfile -t manifest_twp_ini_sources < <( + awk -F: ' + $1 !~ /^($|#)/ && $1 == "config" && $2 ~ /table-rotary_spindle-rotary-nutating\/.*_twp\/.*\.ini$/ { print $2 } + ' "$manifest" | sort -u + ) + twp_config_asset_sources=() + unresolved_twp_config_asset_sources=() + + resolve_twp_config_asset() { + local config_dir=$1 + local source=$2 + local key=$3 + local asset_source=$4 + local candidate + if [[ "$asset_source" = /* ]]; then + candidate=$asset_source + else + candidate="$config_dir/$asset_source" + fi + if [[ -f "$candidate" ]]; then + local candidate_abs + candidate_abs=$(cd "$(dirname "$candidate")" && pwd)/$(basename "$candidate") + case "$candidate_abs" in + "$linuxcnc_root"/*) + twp_config_asset_sources+=("${candidate_abs#"$linuxcnc_root/"}") + ;; + *) + unresolved_twp_config_asset_sources+=("$source $key=$asset_source") + ;; + esac + else + unresolved_twp_config_asset_sources+=("$source $key=$asset_source") + fi + } + + resolve_twp_subroutine_asset() { + local config_dir=$1 + local source=$2 + local key=$3 + local ngc_name=$4 + shift 4 + local subroutine_paths=("$@") + [[ "$ngc_name" == *.ngc ]] || ngc_name="$ngc_name.ngc" + local resolved_source= + local subroutine_path path_part candidate_dir candidate resolved_abs + for subroutine_path in "${subroutine_paths[@]}"; do + [[ -n "$subroutine_path" ]] || continue + IFS=: read -ra path_parts <<<"$subroutine_path" + for path_part in "${path_parts[@]}"; do + [[ -n "$path_part" ]] || continue + if [[ "$path_part" = /* ]]; then + candidate_dir=$path_part + else + candidate_dir="$config_dir/$path_part" + fi + candidate="$candidate_dir/$ngc_name" + if [[ -f "$candidate" ]]; then + resolved_abs=$(cd "$(dirname "$candidate")" && pwd)/$(basename "$candidate") + resolved_source=${resolved_abs#"$linuxcnc_root/"} + break 2 + fi + done + done + if [[ -n "$resolved_source" ]]; then + twp_config_asset_sources+=("$resolved_source") + else + unresolved_twp_config_asset_sources+=("$source $key=$ngc_name") + fi + } + + for source in "${manifest_twp_ini_sources[@]}"; do + config="$linuxcnc_root/$source" + config_dir=$(cd "$(dirname "$config")" && pwd) + mapfile -t subroutine_paths < <( + sed -n 's/^[[:space:]]*SUBROUTINE_PATH[[:space:]]*=[[:space:]]*//Ip' "$config" | + sed 's/[[:space:]]*[#;].*$//; s/^[[:space:]]*//; s/[[:space:]]*$//' + ) + if ((${#subroutine_paths[@]} == 0)); then + subroutine_paths=(".") + fi + + while IFS='|' read -r remap_code ngc_name; do + [[ -n "$remap_code" && -n "$ngc_name" ]] || continue + case "$remap_code" in + M428|m428|M429|m429|M430|m430) + continue + ;; + esac + resolve_twp_subroutine_asset "$config_dir" "$source" "REMAP $remap_code ngc" "$ngc_name" "${subroutine_paths[@]}" + done < <( + awk ' + BEGIN { IGNORECASE = 1 } + /^[[:space:]]*REMAP[[:space:]]*=/ && /[[:space:]]ngc[[:space:]]*=/ { + code = $0 + ngc = $0 + sub(/.*REMAP[[:space:]]*=[[:space:]]*/, "", code) + sub(/[[:space:]].*/, "", code) + sub(/.*[[:space:]]ngc[[:space:]]*=[[:space:]]*/, "", ngc) + sub(/[[:space:]#;].*/, "", ngc) + print code "|" ngc + } + ' "$config" + ) + + while IFS= read -r abort_name; do + [[ -n "$abort_name" ]] || continue + resolve_twp_subroutine_asset "$config_dir" "$source" "ON_ABORT_COMMAND" "$abort_name" "${subroutine_paths[@]}" + done < <( + sed -n 's/^[[:space:]]*ON_ABORT_COMMAND[[:space:]]*=[[:space:]]*o[[:space:]]*<\([^>]*\)>.*/\1/Ip' "$config" + ) + + while IFS= read -r twp_asset; do + [[ -n "$twp_asset" ]] || continue + resolve_twp_config_asset "$config_dir" "$source" "TWP asset" "$twp_asset" + done < <( + awk -F= ' + BEGIN { IGNORECASE = 1 } + /^[[:space:]]*TOPLEVEL[[:space:]]*=/ { + value = $0 + sub(/^[^=]*=/, "", value) + sub(/[[:space:]]*[#;].*$/, "", value) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) + if (value != "") print value + } + /^[[:space:]]*EMBED_TAB_COMMAND[[:space:]]*=/ && /pyvcp[[:space:]]/ { + for (i = 1; i <= NF; i++) { + if ($i == "pyvcp" && (i + 1) <= NF) { + value = $(i + 1) + sub(/[[:space:]#;].*$/, "", value) + if (value != "") print value + } + } + } + /^[[:space:]]*TOOL_TABLE[[:space:]]*=/ { + next + } + /^[[:space:]]*HALCMD[[:space:]]*=/ && /loadusr/ { + for (i = 1; i <= NF; i++) { + if ($i ~ /^\.\.\/.*\.py$/) print $i + } + } + ' "$config" + ) + + mapfile -t twp_loaded_python_sources < <( + awk ' + BEGIN { IGNORECASE = 1 } + /^[[:space:]]*TOPLEVEL[[:space:]]*=/ { + value = $0 + sub(/^[^=]*=/, "", value) + sub(/[[:space:]]*[#;].*$/, "", value) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) + if (value != "") print value + } + /^[[:space:]]*HALCMD[[:space:]]*=/ && /loadusr/ { + for (i = 1; i <= NF; i++) { + if ($i ~ /^\.\.\/.*\.py$/) print $i + } + } + ' "$config" + ) + for twp_python_source in "${twp_loaded_python_sources[@]}"; do + python_candidate="$config_dir/$twp_python_source" + if [[ ! -f "$python_candidate" ]]; then + continue + fi + python_abs=$(cd "$(dirname "$python_candidate")" && pwd)/$(basename "$python_candidate") + python_dir=$(dirname "$python_abs") + if grep -Eq '^[[:space:]]*import[[:space:]]+remap([[:space:]]|$)' "$python_abs"; then + resolve_twp_config_asset "$python_dir" "$source" "python import" "remap.py" + fi + if grep -Eq '^[[:space:]]*from[[:space:]]+util[[:space:]]+import([[:space:]]|$)' "$python_abs"; then + resolve_twp_config_asset "$python_dir" "$source" "python import" "util.py" + fi + if grep -Eq '^[[:space:]]*from[[:space:]]+twp_vismach[[:space:]]+import([[:space:]]|$)' "$python_abs"; then + resolve_twp_config_asset "$python_dir" "$source" "python import" "twp_vismach.py" + fi + if grep -F 'work_piece_1.stl' "$python_abs" >/dev/null; then + resolve_twp_config_asset "$config_dir" "$source" "vismach STL" "work_piece_1.stl" + fi + done + done + + mapfile -t twp_config_asset_sources < <( + printf '%s\n' "${twp_config_asset_sources[@]}" | sed '/^$/d' | sort -u + ) + mapfile -t unresolved_twp_config_asset_sources < <( + printf '%s\n' "${unresolved_twp_config_asset_sources[@]}" | sed '/^$/d' | sort -u + ) + + if ((${#unresolved_twp_config_asset_sources[@]} > 0)); then + echo "LinuxCNC TWP INI source assets could not be resolved: $manifest" >&2 + printf 'unresolved TWP config asset: %s\n' "${unresolved_twp_config_asset_sources[@]}" >&2 + exit 1 + fi + + missing_twp_config_asset_sources=() + for source in "${twp_config_asset_sources[@]}"; do + if ! printf '%s\n' "${manifest_asset_sources[@]}" | grep -Fx -- "$source" >/dev/null; then + missing_twp_config_asset_sources+=("$source") + fi + done + + if ((${#missing_twp_config_asset_sources[@]} > 0)); then + echo "manifest does not cover all LinuxCNC TWP INI source assets as asset sources: $manifest" >&2 + printf 'missing TWP config asset source: %s\n' "${missing_twp_config_asset_sources[@]}" >&2 + exit 1 + fi fi diff --git a/check-linuxcnc-switchkins-remap-table.sh b/check-linuxcnc-switchkins-remap-table.sh index a08bc3d..a7b1e09 100755 --- a/check-linuxcnc-switchkins-remap-table.sh +++ b/check-linuxcnc-switchkins-remap-table.sh @@ -6,16 +6,1074 @@ cd "$(dirname "$0")" linuxcnc_root=${LINUXCNC_ROOT:-../linuxcnc} manifest=${1:-linuxcnc-kinematics-source-files.txt} -generated_table=$(mktemp "${TMPDIR:-/tmp}/linuxcnc_switchkins_remap_table.XXXXXX.inc") -generated_cases=$(mktemp "${TMPDIR:-/tmp}/linuxcnc_switchkins_remap_config_cases.XXXXXX.inc") -generated_json_cases=$(mktemp "${TMPDIR:-/tmp}/linuxcnc_switchkins_remap_config_cases.XXXXXX.json") -trap 'rm -f "$generated_table" "$generated_cases" "$generated_json_cases"' EXIT +usage() { + echo "usage: $0 [manifest]" >&2 +} -LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh "$manifest" > "$generated_table" -diff -u core/src/linuxcnc_switchkins_remap_table.inc "$generated_table" +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" ./generate-linuxcnc-switchkins-remap-table.sh --config-cases "$manifest" > "$generated_cases" -diff -u core/tests/linuxcnc_switchkins_remap_config_cases.inc "$generated_cases" +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="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh --json-cases "$manifest" > "$generated_json_cases" -diff -u web/public/linuxcnc_switchkins_remap_config_cases.json "$generated_json_cases" +linuxcnc_root, manifest_file = sys.argv[1:] +remap_re = re.compile(r'^\s*#\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 # assignment in {path}, found {len(kinstypes)}") + if kinstypes[0] < 0: + raise SystemExit(f"invalid negative # 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*#\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 # assignment in ${sourcePath}, found ${kinstypes.length}`); + } + if (kinstypes[0] < 0) { + throw new Error(`invalid negative # 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 + local cli_dir + cli_dir=$(mktemp -d "${TMPDIR:-/tmp}/linuxcnc_switchkins_remap_cli.XXXXXX") + + 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 + LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh --all-output-dir "$cli_dir/out" "$manifest_file" + for generated_file in \ + linuxcnc_switchkins_remap_table.inc \ + linuxcnc_switchkins_remap_config_cases.inc \ + linuxcnc_switchkins_remap_config_cases.json + do + if [[ ! -s "$cli_dir/out/$generated_file" ]]; then + echo "switchkins remap generator --all-output-dir did not write $generated_file" >&2 + exit 1 + fi + done + rm -rf "$cli_dir" +} +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" +LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh "$manifest" \ + >"$generated_dir/linuxcnc_switchkins_remap_table.stdout.inc" +LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh --config-cases "$manifest" \ + >"$generated_dir/linuxcnc_switchkins_remap_config_cases.stdout.inc" +LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh --json-cases "$manifest" \ + >"$generated_dir/linuxcnc_switchkins_remap_config_cases.stdout.json" + +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 +} + +count_cpp_config_cases() { + local file=$1 + grep -Ec '^[[:space:]]*\{"[^"]+", "[^"]+", -?[0-9]+, -?[0-9]+, -?[0-9]+\},?$' "$file" +} + +count_table_rows() { + local file=$1 + grep -Ec '^[[:space:]]*\{"[^"]+", -?[0-9]+, -?[0-9]+, -?[0-9]+\},?$' "$file" +} + +count_json_cases() { + local file=$1 + if command -v python3 >/dev/null 2>&1; then + python3 - "$file" <<'PY' +import json +import sys + +with open(sys.argv[1], encoding="utf-8") as handle: + print(len(json.load(handle))) +PY + elif command -v node >/dev/null 2>&1; then + node -e 'console.log(JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")).length)' "$file" + else + echo "missing python3 or node for switchkins remap JSON counting" >&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_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) +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}") +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 = []; +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)}`); + } +}); +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 +} + +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" + +for generated_file in \ + linuxcnc_switchkins_remap_table.inc \ + linuxcnc_switchkins_remap_table.stdout.inc \ + linuxcnc_switchkins_remap_config_cases.inc \ + linuxcnc_switchkins_remap_config_cases.stdout.inc \ + linuxcnc_switchkins_remap_config_cases.json \ + linuxcnc_switchkins_remap_config_cases.stdout.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 + +diff -u "$generated_dir/linuxcnc_switchkins_remap_table.inc" \ + "$generated_dir/linuxcnc_switchkins_remap_table.stdout.inc" +diff -u "$generated_dir/linuxcnc_switchkins_remap_config_cases.inc" \ + "$generated_dir/linuxcnc_switchkins_remap_config_cases.stdout.inc" +diff -u "$generated_dir/linuxcnc_switchkins_remap_config_cases.json" \ + "$generated_dir/linuxcnc_switchkins_remap_config_cases.stdout.json" + +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_json "$generated_dir/linuxcnc_switchkins_remap_config_cases.json" "generated linuxcnc_switchkins_remap_config_cases.json" +validate_json "$generated_dir/linuxcnc_switchkins_remap_config_cases.stdout.json" "generated stdout 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" + +generated_table_rows=$(count_table_rows "$generated_dir/linuxcnc_switchkins_remap_table.inc") +if ((generated_table_rows == 0)); then + echo "empty generated switchkins remap table rows" >&2 + exit 1 +fi + +generated_cpp_cases=$(count_cpp_config_cases "$generated_dir/linuxcnc_switchkins_remap_config_cases.inc") +generated_json_cases=$(count_json_cases "$generated_dir/linuxcnc_switchkins_remap_config_cases.json") +tracked_cpp_cases=$(count_cpp_config_cases core/tests/linuxcnc_switchkins_remap_config_cases.inc) +tracked_json_cases=$(count_json_cases web/public/linuxcnc_switchkins_remap_config_cases.json) +if [[ "$generated_cpp_cases" != "$generated_json_cases" ]]; then + echo "generated switchkins remap C++/JSON case count mismatch: $generated_cpp_cases != $generated_json_cases" >&2 + exit 1 +fi +if [[ "$tracked_cpp_cases" != "$tracked_json_cases" ]]; then + echo "tracked switchkins remap C++/JSON case count mismatch: $tracked_cpp_cases != $tracked_json_cases" >&2 + exit 1 +fi +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_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" + +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" diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 442b512..5589b2b 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -544,7 +544,8 @@ if(EMSCRIPTEN) "-sEXPORT_NAME=createCncSimModule" "-sALLOW_MEMORY_GROWTH=1" "-sALLOW_TABLE_GROWTH=1" + "-sFORCE_FILESYSTEM=1" "-sEXPORTED_FUNCTIONS=['_malloc','_free','_cnc_sim_create','_cnc_sim_destroy','_cnc_sim_reset','_cnc_sim_set_dialect','_cnc_sim_set_event_callback','_cnc_sim_load_config_json','_cnc_sim_parse_program','_cnc_sim_last_error']" - "-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8','addFunction','removeFunction']" + "-sEXPORTED_RUNTIME_METHODS=['FS','ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8','addFunction','removeFunction']" ) endif() diff --git a/core/src/cnc_sim_api.cpp b/core/src/cnc_sim_api.cpp index 40fe7a7..033bd09 100644 --- a/core/src/cnc_sim_api.cpp +++ b/core/src/cnc_sim_api.cpp @@ -103,9 +103,17 @@ bool contains_switchkins_config_alias(const std::string &compact, const char *al contains_string_value(compact, "kinematics", alias) || contains_string_value(compact, "halfile", alias) || contains_string_value(compact, "hal_file", alias) || + contains_string_value(compact, "postguihalfile", alias) || + contains_string_value(compact, "postgui_halfile", alias) || + contains_string_value(compact, "postgui_hal_file", alias) || contains_string_value(compact, "config", alias) || contains_string_value(compact, "configpath", alias) || - contains_string_value(compact, "config_path", alias); + contains_string_value(compact, "config_path", alias) || + contains_string_value(compact, "ini", alias) || + contains_string_value(compact, "inifile", alias) || + contains_string_value(compact, "inifilename", alias) || + contains_string_value(compact, "ini_file", alias) || + contains_string_value(compact, "ini_file_name", alias); } bool find_number_value(const std::string &compact, const char *key, double *value) { diff --git a/core/src/linuxcnc_canon_bridge.cpp b/core/src/linuxcnc_canon_bridge.cpp index cf720b8..81f6291 100644 --- a/core/src/linuxcnc_canon_bridge.cpp +++ b/core/src/linuxcnc_canon_bridge.cpp @@ -28,6 +28,19 @@ double motion_control_tolerance = 0.0; double naivecam_tolerance = 0.0; std::string parameter_file_name = "rs274ngc.var"; +void load_nonrandom_tool_to_spindle(int pocket) { + // LinuxCNC Task::load_tool() uses pocket 0 as the non-random + // toolchanger unload handshake. Do not eagerly mirror nonzero + // loads here; the source-linked interpreter observes its own + // post-canonical state before external task-side load completion. + if (pocket == 0) { + CANON_TOOL_TABLE unloaded = tooldata_entry_init(); + unloaded.toolno = 0; + unloaded.pocketno = 0; + tooldata_put(unloaded, 0); + } +} + CncSimPose make_pose(double x, double y, double z, double a, double b, double c, double u, double v, double w) { @@ -215,6 +228,10 @@ void SELECT_TOOL(int tool) { void CHANGE_TOOL() { trace_call("CHANGE_TOOL"); if (active_sink) { + const int selected_tool = active_sink->selected_tool(); + if (selected_tool == 0) { + load_nonrandom_tool_to_spindle(0); + } active_sink->change_tool(event_line()); } } @@ -524,6 +541,7 @@ void USE_TOOL_LENGTH_OFFSET(const EmcPose &offset) { void CHANGE_TOOL_NUMBER(int number) { if (active_sink) { + load_nonrandom_tool_to_spindle(number); active_sink->select_tool(number); active_sink->change_tool(event_line()); } diff --git a/core/src/linuxcnc_switchkins_remap_table.inc b/core/src/linuxcnc_switchkins_remap_table.inc index 38090c7..6956967 100644 --- a/core/src/linuxcnc_switchkins_remap_table.inc +++ b/core/src/linuxcnc_switchkins_remap_table.inc @@ -1,5 +1,5 @@ // Generated by ./generate-linuxcnc-switchkins-remap-table.sh. -// Source: LinuxCNC INI MACHINE/KINEMATICS/HALFILE/SUBROUTINE_PATH/REMAP entries and +// Source: LinuxCNC INI MACHINE/KINEMATICS/HALFILE/POSTGUI_HALFILE/SUBROUTINE_PATH/REMAP entries and // adjacent remap_subs/{428,429,430}remap.ngc # assignments. {"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini", 0, 1, 2}, {"configs/sim/axis/vismach/5axis/bridgemill", 0, 1, 2}, @@ -11,6 +11,8 @@ {"5axiskins", 0, 1, 2}, {"5axisgui.hal", 0, 1, 2}, {"5axisgui", 0, 1, 2}, + {"5axis_postgui.hal", 0, 1, 2}, + {"5axis_postgui", 0, 1, 2}, {"configs/sim/axis/vismach/5axis/bridgemill/remap_subs", 0, 1, 2}, {"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini", 1, 0, -1}, {"configs/sim/axis/vismach/5axis/table-dual-rotary", 1, 0, -1}, @@ -21,6 +23,8 @@ {"xyzab_tdr_kins", 1, 0, -1}, {"xyzab_tdr", 1, 0, -1}, {"xyzab-tdr-kins", 1, 0, -1}, + {"xyzab-tdr-postgui.hal", 1, 0, -1}, + {"xyzab-tdr-postgui", 1, 0, -1}, {"configs/sim/axis/vismach/5axis/table-dual-rotary/remap_subs", 1, 0, -1}, {"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", 1, 0, 2}, {"configs/sim/axis/vismach/5axis/table-rotary-tilting", 1, 0, 2}, @@ -30,6 +34,8 @@ {"sim-xyzac-trt-kins", 1, 0, 2}, {"xyzac-trt-kinssparm=identityfirst", 1, 0, 2}, {"xyzac-trt-kins", 1, 0, 2}, + {"switchkins_postgui.hal", 1, 0, 2}, + {"switchkins_postgui", 1, 0, 2}, {"configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs", 1, 0, 2}, {"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini", 1, 0, 2}, {"xyzbc-trt", 1, 0, 2}, @@ -43,6 +49,8 @@ {"xyzacb-trsrn", 0, 1, 2}, {"xyzacb-trsrn(switchkins)", 0, 1, 2}, {"xyzacb_trsrn", 0, 1, 2}, + {"xyzacb-trsrn_postgui.hal", 0, 1, 2}, + {"xyzacb-trsrn_postgui", 0, 1, 2}, {"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/remap_subs", 0, 1, 2}, {"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating", 0, 1, 2}, {"table-rotary_spindle-rotary-nutating", 0, 1, 2}, @@ -52,6 +60,8 @@ {"xyzbca-trsrn", 0, 1, 2}, {"xyzbca-trsrn(switchkins)", 0, 1, 2}, {"xyzbca_trsrn", 0, 1, 2}, + {"xyzbca-trsrn_postgui.hal", 0, 1, 2}, + {"xyzbca-trsrn_postgui", 0, 1, 2}, {"configs/sim/axis/vismach/hexapod-sim/hexapod.ini", 0, 1, 2}, {"configs/sim/axis/vismach/hexapod-sim", 0, 1, 2}, {"hexapod-sim", 0, 1, 2}, @@ -60,6 +70,8 @@ {"genhexkins", 0, 1, 2}, {"kinematics.hal", 0, 1, 2}, {"kinematics", 0, 1, 2}, + {"hexapod_postgui.hal", 0, 1, 2}, + {"hexapod_postgui", 0, 1, 2}, {"configs/sim/axis/vismach/hexapod-sim/remap_subs", 0, 1, 2}, {"configs/sim/axis/vismach/melfa-sim/melfa.ini", 0, 1, 2}, {"configs/sim/axis/vismach/melfa-sim", 0, 1, 2}, @@ -69,12 +81,16 @@ {"genserkins", 0, 1, 2}, {"melfa_dh.hal", 0, 1, 2}, {"melfa_dh", 0, 1, 2}, + {"melfa-postgui.hal", 0, 1, 2}, + {"melfa-postgui", 0, 1, 2}, {"configs/sim/axis/vismach/melfa-sim/remap_subs", 0, 1, 2}, {"configs/sim/axis/vismach/millturn/millturn.ini", 0, 1, -1}, {"configs/sim/axis/vismach/millturn", 0, 1, -1}, {"millturn", 0, 1, -1}, {"millturn(mm)", 0, 1, -1}, {"millturn.hal", 0, 1, -1}, + {"millturn-postgui.hal", 0, 1, -1}, + {"millturn-postgui", 0, 1, -1}, {"configs/sim/axis/vismach/millturn/remap_subs", 0, 1, -1}, {"configs/sim/axis/vismach/puma/puma.ini", 0, 1, 2}, {"configs/sim/axis/vismach/puma", 0, 1, 2}, @@ -83,6 +99,8 @@ {"pumakins", 0, 1, 2}, {"puma_dh.hal", 0, 1, 2}, {"puma_dh", 0, 1, 2}, + {"puma_postgui.hal", 0, 1, 2}, + {"puma_postgui", 0, 1, 2}, {"configs/sim/axis/vismach/puma/remap_subs", 0, 1, 2}, {"configs/sim/axis/vismach/puma/puma_cube.ini", 0, 1, 2}, {"puma_cube", 0, 1, 2}, @@ -93,6 +111,8 @@ {"puma560(switchkins)(inch)", 0, 1, 2}, {"puma560_dh.hal", 0, 1, 2}, {"puma560_dh", 0, 1, 2}, + {"puma560_postgui.hal", 0, 1, 2}, + {"puma560_postgui", 0, 1, 2}, {"configs/sim/axis/vismach/puma/puma560_uvw.ini", 0, 1, 2}, {"puma560_uvw", 0, 1, 2}, {"configs/sim/axis/vismach/scara/scara.ini", 0, 1, 2}, @@ -101,6 +121,8 @@ {"scara(genserkins,switchkins)", 0, 1, 2}, {"scarakinscoordinates=xyzcab", 0, 1, 2}, {"scarakins", 0, 1, 2}, + {"scara_postgui.hal", 0, 1, 2}, + {"scara_postgui", 0, 1, 2}, {"configs/sim/axis/vismach/scara/remap_subs", 0, 1, 2}, {"configs/sim/qtaxis/non-trivial/scara/scara.ini", 0, 1, 2}, {"configs/sim/qtaxis/non-trivial/scara", 0, 1, 2}, diff --git a/core/tests/cnc_sim_api_linuxcnc_rs274_smoke.cpp b/core/tests/cnc_sim_api_linuxcnc_rs274_smoke.cpp index a61dc0b..4d7a801 100644 --- a/core/tests/cnc_sim_api_linuxcnc_rs274_smoke.cpp +++ b/core/tests/cnc_sim_api_linuxcnc_rs274_smoke.cpp @@ -1,5 +1,6 @@ #include "cnc_sim_api.h" +#include #include #include #include @@ -35,6 +36,31 @@ bool near(double lhs, double rhs) { return std::fabs(lhs - rhs) < 0.000001; } +class ScopedEnv { +public: + ScopedEnv(const char *name, const char *value) : name_(name) { + const char *old = std::getenv(name); + if (old) { + had_old_ = true; + old_ = old; + } + setenv(name, value, 1); + } + + ~ScopedEnv() { + if (had_old_) { + setenv(name_, old_.c_str(), 1); + } else { + unsetenv(name_); + } + } + +private: + const char *name_; + bool had_old_ = false; + std::string old_; +}; + bool saw_comment_state(const std::vector &events, int line, int reserved, @@ -66,6 +92,79 @@ bool saw_any_kinematics_switch(const std::vector &events, int line) return false; } +bool saw_kinematics_switch(const std::vector &events, int line, int kinstype) { + for (const auto &event : events) { + if (event.type == CNC_SIM_EVENT_KINEMATICS_SWITCH && + event.line == line && + event.reserved == kinstype) { + return true; + } + } + return false; +} + +bool expect_switchkins_remap_config(const std::string &json, + int m428_type, + int m429_type, + int m430_type, + const char *message) { + std::vector events; + CncSimHandle *sim = cnc_sim_create(); + cnc_sim_set_event_callback(sim, collect_event, &events); + const char supported_program[] = + "M428\n" + "M429\n"; + const char full_program[] = + "M428\n" + "M429\n" + "M430\n"; + const char *program = m430_type >= 0 ? full_program : supported_program; + const size_t program_len = m430_type >= 0 ? sizeof(full_program) - 1 : sizeof(supported_program) - 1; + + bool ok = true; + const std::string load_message = std::string(message) + " load"; + const std::string parse_message = std::string(message) + " parse M428/M429"; + const std::string m428_message = std::string(message) + " M428 kinstype"; + const std::string m429_message = std::string(message) + " M429 kinstype"; + ok &= expect(cnc_sim_load_config_json(sim, json.c_str(), json.size()) == 0, load_message.c_str()); + ok &= expect(cnc_sim_parse_program(sim, program, program_len) == 0, parse_message.c_str()); + ok &= expect(saw_kinematics_switch(events, 1, m428_type), m428_message.c_str()); + ok &= expect(saw_kinematics_switch(events, 2, m429_type), m429_message.c_str()); + if (m430_type >= 0) { + const std::string m430_message = std::string(message) + " M430 kinstype"; + ok &= expect(saw_kinematics_switch(events, 3, m430_type), m430_message.c_str()); + } else { + const std::string m430_reject_message = std::string(message) + " reject M430"; + const std::string m430_error_message = std::string(message) + " M430 error"; + const std::string m430_no_switch_message = std::string(message) + " M430 no switch"; + const std::string mixed_load_message = std::string(message) + " mixed load"; + const std::string mixed_reject_message = std::string(message) + " reject mixed M428/M430"; + const std::string mixed_error_message = std::string(message) + " mixed M430 error"; + const std::string mixed_no_switch_message = std::string(message) + " mixed no partial switch"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, "M430\n", 5) != 0, m430_reject_message.c_str()); + // Source: LinuxCNC interp_read.cc reports NCE_M_CODE_GREATER_THAN_199; + // linuxcnc_rs274_backend.cpp preserves the read-stage context. + ok &= expect(std::string(cnc_sim_last_error(sim)) == "read failed: M-code greater than 199: M430", + m430_error_message.c_str()); + ok &= expect(!saw_kinematics_switch(events, 1, 2), m430_no_switch_message.c_str()); + + std::vector mixed_events; + CncSimHandle *mixed_sim = cnc_sim_create(); + cnc_sim_set_event_callback(mixed_sim, collect_event, &mixed_events); + ok &= expect(cnc_sim_load_config_json(mixed_sim, json.c_str(), json.size()) == 0, + mixed_load_message.c_str()); + ok &= expect(cnc_sim_parse_program(mixed_sim, "M428 M430\n", 9) != 0, + mixed_reject_message.c_str()); + ok &= expect(std::string(cnc_sim_last_error(mixed_sim)) == "M-code greater than 199: M430", + mixed_error_message.c_str()); + ok &= expect(!saw_any_kinematics_switch(mixed_events, 1), mixed_no_switch_message.c_str()); + cnc_sim_destroy(mixed_sim); + } + cnc_sim_destroy(sim); + return ok; +} + } // namespace int main() { @@ -1577,6 +1676,35 @@ int main() { ok &= expect(cnc_sim_load_config_json(sim, config, sizeof(config) - 1) == 0, cnc_sim_last_error(sim)); + { + struct SourceBackedConfigCase { + const char *field; + const char *value; + int m428_type; + int m429_type; + int m430_type; + }; + const SourceBackedConfigCase source_cases[] = { +#include "linuxcnc_switchkins_remap_config_cases.inc" + }; + for (const SourceBackedConfigCase &source_case : source_cases) { + const std::string json = std::string("{\"backend\":\"linuxcnc-rs274\",\"") + + source_case.field + + "\":\"" + + source_case.value + + "\"}"; + const std::string message = std::string("expected LinuxCNC source-backed switchkins remap config for ") + + source_case.field + + "=" + + source_case.value; + ok &= expect_switchkins_remap_config(json, + source_case.m428_type, + source_case.m429_type, + source_case.m430_type, + message.c_str()); + } + } + const char exponent_m428_control_program[] = "G21 G90\n" "M4.28e2\n"; diff --git a/core/tests/linuxcnc_switchkins_remap_config_cases.inc b/core/tests/linuxcnc_switchkins_remap_config_cases.inc index 657e03a..c6297a6 100644 --- a/core/tests/linuxcnc_switchkins_remap_config_cases.inc +++ b/core/tests/linuxcnc_switchkins_remap_config_cases.inc @@ -1,67 +1,384 @@ // Generated by ./generate-linuxcnc-switchkins-remap-table.sh --config-cases. -// Source: LinuxCNC INI config path, MACHINE, KINEMATICS, and non-LIB HALFILE entries -// for M428/M429/M430 remap # assignments. +// Source: LinuxCNC INI config path, MACHINE, KINEMATICS, non-LIB HALFILE/POSTGUI_HALFILE, +// and adjacent remap_subs entries for M428/M429/M430 # assignments. {"config", "configs/sim/axis/vismach/5axis/bridgemill/5axis.ini", 0, 1, 2}, + {"configPath", "configs/sim/axis/vismach/5axis/bridgemill/5axis.ini", 0, 1, 2}, + {"config_path", "configs/sim/axis/vismach/5axis/bridgemill/5axis.ini", 0, 1, 2}, + {"ini", "configs/sim/axis/vismach/5axis/bridgemill/5axis.ini", 0, 1, 2}, + {"iniFile", "configs/sim/axis/vismach/5axis/bridgemill/5axis.ini", 0, 1, 2}, + {"iniFileName", "configs/sim/axis/vismach/5axis/bridgemill/5axis.ini", 0, 1, 2}, + {"ini_file", "configs/sim/axis/vismach/5axis/bridgemill/5axis.ini", 0, 1, 2}, + {"ini_file_name", "configs/sim/axis/vismach/5axis/bridgemill/5axis.ini", 0, 1, 2}, + {"INI_FILE_NAME", "configs/sim/axis/vismach/5axis/bridgemill/5axis.ini", 0, 1, 2}, {"machine", "Sim-5Axis Bridge Mill (xyzbcw)", 0, 1, 2}, {"kinematics", "5axiskins coordinates=xyzbcwy", 0, 1, 2}, + {"switchkins", "bridgemill", 0, 1, 2}, + {"switchkins", "5axis", 0, 1, 2}, + {"switchkins", "Sim-5Axis Bridge Mill (xyzbcw)", 0, 1, 2}, + {"switchkins", "Sim-5Axis Bridge Mill", 0, 1, 2}, + {"switchkins", "5axiskins coordinates=xyzbcwy", 0, 1, 2}, + {"switchkins", "5axiskins", 0, 1, 2}, + {"halFile", "5axisgui.hal", 0, 1, 2}, + {"halFile", "5axisgui", 0, 1, 2}, {"halfile", "5axisgui.hal", 0, 1, 2}, {"halfile", "5axisgui", 0, 1, 2}, {"hal_file", "5axisgui.hal", 0, 1, 2}, {"hal_file", "5axisgui", 0, 1, 2}, + {"HALFILE", "5axisgui.hal", 0, 1, 2}, + {"HALFILE", "5axisgui", 0, 1, 2}, + {"postguiHalFile", "5axis_postgui.hal", 0, 1, 2}, + {"postguiHalFile", "5axis_postgui", 0, 1, 2}, + {"postgui_halfile", "5axis_postgui.hal", 0, 1, 2}, + {"postgui_halfile", "5axis_postgui", 0, 1, 2}, + {"postgui_hal_file", "5axis_postgui.hal", 0, 1, 2}, + {"postgui_hal_file", "5axis_postgui", 0, 1, 2}, + {"POSTGUI_HALFILE", "5axis_postgui.hal", 0, 1, 2}, + {"POSTGUI_HALFILE", "5axis_postgui", 0, 1, 2}, + {"remap", "configs/sim/axis/vismach/5axis/bridgemill", 0, 1, 2}, + {"remap", "bridgemill", 0, 1, 2}, {"config", "configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini", 1, 0, -1}, + {"configPath", "configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini", 1, 0, -1}, + {"config_path", "configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini", 1, 0, -1}, + {"ini", "configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini", 1, 0, -1}, + {"iniFile", "configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini", 1, 0, -1}, + {"iniFileName", "configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini", 1, 0, -1}, + {"ini_file", "configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini", 1, 0, -1}, + {"ini_file_name", "configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini", 1, 0, -1}, + {"INI_FILE_NAME", "configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini", 1, 0, -1}, {"machine", "sim-xyzab-tdr-kins (switchkins)", 1, 0, -1}, {"kinematics", "xyzab_tdr_kins", 1, 0, -1}, + {"switchkins", "table-dual-rotary", 1, 0, -1}, + {"switchkins", "xyzab-tdr", 1, 0, -1}, + {"switchkins", "sim-xyzab-tdr-kins (switchkins)", 1, 0, -1}, + {"switchkins", "sim-xyzab-tdr-kins", 1, 0, -1}, + {"switchkins", "xyzab_tdr_kins", 1, 0, -1}, + {"switchkins", "xyzab_tdr", 1, 0, -1}, + {"switchkins", "xyzab-tdr-kins", 1, 0, -1}, + {"postguiHalFile", "xyzab-tdr-postgui.hal", 1, 0, -1}, + {"postguiHalFile", "xyzab-tdr-postgui", 1, 0, -1}, + {"postgui_halfile", "xyzab-tdr-postgui.hal", 1, 0, -1}, + {"postgui_halfile", "xyzab-tdr-postgui", 1, 0, -1}, + {"postgui_hal_file", "xyzab-tdr-postgui.hal", 1, 0, -1}, + {"postgui_hal_file", "xyzab-tdr-postgui", 1, 0, -1}, + {"POSTGUI_HALFILE", "xyzab-tdr-postgui.hal", 1, 0, -1}, + {"POSTGUI_HALFILE", "xyzab-tdr-postgui", 1, 0, -1}, + {"remap", "configs/sim/axis/vismach/5axis/table-dual-rotary", 1, 0, -1}, + {"remap", "table-dual-rotary", 1, 0, -1}, {"config", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", 1, 0, 2}, + {"configPath", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", 1, 0, 2}, + {"config_path", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", 1, 0, 2}, + {"ini", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", 1, 0, 2}, + {"iniFile", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", 1, 0, 2}, + {"iniFileName", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", 1, 0, 2}, + {"ini_file", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", 1, 0, 2}, + {"ini_file_name", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", 1, 0, 2}, + {"INI_FILE_NAME", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", 1, 0, 2}, {"machine", "sim-xyzac-trt-kins (switchkins)", 1, 0, 2}, {"kinematics", "xyzac-trt-kins sparm=identityfirst", 1, 0, 2}, + {"switchkins", "table-rotary-tilting", 1, 0, 2}, + {"switchkins", "xyzac-trt", 1, 0, 2}, + {"switchkins", "sim-xyzac-trt-kins (switchkins)", 1, 0, 2}, + {"switchkins", "sim-xyzac-trt-kins", 1, 0, 2}, + {"switchkins", "xyzac-trt-kins sparm=identityfirst", 1, 0, 2}, + {"switchkins", "xyzac-trt-kins", 1, 0, 2}, + {"postguiHalFile", "switchkins_postgui.hal", 1, 0, 2}, + {"postguiHalFile", "switchkins_postgui", 1, 0, 2}, + {"postgui_halfile", "switchkins_postgui.hal", 1, 0, 2}, + {"postgui_halfile", "switchkins_postgui", 1, 0, 2}, + {"postgui_hal_file", "switchkins_postgui.hal", 1, 0, 2}, + {"postgui_hal_file", "switchkins_postgui", 1, 0, 2}, + {"POSTGUI_HALFILE", "switchkins_postgui.hal", 1, 0, 2}, + {"POSTGUI_HALFILE", "switchkins_postgui", 1, 0, 2}, + {"remap", "configs/sim/axis/vismach/5axis/table-rotary-tilting", 1, 0, 2}, + {"remap", "table-rotary-tilting", 1, 0, 2}, {"config", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini", 1, 0, 2}, + {"configPath", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini", 1, 0, 2}, + {"config_path", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini", 1, 0, 2}, + {"ini", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini", 1, 0, 2}, + {"iniFile", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini", 1, 0, 2}, + {"iniFileName", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini", 1, 0, 2}, + {"ini_file", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini", 1, 0, 2}, + {"ini_file_name", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini", 1, 0, 2}, + {"INI_FILE_NAME", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini", 1, 0, 2}, {"machine", "sim-xyzbc-trt-kins (switchkins)", 1, 0, 2}, {"kinematics", "xyzbc-trt-kins sparm=identityfirst", 1, 0, 2}, + {"switchkins", "xyzbc-trt", 1, 0, 2}, + {"switchkins", "sim-xyzbc-trt-kins (switchkins)", 1, 0, 2}, + {"switchkins", "sim-xyzbc-trt-kins", 1, 0, 2}, + {"switchkins", "xyzbc-trt-kins sparm=identityfirst", 1, 0, 2}, + {"switchkins", "xyzbc-trt-kins", 1, 0, 2}, {"config", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini", 0, 1, 2}, + {"configPath", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini", 0, 1, 2}, + {"config_path", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini", 0, 1, 2}, + {"ini", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini", 0, 1, 2}, + {"iniFile", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini", 0, 1, 2}, + {"iniFileName", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini", 0, 1, 2}, + {"ini_file", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini", 0, 1, 2}, + {"ini_file_name", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini", 0, 1, 2}, + {"INI_FILE_NAME", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini", 0, 1, 2}, {"machine", "xyzacb-trsrn (switchkins)", 0, 1, 2}, {"kinematics", "xyzacb_trsrn", 0, 1, 2}, + {"switchkins", "xyzacb-trsrn_twp", 0, 1, 2}, + {"switchkins", "xyzacb-trsrn", 0, 1, 2}, + {"switchkins", "xyzacb-trsrn (switchkins)", 0, 1, 2}, + {"switchkins", "xyzacb_trsrn", 0, 1, 2}, + {"postguiHalFile", "xyzacb-trsrn_postgui.hal", 0, 1, 2}, + {"postguiHalFile", "xyzacb-trsrn_postgui", 0, 1, 2}, + {"postgui_halfile", "xyzacb-trsrn_postgui.hal", 0, 1, 2}, + {"postgui_halfile", "xyzacb-trsrn_postgui", 0, 1, 2}, + {"postgui_hal_file", "xyzacb-trsrn_postgui.hal", 0, 1, 2}, + {"postgui_hal_file", "xyzacb-trsrn_postgui", 0, 1, 2}, + {"POSTGUI_HALFILE", "xyzacb-trsrn_postgui.hal", 0, 1, 2}, + {"POSTGUI_HALFILE", "xyzacb-trsrn_postgui", 0, 1, 2}, + {"remap", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating", 0, 1, 2}, + {"remap", "table-rotary_spindle-rotary-nutating", 0, 1, 2}, {"config", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini", 0, 1, 2}, + {"configPath", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini", 0, 1, 2}, + {"config_path", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini", 0, 1, 2}, + {"ini", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini", 0, 1, 2}, + {"iniFile", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini", 0, 1, 2}, + {"iniFileName", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini", 0, 1, 2}, + {"ini_file", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini", 0, 1, 2}, + {"ini_file_name", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini", 0, 1, 2}, + {"INI_FILE_NAME", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini", 0, 1, 2}, {"machine", "xyzbca-trsrn (switchkins)", 0, 1, 2}, {"kinematics", "xyzbca_trsrn", 0, 1, 2}, + {"switchkins", "xyzbca-trsrn_twp", 0, 1, 2}, + {"switchkins", "xyzbca-trsrn", 0, 1, 2}, + {"switchkins", "xyzbca-trsrn (switchkins)", 0, 1, 2}, + {"switchkins", "xyzbca_trsrn", 0, 1, 2}, + {"postguiHalFile", "xyzbca-trsrn_postgui.hal", 0, 1, 2}, + {"postguiHalFile", "xyzbca-trsrn_postgui", 0, 1, 2}, + {"postgui_halfile", "xyzbca-trsrn_postgui.hal", 0, 1, 2}, + {"postgui_halfile", "xyzbca-trsrn_postgui", 0, 1, 2}, + {"postgui_hal_file", "xyzbca-trsrn_postgui.hal", 0, 1, 2}, + {"postgui_hal_file", "xyzbca-trsrn_postgui", 0, 1, 2}, + {"POSTGUI_HALFILE", "xyzbca-trsrn_postgui.hal", 0, 1, 2}, + {"POSTGUI_HALFILE", "xyzbca-trsrn_postgui", 0, 1, 2}, {"config", "configs/sim/axis/vismach/hexapod-sim/hexapod.ini", 0, 1, 2}, + {"configPath", "configs/sim/axis/vismach/hexapod-sim/hexapod.ini", 0, 1, 2}, + {"config_path", "configs/sim/axis/vismach/hexapod-sim/hexapod.ini", 0, 1, 2}, + {"ini", "configs/sim/axis/vismach/hexapod-sim/hexapod.ini", 0, 1, 2}, + {"iniFile", "configs/sim/axis/vismach/hexapod-sim/hexapod.ini", 0, 1, 2}, + {"iniFileName", "configs/sim/axis/vismach/hexapod-sim/hexapod.ini", 0, 1, 2}, + {"ini_file", "configs/sim/axis/vismach/hexapod-sim/hexapod.ini", 0, 1, 2}, + {"ini_file_name", "configs/sim/axis/vismach/hexapod-sim/hexapod.ini", 0, 1, 2}, + {"INI_FILE_NAME", "configs/sim/axis/vismach/hexapod-sim/hexapod.ini", 0, 1, 2}, {"machine", "hexapod (switchkins)", 0, 1, 2}, {"kinematics", "genhexkins", 0, 1, 2}, + {"switchkins", "hexapod-sim", 0, 1, 2}, + {"switchkins", "hexapod", 0, 1, 2}, + {"switchkins", "hexapod (switchkins)", 0, 1, 2}, + {"switchkins", "genhexkins", 0, 1, 2}, + {"halFile", "kinematics.hal", 0, 1, 2}, + {"halFile", "kinematics", 0, 1, 2}, {"halfile", "kinematics.hal", 0, 1, 2}, {"halfile", "kinematics", 0, 1, 2}, {"hal_file", "kinematics.hal", 0, 1, 2}, {"hal_file", "kinematics", 0, 1, 2}, + {"HALFILE", "kinematics.hal", 0, 1, 2}, + {"HALFILE", "kinematics", 0, 1, 2}, + {"postguiHalFile", "hexapod_postgui.hal", 0, 1, 2}, + {"postguiHalFile", "hexapod_postgui", 0, 1, 2}, + {"postgui_halfile", "hexapod_postgui.hal", 0, 1, 2}, + {"postgui_halfile", "hexapod_postgui", 0, 1, 2}, + {"postgui_hal_file", "hexapod_postgui.hal", 0, 1, 2}, + {"postgui_hal_file", "hexapod_postgui", 0, 1, 2}, + {"POSTGUI_HALFILE", "hexapod_postgui.hal", 0, 1, 2}, + {"POSTGUI_HALFILE", "hexapod_postgui", 0, 1, 2}, + {"remap", "configs/sim/axis/vismach/hexapod-sim", 0, 1, 2}, + {"remap", "hexapod-sim", 0, 1, 2}, {"config", "configs/sim/axis/vismach/melfa-sim/melfa.ini", 0, 1, 2}, + {"configPath", "configs/sim/axis/vismach/melfa-sim/melfa.ini", 0, 1, 2}, + {"config_path", "configs/sim/axis/vismach/melfa-sim/melfa.ini", 0, 1, 2}, + {"ini", "configs/sim/axis/vismach/melfa-sim/melfa.ini", 0, 1, 2}, + {"iniFile", "configs/sim/axis/vismach/melfa-sim/melfa.ini", 0, 1, 2}, + {"iniFileName", "configs/sim/axis/vismach/melfa-sim/melfa.ini", 0, 1, 2}, + {"ini_file", "configs/sim/axis/vismach/melfa-sim/melfa.ini", 0, 1, 2}, + {"ini_file_name", "configs/sim/axis/vismach/melfa-sim/melfa.ini", 0, 1, 2}, + {"INI_FILE_NAME", "configs/sim/axis/vismach/melfa-sim/melfa.ini", 0, 1, 2}, {"machine", "melfa (mm)", 0, 1, 2}, {"kinematics", "genserkins", 0, 1, 2}, + {"switchkins", "melfa-sim", 0, 1, 2}, + {"switchkins", "melfa", 0, 1, 2}, + {"switchkins", "melfa (mm)", 0, 1, 2}, + {"switchkins", "genserkins", 0, 1, 2}, + {"halFile", "melfa_dh.hal", 0, 1, 2}, + {"halFile", "melfa_dh", 0, 1, 2}, {"halfile", "melfa_dh.hal", 0, 1, 2}, {"halfile", "melfa_dh", 0, 1, 2}, {"hal_file", "melfa_dh.hal", 0, 1, 2}, {"hal_file", "melfa_dh", 0, 1, 2}, + {"HALFILE", "melfa_dh.hal", 0, 1, 2}, + {"HALFILE", "melfa_dh", 0, 1, 2}, + {"postguiHalFile", "melfa-postgui.hal", 0, 1, 2}, + {"postguiHalFile", "melfa-postgui", 0, 1, 2}, + {"postgui_halfile", "melfa-postgui.hal", 0, 1, 2}, + {"postgui_halfile", "melfa-postgui", 0, 1, 2}, + {"postgui_hal_file", "melfa-postgui.hal", 0, 1, 2}, + {"postgui_hal_file", "melfa-postgui", 0, 1, 2}, + {"POSTGUI_HALFILE", "melfa-postgui.hal", 0, 1, 2}, + {"POSTGUI_HALFILE", "melfa-postgui", 0, 1, 2}, + {"remap", "configs/sim/axis/vismach/melfa-sim", 0, 1, 2}, + {"remap", "melfa-sim", 0, 1, 2}, {"config", "configs/sim/axis/vismach/millturn/millturn.ini", 0, 1, -1}, + {"configPath", "configs/sim/axis/vismach/millturn/millturn.ini", 0, 1, -1}, + {"config_path", "configs/sim/axis/vismach/millturn/millturn.ini", 0, 1, -1}, + {"ini", "configs/sim/axis/vismach/millturn/millturn.ini", 0, 1, -1}, + {"iniFile", "configs/sim/axis/vismach/millturn/millturn.ini", 0, 1, -1}, + {"iniFileName", "configs/sim/axis/vismach/millturn/millturn.ini", 0, 1, -1}, + {"ini_file", "configs/sim/axis/vismach/millturn/millturn.ini", 0, 1, -1}, + {"ini_file_name", "configs/sim/axis/vismach/millturn/millturn.ini", 0, 1, -1}, + {"INI_FILE_NAME", "configs/sim/axis/vismach/millturn/millturn.ini", 0, 1, -1}, {"machine", "millturn (mm)", 0, 1, -1}, {"kinematics", "millturn", 0, 1, -1}, + {"switchkins", "millturn", 0, 1, -1}, + {"switchkins", "millturn (mm)", 0, 1, -1}, + {"halFile", "millturn.hal", 0, 1, -1}, + {"halFile", "millturn", 0, 1, -1}, {"halfile", "millturn.hal", 0, 1, -1}, {"halfile", "millturn", 0, 1, -1}, {"hal_file", "millturn.hal", 0, 1, -1}, {"hal_file", "millturn", 0, 1, -1}, + {"HALFILE", "millturn.hal", 0, 1, -1}, + {"HALFILE", "millturn", 0, 1, -1}, + {"postguiHalFile", "millturn-postgui.hal", 0, 1, -1}, + {"postguiHalFile", "millturn-postgui", 0, 1, -1}, + {"postgui_halfile", "millturn-postgui.hal", 0, 1, -1}, + {"postgui_halfile", "millturn-postgui", 0, 1, -1}, + {"postgui_hal_file", "millturn-postgui.hal", 0, 1, -1}, + {"postgui_hal_file", "millturn-postgui", 0, 1, -1}, + {"POSTGUI_HALFILE", "millturn-postgui.hal", 0, 1, -1}, + {"POSTGUI_HALFILE", "millturn-postgui", 0, 1, -1}, + {"remap", "configs/sim/axis/vismach/millturn", 0, 1, -1}, + {"remap", "millturn", 0, 1, -1}, {"config", "configs/sim/axis/vismach/puma/puma.ini", 0, 1, 2}, + {"configPath", "configs/sim/axis/vismach/puma/puma.ini", 0, 1, 2}, + {"config_path", "configs/sim/axis/vismach/puma/puma.ini", 0, 1, 2}, + {"ini", "configs/sim/axis/vismach/puma/puma.ini", 0, 1, 2}, + {"iniFile", "configs/sim/axis/vismach/puma/puma.ini", 0, 1, 2}, + {"iniFileName", "configs/sim/axis/vismach/puma/puma.ini", 0, 1, 2}, + {"ini_file", "configs/sim/axis/vismach/puma/puma.ini", 0, 1, 2}, + {"ini_file_name", "configs/sim/axis/vismach/puma/puma.ini", 0, 1, 2}, + {"INI_FILE_NAME", "configs/sim/axis/vismach/puma/puma.ini", 0, 1, 2}, {"machine", "PUMA (pumakins,switchkins)", 0, 1, 2}, {"kinematics", "pumakins", 0, 1, 2}, + {"switchkins", "puma", 0, 1, 2}, + {"switchkins", "PUMA (pumakins,switchkins)", 0, 1, 2}, + {"switchkins", "PUMA", 0, 1, 2}, + {"switchkins", "pumakins", 0, 1, 2}, + {"halFile", "puma_dh.hal", 0, 1, 2}, + {"halFile", "puma_dh", 0, 1, 2}, {"halfile", "puma_dh.hal", 0, 1, 2}, {"halfile", "puma_dh", 0, 1, 2}, {"hal_file", "puma_dh.hal", 0, 1, 2}, {"hal_file", "puma_dh", 0, 1, 2}, + {"HALFILE", "puma_dh.hal", 0, 1, 2}, + {"HALFILE", "puma_dh", 0, 1, 2}, + {"postguiHalFile", "puma_postgui.hal", 0, 1, 2}, + {"postguiHalFile", "puma_postgui", 0, 1, 2}, + {"postgui_halfile", "puma_postgui.hal", 0, 1, 2}, + {"postgui_halfile", "puma_postgui", 0, 1, 2}, + {"postgui_hal_file", "puma_postgui.hal", 0, 1, 2}, + {"postgui_hal_file", "puma_postgui", 0, 1, 2}, + {"POSTGUI_HALFILE", "puma_postgui.hal", 0, 1, 2}, + {"POSTGUI_HALFILE", "puma_postgui", 0, 1, 2}, + {"remap", "configs/sim/axis/vismach/puma", 0, 1, 2}, + {"remap", "puma", 0, 1, 2}, {"config", "configs/sim/axis/vismach/puma/puma_cube.ini", 0, 1, 2}, + {"configPath", "configs/sim/axis/vismach/puma/puma_cube.ini", 0, 1, 2}, + {"config_path", "configs/sim/axis/vismach/puma/puma_cube.ini", 0, 1, 2}, + {"ini", "configs/sim/axis/vismach/puma/puma_cube.ini", 0, 1, 2}, + {"iniFile", "configs/sim/axis/vismach/puma/puma_cube.ini", 0, 1, 2}, + {"iniFileName", "configs/sim/axis/vismach/puma/puma_cube.ini", 0, 1, 2}, + {"ini_file", "configs/sim/axis/vismach/puma/puma_cube.ini", 0, 1, 2}, + {"ini_file_name", "configs/sim/axis/vismach/puma/puma_cube.ini", 0, 1, 2}, + {"INI_FILE_NAME", "configs/sim/axis/vismach/puma/puma_cube.ini", 0, 1, 2}, {"machine", "puma_cube.ini (pumakins)", 0, 1, 2}, + {"switchkins", "puma_cube", 0, 1, 2}, + {"switchkins", "puma_cube.ini (pumakins)", 0, 1, 2}, + {"switchkins", "puma_cube.ini", 0, 1, 2}, {"config", "configs/sim/axis/vismach/puma/puma560.ini", 0, 1, 2}, + {"configPath", "configs/sim/axis/vismach/puma/puma560.ini", 0, 1, 2}, + {"config_path", "configs/sim/axis/vismach/puma/puma560.ini", 0, 1, 2}, + {"ini", "configs/sim/axis/vismach/puma/puma560.ini", 0, 1, 2}, + {"iniFile", "configs/sim/axis/vismach/puma/puma560.ini", 0, 1, 2}, + {"iniFileName", "configs/sim/axis/vismach/puma/puma560.ini", 0, 1, 2}, + {"ini_file", "configs/sim/axis/vismach/puma/puma560.ini", 0, 1, 2}, + {"ini_file_name", "configs/sim/axis/vismach/puma/puma560.ini", 0, 1, 2}, + {"INI_FILE_NAME", "configs/sim/axis/vismach/puma/puma560.ini", 0, 1, 2}, {"machine", "puma560 (switchkins) (inch)", 0, 1, 2}, + {"switchkins", "puma560", 0, 1, 2}, + {"switchkins", "puma560 (switchkins) (inch)", 0, 1, 2}, + {"halFile", "puma560_dh.hal", 0, 1, 2}, + {"halFile", "puma560_dh", 0, 1, 2}, {"halfile", "puma560_dh.hal", 0, 1, 2}, {"halfile", "puma560_dh", 0, 1, 2}, {"hal_file", "puma560_dh.hal", 0, 1, 2}, {"hal_file", "puma560_dh", 0, 1, 2}, + {"HALFILE", "puma560_dh.hal", 0, 1, 2}, + {"HALFILE", "puma560_dh", 0, 1, 2}, + {"postguiHalFile", "puma560_postgui.hal", 0, 1, 2}, + {"postguiHalFile", "puma560_postgui", 0, 1, 2}, + {"postgui_halfile", "puma560_postgui.hal", 0, 1, 2}, + {"postgui_halfile", "puma560_postgui", 0, 1, 2}, + {"postgui_hal_file", "puma560_postgui.hal", 0, 1, 2}, + {"postgui_hal_file", "puma560_postgui", 0, 1, 2}, + {"POSTGUI_HALFILE", "puma560_postgui.hal", 0, 1, 2}, + {"POSTGUI_HALFILE", "puma560_postgui", 0, 1, 2}, {"config", "configs/sim/axis/vismach/puma/puma560_uvw.ini", 0, 1, 2}, + {"configPath", "configs/sim/axis/vismach/puma/puma560_uvw.ini", 0, 1, 2}, + {"config_path", "configs/sim/axis/vismach/puma/puma560_uvw.ini", 0, 1, 2}, + {"ini", "configs/sim/axis/vismach/puma/puma560_uvw.ini", 0, 1, 2}, + {"iniFile", "configs/sim/axis/vismach/puma/puma560_uvw.ini", 0, 1, 2}, + {"iniFileName", "configs/sim/axis/vismach/puma/puma560_uvw.ini", 0, 1, 2}, + {"ini_file", "configs/sim/axis/vismach/puma/puma560_uvw.ini", 0, 1, 2}, + {"ini_file_name", "configs/sim/axis/vismach/puma/puma560_uvw.ini", 0, 1, 2}, + {"INI_FILE_NAME", "configs/sim/axis/vismach/puma/puma560_uvw.ini", 0, 1, 2}, + {"switchkins", "puma560_uvw", 0, 1, 2}, {"config", "configs/sim/axis/vismach/scara/scara.ini", 0, 1, 2}, + {"configPath", "configs/sim/axis/vismach/scara/scara.ini", 0, 1, 2}, + {"config_path", "configs/sim/axis/vismach/scara/scara.ini", 0, 1, 2}, + {"ini", "configs/sim/axis/vismach/scara/scara.ini", 0, 1, 2}, + {"iniFile", "configs/sim/axis/vismach/scara/scara.ini", 0, 1, 2}, + {"iniFileName", "configs/sim/axis/vismach/scara/scara.ini", 0, 1, 2}, + {"ini_file", "configs/sim/axis/vismach/scara/scara.ini", 0, 1, 2}, + {"ini_file_name", "configs/sim/axis/vismach/scara/scara.ini", 0, 1, 2}, + {"INI_FILE_NAME", "configs/sim/axis/vismach/scara/scara.ini", 0, 1, 2}, {"machine", "SCARA (genserkins,switchkins)", 0, 1, 2}, {"kinematics", "scarakins coordinates=xyzcab", 0, 1, 2}, + {"switchkins", "scara", 0, 1, 2}, + {"switchkins", "SCARA (genserkins,switchkins)", 0, 1, 2}, + {"switchkins", "SCARA", 0, 1, 2}, + {"switchkins", "scarakins coordinates=xyzcab", 0, 1, 2}, + {"switchkins", "scarakins", 0, 1, 2}, + {"postguiHalFile", "scara_postgui.hal", 0, 1, 2}, + {"postguiHalFile", "scara_postgui", 0, 1, 2}, + {"postgui_halfile", "scara_postgui.hal", 0, 1, 2}, + {"postgui_halfile", "scara_postgui", 0, 1, 2}, + {"postgui_hal_file", "scara_postgui.hal", 0, 1, 2}, + {"postgui_hal_file", "scara_postgui", 0, 1, 2}, + {"POSTGUI_HALFILE", "scara_postgui.hal", 0, 1, 2}, + {"POSTGUI_HALFILE", "scara_postgui", 0, 1, 2}, + {"remap", "configs/sim/axis/vismach/scara", 0, 1, 2}, + {"remap", "scara", 0, 1, 2}, {"config", "configs/sim/qtaxis/non-trivial/scara/scara.ini", 0, 1, 2}, + {"configPath", "configs/sim/qtaxis/non-trivial/scara/scara.ini", 0, 1, 2}, + {"config_path", "configs/sim/qtaxis/non-trivial/scara/scara.ini", 0, 1, 2}, + {"ini", "configs/sim/qtaxis/non-trivial/scara/scara.ini", 0, 1, 2}, + {"iniFile", "configs/sim/qtaxis/non-trivial/scara/scara.ini", 0, 1, 2}, + {"iniFileName", "configs/sim/qtaxis/non-trivial/scara/scara.ini", 0, 1, 2}, + {"ini_file", "configs/sim/qtaxis/non-trivial/scara/scara.ini", 0, 1, 2}, + {"ini_file_name", "configs/sim/qtaxis/non-trivial/scara/scara.ini", 0, 1, 2}, + {"INI_FILE_NAME", "configs/sim/qtaxis/non-trivial/scara/scara.ini", 0, 1, 2}, + {"remap", "configs/sim/qtaxis/non-trivial/scara", 0, 1, 2}, {"config", "configs/sim/qtvcp_screens/non-trivial/scara/scara.ini", 0, 1, 2}, + {"configPath", "configs/sim/qtvcp_screens/non-trivial/scara/scara.ini", 0, 1, 2}, + {"config_path", "configs/sim/qtvcp_screens/non-trivial/scara/scara.ini", 0, 1, 2}, + {"ini", "configs/sim/qtvcp_screens/non-trivial/scara/scara.ini", 0, 1, 2}, + {"iniFile", "configs/sim/qtvcp_screens/non-trivial/scara/scara.ini", 0, 1, 2}, + {"iniFileName", "configs/sim/qtvcp_screens/non-trivial/scara/scara.ini", 0, 1, 2}, + {"ini_file", "configs/sim/qtvcp_screens/non-trivial/scara/scara.ini", 0, 1, 2}, + {"ini_file_name", "configs/sim/qtvcp_screens/non-trivial/scara/scara.ini", 0, 1, 2}, + {"INI_FILE_NAME", "configs/sim/qtvcp_screens/non-trivial/scara/scara.ini", 0, 1, 2}, + {"remap", "configs/sim/qtvcp_screens/non-trivial/scara", 0, 1, 2}, diff --git a/docs/architecture.md b/docs/architecture.md index 5160c5a..13f451a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -27,6 +27,22 @@ designed project code. See `docs/linuxcnc-source-policy.md`. 4. Compile with Emscripten after replacing `dlopen`, Python, HAL and filesystem-only features. 5. Compare event output with native LinuxCNC using the same G-code corpus. +## Current Workstreams + +The current porting work is being split into six small contexts and should be +advanced in this order: + +1. `switchkins/remap` generator and tables +2. `linuxcnc-kinematics` source manifest +3. WASM filesystem and OPFS persistence +4. browser app integration +5. Node and browser smoke coverage +6. source-link, build, and documentation constraints + +Each step should stay source-backed and close one small gap at a time. A single +pass should advance one workstream context only; crossing into another context +requires naming the dependency and keeping the edit set minimal. + ## Dialect expansion LinuxCNC support should be the baseline. Fanuc and Siemens support should be implemented as dialect adapters, not by forking the simulator core. diff --git a/docs/linuxcnc-porting.md b/docs/linuxcnc-porting.md index c81fdb1..d64cd8a 100644 --- a/docs/linuxcnc-porting.md +++ b/docs/linuxcnc-porting.md @@ -159,8 +159,13 @@ LinuxCNC interpreter sources currently involve Python/Boost.Python remap paths. - disable Python remap - disable dynamic module loading -- replace file-backed parameter persistence with in-memory buffers -- provide browser-safe tool table and INI/config loading through JSON +- route LinuxCNC parameter-file reads and writes through an OPFS-backed wasm + workspace, preserving `rs274ngc.var` and `.bak` persistence in browser runs + as defined by `src/emc/rs274ngc/interp_internal.hh` and exercised by + `Interp::restore_parameters`/`Interp::save_parameters` in + `src/emc/rs274ngc/rs274ngc_pre.cc` +- provide browser-safe tool table and INI/config loading through JSON or + OPFS-backed files ## Step 5: Emscripten build diff --git a/docs/linuxcnc-source-policy.md b/docs/linuxcnc-source-policy.md index 98fe1d0..4894b46 100644 --- a/docs/linuxcnc-source-policy.md +++ b/docs/linuxcnc-source-policy.md @@ -3,8 +3,23 @@ The simulator must not grow independently designed functional behavior. Functional behavior must come from LinuxCNC source code. +Hard rules: + +- Port-first: prefer direct LinuxCNC source porting, trimming, wrapping, or + platform adaptation over project-authored replacements. +- Do not write project-owned functional CNC behavior. +- Functional CNC behavior must be ported, trimmed, wrapped, or routed from + LinuxCNC source code. +- Any G/M-code interpretation, motion behavior, kinematics, coordinate + handling, cutter compensation, canned cycle, parameter expression, modal + state, tool data, remap behavior, or RTCP behavior must first be traced to + LinuxCNC source before implementation. +- Browser-side wasm filesystem behavior must use OPFS-backed storage. Do not + add browser filesystem paths that bypass OPFS. + Allowed project code: +- LinuxCNC source porting, trimming, wrappers, and platform adaptation. - Thin adapters between LinuxCNC source code and the simulator C API. - Platform shims needed to compile LinuxCNC code for native tests or wasm. - Event serialization, test fixtures, build scripts, and documentation. @@ -22,7 +37,81 @@ Required workflow for every functional change: 2. Add or update a test that demonstrates the LinuxCNC behavior. 3. Port, wrap, or route to that LinuxCNC implementation. 4. Record the source file/function in code comments or nearby documentation when the mapping is not obvious. -5. Run `./test-native.sh` and `./test-linuxcnc-source-link.sh`. +5. Keep the change inside one active workstream context unless a dependency is + unavoidable and explicitly named in the final report. +6. Run `./test-native.sh` and `./test-linuxcnc-source-link.sh`. +7. For source-manifest, build, or build-policy changes, also run the aggregate + and guardrail checks: `./test-all-native.sh`, + `./test-linuxcnc-source-syntax.sh`, and + `./test-linuxcnc-wasm-cmake-safe-probe.sh`. + +Efficiency rules: + +- Do not add a smoke-only behavior path when the LinuxCNC-backed route already + exists; add coverage to the source-backed route instead. +- Do not mix generator/table work, manifests, OPFS/browser work, and build + policy cleanup in the same pass unless the files directly depend on each + other. +- Prefer tightening an existing probe or manifest over adding another broad + end-to-end smoke case. +- Default native, source-link, build-wasm, and wasm-safe CMake probe + parallelism to `CNC_SIM_BUILD_JOBS:-8`; raise it only by explicit + positive-integer environment override. +- Persistent native, source-link, and wasm-safe CMake probe build directories + may be overridden for local workflows, but must not point at the filesystem + root. +- Build policy knobs are intentionally narrow: + `CNC_SIM_BUILD_JOBS`, `CNC_SIM_NATIVE_BUILD_DIR`, + `CNC_SIM_SOURCE_LINK_BUILD_DIR`, `CNC_SIM_SOURCE_SYNTAX_BUILD_DIR`, and + `CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR`. Build directory overrides must + stay non-empty, whitespace-free, and outside the filesystem root. +- `build-wasm.sh` keeps fixed artifact paths: CMake writes `build/wasm`, then + the script copies and byte-compares `cnc_sim.js` and `cnc_sim.wasm` under + `web/public` before running Node and browser smoke checks. + +Build and source-link guardrails: + +- `CNC_SIM_BUILD_JOBS` must be a positive integer and defaults to `8`. +- `test-native.sh` uses persistent `build/native-test` objects, dependency + files, and `native-build.signature`. +- `test-linuxcnc-source-link.sh` uses persistent `build/source-link-test` + objects, dependency files, and `source-link-build.signature`. +- `test-linuxcnc-source-syntax.sh` uses persistent + `build/source-syntax-test` stamps, dependency files, and + `source-syntax.signature`. +- `test-linuxcnc-wasm-cmake-safe-probe.sh` uses persistent + `build/wasm-cmake-safe-probe` and `wasm-cmake-safe-probe.signature`. +- Native, source-link, source-syntax, wasm-safe CMake probe, and build-wasm + paths must reject empty, whitespace-containing, or filesystem-root build + directory overrides before doing destructive cleanup. +- Native, source-link, source-syntax, and build-wasm entry points must serialize + shared non-concurrency-safe paths with lock files before writing fixed + outputs. +- Source-link and syntax probes must build from generated makefiles with + `-MMD -MP` dependency tracking and `make --output-sync=target`. +- `build-wasm.sh` must run blocker, syntax, object, CMake, tooldata, + `interp_*`, Python, `rs274ngc_pre`, and runtime link probes before copying + browser artifacts. +- `build-wasm.sh` must check `cmake`, `emcmake`, `emcc`, and `node` before the + Emscripten configure step. +- `build-wasm.sh` must build `cnc_sim_wasm_runtime_probe` before + `cnc_sim_wasm`. +- `build-wasm.sh` must copy `build/wasm/cnc_sim.js` and + `build/wasm/cnc_sim.wasm` exactly once each. +- `build-wasm.sh` must compare copied artifacts with `cmp -s` before running + Node or browser smoke tests. +- `build-wasm.sh` must run the Node smoke before the browser smoke. +- The default wasm manifest partition must remain explicitly checked for 25 + wasm-safe core sources and 9 blocked sources. +- Manifest source listers must reject malformed lines, absolute source paths, + unknown groups, unknown filters, and duplicate source entries. +- Native link flags must include the built LinuxCNC `lib` path and rpath. +- Common CXX flag helpers must surface helper failures instead of hiding them + behind process substitution or `read`. +- Temporary probe reports must be cleaned on failure and preserved only after a + successful report path explicitly opts in. +- Documentation-only build policy edits should still update the guardrail + checks that enforce the relevant script and artifact ordering. Current temporary exceptions: diff --git a/generate-linuxcnc-switchkins-remap-table.sh b/generate-linuxcnc-switchkins-remap-table.sh index 9f4bb48..7d76410 100755 --- a/generate-linuxcnc-switchkins-remap-table.sh +++ b/generate-linuxcnc-switchkins-remap-table.sh @@ -5,15 +5,49 @@ cd "$(dirname "$0")" linuxcnc_root=${LINUXCNC_ROOT:-../linuxcnc} mode=table -if [[ "${1:-}" == "--config-cases" ]]; then - mode=config-cases - shift -elif [[ "${1:-}" == "--json-cases" ]]; then - mode=json-cases - shift -fi +all_output_dir= + +usage() { + echo "usage: $0 [--config-cases|--json-cases|--all-output-dir DIR] [manifest]" >&2 +} + +case "${1:-}" in + --config-cases) + mode=config-cases + shift + ;; + --json-cases) + mode=json-cases + shift + ;; + --all-output-dir) + mode=all + if [[ -z "${2:-}" || "${2:-}" == --* ]]; then + usage + echo "missing output directory for --all-output-dir" >&2 + exit 1 + fi + all_output_dir=$2 + shift 2 + ;; + --help|-h) + usage + exit 0 + ;; + --*) + usage + echo "unknown switchkins remap table generator option: $1" >&2 + exit 1 + ;; +esac manifest=${1:-linuxcnc-kinematics-source-files.txt} +if [[ -n "${2:-}" ]]; then + usage + echo "too many switchkins remap table generator arguments" >&2 + exit 1 +fi + if [[ ! -d "$linuxcnc_root" ]]; then echo "missing LinuxCNC root: $linuxcnc_root" >&2 exit 1 @@ -22,34 +56,58 @@ if [[ ! -f "$manifest" ]]; then echo "missing kinematics manifest: $manifest" >&2 exit 1 fi +if [[ "$mode" == all && -z "$all_output_dir" ]]; then + echo "missing output directory for --all-output-dir" >&2 + exit 1 +fi linuxcnc_root=$(cd "$linuxcnc_root" && pwd) -normalize_alias() { - tr '[:upper:]' '[:lower:]' | tr -d '[:space:]' -} - -trim_value() { - sed 's/[[:space:]]*[#;].*$//; s/^[[:space:]]*//; s/[[:space:]]*$//' -} - -ini_value() { +read_ini_switchkins_config() { local file=$1 - local key=$2 - sed -n "s/^[[:space:]]*$key[[:space:]]*=[[:space:]]*//Ip" "$file" | head -n 1 | trim_value -} + awk ' + function trim(value) { + sub(/^[[:space:]]*/, "", value) + sub(/[[:space:]]*$/, "", value) + return value + } + { + line = $0 + sub(/[[:space:]]*[#;].*$/, "", line) + line = trim(line) + if (line !~ /=/) { + next + } + key = line + sub(/[[:space:]]*=.*/, "", key) + key = tolower(trim(key)) + value = line + sub(/^[^=]*=[[:space:]]*/, "", value) + value = trim(value) -ini_values() { - local file=$1 - local key=$2 - sed -n "s/^[[:space:]]*$key[[:space:]]*=[[:space:]]*//Ip" "$file" | trim_value -} - -remap_ngc_name() { - local file=$1 - local mcode=$2 - sed -n "s/.*REMAP[[:space:]]*=[[:space:]]*M$mcode[[:space:]][^#;]*ngc[[:space:]]*=[[:space:]]*\\([^[:space:]#;]*\\).*/\\1/Ip" "$file" | - head -n 1 + if (key == "machine") { + print "machine\t" value + } else if (key == "kinematics") { + print "kinematics\t" value + } else if (key == "halfile") { + print "halfile\t" value + } else if (key == "postgui_halfile") { + print "postgui_halfile\t" value + } else if (key == "subroutine_path") { + print "subroutine_path\t" value + } else if (key == "remap") { + lower = tolower(value) + for (mcode = 428; mcode <= 430; mcode++) { + if (lower ~ "m" mcode "([^0-9]|$)" && + match(lower, /ngc[[:space:]]*=[[:space:]]*[^[:space:]]+/)) { + ngc = substr(value, RSTART, RLENGTH) + sub(/^[Nn][Gg][Cc][[:space:]]*=[[:space:]]*/, "", ngc) + print "remap_" mcode "\t" ngc + } + } + } + } + ' "$file" } resolve_config_path() { @@ -64,26 +122,92 @@ resolve_config_path() { relative_linuxcnc_path() { local path=$1 - path=$(cd "$(dirname "$path")" && pwd)/$(basename "$path") + path=$(cd "${path%/*}" && pwd)/${path##*/} + if [[ "$path" != "$linuxcnc_root/"* ]]; then + echo "resolved LinuxCNC source path escapes LinuxCNC root: $path" >&2 + exit 1 + fi printf '%s\n' "${path#"$linuxcnc_root/"}" } -remap_source_for_mcode() { - local config=$1 - local mcode=$2 - local ngc_name - ngc_name=$(remap_ngc_name "$config" "$mcode") - if [[ -z "$ngc_name" ]]; then - return 1 +declare -A config_dir_cache=() +declare -A manifest_config_sources=() +declare -A used_config_sources=() +declare -A manifest_remap_sources=() +declare -A used_remap_sources=() +manifest_config_count=0 +manifest_remap_count=0 + +validate_manifest_source_path() { + local group=$1 + local path=$2 + if [[ -z "$path" ]]; then + echo "empty LinuxCNC $group source path in $manifest" >&2 + exit 1 fi + if [[ "$path" = /* || "$path" == *..* ]]; then + echo "LinuxCNC $group source path must stay under LinuxCNC root: $path" >&2 + exit 1 + fi + if [[ ! -f "$linuxcnc_root/$path" ]]; then + echo "missing LinuxCNC $group source listed in $manifest: $path" >&2 + exit 1 + fi +} + +while IFS=: read -r manifest_group manifest_path _manifest_note; do + case "$manifest_group" in + config) + validate_manifest_source_path "$manifest_group" "$manifest_path" + if [[ -n "${manifest_config_sources[$manifest_path]:-}" ]]; then + echo "duplicate LinuxCNC config source in $manifest: $manifest_path" >&2 + exit 1 + fi + manifest_config_sources[$manifest_path]=1 + manifest_config_count=$((manifest_config_count + 1)) + ;; + remap) + validate_manifest_source_path "$manifest_group" "$manifest_path" + if [[ "$manifest_path" != *.ngc ]]; then + echo "LinuxCNC remap source must be an ngc file in $manifest: $manifest_path" >&2 + exit 1 + fi + case "${manifest_path##*/}" in + 428remap.ngc|429remap.ngc|430remap.ngc) + ;; + *) + echo "LinuxCNC switchkins remap source must be 428/429/430remap.ngc in $manifest: $manifest_path" >&2 + exit 1 + ;; + esac + if [[ -n "${manifest_remap_sources[$manifest_path]:-}" ]]; then + echo "duplicate LinuxCNC remap source in $manifest: $manifest_path" >&2 + exit 1 + fi + manifest_remap_sources[$manifest_path]=1 + manifest_remap_count=$((manifest_remap_count + 1)) + ;; + esac +done <"$manifest" + +config_dir_for() { + local config=$1 + if [[ -z "${config_dir_cache[$config]:-}" ]]; then + config_dir_cache[$config]=$(cd "${config%/*}" && pwd) + fi + printf '%s\n' "${config_dir_cache[$config]}" +} + +remap_source_for_ngc() { + local config=$1 + local ngc_name=$2 + local subroutine_paths=${3:-} if [[ "$ngc_name" != *.ngc ]]; then ngc_name="$ngc_name.ngc" fi local config_dir - config_dir=$(cd "$(dirname "$config")" && pwd) - local subroutine_paths - subroutine_paths=$(ini_values "$config" "SUBROUTINE_PATH") + config_dir=$(config_dir_for "$config") if [[ -z "$subroutine_paths" ]]; then subroutine_paths="." fi @@ -107,41 +231,114 @@ remap_source_for_mcode() { return 1 } -kinstype_for_mcode() { +config_source_for_halfile() { local config=$1 - local mcode=$2 + local halfile=$2 + [[ -n "$halfile" ]] || return 1 + [[ "$halfile" == LIB:* ]] && return 1 + + local hal_path=${halfile%%[[:space:]]*} + [[ -n "$hal_path" ]] || return 1 + + local config_dir + config_dir=$(config_dir_for "$config") + local resolved + resolved=$(resolve_config_path "$config_dir" "$hal_path") + if [[ ! -f "$resolved" ]]; then + echo "missing LinuxCNC HAL source referenced by ${config#"$linuxcnc_root/"}: $halfile" >&2 + exit 1 + fi + relative_linuxcnc_path "$resolved" +} + +kinstype_for_remap_source() { local source - if ! source=$(remap_source_for_mcode "$config" "$mcode"); then - printf '%s\n' "-1" + source=$1 + if [[ -z "$source" ]]; then + printf '%s\n' -1 return 0 fi - sed -n 's/^[[:space:]]*#[[:space:]]*=[[:space:]]*\([0-9][0-9]*\).*/\1/p' \ - "$linuxcnc_root/$source" | - head -n 1 + awk ' + /^[[:space:]]*#[[:space:]]*=/ { + value = $0 + sub(/^[[:space:]]*#[[:space:]]*=[[:space:]]*/, "", value) + sub(/[^0-9].*$/, "", value) + print value + exit + } + ' "$linuxcnc_root/$source" } -json_compact_string() { - local value=$1 - printf '%s' "$value" | normalize_alias | sed 's/\\/\\\\/g; s/"/\\"/g' +require_remap_source() { + local config_path=$1 + local mcode=$2 + local ngc_name=$3 + local source=$4 + if [[ -z "$source" ]]; then + echo "missing LinuxCNC M$mcode remap source for $config_path: ngc=$ngc_name" >&2 + exit 1 + fi } -cpp_string() { - local value=$1 - printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g' +require_kinstype() { + local config_path=$1 + local mcode=$2 + local source=$3 + local kinstype=$4 + if [[ -z "$kinstype" || "$kinstype" == -1 ]]; then + echo "missing # assignment for LinuxCNC M$mcode remap source: $source ($config_path)" >&2 + exit 1 + fi +} + +record_remap_source() { + local config_path=$1 + local mcode=$2 + local source=$3 + if [[ -z "${manifest_remap_sources[$source]:-}" ]]; then + echo "LinuxCNC M$mcode remap source is not listed in $manifest: $source ($config_path)" >&2 + exit 1 + fi + used_remap_sources[$source]=1 +} + +record_config_source() { + local config_path=$1 + local source=$2 + local label=$3 + if [[ -z "${manifest_config_sources[$source]:-}" ]]; then + echo "LinuxCNC $label source is not listed in $manifest: $source ($config_path)" >&2 + exit 1 + fi + used_config_sources[$source]=1 } declare -A seen_aliases=() declare -A seen_cases=() +declare -A seen_case_values=() entries=() case_entries=() json_case_entries=() +processed_config_count=0 +resolved_remap_count=0 +unique_config_source_count=0 +unique_resolved_remap_count=0 + +escape_generated_string() { + local value=$1 + value=${value//\\/\\\\} + value=${value//\"/\\\"} + printf '%s\n' "$value" +} add_alias() { local alias=$1 local m428=$2 local m429=$3 local m430=$4 - alias=$(json_compact_string "$alias") + alias=${alias,,} + alias=${alias//[[:space:]]/} + alias=$(escape_generated_string "$alias") [[ -n "$alias" ]] || return 0 local types="$m428,$m429,$m430" @@ -166,18 +363,80 @@ add_case() { [[ -n "$value" ]] || return 0 local types="$m428,$m429,$m430" + local value_key="$field|$value" + if [[ -n "${seen_case_values[$value_key]:-}" && "${seen_case_values[$value_key]}" != "$types" ]]; then + echo "ambiguous switchkins config case '$field=$value': ${seen_case_values[$value_key]} and $types" >&2 + exit 1 + fi + seen_case_values[$value_key]=$types + local key="$field|$value|$types" [[ -z "${seen_cases[$key]:-}" ]] || return 0 seen_cases[$key]=1 - local cpp_field - local cpp_value - cpp_field=$(cpp_string "$field") - cpp_value=$(cpp_string "$value") + local cpp_field=$field + local cpp_value=$value + cpp_field=$(escape_generated_string "$cpp_field") + cpp_value=$(escape_generated_string "$cpp_value") case_entries+=(" {\"$cpp_field\", \"$cpp_value\", $m428, $m429, $m430},") json_case_entries+=("{\"field\":\"$cpp_field\",\"value\":\"$cpp_value\",\"m428\":$m428,\"m429\":$m429,\"m430\":$m430}") } +trim_machine_label() { + local machine=$1 + machine=${machine%%(*} + machine="${machine#"${machine%%[![:space:]]*}"}" + machine="${machine%"${machine##*[![:space:]]}"}" + printf '%s\n' "$machine" +} + +add_kinematics_variants() { + local callback=$1 + local field=$2 + local kinematics=$3 + local m428=$4 + local m429=$5 + local m430=$6 + + [[ -n "$kinematics" ]] || return 0 + "$callback" "$field" "$kinematics" "$m428" "$m429" "$m430" + + local first_word=${kinematics%%[[:space:]]*} + "$callback" "$field" "$first_word" "$m428" "$m429" "$m430" + "$callback" "$field" "${first_word%_kins}" "$m428" "$m429" "$m430" + "$callback" "$field" "${first_word%-kins}" "$m428" "$m429" "$m430" + local first_word_hyphen=${first_word//_/-} + local first_word_without_underscore_kins=${first_word%_kins} + local first_word_without_dash_kins=${first_word%-kins} + "$callback" "$field" "$first_word_hyphen" "$m428" "$m429" "$m430" + "$callback" "$field" "${first_word_without_underscore_kins//_/-}" "$m428" "$m429" "$m430" + "$callback" "$field" "${first_word_without_dash_kins//_/-}" "$m428" "$m429" "$m430" +} + +add_alias_variant() { + local _field=$1 + shift + add_alias "$@" +} + +add_case_variant() { + add_case "$@" +} + +add_halfile_case_fields() { + local field=$1 + local halfile=$2 + local m428=$3 + local m429=$4 + local m430=$5 + + local hal_base=${halfile##*/} + local hal_stem=${hal_base%.hal} + add_case "$field" "$halfile" "$m428" "$m429" "$m430" + add_case "$field" "$hal_base" "$m428" "$m429" "$m430" + add_case "$field" "$hal_stem" "$m428" "$m429" "$m430" +} + add_machine_aliases() { local machine=$1 local m428=$2 @@ -185,7 +444,9 @@ add_machine_aliases() { local m430=$4 [[ -n "$machine" ]] || return 0 add_alias "$machine" "$m428" "$m429" "$m430" - add_alias "${machine%%(*}" "$m428" "$m429" "$m430" + local trimmed_machine + trimmed_machine=$(trim_machine_label "$machine") + add_alias "$trimmed_machine" "$m428" "$m429" "$m430" } add_kinematics_aliases() { @@ -194,15 +455,31 @@ add_kinematics_aliases() { local m429=$3 local m430=$4 [[ -n "$kinematics" ]] || return 0 - add_alias "$kinematics" "$m428" "$m429" "$m430" + add_kinematics_variants add_alias_variant "" "$kinematics" "$m428" "$m429" "$m430" +} - local first_word=${kinematics%%[[:space:]]*} - add_alias "$first_word" "$m428" "$m429" "$m430" - add_alias "${first_word%_kins}" "$m428" "$m429" "$m430" - add_alias "${first_word%-kins}" "$m428" "$m429" "$m430" - add_alias "$(printf '%s' "$first_word" | tr '_' '-')" "$m428" "$m429" "$m430" - add_alias "$(printf '%s' "${first_word%_kins}" | tr '_' '-')" "$m428" "$m429" "$m430" - add_alias "$(printf '%s' "${first_word%-kins}" | tr '_' '-')" "$m428" "$m429" "$m430" +add_switchkins_cases() { + local config_dir=$1 + local config_stem=$2 + local machine=$3 + local kinematics=$4 + local m428=$5 + local m429=$6 + local m430=$7 + + add_case "switchkins" "${config_dir##*/}" "$m428" "$m429" "$m430" + add_case "switchkins" "$config_stem" "$m428" "$m429" "$m430" + + if [[ -n "$machine" ]]; then + add_case "switchkins" "$machine" "$m428" "$m429" "$m430" + local trimmed_machine + trimmed_machine=$(trim_machine_label "$machine") + add_case "switchkins" "$trimmed_machine" "$m428" "$m429" "$m430" + fi + + if [[ -n "$kinematics" ]]; then + add_kinematics_variants add_case_variant "switchkins" "$kinematics" "$m428" "$m429" "$m430" + fi } add_halfile_aliases() { @@ -226,14 +503,24 @@ add_halfile_cases() { [[ -n "$halfile" ]] || return 0 [[ "$halfile" == LIB:* ]] && return 0 - local hal_base=${halfile##*/} - local hal_stem=${hal_base%.hal} - add_case "halfile" "$halfile" "$m428" "$m429" "$m430" - add_case "halfile" "$hal_base" "$m428" "$m429" "$m430" - add_case "halfile" "$hal_stem" "$m428" "$m429" "$m430" - add_case "hal_file" "$halfile" "$m428" "$m429" "$m430" - add_case "hal_file" "$hal_base" "$m428" "$m429" "$m430" - add_case "hal_file" "$hal_stem" "$m428" "$m429" "$m430" + add_halfile_case_fields "halFile" "$halfile" "$m428" "$m429" "$m430" + add_halfile_case_fields "halfile" "$halfile" "$m428" "$m429" "$m430" + add_halfile_case_fields "hal_file" "$halfile" "$m428" "$m429" "$m430" + add_halfile_case_fields "HALFILE" "$halfile" "$m428" "$m429" "$m430" +} + +add_postgui_halfile_cases() { + local halfile=$1 + local m428=$2 + local m429=$3 + local m430=$4 + [[ -n "$halfile" ]] || return 0 + [[ "$halfile" == LIB:* ]] && return 0 + + add_halfile_case_fields "postguiHalFile" "$halfile" "$m428" "$m429" "$m430" + add_halfile_case_fields "postgui_halfile" "$halfile" "$m428" "$m429" "$m430" + add_halfile_case_fields "postgui_hal_file" "$halfile" "$m428" "$m429" "$m430" + add_halfile_case_fields "POSTGUI_HALFILE" "$halfile" "$m428" "$m429" "$m430" } while IFS=: read -r group path _note; do @@ -248,23 +535,96 @@ while IFS=: read -r group path _note; do continue fi - m428=$(kinstype_for_mcode "$config" 428) - m429=$(kinstype_for_mcode "$config" 429) - m430=$(kinstype_for_mcode "$config" 430) + machine= + kinematics= + halfiles= + postgui_halfiles= + subroutine_paths= + remap_ngc_428= + remap_ngc_429= + remap_ngc_430= + while IFS=$'\t' read -r ini_key ini_value; do + case "$ini_key" in + machine) + [[ -n "$machine" ]] || machine=$ini_value + ;; + kinematics) + [[ -n "$kinematics" ]] || kinematics=$ini_value + ;; + halfile) + halfiles+="${halfiles:+$'\n'}$ini_value" + ;; + postgui_halfile) + postgui_halfiles+="${postgui_halfiles:+$'\n'}$ini_value" + ;; + subroutine_path) + subroutine_paths+="${subroutine_paths:+$'\n'}$ini_value" + ;; + remap_428) + [[ -n "$remap_ngc_428" ]] || remap_ngc_428=$ini_value + ;; + remap_429) + [[ -n "$remap_ngc_429" ]] || remap_ngc_429=$ini_value + ;; + remap_430) + [[ -n "$remap_ngc_430" ]] || remap_ngc_430=$ini_value + ;; + esac + done < <(read_ini_switchkins_config "$config") + if [[ -z "$subroutine_paths" ]]; then + subroutine_paths="." + fi + + remap_source_428= + remap_source_429= + remap_source_430= + if [[ -n "$remap_ngc_428" ]]; then + remap_source_428=$(remap_source_for_ngc "$config" "$remap_ngc_428" "$subroutine_paths" || true) + require_remap_source "$path" 428 "$remap_ngc_428" "$remap_source_428" + record_remap_source "$path" 428 "$remap_source_428" + resolved_remap_count=$((resolved_remap_count + 1)) + fi + if [[ -n "$remap_ngc_429" ]]; then + remap_source_429=$(remap_source_for_ngc "$config" "$remap_ngc_429" "$subroutine_paths" || true) + require_remap_source "$path" 429 "$remap_ngc_429" "$remap_source_429" + record_remap_source "$path" 429 "$remap_source_429" + resolved_remap_count=$((resolved_remap_count + 1)) + fi + if [[ -n "$remap_ngc_430" ]]; then + remap_source_430=$(remap_source_for_ngc "$config" "$remap_ngc_430" "$subroutine_paths" || true) + require_remap_source "$path" 430 "$remap_ngc_430" "$remap_source_430" + record_remap_source "$path" 430 "$remap_source_430" + resolved_remap_count=$((resolved_remap_count + 1)) + fi + + m428=$(kinstype_for_remap_source "$remap_source_428") + m429=$(kinstype_for_remap_source "$remap_source_429") + m430=$(kinstype_for_remap_source "$remap_source_430") + [[ -z "$remap_ngc_428" ]] || require_kinstype "$path" 428 "$remap_source_428" "$m428" + [[ -z "$remap_ngc_429" ]] || require_kinstype "$path" 429 "$remap_source_429" "$m429" + [[ -z "$remap_ngc_430" ]] || require_kinstype "$path" 430 "$remap_source_430" "$m430" [[ -n "$m428" ]] || m428=-1 [[ -n "$m429" ]] || m429=-1 [[ -n "$m430" ]] || m430=-1 + processed_config_count=$((processed_config_count + 1)) + record_config_source "$path" "$path" "INI config" - machine=$(ini_value "$config" "MACHINE") - kinematics=$(ini_value "$config" "KINEMATICS") - halfiles=$(ini_values "$config" "HALFILE") config_dir=${path%/*} config_base=${path##*/} config_stem=${config_base%.ini} add_case "config" "$path" "$m428" "$m429" "$m430" + add_case "configPath" "$path" "$m428" "$m429" "$m430" + add_case "config_path" "$path" "$m428" "$m429" "$m430" + add_case "ini" "$path" "$m428" "$m429" "$m430" + add_case "iniFile" "$path" "$m428" "$m429" "$m430" + add_case "iniFileName" "$path" "$m428" "$m429" "$m430" + add_case "ini_file" "$path" "$m428" "$m429" "$m430" + add_case "ini_file_name" "$path" "$m428" "$m429" "$m430" + add_case "INI_FILE_NAME" "$path" "$m428" "$m429" "$m430" add_case "machine" "$machine" "$m428" "$m429" "$m430" add_case "kinematics" "$kinematics" "$m428" "$m429" "$m430" + add_switchkins_cases "$config_dir" "$config_stem" "$machine" "$kinematics" "$m428" "$m429" "$m430" add_alias "$path" "$m428" "$m429" "$m430" add_alias "$config_dir" "$m428" "$m429" "$m430" @@ -273,39 +633,119 @@ while IFS=: read -r group path _note; do add_machine_aliases "$machine" "$m428" "$m429" "$m430" add_kinematics_aliases "$kinematics" "$m428" "$m429" "$m430" while IFS= read -r halfile; do + if hal_source=$(config_source_for_halfile "$config" "$halfile"); then + record_config_source "$path" "$hal_source" "HALFILE" + fi add_halfile_cases "$halfile" "$m428" "$m429" "$m430" add_halfile_aliases "$halfile" "$m428" "$m429" "$m430" done <<<"$halfiles" + while IFS= read -r halfile; do + if hal_source=$(config_source_for_halfile "$config" "$halfile"); then + record_config_source "$path" "$hal_source" "POSTGUI_HALFILE" + fi + add_postgui_halfile_cases "$halfile" "$m428" "$m429" "$m430" + add_halfile_aliases "$halfile" "$m428" "$m429" "$m430" + done <<<"$postgui_halfiles" - for mcode in 428 429 430; do - if remap_source=$(remap_source_for_mcode "$config" "$mcode"); then + for remap_source in "$remap_source_428" "$remap_source_429" "$remap_source_430"; do + if [[ -n "$remap_source" ]]; then remap_dir=${remap_source%/*} add_alias "$remap_dir" "$m428" "$m429" "$m430" add_alias "${remap_dir%/*}" "$m428" "$m429" "$m430" add_alias "${remap_dir%/remap_subs}" "$m428" "$m429" "$m430" remap_parent=${remap_dir%/remap_subs} add_alias "${remap_parent##*/}" "$m428" "$m429" "$m430" + add_case "remap" "$remap_parent" "$m428" "$m429" "$m430" + add_case "remap" "${remap_parent##*/}" "$m428" "$m429" "$m430" fi done done <"$manifest" -if [[ "$mode" == table ]]; then +unique_config_source_count=${#used_config_sources[@]} +unique_resolved_remap_count=${#used_remap_sources[@]} + +validate_generated_content() { + if ((manifest_config_count == 0)); then + echo "no LinuxCNC config sources listed in manifest: $manifest" >&2 + exit 1 + fi + if ((manifest_remap_count == 0)); then + echo "no LinuxCNC remap sources listed in manifest: $manifest" >&2 + exit 1 + fi + if ((processed_config_count == 0)); then + echo "no LinuxCNC switchkins remap configs found in manifest: $manifest" >&2 + exit 1 + fi + if ((resolved_remap_count == 0)); then + echo "no LinuxCNC M428/M429/M430 remap sources resolved from manifest: $manifest" >&2 + exit 1 + fi + if ((unique_resolved_remap_count == 0)); then + echo "no unique LinuxCNC M428/M429/M430 remap sources resolved from manifest: $manifest" >&2 + exit 1 + fi + if ((unique_config_source_count == 0)); then + echo "no unique LinuxCNC config sources used from manifest: $manifest" >&2 + exit 1 + fi + if ((unique_config_source_count != manifest_config_count)); then + echo "LinuxCNC config manifest coverage mismatch: used $unique_config_source_count of $manifest_config_count config sources" >&2 + local config_source + for config_source in "${!manifest_config_sources[@]}"; do + if [[ -z "${used_config_sources[$config_source]:-}" ]]; then + echo "unused LinuxCNC config manifest source: $config_source" >&2 + fi + done + exit 1 + fi + 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 + local remap_source + for remap_source in "${!manifest_remap_sources[@]}"; do + if [[ -z "${used_remap_sources[$remap_source]:-}" ]]; then + echo "unused LinuxCNC remap manifest source: $remap_source" >&2 + fi + done + exit 1 + fi + if ((${#entries[@]} == 0)); then + echo "empty generated switchkins remap alias table" >&2 + exit 1 + fi + if ((${#case_entries[@]} == 0)); then + echo "empty generated switchkins remap config cases" >&2 + exit 1 + fi + if ((${#case_entries[@]} != ${#json_case_entries[@]})); then + echo "generated C++/JSON switchkins config case count mismatch: ${#case_entries[@]} != ${#json_case_entries[@]}" >&2 + exit 1 + fi +} + +validate_generated_content + +write_table() { cat <<'EOF' // Generated by ./generate-linuxcnc-switchkins-remap-table.sh. -// Source: LinuxCNC INI MACHINE/KINEMATICS/HALFILE/SUBROUTINE_PATH/REMAP entries and +// Source: LinuxCNC INI MACHINE/KINEMATICS/HALFILE/POSTGUI_HALFILE/SUBROUTINE_PATH/REMAP entries and // adjacent remap_subs/{428,429,430}remap.ngc # assignments. EOF printf '%s\n' "${entries[@]}" -elif [[ "$mode" == config-cases ]]; then +} + +write_config_cases() { cat <<'EOF' // Generated by ./generate-linuxcnc-switchkins-remap-table.sh --config-cases. -// Source: LinuxCNC INI config path, MACHINE, KINEMATICS, and non-LIB HALFILE entries -// for M428/M429/M430 remap # assignments. +// Source: LinuxCNC INI config path, MACHINE, KINEMATICS, non-LIB HALFILE/POSTGUI_HALFILE, +// and adjacent remap_subs entries for M428/M429/M430 # assignments. EOF printf '%s\n' "${case_entries[@]}" -elif [[ "$mode" == json-cases ]]; then +} + +write_json_cases() { printf '[\n' for i in "${!json_case_entries[@]}"; do suffix=, @@ -315,6 +755,34 @@ elif [[ "$mode" == json-cases ]]; then printf ' %s%s\n' "${json_case_entries[$i]}" "$suffix" done printf ']\n' +} + +write_generated_file() { + local path=$1 + local label=$2 + local writer=$3 + if [[ -e "$path" && ! -f "$path" ]]; then + echo "cannot write generated $label: $path is not a file" >&2 + exit 1 + fi + "$writer" >"$path" +} + +if [[ "$mode" == table ]]; then + write_table +elif [[ "$mode" == config-cases ]]; then + write_config_cases +elif [[ "$mode" == json-cases ]]; then + write_json_cases +elif [[ "$mode" == all ]]; then + if [[ -e "$all_output_dir" && ! -d "$all_output_dir" ]]; then + echo "switchkins remap output path is not a directory: $all_output_dir" >&2 + exit 1 + fi + mkdir -p "$all_output_dir" + write_generated_file "$all_output_dir/linuxcnc_switchkins_remap_table.inc" "table" write_table + write_generated_file "$all_output_dir/linuxcnc_switchkins_remap_config_cases.inc" "config cases" write_config_cases + write_generated_file "$all_output_dir/linuxcnc_switchkins_remap_config_cases.json" "JSON config cases" write_json_cases else echo "unknown switchkins remap table generator mode: $mode" >&2 exit 1 diff --git a/linuxcnc-kinematics-source-files.txt b/linuxcnc-kinematics-source-files.txt index bca01a0..af315f6 100644 --- a/linuxcnc-kinematics-source-files.txt +++ b/linuxcnc-kinematics-source-files.txt @@ -26,23 +26,121 @@ core:src/emc/kinematics/ugenserkins.c:user generic serial kinematics module core:src/emc/kinematics/userkfuncs.c:user kinematics helper functions used by M428/M429/M430 coverage core:src/emc/kinematics/xyzac-trt-kins.c:XYZAC table rotary tilting switchkins module core:src/emc/kinematics/xyzbc-trt-kins.c:XYZBC table rotary tilting switchkins module +core:src/libnml/posemath/_posemath.c:pose math support included by PUMA kinematics adapter +core:src/libnml/posemath/gomath.c:generic serial pose math support included by genser kinematics adapter +core:src/libnml/posemath/posemath.cc:LinuxCNC pose math C++ source tracked with kinematics posemath coverage +core:src/libnml/posemath/sincos.c:shared pose math sincos support included by PUMA/genser kinematics adapters +header:src/libnml/posemath/gomath.h:generic serial pose math declarations included by gomath source +header:src/libnml/posemath/gotypes.h:generic serial pose math type declarations included by gomath header +header:src/libnml/posemath/posemath.h:pose math declarations included by LinuxCNC kinematics adapters +header:src/libnml/posemath/sincos.h:pose math sincos declaration included by posemath sources config:configs/sim/axis/vismach/5axis/bridgemill/5axis.ini:bridgemill MACHINE/KINEMATICS and M428/M429/M430 remap configuration source +config:configs/sim/axis/vismach/5axis/bridgemill/5axisgui.hal:bridgemill HALFILE source referenced by M428/M429/M430 switchkins config +config:configs/sim/axis/vismach/5axis/bridgemill/5axis_postgui.hal:bridgemill POSTGUI_HALFILE source referenced by M428/M429/M430 switchkins config config:configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini:XYZAB table dual rotary MACHINE/KINEMATICS and M428/M429 remap configuration source +config:configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr-postgui.hal:XYZAB table dual rotary POSTGUI_HALFILE source referenced by M428/M429 switchkins config config:configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini:XYZAC TRT switchkins HAL parameter source used by M428 coverage config:configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini:XYZBC TRT switchkins HAL parameter source used by M428 coverage +config:configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal:XYZAC/XYZBC TRT POSTGUI_HALFILE source referenced by M428/M429/M430 switchkins config config:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini:XYZACB TRSRN MACHINE/KINEMATICS and M428/M429/M430 remap configuration source +config:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn_postgui.hal:XYZACB TRSRN POSTGUI_HALFILE source referenced by M428/M429/M430 switchkins config config:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini:XYZBCA TRSRN MACHINE/KINEMATICS and M428/M429/M430 remap configuration source +config:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn_postgui.hal:XYZBCA TRSRN POSTGUI_HALFILE source referenced by M428/M429/M430 switchkins config config:configs/sim/axis/vismach/hexapod-sim/hexapod.ini:hexapod MACHINE/KINEMATICS and M428/M429/M430 remap configuration source +config:configs/sim/axis/vismach/hexapod-sim/kinematics.hal:hexapod HALFILE source referenced by M428/M429/M430 switchkins config +config:configs/sim/axis/vismach/hexapod-sim/hexapod_postgui.hal:hexapod POSTGUI_HALFILE source referenced by M428/M429/M430 switchkins config config:configs/sim/axis/vismach/melfa-sim/melfa.ini:melfa MACHINE/KINEMATICS and M428/M429/M430 remap configuration source +config:configs/sim/axis/vismach/melfa-sim/melfa_dh.hal:melfa DH HALFILE source referenced by M428/M429/M430 switchkins config +config:configs/sim/axis/vismach/melfa-sim/melfa-postgui.hal:melfa POSTGUI_HALFILE source referenced by M428/M429/M430 switchkins config config:configs/sim/axis/vismach/millturn/millturn.ini:millturn MACHINE/KINEMATICS and M428/M429 remap configuration source +config:configs/sim/axis/vismach/millturn/millturn.hal:millturn HALFILE source referenced by M428/M429 switchkins config +config:configs/sim/axis/vismach/millturn/millturn-postgui.hal:millturn POSTGUI_HALFILE source referenced by M428/M429 switchkins config config:configs/sim/axis/vismach/puma/puma.ini:PUMA MACHINE/KINEMATICS and M428/M429/M430 remap configuration source +config:configs/sim/axis/vismach/puma/puma_dh.hal:PUMA DH HAL parameters referenced by PUMA switchkins configs +config:configs/sim/axis/vismach/puma/puma_postgui.hal:PUMA POSTGUI_HALFILE source referenced by M428/M429/M430 switchkins config config:configs/sim/axis/vismach/puma/puma_cube.ini:PUMA cube MACHINE/KINEMATICS and M428/M429/M430 remap configuration source config:configs/sim/axis/vismach/puma/puma560.ini:PUMA 560 MACHINE/KINEMATICS and M428/M429/M430 remap configuration source config:configs/sim/axis/vismach/puma/puma560_dh.hal:PUMA 560 genserkins DH HAL parameters used by M428 coverage +config:configs/sim/axis/vismach/puma/puma560_postgui.hal:PUMA 560 POSTGUI_HALFILE source referenced by M428/M429/M430 switchkins config config:configs/sim/axis/vismach/puma/puma560_uvw.ini:PUMA 560 UVW MACHINE/KINEMATICS and M428/M429/M430 remap configuration source config:configs/sim/axis/vismach/scara/scara.ini:AXIS SCARA MACHINE/KINEMATICS and M428/M429/M430 remap configuration source +config:configs/sim/axis/vismach/scara/scara_postgui.hal:AXIS SCARA POSTGUI_HALFILE source referenced by M428/M429/M430 switchkins config config:configs/sim/qtaxis/non-trivial/scara/scara.ini:QtAxis SCARA MACHINE/KINEMATICS and M428/M429/M430 remap configuration source config:configs/sim/qtvcp_screens/non-trivial/scara/scara.ini:QtVCP SCARA MACHINE/KINEMATICS and M428/M429/M430 remap configuration source +asset:configs/sim/axis/vismach/5axis/bridgemill/5axis.xml:bridgemill PYVCP asset referenced by M428/M429/M430 switchkins INI +asset:configs/sim/axis/vismach/5axis/bridgemill/5axisgui.ngc:bridgemill OPEN_FILE asset referenced by M428/M429/M430 switchkins INI +asset:configs/sim/axis/vismach/5axis/bridgemill/README:bridgemill configuration source notes adjacent to INI coverage +asset:configs/sim/axis/vismach/5axis/table-dual-rotary/demos/xyzab-tdr-demo.ngc:XYZAB table dual rotary OPEN_FILE demo asset referenced by M428/M429 switchkins INI +asset:configs/sim/axis/vismach/5axis/table-dual-rotary/README:XYZAB table dual rotary configuration source notes adjacent to INI coverage +asset:configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.xml:XYZAB table dual rotary PYVCP asset referenced by M428/M429 switchkins INI +asset:configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins.ngc:XYZAC TRT OPEN_FILE demo asset referenced by M428/M429/M430 switchkins INI +asset:configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc:XYZBC TRT OPEN_FILE demo asset referenced by M428/M429/M430 switchkins INI +asset:configs/sim/axis/vismach/5axis/table-rotary-tilting/README:XYZAC/XYZBC TRT configuration source notes adjacent to INI coverage +asset:configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.xml:XYZAC TRT PYVCP asset referenced by M428/M429/M430 switchkins INI +asset:configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.xml:XYZBC TRT PYVCP asset referenced by M428/M429/M430 switchkins INI +asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/incremental_repetition.ngc:TRSRN OPEN_FILE demo asset referenced by XYZACB/XYZBCA M428/M429/M430 switchkins INI files +asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/python/remap.py:TRSRN TWP Python remap source referenced by XYZACB/XYZBCA REMAP python entries +asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/python/toplevel.py:TRSRN TWP Python TOPLEVEL source referenced by XYZACB/XYZBCA INI files +asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/python/twp-helper-comp.py:TRSRN TWP HAL helper source referenced by XYZACB/XYZBCA HALCMD loadusr entries +asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/python/util.py:TRSRN TWP Python utility source imported by remap.py +asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/remap_subs/g531remap.ngc:TRSRN G53.1 remap wrapper referenced by XYZACB/XYZBCA INI REMAP entries +asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/remap_subs/g533remap.ngc:TRSRN G53.3 remap wrapper referenced by XYZACB/XYZBCA INI REMAP entries +asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/remap_subs/g536remap.ngc:TRSRN G53.6 remap wrapper referenced by XYZACB/XYZBCA INI REMAP entries +asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/remap_subs/g69remap.ngc:TRSRN G69 remap wrapper referenced by XYZACB/XYZBCA INI REMAP entries +asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/remap_subs/on_abort_no_twp_reset.ngc:TRSRN ON_ABORT source referenced by XYZACB INI +asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/remap_subs/on_abort_with_twp_reset.ngc:TRSRN ON_ABORT source referenced by XYZBCA INI +asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/vismach/twp_vismach.py:TRSRN shared vismach source imported by XYZACB/XYZBCA GUI files +asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/vismach/xyzacb-trsrn-gui.py:XYZACB TRSRN vismach GUI source referenced by HALCMD loadusr +asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/vismach/xyzbca-trsrn-gui.py:XYZBCA TRSRN vismach GUI source referenced by HALCMD loadusr +asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/README:XYZACB TRSRN TWP configuration source notes adjacent to INI coverage +asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/pyvcp_embed_tab_tcp.xml:XYZACB TRSRN embedded PyVCP TCP asset referenced by EMBED_TAB_COMMAND +asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/work_piece_1.stl:XYZACB TRSRN workpiece STL asset referenced by vismach GUI source +asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.xml:XYZACB TRSRN PYVCP asset referenced by M428/M429/M430 switchkins INI +asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/README:XYZBCA TRSRN TWP configuration source notes adjacent to INI coverage +asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/pyvcp_embed_tab_tcp.xml:XYZBCA TRSRN embedded PyVCP TCP asset referenced by EMBED_TAB_COMMAND +asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/work_piece_1.stl:XYZBCA TRSRN workpiece STL asset referenced by vismach GUI source +asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.xml:XYZBCA TRSRN PYVCP asset referenced by M428/M429/M430 switchkins INI +asset:configs/sim/axis/vismach/hexapod-sim/README:hexapod configuration source notes adjacent to INI coverage +asset:configs/sim/axis/vismach/hexapod-sim/hexapod.xml:hexapod PYVCP asset referenced by M428/M429/M430 switchkins INI +asset:configs/sim/axis/vismach/melfa-sim/example.ngc:melfa OPEN_FILE demo asset referenced by M428/M429/M430 switchkins INI +asset:configs/sim/axis/vismach/melfa-sim/README:melfa configuration source notes adjacent to INI coverage +asset:configs/sim/axis/vismach/melfa-sim/melfa.xml:melfa PYVCP asset referenced by M428/M429/M430 switchkins INI +asset:configs/sim/axis/vismach/millturn/example.ngc:millturn OPEN_FILE demo asset referenced by M428/M429 switchkins INI +asset:configs/sim/axis/vismach/millturn/README:millturn configuration source notes adjacent to INI coverage +asset:configs/sim/axis/vismach/millturn/millturn.xml:millturn PYVCP asset referenced by M428/M429 switchkins INI +asset:configs/sim/axis/vismach/puma/README:PUMA configuration source notes adjacent to INI coverage +asset:configs/sim/axis/vismach/puma/puma.xml:PUMA PYVCP asset referenced by M428/M429/M430 switchkins INI +asset:configs/sim/axis/vismach/puma/puma560.xml:PUMA 560 PYVCP asset referenced by M428/M429/M430 switchkins INI files +asset:configs/sim/axis/vismach/puma/puma_cube.ngc:PUMA cube OPEN_FILE demo asset referenced by M428/M429/M430 switchkins INI +asset:configs/sim/axis/vismach/scara/README:AXIS SCARA configuration source notes adjacent to INI coverage +asset:configs/sim/axis/vismach/scara/scara.xml:AXIS SCARA PYVCP asset referenced by M428/M429/M430 switchkins INI +asset:src/hal/user_comps/vismach/5axisgui.py:bridgemill vismach GUI source referenced by HALFILE loadusr +asset:src/hal/user_comps/vismach/hexagui.py:hexapod vismach GUI source referenced by INI HALCMD loadusr +asset:src/hal/user_comps/vismach/melfagui.py:melfa vismach GUI source referenced by INI HALCMD loadusr +asset:src/hal/user_comps/vismach/millturngui.py:millturn vismach GUI source referenced by HALFILE loadusr +asset:src/hal/user_comps/vismach/puma560gui.py:PUMA 560 vismach GUI source referenced by INI HALCMD loadusr +asset:src/hal/user_comps/vismach/pumagui.py:PUMA vismach GUI source referenced by INI HALCMD loadusr +asset:src/hal/user_comps/vismach/scaragui.py:SCARA vismach GUI source referenced by INI HALCMD loadusr +asset:src/hal/user_comps/vismach/xyzab-tdr-gui.py:XYZAB table dual rotary vismach GUI source referenced by INI HALCMD loadusr +asset:src/hal/user_comps/vismach/xyzac-trt-gui.py:XYZAC TRT vismach GUI source referenced by INI HALCMD loadusr +asset:src/hal/user_comps/vismach/xyzbc-trt-gui.py:XYZBC TRT vismach GUI source referenced by INI HALCMD loadusr +asset:share/qtvcp/panels/vismach_scara/vismach_scara.ui:QtVCP SCARA panel UI source referenced by EMBED_TAB_COMMAND +asset:share/qtvcp/panels/vismach_scara/vismach_scara_handler.py:QtVCP SCARA panel handler source referenced by EMBED_TAB_COMMAND +asset:lib/python/qtvcp/lib/qt_vismach/README.txt:QtVCP vismach library source notes adjacent to SCARA panel coverage +asset:lib/python/qtvcp/lib/qt_vismach/__init__.py:QtVCP vismach package source used by SCARA panel +asset:lib/python/qtvcp/lib/qt_vismach/primitives.py:QtVCP vismach primitives source used by SCARA panel +asset:lib/python/qtvcp/lib/qt_vismach/qt_vismach.py:QtVCP vismach base window source used by SCARA panel +asset:lib/python/qtvcp/lib/qt_vismach/scara.py:QtVCP SCARA vismach source imported by vismach_scara_handler.py +tooldata:configs/sim/axis/vismach/5axis/bridgemill/5axis.tbl:bridgemill tool table source referenced by INI TOOL_TABLE +tooldata:configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.tbl:XYZAB table dual rotary tool table source referenced by INI TOOL_TABLE +tooldata:configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.tbl:XYZAC TRT tool table source referenced by INI TOOL_TABLE +tooldata:configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.tbl:XYZBC TRT tool table source referenced by INI TOOL_TABLE +tooldata:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.tbl:XYZACB TRSRN tool table source referenced by INI TOOL_TABLE +tooldata:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.tbl:XYZBCA TRSRN tool table source referenced by INI TOOL_TABLE +tooldata:configs/sim/axis/vismach/melfa-sim/melfa.tbl:melfa tool table source referenced by INI TOOL_TABLE +tooldata:configs/sim/axis/vismach/millturn/millturn.tbl:millturn tool table source referenced by INI TOOL_TABLE +tooldata:configs/sim/axis/vismach/puma/puma.tbl:PUMA tool table source referenced by PUMA switchkins INI TOOL_TABLE entries +tooldata:configs/sim/qtaxis/non-trivial/scara/scara.tbl:QtAxis SCARA tool table source referenced by INI TOOL_TABLE remap:configs/sim/axis/vismach/5axis/bridgemill/remap_subs/428remap.ngc:LinuxCNC M428 remap source selecting bridgemill kinstype 0 remap:configs/sim/axis/vismach/5axis/bridgemill/remap_subs/429remap.ngc:LinuxCNC M429 remap source selecting bridgemill kinstype 1 remap:configs/sim/axis/vismach/5axis/bridgemill/remap_subs/430remap.ngc:LinuxCNC M430 remap source selecting bridgemill kinstype 2 @@ -74,11 +172,13 @@ remap:configs/sim/qtaxis/non-trivial/scara/remap_subs/430remap.ngc:LinuxCNC M430 remap:configs/sim/qtvcp_screens/non-trivial/scara/remap_subs/428remap.ngc:LinuxCNC M428 remap source selecting qtvcp scara kinstype 0 remap:configs/sim/qtvcp_screens/non-trivial/scara/remap_subs/429remap.ngc:LinuxCNC M429 remap source selecting qtvcp scara kinstype 1 remap:configs/sim/qtvcp_screens/non-trivial/scara/remap_subs/430remap.ngc:LinuxCNC M430 remap source selecting qtvcp scara kinstype 2 +component:src/hal/components/millturn.comp:millturn switchable kinematics component source referenced by millturn INI KINEMATICS component:src/hal/components/xyzab_tdr_kins.comp:XYZAB table dual rotary switchkins component source used by M428/M429/M430 coverage component:src/hal/components/xyzacb_trsrn.comp:XYZACB transformation component source adjacent to LinuxCNC RTCP kinematics coverage component:src/hal/components/xyzbca_trsrn.comp:XYZBCA transformation component source adjacent to LinuxCNC RTCP kinematics coverage component:src/hal/components/matrixkins.comp:matrix kinematics component source for LinuxCNC kinematics completeness component:src/hal/components/userkins.comp:user kinematics component template source for LinuxCNC switchkins/user kinematics coverage +generated:src/objects/hal/components/millturn.c:halcompile-generated millturn switchable kinematics C source used by millturn INI KINEMATICS generated:src/objects/hal/components/xyzab_tdr_kins.c:halcompile-generated XYZAB table dual rotary switchkins C source used by the adapter generated:src/objects/hal/components/xyzacb_trsrn.c:halcompile-generated XYZACB transformation C source generated:src/objects/hal/components/xyzbca_trsrn.c:halcompile-generated XYZBCA transformation C source @@ -95,4 +195,22 @@ header:src/emc/kinematics/rotarydeltakins-common.h:rotary delta shared header header:src/emc/kinematics/switchkins.h:switchable kinematics header metadata:src/emc/kinematics/Submakefile:LinuxCNC kinematics make build metadata metadata:src/emc/kinematics/meson.build:LinuxCNC kinematics meson build metadata +metadata:src/libnml/posemath/Submakefile:LinuxCNC posemath make build metadata used by kinematics pose math coverage +metadata:src/libnml/posemath/meson.build:LinuxCNC posemath meson build metadata used by kinematics pose math coverage +metadata:src/hal/user_comps/vismach/Submakefile:LinuxCNC vismach user component build metadata for loadusr GUI sources +metadata:docs/src/man/man1/5axisgui.1.adoc:LinuxCNC bridgemill vismach GUI command documentation source +metadata:docs/src/man/man1/hexagui.1.adoc:LinuxCNC hexapod vismach GUI command documentation source +metadata:docs/src/man/man1/melfagui.1.adoc:LinuxCNC melfa vismach GUI command documentation source +metadata:docs/src/man/man1/puma560gui.1.adoc:LinuxCNC PUMA 560 vismach GUI command documentation source +metadata:docs/src/man/man1/pumagui.1.adoc:LinuxCNC PUMA vismach GUI command documentation source +metadata:docs/src/man/man1/scaragui.1.adoc:LinuxCNC SCARA vismach GUI command documentation source +metadata:docs/src/man/man1/xyzab-tdr-gui.1.adoc:LinuxCNC XYZAB table dual rotary vismach GUI command documentation source +metadata:docs/src/man/man1/xyzac-trt-gui.1.adoc:LinuxCNC XYZAC TRT vismach GUI command documentation source +metadata:docs/src/man/man1/xyzbc-trt-gui.1.adoc:LinuxCNC XYZBC TRT vismach GUI command documentation source +metadata:src/objects/hal/components/matrixkins.mak:halcompile-generated matrixkins make metadata adjacent to generated kinematics source +metadata:src/objects/hal/components/millturn.mak:halcompile-generated millturn make metadata adjacent to generated kinematics source +metadata:src/objects/hal/components/userkins.mak:halcompile-generated userkins make metadata adjacent to generated kinematics source +metadata:src/objects/hal/components/xyzab_tdr_kins.mak:halcompile-generated XYZAB table dual rotary make metadata adjacent to generated kinematics source +metadata:src/objects/hal/components/xyzacb_trsrn.mak:halcompile-generated XYZACB TRSRN make metadata adjacent to generated kinematics source +metadata:src/objects/hal/components/xyzbca_trsrn.mak:halcompile-generated XYZBCA TRSRN make metadata adjacent to generated kinematics source asset:src/emc/kinematics/blend.fig:LinuxCNC kinematics reference asset tracked for full directory coverage diff --git a/list-linuxcnc-kinematics-manifest-sources.sh b/list-linuxcnc-kinematics-manifest-sources.sh index 5d79351..210edda 100755 --- a/list-linuxcnc-kinematics-manifest-sources.sh +++ b/list-linuxcnc-kinematics-manifest-sources.sh @@ -11,6 +11,6 @@ output_mode=${3:-relative} "$manifest" \ "$filter_group" \ "$output_mode" \ - core,config,remap,component,generated,header,metadata,asset \ + core,config,remap,component,generated,header,metadata,asset,tooldata \ all \ "missing kinematics manifest source" diff --git a/test-all-native.sh b/test-all-native.sh index d6d1f62..4c0dec5 100755 --- a/test-all-native.sh +++ b/test-all-native.sh @@ -23,6 +23,14 @@ missing_bridge_source_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_missing_bridge_smoke missing_support_object_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_missing_linuxcnc_support_object.XXXXXX.log") missing_source_manifest=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_native_syntax_missing_source_manifest.XXXXXX.txt") missing_source_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_native_syntax_missing_source.XXXXXX.log") +bad_line_manifest=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_bad_manifest_line.XXXXXX.txt") +bad_line_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_bad_manifest_line.XXXXXX.log") +absolute_source_manifest=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_absolute_manifest_source.XXXXXX.txt") +absolute_source_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_absolute_manifest_source.XXXXXX.log") +invalid_build_jobs_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_invalid_build_jobs.XXXXXX.log") +invalid_build_dir_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_invalid_build_dir.XXXXXX.log") +empty_build_dir_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_empty_build_dir.XXXXXX.log") +whitespace_build_dir_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_whitespace_build_dir.XXXXXX.log") cleanup_native_test_temps() { rm -f \ @@ -41,7 +49,15 @@ cleanup_native_test_temps() { "$missing_bridge_source_log" \ "$missing_support_object_log" \ "$missing_source_manifest" \ - "$missing_source_log" + "$missing_source_log" \ + "$bad_line_manifest" \ + "$bad_line_log" \ + "$absolute_source_manifest" \ + "$absolute_source_log" \ + "$invalid_build_jobs_log" \ + "$invalid_build_dir_log" \ + "$empty_build_dir_log" \ + "$whitespace_build_dir_log" } trap cleanup_native_test_temps EXIT @@ -67,12 +83,40 @@ for forbidden_manual_cleanup in \ 'rm -f "$missing_core_source_log"' \ 'rm -f "$missing_bridge_source_log"' \ 'rm -f "$missing_support_object_log"' \ - 'rm -f "$missing_source_manifest" "$missing_source_log"'; do + 'rm -f "$missing_source_manifest" "$missing_source_log"' \ + 'rm -f "$bad_line_manifest" "$bad_line_log"' \ + 'rm -f "$absolute_source_manifest" "$absolute_source_log"' \ + 'rm -f "$invalid_build_jobs_log"' \ + 'rm -f "$invalid_build_dir_log"' \ + 'rm -f "$empty_build_dir_log"' \ + 'rm -f "$whitespace_build_dir_log"'; do if grep -Fx "$forbidden_manual_cleanup" test-all-native.sh >/dev/null; then echo "native aggregate test still relies on manual cleanup: $forbidden_manual_cleanup" >&2 exit 1 fi done + +printf 'core:src/emc/rs274ngc/interp_arc.cc\n' >"$bad_line_manifest" +if ./check-linuxcnc-inputs.sh "$bad_line_manifest" >"$bad_line_log" 2>&1; then + echo "check-linuxcnc-inputs accepted a manifest line without a note" >&2 + exit 1 +fi +if ! grep -F "bad manifest line 1: core:src/emc/rs274ngc/interp_arc.cc" "$bad_line_log" >/dev/null; then + echo "check-linuxcnc-inputs did not report malformed manifest lines clearly" >&2 + sed -n '1,20p' "$bad_line_log" >&2 + exit 1 +fi + +printf 'core:/src/emc/rs274ngc/interp_arc.cc:absolute source path\n' >"$absolute_source_manifest" +if ./check-linuxcnc-inputs.sh "$absolute_source_manifest" >"$absolute_source_log" 2>&1; then + echo "check-linuxcnc-inputs accepted an absolute manifest source path" >&2 + exit 1 +fi +if ! grep -F "manifest source must be LinuxCNC-root relative: /src/emc/rs274ngc/interp_arc.cc" "$absolute_source_log" >/dev/null; then + echo "check-linuxcnc-inputs did not report absolute manifest paths clearly" >&2 + sed -n '1,20p' "$absolute_source_log" >&2 + exit 1 +fi grep -F 'exec 9>"${TMPDIR:-/tmp}/cnc_sim_rs274_source_link.lock"' test-linuxcnc-source-link.sh >/dev/null grep -F "flock 9" test-linuxcnc-source-link.sh >/dev/null grep -F "missing LinuxCNC support object" list-linuxcnc-source-support-objects.sh >/dev/null @@ -86,7 +130,6 @@ grep -F 'exec 9>"${TMPDIR:-/tmp}/cnc_sim_linuxcnc_rs274_native.lock"' test-linux grep -F "flock 9" test-linuxcnc-rs274-native.sh >/dev/null for build_dir_probe_script in \ test-linuxcnc-source-objects.sh \ - test-linuxcnc-source-link.sh \ test-linuxcnc-bridge-native.sh \ test-linuxcnc-rs274-native.sh \ test-linuxcnc-api-native.sh; do @@ -99,11 +142,261 @@ for build_dir_probe_script in \ exit 1 fi done +if ! awk ' + /cleanup_wasm_cmake_probe_temps\(\)/ { in_cleanup = 1 } + in_cleanup && index($0, "\"$build_dir\"") { found_build_dir = 1 } + in_cleanup && /^}/ { in_cleanup = 0 } + END { exit found_build_dir } +' test-linuxcnc-wasm-cmake-safe-probe.sh; then + echo "wasm CMake safe probe cleanup no longer removes its persistent build dir" >&2 + exit 1 +fi +grep -F 'build_dir=${CNC_SIM_SOURCE_LINK_BUILD_DIR-build/source-link-test}' test-linuxcnc-source-link.sh >/dev/null +grep -F 'CNC_SIM_SOURCE_LINK_BUILD_DIR must not be empty' test-linuxcnc-source-link.sh >/dev/null +grep -F 'CNC_SIM_SOURCE_LINK_BUILD_DIR must not contain whitespace: $build_dir' test-linuxcnc-source-link.sh >/dev/null +grep -F 'CNC_SIM_SOURCE_LINK_BUILD_DIR must not be the filesystem root' test-linuxcnc-source-link.sh >/dev/null +grep -F 'CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs' test-linuxcnc-source-link.sh >/dev/null +grep -F 'exec 9>"${TMPDIR:-/tmp}/cnc_sim_rs274_source_link.lock"' test-linuxcnc-source-link.sh >/dev/null +grep -F 'source_link_signature="$build_dir/source-link-build.signature"' test-linuxcnc-source-link.sh >/dev/null +grep -F 'if [[ ! -f "$source_link_signature" ]] || ! cmp -s "$source_link_signature" "$source_link_next_signature"; then' test-linuxcnc-source-link.sh >/dev/null +grep -F 'printf '\''PWD=%s\n'\'' "$PWD"' test-linuxcnc-source-link.sh >/dev/null +grep -F 'printf '\''LINUXCNC_ROOT=%s\n'\'' "$(cd "$linuxcnc_root" && pwd)"' test-linuxcnc-source-link.sh >/dev/null +grep -F 'printf '\''%s: %s\n'\'' "$build_dir/linuxcnc_${linuxcnc_index}.o" "$source"' test-linuxcnc-source-link.sh >/dev/null +grep -F 'printf '\''%s: %s\n'\'' "$build_dir/project_${project_index}.o" "$source"' test-linuxcnc-source-link.sh >/dev/null +grep -F 'printf '\''\t@$(CXX) $(COMMON_FLAGS) -MMD -MP -MF %s.d -c %s -o %s\n\n'\''' test-linuxcnc-source-link.sh >/dev/null +grep -F 'build_dir=${CNC_SIM_NATIVE_BUILD_DIR-build/native-test}' test-native.sh >/dev/null +grep -F 'CNC_SIM_NATIVE_BUILD_DIR must not be empty' test-native.sh >/dev/null +grep -F 'CNC_SIM_NATIVE_BUILD_DIR must not contain whitespace: $build_dir' test-native.sh >/dev/null +grep -F 'CNC_SIM_NATIVE_BUILD_DIR must not be the filesystem root' test-native.sh >/dev/null +grep -F 'CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs' test-native.sh >/dev/null +grep -F 'exec 9>"$build_dir/native-test.lock"' test-native.sh >/dev/null +grep -F 'native_signature="$build_dir/native-build.signature"' test-native.sh >/dev/null +grep -F 'if [[ ! -f "$native_signature" ]] || ! cmp -s "$native_signature" "$native_next_signature"; then' test-native.sh >/dev/null +grep -F 'printf '\''PWD=%s\n'\'' "$PWD"' test-native.sh >/dev/null +grep -F 'printf '\''%s: %s\n'\'' "${core_objects[$core_index]}" "$source"' test-native.sh >/dev/null +grep -F 'printf '\''\t@$(CXX) $(SMOKE_CXXFLAGS) -MMD -MP -MF %s.d -c %s -o %s\n\n'\''' test-native.sh >/dev/null +grep -F 'build_dir=${CNC_SIM_SOURCE_SYNTAX_BUILD_DIR-build/source-syntax-test}' test-linuxcnc-source-syntax.sh >/dev/null +grep -F 'CNC_SIM_SOURCE_SYNTAX_BUILD_DIR must not be empty' test-linuxcnc-source-syntax.sh >/dev/null +grep -F 'CNC_SIM_SOURCE_SYNTAX_BUILD_DIR must not contain whitespace: $build_dir' test-linuxcnc-source-syntax.sh >/dev/null +grep -F 'CNC_SIM_SOURCE_SYNTAX_BUILD_DIR must not be the filesystem root' test-linuxcnc-source-syntax.sh >/dev/null +grep -F 'CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs' test-linuxcnc-source-syntax.sh >/dev/null +grep -F 'exec 9>"$build_dir/source-syntax.lock"' test-linuxcnc-source-syntax.sh >/dev/null +grep -F 'source_list="$build_dir/linuxcnc_source_syntax_sources.txt"' test-linuxcnc-source-syntax.sh >/dev/null +grep -F 'source_syntax_signature="$build_dir/source-syntax.signature"' test-linuxcnc-source-syntax.sh >/dev/null +grep -F 'if [[ ! -f "$source_syntax_signature" ]] || ! cmp -s "$source_syntax_signature" "$source_syntax_next_signature"; then' test-linuxcnc-source-syntax.sh >/dev/null +grep -F 'printf '\''LINUXCNC_ROOT=%s\n'\'' "$(cd "$linuxcnc_root" && pwd)"' test-linuxcnc-source-syntax.sh >/dev/null +grep -F 'printf '\''MANIFEST=%s\n'\'' "$(cd "$(dirname "$manifest")" && pwd)/$(basename "$manifest")"' test-linuxcnc-source-syntax.sh >/dev/null +grep -F 'printf '\''BUILD_JOBS=%s\n'\'' "$build_jobs"' test-linuxcnc-source-syntax.sh >/dev/null +grep -F 'printf '\''%s/source_%s.stamp: %s\n'\'' "$build_dir" "$source_index" "$source"' test-linuxcnc-source-syntax.sh >/dev/null +grep -F 'printf '\''\t@$(CXX) $(COMMON_FLAGS) -MMD -MP -MF %s/source_%s.d %s\n'\''' test-linuxcnc-source-syntax.sh >/dev/null +grep -F 'make --output-sync=target -j"$build_jobs" -f "$source_syntax_makefile"' test-linuxcnc-source-syntax.sh >/dev/null +grep -F 'build_jobs=${CNC_SIM_BUILD_JOBS:-8}' build-wasm.sh >/dev/null +grep -F 'CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs' build-wasm.sh >/dev/null +grep -F 'cmake --build build/wasm --target cnc_sim_wasm_runtime_probe --parallel "$build_jobs"' build-wasm.sh >/dev/null +grep -F 'cmake --build build/wasm --target cnc_sim_wasm --parallel "$build_jobs"' build-wasm.sh >/dev/null +grep -F 'build_jobs=${CNC_SIM_BUILD_JOBS:-8}' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'build_dir=${CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR-build/wasm-cmake-safe-probe}' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not be empty' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not contain whitespace: $build_dir' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not be the filesystem root' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'build_signature="$build_dir/wasm-cmake-safe-probe.signature"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'build_next_signature=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_wasm_cmake_safe_probe.signature.XXXXXX")' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'if [[ ! -f "$build_signature" ]] || ! cmp -s "$build_signature" "$build_next_signature"; then' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'printf '\''LINUXCNC_ROOT=%s\n'\'' "$linuxcnc_root"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'printf '\''MANIFEST=%s\n'\'' "$manifest"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'printf '\''BUILD_JOBS=%s\n'\'' "$build_jobs"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'cmake --build "$build_dir" --target linuxcnc_rs274_wasm_safe_probe_objects --parallel "$build_jobs"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'cmake --build "$build_dir" --target linuxcnc_rs274_wasm_safe_probe --parallel "$build_jobs"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'cmake --build "$build_dir" --target cnc_sim_wasm_runtime_probe --parallel "$build_jobs"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'CNC_SIM_NATIVE_BUILD_DIR' docs/linuxcnc-source-policy.md >/dev/null +grep -F 'CNC_SIM_SOURCE_LINK_BUILD_DIR' docs/linuxcnc-source-policy.md >/dev/null +grep -F 'CNC_SIM_SOURCE_SYNTAX_BUILD_DIR' docs/linuxcnc-source-policy.md >/dev/null +grep -F 'CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR' docs/linuxcnc-source-policy.md >/dev/null +grep -F 'Build directory overrides must' docs/linuxcnc-source-policy.md >/dev/null +grep -F 'copies and byte-compares `cnc_sim.js` and `cnc_sim.wasm` under' docs/linuxcnc-source-policy.md >/dev/null +grep -F '`CNC_SIM_BUILD_JOBS` must be a positive integer and defaults to `8`.' docs/linuxcnc-source-policy.md >/dev/null +grep -F '`test-native.sh` uses persistent `build/native-test` objects' docs/linuxcnc-source-policy.md >/dev/null +grep -F '`test-linuxcnc-source-link.sh` uses persistent `build/source-link-test`' docs/linuxcnc-source-policy.md >/dev/null +grep -F '`test-linuxcnc-source-syntax.sh` uses persistent' docs/linuxcnc-source-policy.md >/dev/null +grep -F '`test-linuxcnc-wasm-cmake-safe-probe.sh` uses persistent' docs/linuxcnc-source-policy.md >/dev/null +grep -F 'must reject empty, whitespace-containing, or filesystem-root build' docs/linuxcnc-source-policy.md >/dev/null +grep -F 'entry points must serialize' docs/linuxcnc-source-policy.md >/dev/null +grep -F 'shared non-concurrency-safe paths with lock files' docs/linuxcnc-source-policy.md >/dev/null +grep -F '`-MMD -MP` dependency tracking and `make --output-sync=target`' docs/linuxcnc-source-policy.md >/dev/null +grep -F 'must run blocker, syntax, object, CMake, tooldata,' docs/linuxcnc-source-policy.md >/dev/null +grep -F 'must check `cmake`, `emcmake`, `emcc`, and `node`' docs/linuxcnc-source-policy.md >/dev/null +grep -F 'must build `cnc_sim_wasm_runtime_probe` before' docs/linuxcnc-source-policy.md >/dev/null +grep -F 'must copy `build/wasm/cnc_sim.js` and' docs/linuxcnc-source-policy.md >/dev/null +grep -F 'must compare copied artifacts with `cmp -s` before running' docs/linuxcnc-source-policy.md >/dev/null +grep -F 'must run the Node smoke before the browser smoke' docs/linuxcnc-source-policy.md >/dev/null +grep -F 'explicitly checked for 25' docs/linuxcnc-source-policy.md >/dev/null +grep -F 'wasm-safe core sources and 9 blocked sources' docs/linuxcnc-source-policy.md >/dev/null +grep -F 'must reject malformed lines, absolute source paths,' docs/linuxcnc-source-policy.md >/dev/null +grep -F 'must include the built LinuxCNC `lib` path and rpath' docs/linuxcnc-source-policy.md >/dev/null +grep -F 'must surface helper failures instead of hiding them' docs/linuxcnc-source-policy.md >/dev/null +grep -F 'Temporary probe reports must be cleaned on failure' docs/linuxcnc-source-policy.md >/dev/null +grep -F 'Documentation-only build policy edits should still update the guardrail' docs/linuxcnc-source-policy.md >/dev/null grep -F 'python_include_output=$(python3.13-config --includes 2>/dev/null || python3-config --includes)' list-linuxcnc-source-common-cxxflags.sh >/dev/null if grep -F 'read -r -a python_includes <<<"$(' list-linuxcnc-source-common-cxxflags.sh >/dev/null; then echo "source common cxxflags still hides python-config failures behind read" >&2 exit 1 fi +for script in test-native.sh test-linuxcnc-source-syntax.sh test-linuxcnc-source-link.sh build-wasm.sh; do + if CNC_SIM_BUILD_JOBS=0 "./$script" >"$invalid_build_jobs_log" 2>&1; then + echo "$script accepted invalid CNC_SIM_BUILD_JOBS" >&2 + exit 1 + fi + if ! grep -F "CNC_SIM_BUILD_JOBS must be a positive integer: 0" "$invalid_build_jobs_log" >/dev/null; then + echo "$script did not report invalid CNC_SIM_BUILD_JOBS clearly" >&2 + sed -n '1,20p' "$invalid_build_jobs_log" >&2 + exit 1 + fi +done +if CNC_SIM_BUILD_JOBS=0 ./test-linuxcnc-wasm-cmake-safe-probe.sh >"$invalid_build_jobs_log" 2>&1; then + echo "test-linuxcnc-wasm-cmake-safe-probe.sh accepted invalid CNC_SIM_BUILD_JOBS" >&2 + exit 1 +fi +if ! grep -F "CNC_SIM_BUILD_JOBS must be a positive integer: 0" "$invalid_build_jobs_log" >/dev/null; then + echo "test-linuxcnc-wasm-cmake-safe-probe.sh did not report invalid CNC_SIM_BUILD_JOBS clearly" >&2 + sed -n '1,20p' "$invalid_build_jobs_log" >&2 + exit 1 +fi +if CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR=/ ./test-linuxcnc-wasm-cmake-safe-probe.sh >"$invalid_build_dir_log" 2>&1; then + echo "test-linuxcnc-wasm-cmake-safe-probe.sh accepted filesystem root as build dir" >&2 + exit 1 +fi +if ! grep -F "CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not be the filesystem root" "$invalid_build_dir_log" >/dev/null; then + echo "test-linuxcnc-wasm-cmake-safe-probe.sh did not report filesystem-root build dir clearly" >&2 + sed -n '1,20p' "$invalid_build_dir_log" >&2 + exit 1 +fi +if CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR= ./test-linuxcnc-wasm-cmake-safe-probe.sh >"$empty_build_dir_log" 2>&1; then + echo "test-linuxcnc-wasm-cmake-safe-probe.sh accepted empty build dir" >&2 + exit 1 +fi +if ! grep -F "CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not be empty" "$empty_build_dir_log" >/dev/null; then + echo "test-linuxcnc-wasm-cmake-safe-probe.sh did not report empty build dir clearly" >&2 + sed -n '1,20p' "$empty_build_dir_log" >&2 + exit 1 +fi +if CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR="build/wasm safe probe" ./test-linuxcnc-wasm-cmake-safe-probe.sh >"$whitespace_build_dir_log" 2>&1; then + echo "test-linuxcnc-wasm-cmake-safe-probe.sh accepted whitespace in build dir" >&2 + exit 1 +fi +if ! grep -F "CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not contain whitespace: build/wasm safe probe" "$whitespace_build_dir_log" >/dev/null; then + echo "test-linuxcnc-wasm-cmake-safe-probe.sh did not report whitespace build dir clearly" >&2 + sed -n '1,20p' "$whitespace_build_dir_log" >&2 + exit 1 +fi +if CNC_SIM_NATIVE_BUILD_DIR=/ ./test-native.sh >"$invalid_build_dir_log" 2>&1; then + echo "test-native.sh accepted filesystem root as build dir" >&2 + exit 1 +fi +if ! grep -F "CNC_SIM_NATIVE_BUILD_DIR must not be the filesystem root" "$invalid_build_dir_log" >/dev/null; then + echo "test-native.sh did not report filesystem-root build dir clearly" >&2 + sed -n '1,20p' "$invalid_build_dir_log" >&2 + exit 1 +fi +if CNC_SIM_NATIVE_BUILD_DIR=/tmp/.. ./test-native.sh >"$invalid_build_dir_log" 2>&1; then + echo "test-native.sh accepted canonical filesystem root as build dir" >&2 + exit 1 +fi +if ! grep -F "CNC_SIM_NATIVE_BUILD_DIR must not be the filesystem root" "$invalid_build_dir_log" >/dev/null; then + echo "test-native.sh did not report canonical filesystem-root build dir clearly" >&2 + sed -n '1,20p' "$invalid_build_dir_log" >&2 + exit 1 +fi +if CNC_SIM_NATIVE_BUILD_DIR= ./test-native.sh >"$empty_build_dir_log" 2>&1; then + echo "test-native.sh accepted empty build dir" >&2 + exit 1 +fi +if ! grep -F "CNC_SIM_NATIVE_BUILD_DIR must not be empty" "$empty_build_dir_log" >/dev/null; then + echo "test-native.sh did not report empty build dir clearly" >&2 + sed -n '1,20p' "$empty_build_dir_log" >&2 + exit 1 +fi +if CNC_SIM_NATIVE_BUILD_DIR="build/native test" ./test-native.sh >"$whitespace_build_dir_log" 2>&1; then + echo "test-native.sh accepted whitespace in build dir" >&2 + exit 1 +fi +if ! grep -F "CNC_SIM_NATIVE_BUILD_DIR must not contain whitespace: build/native test" "$whitespace_build_dir_log" >/dev/null; then + echo "test-native.sh did not report whitespace build dir clearly" >&2 + sed -n '1,20p' "$whitespace_build_dir_log" >&2 + exit 1 +fi +if CNC_SIM_SOURCE_SYNTAX_BUILD_DIR=/ ./test-linuxcnc-source-syntax.sh >"$invalid_build_dir_log" 2>&1; then + echo "test-linuxcnc-source-syntax.sh accepted filesystem root as build dir" >&2 + exit 1 +fi +if ! grep -F "CNC_SIM_SOURCE_SYNTAX_BUILD_DIR must not be the filesystem root" "$invalid_build_dir_log" >/dev/null; then + echo "test-linuxcnc-source-syntax.sh did not report filesystem-root build dir clearly" >&2 + sed -n '1,20p' "$invalid_build_dir_log" >&2 + exit 1 +fi +if CNC_SIM_SOURCE_SYNTAX_BUILD_DIR=/tmp/.. ./test-linuxcnc-source-syntax.sh >"$invalid_build_dir_log" 2>&1; then + echo "test-linuxcnc-source-syntax.sh accepted canonical filesystem root as build dir" >&2 + exit 1 +fi +if ! grep -F "CNC_SIM_SOURCE_SYNTAX_BUILD_DIR must not be the filesystem root" "$invalid_build_dir_log" >/dev/null; then + echo "test-linuxcnc-source-syntax.sh did not report canonical filesystem-root build dir clearly" >&2 + sed -n '1,20p' "$invalid_build_dir_log" >&2 + exit 1 +fi +if CNC_SIM_SOURCE_SYNTAX_BUILD_DIR= ./test-linuxcnc-source-syntax.sh >"$empty_build_dir_log" 2>&1; then + echo "test-linuxcnc-source-syntax.sh accepted empty build dir" >&2 + exit 1 +fi +if ! grep -F "CNC_SIM_SOURCE_SYNTAX_BUILD_DIR must not be empty" "$empty_build_dir_log" >/dev/null; then + echo "test-linuxcnc-source-syntax.sh did not report empty build dir clearly" >&2 + sed -n '1,20p' "$empty_build_dir_log" >&2 + exit 1 +fi +if CNC_SIM_SOURCE_SYNTAX_BUILD_DIR="build/source syntax test" ./test-linuxcnc-source-syntax.sh >"$whitespace_build_dir_log" 2>&1; then + echo "test-linuxcnc-source-syntax.sh accepted whitespace in build dir" >&2 + exit 1 +fi +if ! grep -F "CNC_SIM_SOURCE_SYNTAX_BUILD_DIR must not contain whitespace: build/source syntax test" "$whitespace_build_dir_log" >/dev/null; then + echo "test-linuxcnc-source-syntax.sh did not report whitespace build dir clearly" >&2 + sed -n '1,20p' "$whitespace_build_dir_log" >&2 + exit 1 +fi +if CNC_SIM_SOURCE_LINK_BUILD_DIR=/ ./test-linuxcnc-source-link.sh >"$invalid_build_dir_log" 2>&1; then + echo "test-linuxcnc-source-link.sh accepted filesystem root as build dir" >&2 + exit 1 +fi +if ! grep -F "CNC_SIM_SOURCE_LINK_BUILD_DIR must not be the filesystem root" "$invalid_build_dir_log" >/dev/null; then + echo "test-linuxcnc-source-link.sh did not report filesystem-root build dir clearly" >&2 + sed -n '1,20p' "$invalid_build_dir_log" >&2 + exit 1 +fi +if CNC_SIM_SOURCE_LINK_BUILD_DIR=/tmp/.. ./test-linuxcnc-source-link.sh >"$invalid_build_dir_log" 2>&1; then + echo "test-linuxcnc-source-link.sh accepted canonical filesystem root as build dir" >&2 + exit 1 +fi +if ! grep -F "CNC_SIM_SOURCE_LINK_BUILD_DIR must not be the filesystem root" "$invalid_build_dir_log" >/dev/null; then + echo "test-linuxcnc-source-link.sh did not report canonical filesystem-root build dir clearly" >&2 + sed -n '1,20p' "$invalid_build_dir_log" >&2 + exit 1 +fi +if CNC_SIM_SOURCE_LINK_BUILD_DIR= ./test-linuxcnc-source-link.sh >"$empty_build_dir_log" 2>&1; then + echo "test-linuxcnc-source-link.sh accepted empty build dir" >&2 + exit 1 +fi +if ! grep -F "CNC_SIM_SOURCE_LINK_BUILD_DIR must not be empty" "$empty_build_dir_log" >/dev/null; then + echo "test-linuxcnc-source-link.sh did not report empty build dir clearly" >&2 + sed -n '1,20p' "$empty_build_dir_log" >&2 + exit 1 +fi +if CNC_SIM_SOURCE_LINK_BUILD_DIR="build/source link test" ./test-linuxcnc-source-link.sh >"$whitespace_build_dir_log" 2>&1; then + echo "test-linuxcnc-source-link.sh accepted whitespace in build dir" >&2 + exit 1 +fi +if ! grep -F "CNC_SIM_SOURCE_LINK_BUILD_DIR must not contain whitespace: build/source link test" "$whitespace_build_dir_log" >/dev/null; then + echo "test-linuxcnc-source-link.sh did not report whitespace build dir clearly" >&2 + sed -n '1,20p' "$whitespace_build_dir_log" >&2 + exit 1 +fi forbidden_common_flags_process_substitution='readarray -t common_flags < <(' forbidden_link_flags_process_substitution='readarray -t link_flags < <(' forbidden_source_common_flags_helper='./list-linuxcnc-source-common-cxxflags.sh' @@ -155,8 +448,8 @@ grep -F 'CNC_SIM_ENABLE_SMOKE_BACKEND cannot be combined with CNC_SIM_ENABLE_LIN grep -F 'smoke backend is not compiled into this build' core/src/gcode_backend.cpp >/dev/null grep -F './list-linuxcnc-bridge-smoke-project-sources.sh > "$project_source_list"' test-linuxcnc-bridge-native.sh >/dev/null grep -F 'LINUXCNC_ROOT="$linuxcnc_root" ./list-linuxcnc-source-support-objects.sh > "$support_object_list"' test-linuxcnc-source-link.sh >/dev/null -grep -F 'trap '\''rm -f "$source_list" "$missing_source_manifest" "$missing_source_log"'\'' EXIT' test-linuxcnc-source-syntax.sh >/dev/null -if grep -F 'rm -f "$missing_source_manifest" "$missing_source_log"' test-linuxcnc-source-syntax.sh >/dev/null; then +grep -Fx 'trap cleanup_source_syntax_temps EXIT' test-linuxcnc-source-syntax.sh >/dev/null +if grep -Fx "trap 'rm -f \"\$source_list\" \"\$missing_source_manifest\" \"\$missing_source_log\"' EXIT" test-linuxcnc-source-syntax.sh >/dev/null; then echo "linuxcnc source syntax probe still relies on manual cleanup for missing-source temporaries" >&2 exit 1 fi @@ -421,11 +714,11 @@ fi printf 'core:src/emc/rs274ngc/does_not_exist.cc:test missing source\n' >"$missing_source_manifest" if ./test-linuxcnc-source-syntax.sh "$missing_source_manifest" >"$missing_source_log" 2>&1; then - echo "test-linuxcnc-source-syntax.sh accepted missing manifest file" >&2 + echo "test-linuxcnc-source-syntax.sh accepted missing manifest source" >&2 exit 1 fi -if ! grep -F "missing manifest file: src/emc/rs274ngc/does_not_exist.cc" "$missing_source_log" >/dev/null; then - echo "test-linuxcnc-source-syntax.sh did not report missing manifest file clearly" >&2 +if ! grep -F "missing manifest source: src/emc/rs274ngc/does_not_exist.cc" "$missing_source_log" >/dev/null; then + echo "test-linuxcnc-source-syntax.sh did not report missing manifest source clearly" >&2 sed -n '1,20p' "$missing_source_log" >&2 exit 1 fi diff --git a/test-linuxcnc-source-link.sh b/test-linuxcnc-source-link.sh index f3a7ee2..9874f68 100755 --- a/test-linuxcnc-source-link.sh +++ b/test-linuxcnc-source-link.sh @@ -5,10 +5,29 @@ cd "$(dirname "$0")" linuxcnc_root=${LINUXCNC_ROOT:-../linuxcnc} cxx=${CXX:-g++} -build_dir=$(mktemp -d "${TMPDIR:-/tmp}/cnc_sim_rs274_source_link.XXXXXX") +build_jobs=${CNC_SIM_BUILD_JOBS:-8} +if ! [[ "$build_jobs" =~ ^[1-9][0-9]*$ ]]; then + echo "CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs" >&2 + exit 1 +fi +build_dir=${CNC_SIM_SOURCE_LINK_BUILD_DIR-build/source-link-test} +if [[ -z "$build_dir" ]]; then + echo "CNC_SIM_SOURCE_LINK_BUILD_DIR must not be empty" >&2 + exit 1 +fi +if [[ "$build_dir" =~ [[:space:]] ]]; then + echo "CNC_SIM_SOURCE_LINK_BUILD_DIR must not contain whitespace: $build_dir" >&2 + exit 1 +fi manifest=${1:-linuxcnc-rs274-source-files.txt} -trap 'rm -rf "$build_dir"' EXIT +mkdir -p "$build_dir" +build_dir=$(cd "$build_dir" && pwd) +if [[ "$build_dir" == "/" ]]; then + echo "CNC_SIM_SOURCE_LINK_BUILD_DIR must not be the filesystem root" >&2 + exit 1 +fi + LINUXCNC_ROOT="$linuxcnc_root" ./check-linuxcnc-inputs.sh "$manifest" LINUXCNC_ROOT="$linuxcnc_root" ./check-linuxcnc-switchkins-remap-table.sh linuxcnc-kinematics-source-files.txt # The LinuxCNC source-linked dump path is not concurrency-safe under parallel execution. @@ -33,7 +52,6 @@ objects=() linuxcnc_index=0 for source in "${linuxcnc_sources[@]}"; do obj="$build_dir/linuxcnc_${linuxcnc_index}.o" - "$cxx" "${common_flags[@]}" -c "$source" -o "$obj" objects+=("$obj") linuxcnc_index=$((linuxcnc_index + 1)) done @@ -44,33 +62,94 @@ project_source_list="$build_dir/linuxcnc_rs274_dump_project_sources.txt" mapfile -t project_sources < "$project_source_list" for source in "${project_sources[@]}"; do obj="$build_dir/project_${project_index}.o" - "$cxx" "${common_flags[@]}" -c "$source" -o "$obj" objects+=("$obj") project_index=$((project_index + 1)) done -"$cxx" -std=c++17 -I core/include -I core/src \ - core/tests/linuxcnc_gees_table_smoke.cpp \ - -o "$build_dir/linuxcnc_gees_table_smoke" - support_object_list="$build_dir/linuxcnc_support_objects.txt" LINUXCNC_ROOT="$linuxcnc_root" ./list-linuxcnc-source-support-objects.sh > "$support_object_list" mapfile -t support_objects < "$support_object_list" -"$cxx" "${objects[@]}" "${support_objects[@]}" \ - "${link_flags[@]}" \ - -llinuxcnc-uspace-posix \ - -llinuxcncini \ - -llinuxcnchal \ - -lpyplugin \ - -ltooldata \ - -lnml \ - -lposemath \ - -lboost_python313 \ - -lpython3.13 \ - -lfmt \ - -ldl \ - -o "$build_dir/linuxcnc_rs274_source_dump" +source_link_signature="$build_dir/source-link-build.signature" +source_link_next_signature="$build_dir/source-link-build.signature.next" +{ + printf 'CXX=%s\n' "$cxx" + printf 'PWD=%s\n' "$PWD" + printf 'LINUXCNC_ROOT=%s\n' "$(cd "$linuxcnc_root" && pwd)" + printf 'COMMON_FLAGS=' + printf ' %s' "${common_flags[@]}" + printf '\n' + printf 'LINK_FLAGS=' + printf ' %s' "${link_flags[@]}" + printf '\n' + printf 'LINUXCNC_SOURCES:\n' + printf '%s\n' "${linuxcnc_sources[@]}" + printf 'PROJECT_SOURCES:\n' + printf '%s\n' "${project_sources[@]}" + printf 'SUPPORT_OBJECTS:\n' + printf '%s\n' "${support_objects[@]}" +} > "$source_link_next_signature" +if [[ ! -f "$source_link_signature" ]] || ! cmp -s "$source_link_signature" "$source_link_next_signature"; then + rm -f "$build_dir"/linuxcnc_*.o \ + "$build_dir"/*.d \ + "$build_dir"/project_*.o \ + "$build_dir"/linuxcnc_gees_table_smoke.o \ + "$build_dir"/linuxcnc_rs274_source_dump \ + "$build_dir"/linuxcnc_gees_table_smoke +fi +mv "$source_link_next_signature" "$source_link_signature" + +source_link_makefile="$build_dir/source-link.mk" +{ + printf 'CXX := %s\n' "$cxx" + printf 'COMMON_FLAGS :=' + printf ' %s' "${common_flags[@]}" + printf '\n' + printf 'LINK_FLAGS :=' + printf ' %s' "${link_flags[@]}" + printf '\n' + printf 'OBJECTS :=' + printf ' %s' "${objects[@]}" + printf '\n' + printf 'SUPPORT_OBJECTS :=' + printf ' %s' "${support_objects[@]}" + printf '\n\n' + printf 'DEP_FILES :=\n' + for object in "${objects[@]}"; do + printf 'DEP_FILES += %s.d\n' "$object" + done + printf 'DEP_FILES += %s.d\n\n' "$build_dir/linuxcnc_gees_table_smoke.o" + printf '.PHONY: all\n' + printf 'all: %s %s\n\n' \ + "$build_dir/linuxcnc_rs274_source_dump" \ + "$build_dir/linuxcnc_gees_table_smoke" + + linuxcnc_index=0 + for source in "${linuxcnc_sources[@]}"; do + printf '%s: %s\n' "$build_dir/linuxcnc_${linuxcnc_index}.o" "$source" + printf '\t@$(CXX) $(COMMON_FLAGS) -MMD -MP -MF %s.d -c %s -o %s\n\n' \ + "$build_dir/linuxcnc_${linuxcnc_index}.o" "$source" "$build_dir/linuxcnc_${linuxcnc_index}.o" + linuxcnc_index=$((linuxcnc_index + 1)) + done + + project_index=0 + for source in "${project_sources[@]}"; do + printf '%s: %s\n' "$build_dir/project_${project_index}.o" "$source" + printf '\t@$(CXX) $(COMMON_FLAGS) -MMD -MP -MF %s.d -c %s -o %s\n\n' \ + "$build_dir/project_${project_index}.o" "$source" "$build_dir/project_${project_index}.o" + project_index=$((project_index + 1)) + done + + printf '%s: core/tests/linuxcnc_gees_table_smoke.cpp\n' "$build_dir/linuxcnc_gees_table_smoke.o" + printf '\t@$(CXX) -std=c++17 -I core/include -I core/src -MMD -MP -MF %s.d -c core/tests/linuxcnc_gees_table_smoke.cpp -o %s\n\n' \ + "$build_dir/linuxcnc_gees_table_smoke.o" "$build_dir/linuxcnc_gees_table_smoke.o" + printf '%s: %s\n' "$build_dir/linuxcnc_gees_table_smoke" "$build_dir/linuxcnc_gees_table_smoke.o" + printf '\t@$(CXX) -std=c++17 -I core/include -I core/src %s -o %s\n\n' "$build_dir/linuxcnc_gees_table_smoke.o" "$build_dir/linuxcnc_gees_table_smoke" + printf '%s: $(OBJECTS) $(SUPPORT_OBJECTS)\n' "$build_dir/linuxcnc_rs274_source_dump" + printf '\t@$(CXX) $(OBJECTS) $(SUPPORT_OBJECTS) $(LINK_FLAGS) -llinuxcnc-uspace-posix -llinuxcncini -llinuxcnchal -lpyplugin -ltooldata -lnml -lposemath -lboost_python313 -lpython3.13 -lfmt -ldl -o %s\n' "$build_dir/linuxcnc_rs274_source_dump" + printf '\n-include $(DEP_FILES)\n' +} > "$source_link_makefile" +make --output-sync=target -j"$build_jobs" -f "$source_link_makefile" var_file="$build_dir/rs274ngc-source.var" base_var_file="$build_dir/rs274ngc-source-base.var" diff --git a/test-linuxcnc-source-syntax.sh b/test-linuxcnc-source-syntax.sh index cb5624a..1fcd19f 100755 --- a/test-linuxcnc-source-syntax.sh +++ b/test-linuxcnc-source-syntax.sh @@ -5,15 +5,40 @@ cd "$(dirname "$0")" linuxcnc_root=${LINUXCNC_ROOT:-../linuxcnc} cxx=${CXX:-g++} +build_jobs=${CNC_SIM_BUILD_JOBS:-8} +if ! [[ "$build_jobs" =~ ^[1-9][0-9]*$ ]]; then + echo "CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs" >&2 + exit 1 +fi +build_dir=${CNC_SIM_SOURCE_SYNTAX_BUILD_DIR-build/source-syntax-test} +if [[ -z "$build_dir" ]]; then + echo "CNC_SIM_SOURCE_SYNTAX_BUILD_DIR must not be empty" >&2 + exit 1 +fi +if [[ "$build_dir" =~ [[:space:]] ]]; then + echo "CNC_SIM_SOURCE_SYNTAX_BUILD_DIR must not contain whitespace: $build_dir" >&2 + exit 1 +fi +mkdir -p "$build_dir" +build_dir=$(cd "$build_dir" && pwd) +if [[ "$build_dir" == "/" ]]; then + echo "CNC_SIM_SOURCE_SYNTAX_BUILD_DIR must not be the filesystem root" >&2 + exit 1 +fi +exec 9>"$build_dir/source-syntax.lock" +flock 9 manifest=${1:-linuxcnc-rs274-source-files.txt} LINUXCNC_ROOT="$linuxcnc_root" ./check-linuxcnc-inputs.sh "$manifest" missing_source_manifest=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_missing_rs274_manifest_source.XXXXXX.txt") missing_source_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_missing_rs274_manifest_source.XXXXXX.log") -source_list=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_linuxcnc_source_syntax_sources.XXXXXX.txt") -trap 'rm -f "$source_list" "$missing_source_manifest" "$missing_source_log"' EXIT -grep -Fx 'trap '\''rm -f "$source_list" "$missing_source_manifest" "$missing_source_log"'\'' EXIT' test-linuxcnc-source-syntax.sh >/dev/null +source_list="$build_dir/linuxcnc_source_syntax_sources.txt" +cleanup_source_syntax_temps() { + rm -f "$missing_source_manifest" "$missing_source_log" +} +trap cleanup_source_syntax_temps EXIT +grep -Fx 'trap cleanup_source_syntax_temps EXIT' test-linuxcnc-source-syntax.sh >/dev/null if awk '/^rm -f "\$missing_source_manifest" "\$missing_source_log"$/{ found = 1 } END { exit !found }' test-linuxcnc-source-syntax.sh >/dev/null; then echo "linuxcnc source syntax probe still relies on manual cleanup for missing-source temporaries" >&2 exit 1 @@ -24,20 +49,20 @@ LINUXCNC_ROOT="$linuxcnc_root" ./list-linuxcnc-source-manifest-sources.sh "$mani printf 'core:src/emc/rs274ngc/does_not_exist.cc:test missing source\n' >"$missing_source_manifest" if ./test-linuxcnc-source-objects.sh "$missing_source_manifest" >"$missing_source_log" 2>&1; then - echo "linuxcnc source object probe accepted missing manifest file" >&2 + echo "linuxcnc source object probe accepted missing manifest source" >&2 exit 1 fi -if ! grep -F "missing manifest file: src/emc/rs274ngc/does_not_exist.cc" "$missing_source_log" >/dev/null; then - echo "linuxcnc source object probe did not report missing manifest file clearly" >&2 +if ! grep -F "missing manifest source: src/emc/rs274ngc/does_not_exist.cc" "$missing_source_log" >/dev/null; then + echo "linuxcnc source object probe did not report missing manifest source clearly" >&2 sed -n '1,20p' "$missing_source_log" >&2 exit 1 fi if ./test-linuxcnc-source-link.sh "$missing_source_manifest" >"$missing_source_log" 2>&1; then - echo "linuxcnc source link probe accepted missing manifest file" >&2 + echo "linuxcnc source link probe accepted missing manifest source" >&2 exit 1 fi -if ! grep -F "missing manifest file: src/emc/rs274ngc/does_not_exist.cc" "$missing_source_log" >/dev/null; then - echo "linuxcnc source link probe did not report missing manifest file clearly" >&2 +if ! grep -F "missing manifest source: src/emc/rs274ngc/does_not_exist.cc" "$missing_source_log" >/dev/null; then + echo "linuxcnc source link probe did not report missing manifest source clearly" >&2 sed -n '1,20p' "$missing_source_log" >&2 exit 1 fi @@ -45,9 +70,59 @@ fi common_flag_output=$(LINUXCNC_ROOT="$linuxcnc_root" ./list-linuxcnc-source-common-cxxflags.sh --python --syntax-only) mapfile -t common_flags <<<"$common_flag_output" -while IFS= read -r source; do - [[ -z "$source" ]] && continue - "$cxx" "${common_flags[@]}" "$source" -done < "$source_list" +source_syntax_signature="$build_dir/source-syntax.signature" +source_syntax_next_signature="$build_dir/source-syntax.signature.next" +{ + printf 'CXX=%s\n' "$cxx" + printf 'PWD=%s\n' "$PWD" + printf 'LINUXCNC_ROOT=%s\n' "$(cd "$linuxcnc_root" && pwd)" + printf 'MANIFEST=%s\n' "$(cd "$(dirname "$manifest")" && pwd)/$(basename "$manifest")" + printf 'BUILD_JOBS=%s\n' "$build_jobs" + printf 'COMMON_FLAGS=' + printf ' %s' "${common_flags[@]}" + printf '\n' + printf 'SOURCES:\n' + cat "$source_list" +} > "$source_syntax_next_signature" +if [[ ! -f "$source_syntax_signature" ]] || ! cmp -s "$source_syntax_signature" "$source_syntax_next_signature"; then + rm -f "$build_dir"/source_*.stamp "$build_dir"/source_*.d +fi +mv "$source_syntax_next_signature" "$source_syntax_signature" + +source_syntax_makefile="$build_dir/source-syntax.mk" +{ + printf 'CXX := %s\n' "$cxx" + printf 'COMMON_FLAGS :=' + printf ' %s' "${common_flags[@]}" + printf '\n\n' + printf 'STAMPS :=\n' + source_index=0 + while IFS= read -r source; do + [[ -z "$source" ]] && continue + printf 'STAMPS += %s/source_%s.stamp\n' "$build_dir" "$source_index" + source_index=$((source_index + 1)) + done < "$source_list" + printf '\n.PHONY: all\n' + printf 'all: $(STAMPS)\n\n' + + source_index=0 + while IFS= read -r source; do + [[ -z "$source" ]] && continue + printf '%s/source_%s.stamp: %s\n' "$build_dir" "$source_index" "$source" + printf '\t@$(CXX) $(COMMON_FLAGS) -MMD -MP -MF %s/source_%s.d %s\n' "$build_dir" "$source_index" "$source" + printf '\t@touch %s/source_%s.stamp\n\n' "$build_dir" "$source_index" + source_index=$((source_index + 1)) + done < "$source_list" + + printf '%s' '-include' + source_index=0 + while IFS= read -r source; do + [[ -z "$source" ]] && continue + printf ' %s/source_%s.d' "$build_dir" "$source_index" + source_index=$((source_index + 1)) + done < "$source_list" + printf '\n' +} > "$source_syntax_makefile" +make --output-sync=target -j"$build_jobs" -f "$source_syntax_makefile" echo "linuxcnc rs274 source syntax probe passed" diff --git a/test-linuxcnc-wasm-cmake-safe-probe.sh b/test-linuxcnc-wasm-cmake-safe-probe.sh index 6afbbe6..34d46d7 100755 --- a/test-linuxcnc-wasm-cmake-safe-probe.sh +++ b/test-linuxcnc-wasm-cmake-safe-probe.sh @@ -4,20 +4,47 @@ set -euo pipefail cd "$(dirname "$0")" linuxcnc_root=${LINUXCNC_ROOT:-../linuxcnc} -build_dir=$(mktemp -d "${TMPDIR:-/tmp}/cnc_sim_linuxcnc_wasm_cmake_safe_probe.XXXXXX") +build_jobs=${CNC_SIM_BUILD_JOBS:-8} +if ! [[ "$build_jobs" =~ ^[1-9][0-9]*$ ]]; then + echo "CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs" >&2 + exit 1 +fi +build_dir=${CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR-build/wasm-cmake-safe-probe} +if [[ -z "$build_dir" ]]; then + echo "CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not be empty" >&2 + exit 1 +fi +if [[ "$build_dir" =~ [[:space:]] ]]; then + echo "CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not contain whitespace: $build_dir" >&2 + exit 1 +fi +mkdir -p "$build_dir" +build_dir=$(cd "$build_dir" && pwd) +if [[ "$build_dir" == "/" ]]; then + echo "CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not be the filesystem root" >&2 + exit 1 +fi manifest=${1:-linuxcnc-rs274-wasm-source-files.txt} missing_manifest=${TMPDIR:-/tmp}/cnc_sim_missing_build_wasm_manifest.txt missing_manifest_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_missing_build_wasm_manifest.XXXXXX.log") missing_root=/tmp/does-not-exist-linuxcnc missing_root_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_build_wasm_missing_root.XXXXXX.log") +invalid_build_jobs_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_build_wasm_invalid_jobs.XXXXXX.log") +invalid_build_dir_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_build_wasm_invalid_dir.XXXXXX.log") +empty_build_dir_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_build_wasm_empty_dir.XXXXXX.log") +whitespace_build_dir_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_build_wasm_whitespace_dir.XXXXXX.log") cleanup_wasm_cmake_probe_temps() { local temp_path for temp_path in \ - "$build_dir" \ "$missing_manifest" \ "$missing_manifest_log" \ "$missing_root_log" \ + "$invalid_build_jobs_log" \ + "$invalid_build_dir_log" \ + "$empty_build_dir_log" \ + "$whitespace_build_dir_log" \ + "${build_next_signature:-}" \ "${unknown_manifest_filter_log:-}" \ "${unknown_manifest_output_log:-}" \ "${missing_shim_log:-}" \ @@ -62,6 +89,22 @@ cleanup_wasm_cmake_probe_temps() { trap cleanup_wasm_cmake_probe_temps EXIT LINUXCNC_ROOT="$linuxcnc_root" ./check-linuxcnc-inputs.sh "$manifest" +manifest=$(cd "$(dirname "$manifest")" && pwd)/$(basename "$manifest") +linuxcnc_root=$(cd "$linuxcnc_root" && pwd) +build_signature="$build_dir/wasm-cmake-safe-probe.signature" +build_next_signature=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_wasm_cmake_safe_probe.signature.XXXXXX") +{ + printf 'CXX=%s\n' "${CXX:-g++}" + printf 'PWD=%s\n' "$PWD" + printf 'LINUXCNC_ROOT=%s\n' "$linuxcnc_root" + printf 'MANIFEST=%s\n' "$manifest" + printf 'BUILD_JOBS=%s\n' "$build_jobs" +} > "$build_next_signature" +if [[ ! -f "$build_signature" ]] || ! cmp -s "$build_signature" "$build_next_signature"; then + rm -rf "$build_dir" + mkdir -p "$build_dir" +fi +mv "$build_next_signature" "$build_signature" rm -f "$missing_manifest" if CNC_SIM_BUILD_WASM_SKIP_LOCK=1 ./build-wasm.sh "$missing_manifest" >"$missing_manifest_log" 2>&1; then @@ -83,6 +126,43 @@ if ! grep -F "missing LinuxCNC root: $missing_root" "$missing_root_log" >/dev/nu sed -n '1,20p' "$missing_root_log" >&2 exit 1 fi + +if CNC_SIM_BUILD_WASM_SKIP_LOCK=1 CNC_SIM_BUILD_JOBS=0 ./build-wasm.sh >"$invalid_build_jobs_log" 2>&1; then + echo "build-wasm accepted invalid CNC_SIM_BUILD_JOBS" >&2 + exit 1 +fi +if ! grep -F "CNC_SIM_BUILD_JOBS must be a positive integer: 0" "$invalid_build_jobs_log" >/dev/null; then + echo "build-wasm did not report invalid CNC_SIM_BUILD_JOBS clearly" >&2 + sed -n '1,20p' "$invalid_build_jobs_log" >&2 + exit 1 +fi +if CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR=/ ./test-linuxcnc-wasm-cmake-safe-probe.sh >"$invalid_build_dir_log" 2>&1; then + echo "test-linuxcnc-wasm-cmake-safe-probe.sh accepted filesystem root as build dir" >&2 + exit 1 +fi +if ! grep -F "CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not be the filesystem root" "$invalid_build_dir_log" >/dev/null; then + echo "test-linuxcnc-wasm-cmake-safe-probe.sh did not report filesystem-root build dir clearly" >&2 + sed -n '1,20p' "$invalid_build_dir_log" >&2 + exit 1 +fi +if CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR= ./test-linuxcnc-wasm-cmake-safe-probe.sh >"$empty_build_dir_log" 2>&1; then + echo "test-linuxcnc-wasm-cmake-safe-probe.sh accepted empty build dir" >&2 + exit 1 +fi +if ! grep -F "CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not be empty" "$empty_build_dir_log" >/dev/null; then + echo "test-linuxcnc-wasm-cmake-safe-probe.sh did not report empty build dir clearly" >&2 + sed -n '1,20p' "$empty_build_dir_log" >&2 + exit 1 +fi +if CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR="build/wasm safe probe" ./test-linuxcnc-wasm-cmake-safe-probe.sh >"$whitespace_build_dir_log" 2>&1; then + echo "test-linuxcnc-wasm-cmake-safe-probe.sh accepted whitespace in build dir" >&2 + exit 1 +fi +if ! grep -F "CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not contain whitespace: build/wasm safe probe" "$whitespace_build_dir_log" >/dev/null; then + echo "test-linuxcnc-wasm-cmake-safe-probe.sh did not report whitespace build dir clearly" >&2 + sed -n '1,20p' "$whitespace_build_dir_log" >&2 + exit 1 +fi grep -F 'exec 9>"${TMPDIR:-/tmp}/cnc_sim_build_wasm.lock"' build-wasm.sh >/dev/null grep -F "flock 9" build-wasm.sh >/dev/null grep -F -- './test-linuxcnc-wasm-blockers.sh "$manifest"' build-wasm.sh >/dev/null @@ -207,9 +287,50 @@ grep -F -- 'for required_tool in cmake emcmake emcc node; do' build-wasm.sh >/de grep -F -- 'missing_tools+=("$required_tool")' build-wasm.sh >/dev/null grep -F "WASM build tools are required. Missing:" build-wasm.sh >/dev/null grep -F "Install/activate cmake, emsdk, and Node.js so cmake, emcmake, emcc, and node are in PATH." build-wasm.sh >/dev/null +grep -F 'build_jobs=${CNC_SIM_BUILD_JOBS:-8}' build-wasm.sh >/dev/null +grep -F 'CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs' build-wasm.sh >/dev/null +grep -F 'build_jobs=${CNC_SIM_BUILD_JOBS:-8}' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'build_dir=${CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR-build/wasm-cmake-safe-probe}' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not be empty' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not contain whitespace: $build_dir' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not be the filesystem root' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'build_signature="$build_dir/wasm-cmake-safe-probe.signature"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'build_next_signature=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_wasm_cmake_safe_probe.signature.XXXXXX")' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'if [[ ! -f "$build_signature" ]] || ! cmp -s "$build_signature" "$build_next_signature"; then' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'printf '\''LINUXCNC_ROOT=%s\n'\'' "$linuxcnc_root"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'printf '\''MANIFEST=%s\n'\'' "$manifest"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'printf '\''BUILD_JOBS=%s\n'\'' "$build_jobs"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F 'build_jobs=${CNC_SIM_BUILD_JOBS:-8}' test-native.sh >/dev/null +grep -F 'CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs' test-native.sh >/dev/null +grep -F 'build_dir=${CNC_SIM_NATIVE_BUILD_DIR-build/native-test}' test-native.sh >/dev/null +grep -F 'CNC_SIM_NATIVE_BUILD_DIR must not be empty' test-native.sh >/dev/null +grep -F 'CNC_SIM_NATIVE_BUILD_DIR must not contain whitespace: $build_dir' test-native.sh >/dev/null +grep -F 'exec 9>"$build_dir/native-test.lock"' test-native.sh >/dev/null +grep -F 'native_signature="$build_dir/native-build.signature"' test-native.sh >/dev/null +grep -F 'if [[ ! -f "$native_signature" ]] || ! cmp -s "$native_signature" "$native_next_signature"; then' test-native.sh >/dev/null +grep -F 'printf '\''PWD=%s\n'\'' "$PWD"' test-native.sh >/dev/null +grep -F 'printf '\''\t@$(CXX) $(SMOKE_CXXFLAGS) -MMD -MP -MF %s.d -c %s -o %s\n\n'\''' test-native.sh >/dev/null +grep -F 'printf '\''\n-include $(DEP_FILES)\n'\''' test-native.sh >/dev/null +grep -F 'make --output-sync=target -j"$build_jobs" -f "$native_makefile"' test-native.sh >/dev/null +grep -F 'build_jobs=${CNC_SIM_BUILD_JOBS:-8}' test-linuxcnc-source-link.sh >/dev/null +grep -F 'CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs' test-linuxcnc-source-link.sh >/dev/null +grep -F 'build_dir=${CNC_SIM_SOURCE_LINK_BUILD_DIR-build/source-link-test}' test-linuxcnc-source-link.sh >/dev/null +grep -F 'CNC_SIM_SOURCE_LINK_BUILD_DIR must not be empty' test-linuxcnc-source-link.sh >/dev/null +grep -F 'CNC_SIM_SOURCE_LINK_BUILD_DIR must not contain whitespace: $build_dir' test-linuxcnc-source-link.sh >/dev/null +grep -F 'source_link_signature="$build_dir/source-link-build.signature"' test-linuxcnc-source-link.sh >/dev/null +grep -F 'if [[ ! -f "$source_link_signature" ]] || ! cmp -s "$source_link_signature" "$source_link_next_signature"; then' test-linuxcnc-source-link.sh >/dev/null +grep -F 'printf '\''PWD=%s\n'\'' "$PWD"' test-linuxcnc-source-link.sh >/dev/null +grep -F 'printf '\''LINUXCNC_ROOT=%s\n'\'' "$(cd "$linuxcnc_root" && pwd)"' test-linuxcnc-source-link.sh >/dev/null +grep -F 'printf '\''\t@$(CXX) $(COMMON_FLAGS) -MMD -MP -MF %s.d -c %s -o %s\n\n'\''' test-linuxcnc-source-link.sh >/dev/null +grep -F 'printf '\''\n-include $(DEP_FILES)\n'\''' test-linuxcnc-source-link.sh >/dev/null +grep -F 'make --output-sync=target -j"$build_jobs" -f "$source_link_makefile"' test-linuxcnc-source-link.sh >/dev/null grep -F -- 'emcmake cmake -S core -B build/wasm \' build-wasm.sh >/dev/null -grep -F -- 'cmake --build build/wasm --target cnc_sim_wasm_runtime_probe' build-wasm.sh >/dev/null -grep -F -- 'cmake --build build/wasm --target cnc_sim_wasm' build-wasm.sh >/dev/null +grep -F -- 'cmake --build build/wasm --target cnc_sim_wasm_runtime_probe --parallel "$build_jobs"' build-wasm.sh >/dev/null +grep -F -- 'cmake --build build/wasm --target cnc_sim_wasm --parallel "$build_jobs"' build-wasm.sh >/dev/null +grep -F -- 'cmake --build "$build_dir" --target linuxcnc_rs274_wasm_safe_probe_objects --parallel "$build_jobs"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F -- 'cmake --build "$build_dir" --target linuxcnc_rs274_wasm_safe_probe --parallel "$build_jobs"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null +grep -F -- 'cmake --build "$build_dir" --target cnc_sim_wasm_runtime_probe --parallel "$build_jobs"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null grep -F -- 'mkdir -p web/public' build-wasm.sh >/dev/null grep -F -- 'cp build/wasm/cnc_sim.js web/public/cnc_sim.js' build-wasm.sh >/dev/null grep -F -- 'cp build/wasm/cnc_sim.wasm web/public/cnc_sim.wasm' build-wasm.sh >/dev/null @@ -217,6 +338,41 @@ grep -F -- 'cmp -s build/wasm/cnc_sim.js web/public/cnc_sim.js' build-wasm.sh >/ grep -F -- 'cmp -s build/wasm/cnc_sim.wasm web/public/cnc_sim.wasm' build-wasm.sh >/dev/null grep -F -- 'node test-web-wasm-node-smoke.cjs' build-wasm.sh >/dev/null grep -F -- './test-web-wasm-browser-smoke.sh' build-wasm.sh >/dev/null +grep -F 'const opfsOptions = options.opfs ?? (isOpfsAvailable() ? {} : false)' web/src/wasm-core.js >/dev/null +grep -F 'OPFS cannot be disabled in browser contexts with OPFS support' web/src/wasm-core.js >/dev/null +grep -F 'async function opfsEntryExists(workspacePath, relativePath)' web/src/wasm-core.js >/dev/null +grep -F 'async function statOpfsEntry(workspacePath, relativePath)' web/src/wasm-core.js >/dev/null +grep -F 'async function readOpfsDirectoryTree(workspacePath, relativePath, relativeRoot = "")' web/src/wasm-core.js >/dev/null +grep -F 'function readWasmDirectoryTree(module, rootPath, relativeRoot = "")' web/src/wasm-core.js >/dev/null +grep -F 'function writeWasmFileReplacingPath(module, path, data)' web/src/wasm-core.js >/dev/null +grep -F 'copyFile(fromPath: string, toPath: string): Promise;' web/src/wasm-core.d.ts >/dev/null +grep -F 'moveFile(fromPath: string, toPath: string): Promise;' web/src/wasm-core.d.ts >/dev/null +grep -F 'exists(path: string): Promise;' web/src/wasm-core.d.ts >/dev/null +grep -F 'stat(path: string): Promise;' web/src/wasm-core.d.ts >/dev/null +grep -F 'LinuxCNC source basis: rs274ngc_pre.cc restore_parameters() reads the' web/src/wasm-core.js >/dev/null +grep -F 'before replacing the main file and managing filename + ".bak".' web/src/wasm-core.js >/dev/null +grep -F 'await removeOpfsEntry(workspacePath, `${path}.new`, false);' web/src/wasm-core.js >/dev/null +grep -F 'OPFS workspace is required for browser program parsing' web/src/app.js >/dev/null +grep -F 'const events = await simulator.parseFileWithParameterFile(programPath, parameterPath, "linuxcnc", parseOptions);' web/src/app.js >/dev/null +grep -F 'default browser simulator did not create an OPFS workspace' web/test-browser-wasm-smoke-opfs-policy-sections.js >/dev/null +grep -F 'browser simulator allowed OPFS to be disabled' web/test-browser-wasm-smoke-opfs-policy-sections.js >/dev/null +grep -F 'app frame did not restore current program from OPFS after reload' web/test-browser-wasm-smoke-app-sections.js >/dev/null +grep -F 'app frame did not report OPFS program restore after reload' web/test-browser-wasm-smoke-app-sections.js >/dev/null +grep -F 'app frame did not persist LinuxCNC parameter state into OPFS' web/test-browser-wasm-smoke-app-sections.js >/dev/null +grep -F 'app frame did not reload LinuxCNC parameter state from OPFS after reload' web/test-browser-wasm-smoke-app-sections.js >/dev/null +grep -F '"parameters/missing-main.var.new", "stale temporary without main file' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null +grep -F 'OPFS copyFile did not create the backup file' web/test-browser-wasm-smoke-opfs-mirror-sections.js >/dev/null +grep -F 'OPFS copyFile did not update the WASM mirror destination' web/test-browser-wasm-smoke-opfs-mirror-sections.js >/dev/null +grep -F 'OPFS moveFile did not replace the destination file' web/test-browser-wasm-smoke-opfs-mirror-sections.js >/dev/null +grep -F 'OPFS moveFile left the WASM mirror source behind' web/test-browser-wasm-smoke-opfs-mirror-sections.js >/dev/null +grep -F 'OPFS metadata queries polluted the WASM filesystem mirror' web/test-browser-wasm-smoke-opfs-directory-sections.js >/dev/null +grep -F 'OPFS persistDirectory did not preserve an empty directory' web/test-browser-wasm-smoke-opfs-directory-sections.js >/dev/null +grep -F 'OPFS readDirectory did not mirror an empty directory into WASM FS' web/test-browser-wasm-smoke-opfs-directory-sections.js >/dev/null +grep -F 'OPFS readFile did not replace the WASM mirror directory with a file' web/test-browser-wasm-smoke-opfs-directory-sections.js >/dev/null +grep -F 'stale LinuxCNC missing-parameter temporary file remained in OPFS' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null +grep -F 'stale LinuxCNC missing-parameter temporary file remained in WASM FS' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null +grep -F 'LinuxCNC out-of-order parameter file remained in WASM FS after restore failure' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null +grep -F 'LinuxCNC out-of-range parameter file remained in WASM FS after restore failure' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null if [[ "$(grep -Fc 'cp build/wasm/cnc_sim.js' build-wasm.sh)" -ne 1 ]]; then echo "build-wasm should copy cnc_sim.js exactly once" >&2 exit 1 @@ -251,8 +407,8 @@ if ! awk ' exit 1 fi if ! awk ' - index($0, "cmake --build build/wasm --target cnc_sim_wasm_runtime_probe") { runtime_probe_line = NR } - index($0, "cmake --build build/wasm --target cnc_sim_wasm") && + index($0, "cmake --build build/wasm --target cnc_sim_wasm_runtime_probe --parallel \"$build_jobs\"") { runtime_probe_line = NR } + index($0, "cmake --build build/wasm --target cnc_sim_wasm --parallel \"$build_jobs\"") && !index($0, "cnc_sim_wasm_runtime_probe") { wasm_line = NR } END { exit !(runtime_probe_line && wasm_line && runtime_probe_line < wasm_line) } ' build-wasm.sh; then @@ -838,10 +994,10 @@ cmake -S core -B "$build_dir" \ -DCNC_SIM_LINUXCNC_WASM_SOURCE_MANIFEST="$manifest" \ -DCNC_SIM_ENABLE_LINUXCNC_WASM_SAFE_PROBE=ON -cmake --build "$build_dir" --target linuxcnc_rs274_wasm_safe_probe_objects -cmake --build "$build_dir" --target linuxcnc_rs274_wasm_safe_probe +cmake --build "$build_dir" --target linuxcnc_rs274_wasm_safe_probe_objects --parallel "$build_jobs" +cmake --build "$build_dir" --target linuxcnc_rs274_wasm_safe_probe --parallel "$build_jobs" "$build_dir/linuxcnc_rs274_wasm_safe_probe" -cmake --build "$build_dir" --target cnc_sim_wasm_runtime_probe +cmake --build "$build_dir" --target cnc_sim_wasm_runtime_probe --parallel "$build_jobs" "$build_dir/cnc_sim_wasm_runtime_probe" echo "linuxcnc wasm-safe CMake probe target passed" diff --git a/test-linuxcnc-wasm-rs274ngc-pre-link-blockers.sh b/test-linuxcnc-wasm-rs274ngc-pre-link-blockers.sh index 8305582..877a9a1 100755 --- a/test-linuxcnc-wasm-rs274ngc-pre-link-blockers.sh +++ b/test-linuxcnc-wasm-rs274ngc-pre-link-blockers.sh @@ -100,7 +100,7 @@ nm --defined-only "$build_dir"/*.o \ | awk ' $0 ~ /^(_GLOBAL_OFFSET_TABLE_|_Unwind_Resume)$/ { next } $0 ~ /^(__assert_fail|__cxa_|__divdc3|__dso_handle|__errno_location|__gxx_personality_v0|__isoc23_|__muldc3|__stack_chk_fail)/ { next } - $0 ~ /^(abort|access|acos|asin|atan2|atof|atoi|basename|cabs|calloc|ceil|close|closedir|copysign|cos|cosl|exit|exp|fclose|fdatasync|fdopen|feof|fflush|fgetc|fgets|fileno|floor|fmax|fmod|fopen|fprintf|fputs|free|freelocale|fseek|fstat|ftell|fwrite|getcwd|getenv|getpid|gettimeofday|hypot|islower|isprint|isspace|isupper|isxdigit|link|localtime|log|lstat|malloc|memchr|memcmp|memcpy|memmove|memset|mkstemp|nearbyint|newlocale|open|opendir|perror|pow|printf|pthread_mutex_lock|pthread_mutex_unlock|putc|puts|read|readdir|realpath|realloc|remove|rename|setlocale|sin|sinl|snprintf|sprintf|sqrt|sscanf|stat|stderr|stdout|strcasecmp|strcat|strchr|strcmp|strcpy|strdup|strerror|strlen|strncasecmp|strncat|strncmp|strncpy|strrchr|strspn|strstr|strtod|strtok_r|strtol|strtoul|tan|tanl|tolower|toupper|towlower|unlink|uselocale|vfprintf|vsnprintf|wordexp|wordfree)$/ { next } + $0 ~ /^(abort|access|acos|asin|atan2|atof|atoi|basename|cabs|calloc|ceil|close|closedir|copysign|cos|cosl|exit|exp|fclose|fdatasync|fdopen|feof|fflush|fgetc|fgets|fileno|floor|fmax|fmod|fopen|fprintf|fputs|free|freelocale|fseek|fstat|ftell|fwrite|getcwd|getenv|getpid|gettimeofday|hypot|isalnum|isalpha|islower|isprint|isspace|isupper|isxdigit|link|localtime|log|lstat|malloc|memchr|memcmp|memcpy|memmove|memset|mkstemp|nearbyint|newlocale|open|opendir|perror|pow|printf|pthread_mutex_lock|pthread_mutex_unlock|putc|puts|read|readdir|realpath|realloc|remove|rename|setlocale|sin|sinl|snprintf|sprintf|sqrt|sscanf|stat|stderr|stdout|strcasecmp|strcat|strchr|strcmp|strcpy|strdup|strerror|strlen|strncasecmp|strncat|strncmp|strncpy|strrchr|strspn|strstr|strtod|strtok_r|strtol|strtoul|tan|tanl|tolower|toupper|towlower|unlink|uselocale|vfprintf|vsnprintf|wordexp|wordfree)$/ { next } $0 ~ /^(_ZN3fmt|_ZSt|_ZNSt|_ZNKSt|_ZNKRSt|_ZNS|_ZTI|_ZTS|_ZTV|_Zdl|_Znwm|_Znam|_Zda|_ZdaPv|_ZdaPvm|_ZdlPvm|_ZTv|_ZTh)/ { next } { print } ' diff --git a/test-native.sh b/test-native.sh index c1ec568..fecebcd 100755 --- a/test-native.sh +++ b/test-native.sh @@ -4,24 +4,86 @@ set -euo pipefail cd "$(dirname "$0")" cxx=${CXX:-g++} -build_dir=$(mktemp -d "${TMPDIR:-/tmp}/cnc_sim_native.XXXXXX") -trap 'rm -rf "$build_dir"' EXIT +build_jobs=${CNC_SIM_BUILD_JOBS:-8} +if ! [[ "$build_jobs" =~ ^[1-9][0-9]*$ ]]; then + echo "CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs" >&2 + exit 1 +fi +build_dir=${CNC_SIM_NATIVE_BUILD_DIR-build/native-test} +if [[ -z "$build_dir" ]]; then + echo "CNC_SIM_NATIVE_BUILD_DIR must not be empty" >&2 + exit 1 +fi +if [[ "$build_dir" =~ [[:space:]] ]]; then + echo "CNC_SIM_NATIVE_BUILD_DIR must not contain whitespace: $build_dir" >&2 + exit 1 +fi +mkdir -p "$build_dir" +build_dir=$(cd "$build_dir" && pwd) +if [[ "$build_dir" == "/" ]]; then + echo "CNC_SIM_NATIVE_BUILD_DIR must not be the filesystem root" >&2 + exit 1 +fi +exec 9>"$build_dir/native-test.lock" +flock 9 + ./check-linuxcnc-inputs.sh linuxcnc-rs274-source-files.txt --require-rs274-complete ./check-linuxcnc-inputs.sh linuxcnc-rs274-wasm-source-files.txt --require-rs274-complete ./check-linuxcnc-inputs.sh linuxcnc-kinematics-source-files.txt --require-kinematics-complete ./check-linuxcnc-switchkins-remap-table.sh linuxcnc-kinematics-source-files.txt grep -F '#include "linuxcnc_switchkins_remap_table.inc"' core/src/cnc_sim_api.cpp >/dev/null grep -F 'switchkins: options.switchkins' web/src/wasm-core.js >/dev/null +grep -F 'remap: options.remap' web/src/wasm-core.js >/dev/null +grep -F 'iniFile: options.iniFile' web/src/wasm-core.js >/dev/null +grep -F 'iniFileName: options.iniFileName' web/src/wasm-core.js >/dev/null +grep -F 'INI_FILE_NAME: options.INI_FILE_NAME' web/src/wasm-core.js >/dev/null +grep -F 'halFile: options.halFile' web/src/wasm-core.js >/dev/null +grep -F 'postguiHalFile: options.postguiHalFile' web/src/wasm-core.js >/dev/null +grep -F 'POSTGUI_HALFILE: options.POSTGUI_HALFILE' web/src/wasm-core.js >/dev/null grep -F 'kinematics: options.kinematics' web/src/wasm-core.js >/dev/null grep -F 'pivotLength: options.pivotLength' web/src/wasm-core.js >/dev/null grep -F 'xyzbcTrt: options.xyzbcTrt' web/src/wasm-core.js >/dev/null grep -F 'globalThis.createCncSimModule' web/src/wasm-core.js >/dev/null grep -F 'createModule(moduleOptions)' web/src/wasm-core.js >/dev/null +grep -F 'async parseFile(path, dialect = "linuxcnc", options = {})' web/src/wasm-core.js >/dev/null +grep -F 'async parseWithParameterFile(program, parameterPath, dialect = "linuxcnc", options = {})' web/src/wasm-core.js >/dev/null +grep -F 'async parseFileWithParameterFile(path, parameterPath, dialect = "linuxcnc", options = {})' web/src/wasm-core.js >/dev/null +grep -F 'LINUXCNC_DEFAULT_PARAMETER_FILE = "rs274ngc.var"' web/src/wasm-core.js >/dev/null grep -F 'export type CncParseOptions' web/src/index.ts >/dev/null +grep -F 'switchkins?: string;' web/src/index.ts >/dev/null +grep -F 'remap?: string;' web/src/index.ts >/dev/null +grep -F 'iniFile?: string;' web/src/index.ts >/dev/null +grep -F 'iniFileName?: string;' web/src/index.ts >/dev/null +grep -F 'INI_FILE_NAME?: string;' web/src/index.ts >/dev/null +grep -F 'halFile?: string;' web/src/index.ts >/dev/null +grep -F 'postguiHalFile?: string;' web/src/index.ts >/dev/null +grep -F 'POSTGUI_HALFILE?: string;' web/src/index.ts >/dev/null grep -F '): CncEvent[];' web/src/index.ts >/dev/null grep -F 'options?: CncParseOptions' web/src/wasm-core.d.ts >/dev/null +grep -F 'parseFile(path: string, dialect?: CncDialect, options?: CncParseOptions): Promise;' web/src/wasm-core.d.ts >/dev/null +grep -F 'parseWithParameterFile(' web/src/wasm-core.d.ts >/dev/null +grep -F 'parseFileWithParameterFile(' web/src/wasm-core.d.ts >/dev/null +grep -F 'copyFile(fromPath: string, toPath: string): Promise;' web/src/wasm-core.d.ts >/dev/null +grep -F 'moveFile(fromPath: string, toPath: string): Promise;' web/src/wasm-core.d.ts >/dev/null +grep -F 'exists(path: string): Promise;' web/src/wasm-core.d.ts >/dev/null +grep -F 'stat(path: string): Promise;' web/src/wasm-core.d.ts >/dev/null +grep -F 'loadParameterFile(path: string): Promise;' web/src/wasm-core.d.ts >/dev/null +grep -F 'async function opfsEntryExists(workspacePath, relativePath)' web/src/wasm-core.js >/dev/null +grep -F 'async function statOpfsEntry(workspacePath, relativePath)' web/src/wasm-core.js >/dev/null +grep -F 'async function readOpfsDirectoryTree(workspacePath, relativePath, relativeRoot = "")' web/src/wasm-core.js >/dev/null +grep -F 'function readWasmDirectoryTree(module, rootPath, relativeRoot = "")' web/src/wasm-core.js >/dev/null +grep -F 'function writeWasmFileReplacingPath(module, path, data)' web/src/wasm-core.js >/dev/null +grep -F '`${LINUXCNC_DEFAULT_PARAMETER_FILE}.bak`' web/src/wasm-core.js >/dev/null grep -F 'export type WasmModuleOptions' web/src/wasm-core.d.ts >/dev/null grep -F 'printErr?: (text: string) => void;' web/src/wasm-core.d.ts >/dev/null +grep -F '"field":"switchkins"' web/public/linuxcnc_switchkins_remap_config_cases.json >/dev/null +grep -F '"field":"remap"' web/public/linuxcnc_switchkins_remap_config_cases.json >/dev/null +grep -F '"field":"postguiHalFile"' web/public/linuxcnc_switchkins_remap_config_cases.json >/dev/null +grep -F '"field":"POSTGUI_HALFILE"' web/public/linuxcnc_switchkins_remap_config_cases.json >/dev/null +grep -F '{"switchkins", ' core/tests/linuxcnc_switchkins_remap_config_cases.inc >/dev/null +grep -F '{"remap", ' core/tests/linuxcnc_switchkins_remap_config_cases.inc >/dev/null +grep -F '{"postguiHalFile", ' core/tests/linuxcnc_switchkins_remap_config_cases.inc >/dev/null +grep -F '{"POSTGUI_HALFILE", ' core/tests/linuxcnc_switchkins_remap_config_cases.inc >/dev/null grep -F '"moduleResolution": "Bundler"' web/tsconfig.json >/dev/null grep -F '"include": ["src/index.ts", "src/**/*.d.ts"]' web/tsconfig.json >/dev/null grep -F 'createWasmSimulator' test-web-wasm-node-smoke.cjs >/dev/null @@ -29,10 +91,90 @@ grep -F 'locateFile: (file) => path.join(wasmDir, file)' test-web-wasm-node-smok grep -F 'printErr: () => {}' test-web-wasm-node-smoke.cjs >/dev/null grep -F 'expected web wasm-core options to pass LinuxCNC xyzac-trt RTCP config into WASM' test-web-wasm-node-smoke.cjs >/dev/null grep -F 'node test-web-wasm-node-smoke.cjs' test-web-wasm-node-smoke.sh >/dev/null +grep -F 'WASM_NODE_SMOKE_MIN_STEPS:-50' test-web-wasm-node-smoke.sh >/dev/null +grep -F 'web wasm node smoke passed ([0-9][0-9]* steps)' test-web-wasm-node-smoke.sh >/dev/null grep -F 'createWasmSimulator' web/test-browser-wasm-smoke.html >/dev/null +grep -F 'runBrowserWasmSmoke' web/test-browser-wasm-smoke.html >/dev/null +grep -F 'runBrowserOpfsWorkspaceSections' web/test-browser-wasm-smoke-sections.js >/dev/null +grep -F 'runBrowserOpfsPolicySections' web/test-browser-wasm-smoke-sections.js >/dev/null +grep -F 'runBrowserAppOpfsSections' web/test-browser-wasm-smoke-sections.js >/dev/null +grep -F 'runLinuxCncBrowserSections' web/test-browser-wasm-smoke-sections.js >/dev/null +grep -F 'runBrowserOpfsBasicSections' web/test-browser-wasm-smoke-opfs-workspace-sections.js >/dev/null +grep -F 'runBrowserOpfsParameterSections' web/test-browser-wasm-smoke-opfs-workspace-sections.js >/dev/null +grep -F 'runBrowserOpfsMirrorSections' web/test-browser-wasm-smoke-opfs-workspace-sections.js >/dev/null +grep -F 'runBrowserOpfsDirectorySections' web/test-browser-wasm-smoke-opfs-workspace-sections.js >/dev/null +grep -F 'await simulator.parseFile("programs/browser-smoke.ngc", "linuxcnc"' web/test-browser-wasm-smoke-opfs-basic-sections.js >/dev/null +grep -F 'await simulator.parseWithParameterFile(' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null +grep -F 'await simulator.parseFileWithParameterFile(' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null +grep -F 'OPFS copyFile did not create the backup file' web/test-browser-wasm-smoke-opfs-mirror-sections.js >/dev/null +grep -F 'OPFS copyFile did not update the WASM mirror destination' web/test-browser-wasm-smoke-opfs-mirror-sections.js >/dev/null +grep -F 'OPFS moveFile did not replace the destination file' web/test-browser-wasm-smoke-opfs-mirror-sections.js >/dev/null +grep -F 'OPFS moveFile left the WASM mirror source behind' web/test-browser-wasm-smoke-opfs-mirror-sections.js >/dev/null +grep -F 'OPFS metadata queries polluted the WASM filesystem mirror' web/test-browser-wasm-smoke-opfs-directory-sections.js >/dev/null +grep -F 'OPFS persistDirectory did not preserve an empty directory' web/test-browser-wasm-smoke-opfs-directory-sections.js >/dev/null +grep -F 'OPFS readDirectory did not mirror an empty directory into WASM FS' web/test-browser-wasm-smoke-opfs-directory-sections.js >/dev/null +grep -F 'OPFS readFile did not replace the WASM mirror directory with a file' web/test-browser-wasm-smoke-opfs-directory-sections.js >/dev/null +grep -F '"parameters/rs274ngc.var"' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null +grep -F '"parameters/rs274ngc.var.bak"' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null +grep -F 'const opfsOptions = options.opfs ?? (isOpfsAvailable() ? {} : false)' web/src/wasm-core.js >/dev/null +grep -F 'OPFS cannot be disabled in browser contexts with OPFS support' web/src/wasm-core.js >/dev/null +grep -F 'LinuxCNC source basis: rs274ngc_pre.cc restore_parameters() reads the' web/src/wasm-core.js >/dev/null +grep -F 'before replacing the main file and managing filename + ".bak".' web/src/wasm-core.js >/dev/null +grep -F 'await removeOpfsEntry(workspacePath, `${path}.new`, false);' web/src/wasm-core.js >/dev/null +grep -F 'OPFS workspace is required for browser program parsing' web/src/app.js >/dev/null +grep -F 'const events = await simulator.parseFileWithParameterFile(programPath, parameterPath, "linuxcnc", parseOptions);' web/src/app.js >/dev/null +grep -F 'default browser simulator did not create an OPFS workspace' web/test-browser-wasm-smoke-opfs-policy-sections.js >/dev/null +grep -F 'browser simulator allowed OPFS to be disabled' web/test-browser-wasm-smoke-opfs-policy-sections.js >/dev/null +grep -F 'app frame did not restore current program from OPFS after reload' web/test-browser-wasm-smoke-app-sections.js >/dev/null +grep -F 'app frame did not report OPFS program restore after reload' web/test-browser-wasm-smoke-app-sections.js >/dev/null +grep -F 'app frame did not persist LinuxCNC parameter state into OPFS' web/test-browser-wasm-smoke-app-sections.js >/dev/null +grep -F 'app frame did not reload LinuxCNC parameter state from OPFS after reload' web/test-browser-wasm-smoke-app-sections.js >/dev/null +grep -F '"parameters/missing-main.var.new", "stale temporary without main file' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null +grep -F 'stale LinuxCNC missing-parameter temporary file remained in OPFS' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null +grep -F 'stale LinuxCNC missing-parameter temporary file remained in WASM FS' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null +grep -F 'LinuxCNC out-of-order parameter file remained in WASM FS after restore failure' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null +grep -F 'LinuxCNC out-of-range parameter file remained in WASM FS after restore failure' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null +grep -F 'browser wasm smoke passed' web/test-browser-wasm-smoke-helpers.js >/dev/null grep -F 'browser wasm smoke passed' web/test-browser-wasm-smoke.html >/dev/null grep -F 'chromium chromium-browser google-chrome' test-web-wasm-browser-smoke.sh >/dev/null grep -F 'browser wasm smoke passed' test-web-wasm-browser-smoke.sh >/dev/null +grep -F 'BROWSER_SMOKE_MIN_SECTIONS:-50' test-web-wasm-browser-smoke.sh >/dev/null +grep -F 'browser wasm smoke passed ([0-9][0-9]* sections)' test-web-wasm-browser-smoke.sh >/dev/null +python3 - <<'PY' +from pathlib import Path +import re +import sys + +node_steps = len(re.findall(r'await\s+runStep\(', Path("test-web-wasm-node-smoke.cjs").read_text(encoding="utf-8"))) +browser_sections = sum( + len(re.findall(r'await\s+runSection\(', path.read_text(encoding="utf-8"))) + for path in Path("web").glob("test-browser-wasm-smoke-*-sections.js") +) +total = node_steps + browser_sections +if total < 50: + print( + f"expected Node/browser smoke to keep at least 50 explicit closed-loop steps/sections, got {total}", + file=sys.stderr, + ) + sys.exit(1) + +html_lines = Path("web/test-browser-wasm-smoke.html").read_text(encoding="utf-8").count("\n") + 1 +if html_lines > 80: + print( + f"expected browser smoke HTML to stay as a small loader, got {html_lines} lines", + file=sys.stderr, + ) + sys.exit(1) + +for path in sorted(Path("web").glob("test-browser-wasm-smoke-*-sections.js")): + line_count = path.read_text(encoding="utf-8").count("\n") + 1 + if line_count > 300: + print( + f"expected browser smoke section module {path} to stay below 300 lines, got {line_count}", + file=sys.stderr, + ) + sys.exit(1) +PY kinematics_manifest_sources="$build_dir/linuxcnc_kinematics_manifest_sources.txt" ./list-linuxcnc-kinematics-manifest-sources.sh linuxcnc-kinematics-source-files.txt all > "$kinematics_manifest_sources" grep -Fx "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini" "$kinematics_manifest_sources" >/dev/null @@ -51,7 +193,21 @@ grep -Fx "configs/sim/axis/vismach/puma/puma560_uvw.ini" "$kinematics_manifest_s grep -Fx "configs/sim/axis/vismach/scara/scara.ini" "$kinematics_manifest_sources" >/dev/null grep -Fx "configs/sim/qtaxis/non-trivial/scara/scara.ini" "$kinematics_manifest_sources" >/dev/null grep -Fx "configs/sim/qtvcp_screens/non-trivial/scara/scara.ini" "$kinematics_manifest_sources" >/dev/null +grep -Fx "configs/sim/axis/vismach/5axis/bridgemill/5axisgui.hal" "$kinematics_manifest_sources" >/dev/null +grep -Fx "configs/sim/axis/vismach/hexapod-sim/kinematics.hal" "$kinematics_manifest_sources" >/dev/null +grep -Fx "configs/sim/axis/vismach/melfa-sim/melfa_dh.hal" "$kinematics_manifest_sources" >/dev/null +grep -Fx "configs/sim/axis/vismach/millturn/millturn.hal" "$kinematics_manifest_sources" >/dev/null +grep -Fx "configs/sim/axis/vismach/puma/puma_dh.hal" "$kinematics_manifest_sources" >/dev/null grep -Fx "configs/sim/axis/vismach/puma/puma560_dh.hal" "$kinematics_manifest_sources" >/dev/null +grep -Fx "configs/sim/axis/vismach/5axis/bridgemill/5axis_postgui.hal" "$kinematics_manifest_sources" >/dev/null +grep -Fx "configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal" "$kinematics_manifest_sources" >/dev/null +grep -Fx "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn_postgui.hal" "$kinematics_manifest_sources" >/dev/null +grep -Fx "configs/sim/axis/vismach/melfa-sim/melfa-postgui.hal" "$kinematics_manifest_sources" >/dev/null +grep -Fx "configs/sim/axis/vismach/puma/puma560_postgui.hal" "$kinematics_manifest_sources" >/dev/null +if [[ "$(grep -Ec '_postgui\.hal$|-postgui\.hal$' "$kinematics_manifest_sources")" -ne 11 ]]; then + echo "expected kinematics manifest to cover all LinuxCNC POSTGUI_HALFILE switchkins sources" >&2 + exit 1 +fi grep -Fx "configs/sim/axis/vismach/5axis/bridgemill/remap_subs/428remap.ngc" "$kinematics_manifest_sources" >/dev/null grep -Fx "configs/sim/axis/vismach/5axis/table-dual-rotary/remap_subs/428remap.ngc" "$kinematics_manifest_sources" >/dev/null grep -Fx "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/430remap.ngc" "$kinematics_manifest_sources" >/dev/null @@ -73,43 +229,216 @@ grep -Fx "src/hal/components/xyzacb_trsrn.comp" "$kinematics_manifest_sources" > grep -Fx "src/objects/hal/components/xyzacb_trsrn.c" "$kinematics_manifest_sources" >/dev/null grep -Fx "src/hal/components/xyzbca_trsrn.comp" "$kinematics_manifest_sources" >/dev/null grep -Fx "src/objects/hal/components/xyzbca_trsrn.c" "$kinematics_manifest_sources" >/dev/null +grep -Fx "src/hal/components/millturn.comp" "$kinematics_manifest_sources" >/dev/null +grep -Fx "src/objects/hal/components/millturn.c" "$kinematics_manifest_sources" >/dev/null grep -Fx "src/hal/components/matrixkins.comp" "$kinematics_manifest_sources" >/dev/null grep -Fx "src/objects/hal/components/matrixkins.c" "$kinematics_manifest_sources" >/dev/null grep -Fx "src/hal/components/userkins.comp" "$kinematics_manifest_sources" >/dev/null grep -Fx "src/objects/hal/components/userkins.c" "$kinematics_manifest_sources" >/dev/null +grep -Fx "src/objects/hal/components/millturn.mak" "$kinematics_manifest_sources" >/dev/null +grep -Fx "src/objects/hal/components/xyzab_tdr_kins.mak" "$kinematics_manifest_sources" >/dev/null +grep -Fx "src/objects/hal/components/xyzacb_trsrn.mak" "$kinematics_manifest_sources" >/dev/null +grep -Fx "src/objects/hal/components/xyzbca_trsrn.mak" "$kinematics_manifest_sources" >/dev/null +grep -Fx "src/objects/hal/components/matrixkins.mak" "$kinematics_manifest_sources" >/dev/null +grep -Fx "src/objects/hal/components/userkins.mak" "$kinematics_manifest_sources" >/dev/null +if [[ "$(grep -Ec '^src/objects/hal/components/.*\.mak$' "$kinematics_manifest_sources")" -ne 6 ]]; then + echo "expected kinematics manifest to cover all generated halcompile make metadata sources" >&2 + exit 1 +fi +grep -Fx "configs/sim/axis/vismach/5axis/bridgemill/5axis.tbl" "$kinematics_manifest_sources" >/dev/null +grep -Fx "configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.tbl" "$kinematics_manifest_sources" >/dev/null +grep -Fx "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.tbl" "$kinematics_manifest_sources" >/dev/null +grep -Fx "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.tbl" "$kinematics_manifest_sources" >/dev/null +grep -Fx "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.tbl" "$kinematics_manifest_sources" >/dev/null +grep -Fx "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.tbl" "$kinematics_manifest_sources" >/dev/null +grep -Fx "configs/sim/axis/vismach/melfa-sim/melfa.tbl" "$kinematics_manifest_sources" >/dev/null +grep -Fx "configs/sim/axis/vismach/millturn/millturn.tbl" "$kinematics_manifest_sources" >/dev/null +grep -Fx "configs/sim/axis/vismach/puma/puma.tbl" "$kinematics_manifest_sources" >/dev/null +grep -Fx "configs/sim/qtaxis/non-trivial/scara/scara.tbl" "$kinematics_manifest_sources" >/dev/null +if [[ "$(grep -Ec '\.tbl$' "$kinematics_manifest_sources")" -ne 10 ]]; then + echo "expected kinematics manifest to cover all resolvable LinuxCNC TOOL_TABLE switchkins sources" >&2 + exit 1 +fi +grep -Fx "configs/sim/axis/vismach/5axis/bridgemill/README" "$kinematics_manifest_sources" >/dev/null +grep -Fx "configs/sim/axis/vismach/5axis/table-dual-rotary/README" "$kinematics_manifest_sources" >/dev/null +grep -Fx "configs/sim/axis/vismach/5axis/table-rotary-tilting/README" "$kinematics_manifest_sources" >/dev/null +grep -Fx "configs/sim/axis/vismach/hexapod-sim/README" "$kinematics_manifest_sources" >/dev/null +grep -Fx "configs/sim/axis/vismach/melfa-sim/README" "$kinematics_manifest_sources" >/dev/null +grep -Fx "configs/sim/axis/vismach/millturn/README" "$kinematics_manifest_sources" >/dev/null +grep -Fx "configs/sim/axis/vismach/puma/README" "$kinematics_manifest_sources" >/dev/null +grep -Fx "configs/sim/axis/vismach/scara/README" "$kinematics_manifest_sources" >/dev/null +if [[ "$(grep -Ec '/README$' "$kinematics_manifest_sources")" -ne 10 ]]; then + echo "expected kinematics manifest to cover all LinuxCNC README sources adjacent to switchkins INI files" >&2 + exit 1 +fi +grep -Fx "src/hal/user_comps/vismach/5axisgui.py" "$kinematics_manifest_sources" >/dev/null +grep -Fx "src/hal/user_comps/vismach/hexagui.py" "$kinematics_manifest_sources" >/dev/null +grep -Fx "src/hal/user_comps/vismach/melfagui.py" "$kinematics_manifest_sources" >/dev/null +grep -Fx "src/hal/user_comps/vismach/millturngui.py" "$kinematics_manifest_sources" >/dev/null +grep -Fx "src/hal/user_comps/vismach/puma560gui.py" "$kinematics_manifest_sources" >/dev/null +grep -Fx "src/hal/user_comps/vismach/pumagui.py" "$kinematics_manifest_sources" >/dev/null +grep -Fx "src/hal/user_comps/vismach/scaragui.py" "$kinematics_manifest_sources" >/dev/null +grep -Fx "src/hal/user_comps/vismach/xyzab-tdr-gui.py" "$kinematics_manifest_sources" >/dev/null +grep -Fx "src/hal/user_comps/vismach/xyzac-trt-gui.py" "$kinematics_manifest_sources" >/dev/null +grep -Fx "src/hal/user_comps/vismach/xyzbc-trt-gui.py" "$kinematics_manifest_sources" >/dev/null +if [[ "$(grep -Ec '^src/hal/user_comps/vismach/.*gui\.py$|^src/hal/user_comps/vismach/xyz[abc-]+trt-gui\.py$|^src/hal/user_comps/vismach/xyzab-tdr-gui\.py$' "$kinematics_manifest_sources")" -ne 10 ]]; then + echo "expected kinematics manifest to cover all LinuxCNC vismach loadusr GUI sources" >&2 + exit 1 +fi +grep -Fx "share/qtvcp/panels/vismach_scara/vismach_scara.ui" "$kinematics_manifest_sources" >/dev/null +grep -Fx "share/qtvcp/panels/vismach_scara/vismach_scara_handler.py" "$kinematics_manifest_sources" >/dev/null +grep -Fx "lib/python/qtvcp/lib/qt_vismach/README.txt" "$kinematics_manifest_sources" >/dev/null +grep -Fx "lib/python/qtvcp/lib/qt_vismach/__init__.py" "$kinematics_manifest_sources" >/dev/null +grep -Fx "lib/python/qtvcp/lib/qt_vismach/primitives.py" "$kinematics_manifest_sources" >/dev/null +grep -Fx "lib/python/qtvcp/lib/qt_vismach/qt_vismach.py" "$kinematics_manifest_sources" >/dev/null +grep -Fx "lib/python/qtvcp/lib/qt_vismach/scara.py" "$kinematics_manifest_sources" >/dev/null +if [[ "$(grep -Ec '^(share/qtvcp/panels/vismach_scara/|lib/python/qtvcp/lib/qt_vismach/)' "$kinematics_manifest_sources")" -ne 7 ]]; then + echo "expected kinematics manifest to cover all LinuxCNC QtVCP vismach_scara panel sources" >&2 + exit 1 +fi +grep -Fx "src/hal/user_comps/vismach/Submakefile" "$kinematics_manifest_sources" >/dev/null +grep -Fx "docs/src/man/man1/5axisgui.1.adoc" "$kinematics_manifest_sources" >/dev/null +grep -Fx "docs/src/man/man1/hexagui.1.adoc" "$kinematics_manifest_sources" >/dev/null +grep -Fx "docs/src/man/man1/melfagui.1.adoc" "$kinematics_manifest_sources" >/dev/null +grep -Fx "docs/src/man/man1/puma560gui.1.adoc" "$kinematics_manifest_sources" >/dev/null +grep -Fx "docs/src/man/man1/pumagui.1.adoc" "$kinematics_manifest_sources" >/dev/null +grep -Fx "docs/src/man/man1/scaragui.1.adoc" "$kinematics_manifest_sources" >/dev/null +grep -Fx "docs/src/man/man1/xyzab-tdr-gui.1.adoc" "$kinematics_manifest_sources" >/dev/null +grep -Fx "docs/src/man/man1/xyzac-trt-gui.1.adoc" "$kinematics_manifest_sources" >/dev/null +grep -Fx "docs/src/man/man1/xyzbc-trt-gui.1.adoc" "$kinematics_manifest_sources" >/dev/null +if [[ "$(grep -Ec '^docs/src/man/man1/(5axisgui|hexagui|melfagui|puma560gui|pumagui|scaragui|xyzab-tdr-gui|xyzac-trt-gui|xyzbc-trt-gui)\.1\.adoc$' "$kinematics_manifest_sources")" -ne 9 ]]; then + echo "expected kinematics manifest to cover all LinuxCNC vismach GUI manpage sources" >&2 + exit 1 +fi +grep -Fx "src/emc/kinematics/switchkins.h" "$kinematics_manifest_sources" >/dev/null +grep -Fx "src/emc/kinematics/Submakefile" "$kinematics_manifest_sources" >/dev/null +grep -Fx "src/emc/kinematics/blend.fig" "$kinematics_manifest_sources" >/dev/null +grep -F 'manifest does not group all LinuxCNC kinematics headers as header sources' check-linuxcnc-inputs.sh >/dev/null +grep -F 'manifest does not group all LinuxCNC kinematics build metadata as metadata sources' check-linuxcnc-inputs.sh >/dev/null +grep -F 'manifest does not group all LinuxCNC kinematics reference assets as asset sources' check-linuxcnc-inputs.sh >/dev/null +grep -F 'manifest does not cover all LinuxCNC component sources referenced by M428/M429/M430 INI KINEMATICS entries' check-linuxcnc-inputs.sh >/dev/null +grep -F 'manifest does not cover all LinuxCNC generated component sources referenced by M428/M429/M430 INI KINEMATICS entries' check-linuxcnc-inputs.sh >/dev/null +grep -F 'manifest does not cover all LinuxCNC generated component make metadata adjacent to generated kinematics sources' check-linuxcnc-inputs.sh >/dev/null +grep -F 'manifest does not cover all resolvable LinuxCNC TOOL_TABLE files referenced by M428/M429/M430 INI files as tooldata sources' check-linuxcnc-inputs.sh >/dev/null +grep -F 'manifest does not cover LinuxCNC README sources adjacent to M428/M429/M430 INI files as asset sources' check-linuxcnc-inputs.sh >/dev/null +grep -F 'manifest does not cover all LinuxCNC vismach user component sources referenced by M428/M429/M430 INI/HAL loadusr commands as asset sources' check-linuxcnc-inputs.sh >/dev/null +grep -F 'manifest does not cover LinuxCNC vismach user component build metadata as metadata sources' check-linuxcnc-inputs.sh >/dev/null +grep -F 'manifest does not cover LinuxCNC vismach user component manpage sources as metadata sources' check-linuxcnc-inputs.sh >/dev/null +grep -F 'manifest does not cover all LinuxCNC QtVCP vismach_scara sources referenced by M428/M429/M430 INI EMBED_TAB_COMMAND entries as asset sources' check-linuxcnc-inputs.sh >/dev/null +grep -F 'bad manifest line $manifest_line_number' check-linuxcnc-inputs.sh >/dev/null +grep -F 'manifest source must be LinuxCNC-root relative' check-linuxcnc-inputs.sh >/dev/null +grep -F 'missing manifest source: $path' check-linuxcnc-inputs.sh >/dev/null core_source_list="$build_dir/cnc_sim_core_sources.txt" ./list-cnc-sim-core-sources.sh --with-smoke > "$core_source_list" mapfile -t core_sources < "$core_source_list" smoke_cxxflags=(-std=c++17 -fpermissive -DCNC_SIM_ENABLE_SMOKE_BACKEND -I core/include -I core/src -I ../linuxcnc/include) -"$cxx" "${smoke_cxxflags[@]}" \ - "${core_sources[@]}" \ - core/tests/canon_event_sink_smoke.cpp \ - -o "$build_dir/canon_event_sink_smoke" +native_signature="$build_dir/native-build.signature" +native_next_signature="$build_dir/native-build.signature.next" +{ + printf 'CXX=%s\n' "$cxx" + printf 'PWD=%s\n' "$PWD" + printf 'SMOKE_CXXFLAGS=' + printf ' %s' "${smoke_cxxflags[@]}" + printf '\n' + printf 'CORE_SOURCES:\n' + printf '%s\n' "${core_sources[@]}" +} > "$native_next_signature" +if [[ ! -f "$native_signature" ]] || ! cmp -s "$native_signature" "$native_next_signature"; then + rm -f "$build_dir"/core_*.o \ + "$build_dir"/*.d \ + "$build_dir"/canon_event_sink_smoke.o \ + "$build_dir"/rtcp_kinematics_smoke.o \ + "$build_dir"/simulator_gcode_controls_smoke.o \ + "$build_dir"/cnc_sim_api_smoke.o \ + "$build_dir"/cnc_sim_dump.o \ + "$build_dir"/linuxcnc_gees_table_smoke.o \ + "$build_dir"/canon_event_sink_smoke \ + "$build_dir"/rtcp_kinematics_smoke \ + "$build_dir"/linuxcnc_gees_table_smoke \ + "$build_dir"/simulator_gcode_controls_smoke \ + "$build_dir"/cnc_sim_api_smoke \ + "$build_dir"/cnc_sim_dump +fi +mv "$native_next_signature" "$native_signature" -"$cxx" "${smoke_cxxflags[@]}" \ - "${core_sources[@]}" \ - core/tests/rtcp_kinematics_smoke.cpp \ - -o "$build_dir/rtcp_kinematics_smoke" +core_objects=() +dep_files=() +core_index=0 +for source in "${core_sources[@]}"; do + obj="$build_dir/core_${core_index}.o" + core_objects+=("$obj") + dep_files+=("$obj.d") + core_index=$((core_index + 1)) +done +for obj in \ + "$build_dir/canon_event_sink_smoke.o" \ + "$build_dir/rtcp_kinematics_smoke.o" \ + "$build_dir/simulator_gcode_controls_smoke.o" \ + "$build_dir/cnc_sim_api_smoke.o" \ + "$build_dir/cnc_sim_dump.o" \ + "$build_dir/linuxcnc_gees_table_smoke.o" +do + dep_files+=("$obj.d") +done -"$cxx" "${smoke_cxxflags[@]}" \ - core/tests/linuxcnc_gees_table_smoke.cpp \ - -o "$build_dir/linuxcnc_gees_table_smoke" +native_makefile="$build_dir/native.mk" +{ + printf 'CXX := %s\n' "$cxx" + printf 'SMOKE_CXXFLAGS :=' + printf ' %s' "${smoke_cxxflags[@]}" + printf '\n\n' + printf 'CORE_OBJECTS :=' + printf ' %s' "${core_objects[@]}" + printf '\n\n' + printf 'DEP_FILES :=' + printf ' %s' "${dep_files[@]}" + printf '\n\n' + printf '.PHONY: all\n' + printf 'all: %s %s %s %s %s %s\n\n' \ + "$build_dir/canon_event_sink_smoke" \ + "$build_dir/rtcp_kinematics_smoke" \ + "$build_dir/linuxcnc_gees_table_smoke" \ + "$build_dir/simulator_gcode_controls_smoke" \ + "$build_dir/cnc_sim_api_smoke" \ + "$build_dir/cnc_sim_dump" -"$cxx" "${smoke_cxxflags[@]}" \ - "${core_sources[@]}" \ - core/tests/simulator_gcode_controls_smoke.cpp \ - -o "$build_dir/simulator_gcode_controls_smoke" + core_index=0 + for source in "${core_sources[@]}"; do + printf '%s: %s\n' "${core_objects[$core_index]}" "$source" + printf '\t@$(CXX) $(SMOKE_CXXFLAGS) -MMD -MP -MF %s.d -c %s -o %s\n\n' \ + "${core_objects[$core_index]}" "$source" "${core_objects[$core_index]}" + core_index=$((core_index + 1)) + done -"$cxx" "${smoke_cxxflags[@]}" \ - "${core_sources[@]}" \ - core/tests/cnc_sim_api_smoke.cpp \ - -o "$build_dir/cnc_sim_api_smoke" + printf '%s: core/tests/canon_event_sink_smoke.cpp\n' "$build_dir/canon_event_sink_smoke.o" + printf '\t@$(CXX) $(SMOKE_CXXFLAGS) -MMD -MP -MF %s.d -c core/tests/canon_event_sink_smoke.cpp -o %s\n\n' "$build_dir/canon_event_sink_smoke.o" "$build_dir/canon_event_sink_smoke.o" + printf '%s: core/tests/rtcp_kinematics_smoke.cpp\n' "$build_dir/rtcp_kinematics_smoke.o" + printf '\t@$(CXX) $(SMOKE_CXXFLAGS) -MMD -MP -MF %s.d -c core/tests/rtcp_kinematics_smoke.cpp -o %s\n\n' "$build_dir/rtcp_kinematics_smoke.o" "$build_dir/rtcp_kinematics_smoke.o" + printf '%s: core/tests/simulator_gcode_controls_smoke.cpp\n' "$build_dir/simulator_gcode_controls_smoke.o" + printf '\t@$(CXX) $(SMOKE_CXXFLAGS) -MMD -MP -MF %s.d -c core/tests/simulator_gcode_controls_smoke.cpp -o %s\n\n' "$build_dir/simulator_gcode_controls_smoke.o" "$build_dir/simulator_gcode_controls_smoke.o" + printf '%s: core/tests/cnc_sim_api_smoke.cpp\n' "$build_dir/cnc_sim_api_smoke.o" + printf '\t@$(CXX) $(SMOKE_CXXFLAGS) -MMD -MP -MF %s.d -c core/tests/cnc_sim_api_smoke.cpp -o %s\n\n' "$build_dir/cnc_sim_api_smoke.o" "$build_dir/cnc_sim_api_smoke.o" + printf '%s: core/tools/cnc_sim_dump.cpp\n' "$build_dir/cnc_sim_dump.o" + printf '\t@$(CXX) $(SMOKE_CXXFLAGS) -MMD -MP -MF %s.d -c core/tools/cnc_sim_dump.cpp -o %s\n\n' "$build_dir/cnc_sim_dump.o" "$build_dir/cnc_sim_dump.o" + printf '%s: core/tests/linuxcnc_gees_table_smoke.cpp\n' "$build_dir/linuxcnc_gees_table_smoke.o" + printf '\t@$(CXX) $(SMOKE_CXXFLAGS) -MMD -MP -MF %s.d -c core/tests/linuxcnc_gees_table_smoke.cpp -o %s\n\n' "$build_dir/linuxcnc_gees_table_smoke.o" "$build_dir/linuxcnc_gees_table_smoke.o" -"$cxx" "${smoke_cxxflags[@]}" \ - "${core_sources[@]}" \ - core/tools/cnc_sim_dump.cpp \ - -o "$build_dir/cnc_sim_dump" + printf '%s: %s\n' "$build_dir/linuxcnc_gees_table_smoke" "$build_dir/linuxcnc_gees_table_smoke.o" + printf '\t@$(CXX) $(SMOKE_CXXFLAGS) %s -o %s\n\n' "$build_dir/linuxcnc_gees_table_smoke.o" "$build_dir/linuxcnc_gees_table_smoke" + printf '%s: $(CORE_OBJECTS) %s\n' "$build_dir/canon_event_sink_smoke" "$build_dir/canon_event_sink_smoke.o" + printf '\t@$(CXX) $(SMOKE_CXXFLAGS) $(CORE_OBJECTS) %s -o %s\n\n' "$build_dir/canon_event_sink_smoke.o" "$build_dir/canon_event_sink_smoke" + printf '%s: $(CORE_OBJECTS) %s\n' "$build_dir/rtcp_kinematics_smoke" "$build_dir/rtcp_kinematics_smoke.o" + printf '\t@$(CXX) $(SMOKE_CXXFLAGS) $(CORE_OBJECTS) %s -o %s\n\n' "$build_dir/rtcp_kinematics_smoke.o" "$build_dir/rtcp_kinematics_smoke" + printf '%s: $(CORE_OBJECTS) %s\n' "$build_dir/simulator_gcode_controls_smoke" "$build_dir/simulator_gcode_controls_smoke.o" + printf '\t@$(CXX) $(SMOKE_CXXFLAGS) $(CORE_OBJECTS) %s -o %s\n\n' "$build_dir/simulator_gcode_controls_smoke.o" "$build_dir/simulator_gcode_controls_smoke" + printf '%s: $(CORE_OBJECTS) %s\n' "$build_dir/cnc_sim_api_smoke" "$build_dir/cnc_sim_api_smoke.o" + printf '\t@$(CXX) $(SMOKE_CXXFLAGS) $(CORE_OBJECTS) %s -o %s\n\n' "$build_dir/cnc_sim_api_smoke.o" "$build_dir/cnc_sim_api_smoke" + printf '%s: $(CORE_OBJECTS) %s\n' "$build_dir/cnc_sim_dump" "$build_dir/cnc_sim_dump.o" + printf '\t@$(CXX) $(SMOKE_CXXFLAGS) $(CORE_OBJECTS) %s -o %s\n' "$build_dir/cnc_sim_dump.o" "$build_dir/cnc_sim_dump" + printf '\n-include $(DEP_FILES)\n' +} > "$native_makefile" +make --output-sync=target -j"$build_jobs" -f "$native_makefile" "$build_dir/canon_event_sink_smoke" "$build_dir/rtcp_kinematics_smoke" diff --git a/test-web-wasm-browser-smoke.sh b/test-web-wasm-browser-smoke.sh index 87b7027..5791658 100755 --- a/test-web-wasm-browser-smoke.sh +++ b/test-web-wasm-browser-smoke.sh @@ -11,32 +11,197 @@ if [[ -z "$chromium_bin" ]]; then break fi done +elif [[ ! -x "$chromium_bin" ]] && command -v "$chromium_bin" >/dev/null 2>&1; then + chromium_bin=$(command -v "$chromium_bin") fi missing=() +minimum_sections=${BROWSER_SMOKE_MIN_SECTIONS:-50} +if ! [[ "$minimum_sections" =~ ^[1-9][0-9]*$ ]]; then + echo "BROWSER_SMOKE_MIN_SECTIONS must be a positive integer: $minimum_sections" >&2 + exit 1 +fi + +require_command() { + local command_name=$1 + if ! command -v "$command_name" >/dev/null 2>&1; then + missing+=("$command_name") + fi +} + +require_file() { + local file_path=$1 + if [[ ! -f "$file_path" ]]; then + missing+=("$file_path") + elif [[ ! -r "$file_path" || ! -s "$file_path" ]]; then + missing+=("readable non-empty $file_path") + fi +} + +report_missing_prerequisites() { + echo "WASM browser smoke prerequisites are missing: ${missing[*]}." >&2 + echo "Run ./build-wasm.sh after activating emsdk to generate web/public/cnc_sim.js and web/public/cnc_sim.wasm." >&2 +} + if [[ -z "$chromium_bin" ]]; then missing+=("chromium") +elif [[ ! -x "$chromium_bin" ]]; then + missing+=("executable chromium at $chromium_bin") fi -for required in python3 web/public/cnc_sim.js web/public/cnc_sim.wasm web/test-browser-wasm-smoke.html; do - if [[ "$required" == python3 ]]; then - if ! command -v python3 >/dev/null 2>&1; then - missing+=("python3") - fi - elif [[ ! -f "$required" ]]; then - missing+=("$required") +required_files=( + web/public/cnc_sim.js + web/public/cnc_sim.wasm + web/public/linuxcnc_switchkins_remap_config_cases.json + web/src/wasm-core.js + web/src/app.js + web/index.html + web/styles.css + web/test-browser-wasm-smoke.html + web/test-browser-wasm-smoke-helpers.js + web/test-browser-wasm-smoke-sections.js + web/test-browser-wasm-smoke-linuxcnc-sections.js + web/test-browser-wasm-smoke-opfs-workspace-sections.js + web/test-browser-wasm-smoke-opfs-basic-sections.js + web/test-browser-wasm-smoke-opfs-parameter-sections.js + web/test-browser-wasm-smoke-opfs-mirror-sections.js + web/test-browser-wasm-smoke-opfs-directory-sections.js + web/test-browser-wasm-smoke-opfs-policy-sections.js + web/test-browser-wasm-smoke-app-sections.js +) + +for required in \ + node \ + python3 \ + "${required_files[@]}" +do + if [[ "$required" == node || "$required" == python3 ]]; then + require_command "$required" + else + require_file "$required" fi done if ((${#missing[@]} > 0)); then - echo "WASM browser smoke prerequisites are missing: ${missing[*]}." >&2 - echo "Run ./build-wasm.sh after activating emsdk to generate web/public/cnc_sim.js and web/public/cnc_sim.wasm." >&2 + report_missing_prerequisites exit 1 fi +if ! python3 - <<'PY' +from pathlib import Path +import json +import sys + +cases = json.loads(Path("web/public/linuxcnc_switchkins_remap_config_cases.json").read_text(encoding="utf-8")) +if not isinstance(cases, list) or not cases: + print("expected non-empty LinuxCNC switchkins config case JSON", file=sys.stderr) + sys.exit(1) +for index, config_case in enumerate(cases): + if ( + not isinstance(config_case, dict) + or not isinstance(config_case.get("field"), str) + or not isinstance(config_case.get("value"), str) + or not isinstance(config_case.get("m428"), int) + or not isinstance(config_case.get("m429"), int) + or not isinstance(config_case.get("m430"), int) + ): + print(f"invalid LinuxCNC switchkins config case at index {index}", file=sys.stderr) + sys.exit(1) +PY +then + echo "WASM browser smoke switchkins config case JSON check failed" >&2 + exit 1 +fi + +if ! python3 - <<'PY' +from pathlib import Path +import re +import sys + +html = Path("web/test-browser-wasm-smoke.html").read_text(encoding="utf-8") +if '' not in html: + print("browser smoke HTML must load /public/cnc_sim.js", file=sys.stderr) + sys.exit(1) +module_scripts = re.findall(r'(.*?)', html, flags=re.S) +if len(module_scripts) != 1 or not module_scripts[0].strip(): + print("browser smoke HTML must contain exactly one non-empty module script", file=sys.stderr) + sys.exit(1) +if 'from "/src/wasm-core.js"' not in module_scripts[0]: + print("browser smoke module script must import /src/wasm-core.js", file=sys.stderr) + sys.exit(1) +if 'from "/test-browser-wasm-smoke-sections.js"' not in module_scripts[0]: + print("browser smoke module script must import /test-browser-wasm-smoke-sections.js", file=sys.stderr) + sys.exit(1) +section_entry = Path("web/test-browser-wasm-smoke-sections.js").read_text(encoding="utf-8") +for imported_module in ( + "/test-browser-wasm-smoke-helpers.js", + "/test-browser-wasm-smoke-linuxcnc-sections.js", + "/test-browser-wasm-smoke-opfs-workspace-sections.js", + "/test-browser-wasm-smoke-opfs-policy-sections.js", + "/test-browser-wasm-smoke-app-sections.js", +): + if f'from "{imported_module}"' not in section_entry: + print(f"browser smoke sections module must import {imported_module}", file=sys.stderr) + sys.exit(1) +opfs_workspace = Path("web/test-browser-wasm-smoke-opfs-workspace-sections.js").read_text(encoding="utf-8") +for imported_module in ( + "/test-browser-wasm-smoke-opfs-basic-sections.js", + "/test-browser-wasm-smoke-opfs-parameter-sections.js", + "/test-browser-wasm-smoke-opfs-mirror-sections.js", + "/test-browser-wasm-smoke-opfs-directory-sections.js", +): + if f'from "{imported_module}"' not in opfs_workspace: + print(f"browser smoke OPFS workspace module must import {imported_module}", file=sys.stderr) + sys.exit(1) +app_html = Path("web/index.html").read_text(encoding="utf-8") +if '' not in app_html: + print("browser app HTML must load /styles.css", file=sys.stderr) + sys.exit(1) +if '' not in app_html: + print("browser app HTML must load /src/app.js", file=sys.stderr) + sys.exit(1) +for required_id in ("parseBtn", "programInput", "alarmList", "axisX", "wasmState", "storageState"): + if f'id="{required_id}"' not in app_html: + print(f"browser app HTML missing #{required_id}", file=sys.stderr) + sys.exit(1) +PY +then + exit 1 +fi + +for web_module in \ + web/src/wasm-core.js \ + web/src/app.js \ + web/test-browser-wasm-smoke-helpers.js \ + web/test-browser-wasm-smoke-sections.js \ + web/test-browser-wasm-smoke-linuxcnc-sections.js \ + web/test-browser-wasm-smoke-opfs-workspace-sections.js \ + web/test-browser-wasm-smoke-opfs-basic-sections.js \ + web/test-browser-wasm-smoke-opfs-parameter-sections.js \ + web/test-browser-wasm-smoke-opfs-mirror-sections.js \ + web/test-browser-wasm-smoke-opfs-directory-sections.js \ + web/test-browser-wasm-smoke-opfs-policy-sections.js \ + web/test-browser-wasm-smoke-app-sections.js +do + if ! node --check "$web_module"; then + echo "WASM browser smoke web module syntax check failed for $web_module" >&2 + exit 1 + fi +done + +chromium_version=$("$chromium_bin" --version 2>/dev/null || true) +if [[ -n "$chromium_version" ]]; then + echo "browser smoke chromium version: $chromium_version" +fi +virtual_time_budget=${BROWSER_SMOKE_VIRTUAL_TIME_BUDGET:-20000} + server_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_browser_smoke_server.XXXXXX.log") dom_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_browser_smoke_dom.XXXXXX.html") profile_dir=$(mktemp -d "${TMPDIR:-/tmp}/cnc_sim_browser_smoke_profile.XXXXXX") cleanup() { + if [[ -n "${chromium_pid:-}" ]]; then + kill "$chromium_pid" >/dev/null 2>&1 || true + wait "$chromium_pid" >/dev/null 2>&1 || true + fi if [[ -n "${server_pid:-}" ]]; then kill "$server_pid" >/dev/null 2>&1 || true wait "$server_pid" >/dev/null 2>&1 || true @@ -46,15 +211,75 @@ cleanup() { } trap cleanup EXIT -port=$(python3 - <<'PY' +pick_free_port() { + python3 - <<'PY' import socket with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.bind(("127.0.0.1", 0)) print(sock.getsockname()[1]) PY -) +} +print_browser_failure_context() { + if grep -F "browser wasm smoke failed:" "$dom_log" >/dev/null; then + grep -o "browser wasm smoke failed:[^<]*" "$dom_log" | sed -n '1,20p' >&2 + elif grep -F "running:" "$dom_log" >/dev/null; then + grep -o "running:[^<]*" "$dom_log" | tail -n 1 >&2 + elif [[ ! -s "$dom_log" ]]; then + echo "browser smoke did not produce DOM output" >&2 + fi + echo "browser smoke HTTP server log:" >&2 + sed -n '1,40p' "$server_log" >&2 + echo "browser smoke DOM excerpt:" >&2 + sed -n '1,120p' "$dom_log" >&2 +} + +probe_http_resource() { + local resource_path=$1 + local expected_prefix=$2 + local expected_type=${3:-} + if ! python3 - "$port" "$resource_path" "$expected_prefix" "$expected_type" <<'PY' +import http.client +import sys + +port = int(sys.argv[1]) +path = sys.argv[2] +expected_prefix = sys.argv[3].encode("utf-8") +expected_type = sys.argv[4] + +conn = http.client.HTTPConnection("127.0.0.1", port, timeout=2) +try: + conn.request("GET", path) + response = conn.getresponse() + body = response.read(max(len(expected_prefix), 1)) +finally: + conn.close() + +if response.status != 200: + print(f"{path} returned HTTP {response.status}", file=sys.stderr) + sys.exit(1) +content_type = response.getheader("Content-Type", "").split(";", 1)[0] +if expected_type and content_type != expected_type: + print(f"{path} returned Content-Type {content_type!r}, expected {expected_type!r}", file=sys.stderr) + sys.exit(1) +if not body: + print(f"{path} returned an empty body", file=sys.stderr) + sys.exit(1) +if expected_prefix and not body.startswith(expected_prefix): + print(f"{path} did not start with expected content", file=sys.stderr) + sys.exit(1) +PY + then + echo "browser smoke HTTP resource probe failed for $resource_path" >&2 + sed -n '1,40p' "$server_log" >&2 + exit 1 + fi +} + +port=$(pick_free_port) + +echo "running WASM browser smoke with $chromium_bin on http://127.0.0.1:$port/test-browser-wasm-smoke.html" python3 -m http.server "$port" --bind 127.0.0.1 --directory web >"$server_log" 2>&1 & server_pid=$! @@ -92,19 +317,211 @@ then exit 1 fi +probe_http_resource "/test-browser-wasm-smoke.html" "" "text/html" +probe_http_resource "/" "" "text/html" +probe_http_resource "/styles.css" "" "text/css" +probe_http_resource "/public/cnc_sim.js" "" "text/javascript" +probe_http_resource "/public/cnc_sim.wasm" "" "application/wasm" +probe_http_resource "/public/linuxcnc_switchkins_remap_config_cases.json" "[" "application/json" +probe_http_resource "/src/app.js" "" "text/javascript" +probe_http_resource "/src/wasm-core.js" "" "text/javascript" +probe_http_resource "/test-browser-wasm-smoke-helpers.js" "" "text/javascript" +probe_http_resource "/test-browser-wasm-smoke-sections.js" "" "text/javascript" +probe_http_resource "/test-browser-wasm-smoke-linuxcnc-sections.js" "" "text/javascript" +probe_http_resource "/test-browser-wasm-smoke-opfs-workspace-sections.js" "" "text/javascript" +probe_http_resource "/test-browser-wasm-smoke-opfs-basic-sections.js" "" "text/javascript" +probe_http_resource "/test-browser-wasm-smoke-opfs-parameter-sections.js" "" "text/javascript" +probe_http_resource "/test-browser-wasm-smoke-opfs-mirror-sections.js" "" "text/javascript" +probe_http_resource "/test-browser-wasm-smoke-opfs-directory-sections.js" "" "text/javascript" +probe_http_resource "/test-browser-wasm-smoke-opfs-policy-sections.js" "" "text/javascript" +probe_http_resource "/test-browser-wasm-smoke-app-sections.js" "" "text/javascript" + +debug_port=$(pick_free_port) "$chromium_bin" \ --headless=new \ --disable-gpu \ --no-sandbox \ --user-data-dir="$profile_dir" \ - --virtual-time-budget=5000 \ - --dump-dom \ - "http://127.0.0.1:$port/test-browser-wasm-smoke.html" >"$dom_log" 2>&1 + --remote-debugging-port="$debug_port" \ + "http://127.0.0.1:$port/test-browser-wasm-smoke.html" >"$dom_log" 2>&1 & +chromium_pid=$! -if ! grep -F "browser wasm smoke passed" "$dom_log" >/dev/null; then - echo "browser WASM smoke did not pass" >&2 - sed -n '1,120p' "$dom_log" >&2 +if ! python3 - "$debug_port" "$dom_log" "$virtual_time_budget" <<'PY' +import base64 +import http.client +import json +import os +import socket +import struct +import sys +import time +from urllib.parse import urlparse + +debug_port = int(sys.argv[1]) +dom_log = sys.argv[2] +timeout_ms = int(sys.argv[3]) +deadline = time.monotonic() + (timeout_ms / 1000.0) + +def http_json(path): + conn = http.client.HTTPConnection("127.0.0.1", debug_port, timeout=1) + try: + conn.request("GET", path) + response = conn.getresponse() + body = response.read() + finally: + conn.close() + if response.status != 200: + raise RuntimeError(f"{path} returned HTTP {response.status}") + return json.loads(body.decode("utf-8")) + +def websocket_connect(url): + parsed = urlparse(url) + key = base64.b64encode(os.urandom(16)).decode("ascii") + sock = socket.create_connection((parsed.hostname, parsed.port), timeout=2) + request = ( + f"GET {parsed.path} HTTP/1.1\r\n" + f"Host: {parsed.hostname}:{parsed.port}\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + f"Sec-WebSocket-Key: {key}\r\n" + "Sec-WebSocket-Version: 13\r\n\r\n" + ) + sock.sendall(request.encode("ascii")) + response = sock.recv(4096) + if b" 101 " not in response.split(b"\r\n", 1)[0]: + raise RuntimeError("DevTools websocket handshake failed") + sock.settimeout(1) + return sock + +def websocket_send(sock, payload): + data = payload.encode("utf-8") + header = bytearray([0x81]) + if len(data) < 126: + header.append(0x80 | len(data)) + elif len(data) < 65536: + header.append(0x80 | 126) + header.extend(struct.pack("!H", len(data))) + else: + header.append(0x80 | 127) + header.extend(struct.pack("!Q", len(data))) + mask = os.urandom(4) + header.extend(mask) + masked = bytes(byte ^ mask[index % 4] for index, byte in enumerate(data)) + sock.sendall(header + masked) + +def recv_exact(sock, size): + chunks = [] + remaining = size + while remaining: + chunk = sock.recv(remaining) + if not chunk: + raise RuntimeError("DevTools websocket closed") + chunks.append(chunk) + remaining -= len(chunk) + return b"".join(chunks) + +def websocket_recv(sock): + first, second = recv_exact(sock, 2) + opcode = first & 0x0F + length = second & 0x7F + if length == 126: + length = struct.unpack("!H", recv_exact(sock, 2))[0] + elif length == 127: + length = struct.unpack("!Q", recv_exact(sock, 8))[0] + masked = bool(second & 0x80) + mask = recv_exact(sock, 4) if masked else b"" + payload = recv_exact(sock, length) + if masked: + payload = bytes(byte ^ mask[index % 4] for index, byte in enumerate(payload)) + if opcode == 8: + raise RuntimeError("DevTools websocket closed") + if opcode != 1: + return None + return json.loads(payload.decode("utf-8")) + +def find_page_ws_url(): + while time.monotonic() < deadline: + try: + for target in http_json("/json/list"): + if target.get("type") == "page" and target.get("url", "").endswith("/test-browser-wasm-smoke.html"): + return target["webSocketDebuggerUrl"] + except Exception: + pass + time.sleep(0.1) + raise RuntimeError("timed out waiting for browser DevTools page target") + +sock = websocket_connect(find_page_ws_url()) +next_id = 0 + +def cdp(method, params=None): + global next_id + next_id += 1 + message_id = next_id + websocket_send(sock, json.dumps({"id": message_id, "method": method, "params": params or {}})) + while time.monotonic() < deadline: + try: + message = websocket_recv(sock) + except TimeoutError: + continue + except socket.timeout: + continue + if message and message.get("id") == message_id: + if "error" in message: + raise RuntimeError(message["error"]) + return message["result"] + raise RuntimeError(f"timed out waiting for DevTools response to {method}") + +def evaluate(expression): + result = cdp("Runtime.evaluate", {"expression": expression, "returnByValue": True}) + return result.get("result", {}).get("value", "") + +try: + cdp("Runtime.enable") + text = "" + while time.monotonic() < deadline: + text = evaluate('document.querySelector("#result")?.textContent || ""') + if "browser wasm smoke passed" in text: + html = evaluate("document.documentElement.outerHTML") + with open(dom_log, "w", encoding="utf-8") as handle: + handle.write(html) + sys.exit(0) + if "browser wasm smoke failed:" in text: + print(text, file=sys.stderr) + html = evaluate("document.documentElement.outerHTML") + with open(dom_log, "w", encoding="utf-8") as handle: + handle.write(html) + sys.exit(1) + time.sleep(0.2) + print(f"timed out waiting for browser smoke result; last status: {text}", file=sys.stderr) + html = evaluate("document.documentElement.outerHTML") + with open(dom_log, "w", encoding="utf-8") as handle: + handle.write(html) + sys.exit(1) +finally: + sock.close() +PY +then + echo "browser WASM smoke failed or timed out" >&2 + print_browser_failure_context exit 1 fi -echo "browser wasm smoke passed" +if ! grep -F "browser wasm smoke passed" "$dom_log" >/dev/null; then + echo "browser WASM smoke did not pass" >&2 + print_browser_failure_context + exit 1 +fi + +passed_text=$(grep -o "browser wasm smoke passed ([0-9][0-9]* sections)" "$dom_log" | tail -n 1 || true) +passed_sections=$(printf '%s\n' "$passed_text" | grep -o '[0-9][0-9]*' || true) +if [[ -z "$passed_sections" ]]; then + echo "browser WASM smoke did not report a completed section count" >&2 + print_browser_failure_context + exit 1 +fi +if ((passed_sections < minimum_sections)); then + echo "browser WASM smoke covered $passed_sections sections, expected at least $minimum_sections" >&2 + print_browser_failure_context + exit 1 +fi +echo "${passed_text:-browser wasm smoke passed}" diff --git a/test-web-wasm-node-smoke.cjs b/test-web-wasm-node-smoke.cjs index 4c4eeb2..cfc8d65 100644 --- a/test-web-wasm-node-smoke.cjs +++ b/test-web-wasm-node-smoke.cjs @@ -7,7 +7,13 @@ const { pathToFileURL } = require("url"); const wasmDir = path.join(__dirname, "web", "public"); const cjsLoader = path.join(os.tmpdir(), `cnc_sim_node_loader_${process.pid}.cjs`); fs.copyFileSync(path.join(wasmDir, "cnc_sim.js"), cjsLoader); +process.on("exit", () => { + fs.rmSync(cjsLoader, { force: true }); +}); const createCncSimModule = require(cjsLoader); +if (typeof createCncSimModule !== "function") { + throw new Error("cnc_sim.js did not export a Node-loadable createCncSimModule function"); +} const EVENT_TYPE = { 2: "comment", @@ -61,10 +67,87 @@ function expectKinematicsSwitch(events, line, kinstype, label) { } } +function expectEvent(events, predicate, message) { + if (!events.some(predicate)) { + throw new Error(message); + } +} + +async function expectErrorContaining(action, expectedText, message) { + try { + await action(); + throw new Error(message); + } catch (error) { + if (!String(error?.message ?? error).includes(expectedText)) { + throw error; + } + } +} + +function expectRtcpPivot(events, expected, message) { + expectEvent( + events, + (event) => event.type === "rtcp-pivot" && + event.line === expected.line && + event.reserved === expected.reserved && + near(event.dwellSeconds, expected.dwellSeconds) && + (expected.endX === undefined || near(event.endX, expected.endX)) && + (expected.endY === undefined || near(event.endY, expected.endY)) && + (expected.endZ === undefined || near(event.endZ, expected.endZ)) && + (expected.endA === undefined || near(event.endA, expected.endA)) && + (expected.endB === undefined || near(event.endB, expected.endB)) && + (expected.endC === undefined || near(event.endC, expected.endC)), + message, + ); +} + function near(actual, expected) { return Math.abs(actual - expected) < 1e-6; } +let completedSteps = 0; + +function loadSwitchkinsCases() { + const configCases = JSON.parse( + fs.readFileSync(path.join(wasmDir, "linuxcnc_switchkins_remap_config_cases.json"), "utf8"), + ); + if (!Array.isArray(configCases) || configCases.length === 0) { + throw new Error("expected non-empty generated LinuxCNC switchkins config cases"); + } + for (const [index, configCase] of configCases.entries()) { + if ( + !configCase || + typeof configCase.field !== "string" || + typeof configCase.value !== "string" || + !Number.isInteger(configCase.m428) || + !Number.isInteger(configCase.m429) || + !Number.isInteger(configCase.m430) + ) { + throw new Error(`invalid generated LinuxCNC switchkins config case at index ${index}`); + } + } + return configCases.map((configCase) => ({ + label: `${configCase.field}=${configCase.value}`, + config: { backend: "linuxcnc-rs274", [configCase.field]: configCase.value }, + program: configCase.m430 >= 0 ? "M428\nM429\nM430\n" : "M428\nM429\n", + expected: [ + [1, configCase.m428], + [2, configCase.m429], + ...(configCase.m430 >= 0 ? [[3, configCase.m430]] : []), + ], + })); +} + +async function runStep(name, action) { + try { + await action(); + completedSteps += 1; + } catch (error) { + const detail = error && error.stack ? error.stack : error; + throw new Error(`Node WASM smoke step "${name}" failed: ${detail}`); + } +} + (async () => { const module = await createCncSimModule({ locateFile: (file) => path.join(wasmDir, file), @@ -80,7 +163,40 @@ function near(actual, expected) { const loadConfig = module.cwrap("cnc_sim_load_config_json", "number", ["number", "number", "number"]); const parseProgram = module.cwrap("cnc_sim_parse_program", "number", ["number", "number", "number"]); const lastError = module.cwrap("cnc_sim_last_error", "number", ["number"]); - + const throwLastError = (targetHandle) => { + throw new Error(module.UTF8ToString(lastError(targetHandle))); + }; + const initializeHandle = (targetHandle) => { + reset(targetHandle); + if (setDialect(targetHandle, 0) !== 0) { + throwLastError(targetHandle); + } + }; + const loadJsonConfig = (targetHandle, value) => { + const config = writeString(module, JSON.stringify(value)); + try { + if (loadConfig(targetHandle, config.ptr, config.len) !== 0) { + throwLastError(targetHandle); + } + } finally { + module._free(config.ptr); + } + }; + const parseText = (targetHandle, value) => { + const program = writeString(module, value); + try { + if (parseProgram(targetHandle, program.ptr, program.len) !== 0) { + throwLastError(targetHandle); + } + } finally { + module._free(program.ptr); + } + }; + const attachCallback = (targetHandle, targetCallbackPtr) => { + if (setCallback(targetHandle, targetCallbackPtr, 0) !== 0) { + throwLastError(targetHandle); + } + }; const handle = create(); if (!handle) { throw new Error("cnc_sim_create returned null"); @@ -88,258 +204,146 @@ function near(actual, expected) { let callbackPtr = 0; try { - reset(handle); - if (setDialect(handle, 0) !== 0) { - throw new Error(module.UTF8ToString(lastError(handle))); - } - - const config = writeString(module, JSON.stringify({ backend: "linuxcnc-rs274" })); - try { - if (loadConfig(handle, config.ptr, config.len) !== 0) { - throw new Error(module.UTF8ToString(lastError(handle))); - } - } finally { - module._free(config.ptr); - } + initializeHandle(handle); + loadJsonConfig(handle, { backend: "linuxcnc-rs274" }); const events = []; callbackPtr = module.addFunction((eventPtr) => { events.push(readEvent(module, eventPtr)); return 0; }, "iii"); - if (setCallback(handle, callbackPtr, 0) !== 0) { - throw new Error(module.UTF8ToString(lastError(handle))); - } - - const program = writeString(module, "G21 G90\nG0 X0\nG1 X5 F100\nM30\n"); - try { - if (parseProgram(handle, program.ptr, program.len) !== 0) { - throw new Error(module.UTF8ToString(lastError(handle))); - } - } finally { - module._free(program.ptr); - } - - if (!events.some((event) => event.type === "linear-feed" && event.endX === 5)) { - throw new Error("expected LinuxCNC WASM linear-feed event ending at X5"); - } - if (!events.some((event) => event.type === "program-end")) { - throw new Error("expected LinuxCNC WASM program-end event"); - } - - events.length = 0; - const switchProgram = writeString(module, "M428\nM429\nM430\n"); - try { - if (parseProgram(handle, switchProgram.ptr, switchProgram.len) !== 0) { - throw new Error(module.UTF8ToString(lastError(handle))); - } - } finally { - module._free(switchProgram.ptr); - } - - for (const kinstype of [1, 0, 2]) { - if (!events.some((event) => event.type === "kinematics-switch" && event.reserved === kinstype)) { - throw new Error(`expected LinuxCNC WASM kinematics switch ${kinstype}`); - } - if (!events.some((event) => event.type === "comment" && - event.reserved === 68 && - event.tool === 3 && - event.feed === kinstype)) { - throw new Error(`expected LinuxCNC WASM M68 E3 Q${kinstype} remap side effect`); - } - } - if (!events.some((event) => event.type === "comment" && - event.reserved === 66 && - event.tool === 0 && - event.feed === 0)) { - throw new Error("expected LinuxCNC WASM M66 E0 L0 remap side effect"); - } - - const switchkinsCases = [ - { - label: "scara", - config: { backend: "linuxcnc-rs274", switchkins: "scara" }, - program: "M428\nM429\nM430\n", - expected: [ - [1, 0], - [2, 1], - [3, 2], - ], - }, - { - label: "table-dual-rotary", - config: { backend: "linuxcnc-rs274", switchkins: "table-dual-rotary" }, - program: "M428\nM429\n", - expected: [ - [1, 1], - [2, 0], - ], - }, - { - label: "sim-xyzab-tdr-kins", - config: { backend: "linuxcnc-rs274", switchkins: "sim-xyzab-tdr-kins" }, - program: "M428\nM429\n", - expected: [ - [1, 1], - [2, 0], - ], - }, - { - label: "puma", - config: { backend: "linuxcnc-rs274", switchkins: "puma" }, - program: "M428\nM429\nM430\n", - expected: [ - [1, 0], - [2, 1], - [3, 2], - ], - }, - { - label: "melfa-sim", - config: { backend: "linuxcnc-rs274", switchkins: "melfa-sim" }, - program: "M428\nM429\nM430\n", - expected: [ - [1, 0], - [2, 1], - [3, 2], - ], - }, - { - label: "hexapod-sim", - config: { backend: "linuxcnc-rs274", remap: "hexapod-sim" }, - program: "M428\nM429\nM430\n", - expected: [ - [1, 0], - [2, 1], - [3, 2], - ], - }, - { - label: "xyzbca-trsrn", - config: { backend: "linuxcnc-rs274", switchkins: "xyzbca-trsrn" }, - program: "M428\nM429\nM430\n", - expected: [ - [1, 0], - [2, 1], - [3, 2], - ], - }, - ]; - - for (const testCase of switchkinsCases) { + attachCallback(handle, callbackPtr); + const parseCaseWithHandle = (testCase) => { reset(handle); events.length = 0; - const switchkinsConfig = writeString(module, JSON.stringify(testCase.config)); + loadJsonConfig(handle, testCase.config); + parseText(handle, testCase.program); + }; + const withConfiguredHandle = async (label, config, program, action) => { + events.length = 0; + const targetHandle = create(); + if (!targetHandle) { + throw new Error(`cnc_sim_create returned null for ${label} case`); + } try { - if (loadConfig(handle, switchkinsConfig.ptr, switchkinsConfig.len) !== 0) { - throw new Error(module.UTF8ToString(lastError(handle))); - } + initializeHandle(targetHandle); + attachCallback(targetHandle, callbackPtr); + loadJsonConfig(targetHandle, config); + parseText(targetHandle, program); + await action(); } finally { - module._free(switchkinsConfig.ptr); + destroy(targetHandle); } + }; - const switchkinsProgram = writeString(module, testCase.program); - try { - if (parseProgram(handle, switchkinsProgram.ptr, switchkinsProgram.len) !== 0) { - throw new Error(module.UTF8ToString(lastError(handle))); + await runStep("basic LinuxCNC RS274 parse", async () => { + parseText(handle, "G21 G90\nG0 X0\nG1 X5 F100\nM30\n"); + }); + await runStep("basic LinuxCNC RS274 linear-feed event", async () => { + expectEvent(events, (event) => event.type === "linear-feed" && event.endX === 5, + "expected LinuxCNC WASM linear-feed event ending at X5"); + }); + await runStep("basic LinuxCNC RS274 program-end event", async () => { + expectEvent(events, (event) => event.type === "program-end", + "expected LinuxCNC WASM program-end event"); + }); + + await runStep("default switchkins remap parse", async () => { + events.length = 0; + parseText(handle, "M428\nM429\nM430\n"); + }); + for (const kinstype of [1, 0, 2]) { + await runStep(`default switchkins kinematics switch ${kinstype}`, async () => { + expectEvent(events, (event) => event.type === "kinematics-switch" && event.reserved === kinstype, + `expected LinuxCNC WASM kinematics switch ${kinstype}`); + }); + await runStep(`default switchkins M68 E3 Q${kinstype} side effect`, async () => { + expectEvent(events, (event) => event.type === "comment" && + event.reserved === 68 && + event.tool === 3 && + event.feed === kinstype, + `expected LinuxCNC WASM M68 E3 Q${kinstype} remap side effect`); + }); + } + await runStep("default switchkins M66 E0 L0 side effect", async () => { + expectEvent(events, (event) => event.type === "comment" && + event.reserved === 66 && + event.tool === 0 && + event.feed === 0, + "expected LinuxCNC WASM M66 E0 L0 remap side effect"); + }); + + const switchkinsCases = loadSwitchkinsCases(); + await runStep("generated switchkins config cases load", async () => { + if (switchkinsCases.length === 0) { + throw new Error("expected generated LinuxCNC WASM switchkins config cases"); + } + }); + for (const testCase of switchkinsCases) { + await runStep(`generated switchkins ${testCase.label}`, async () => { + parseCaseWithHandle(testCase); + for (const [line, kinstype] of testCase.expected) { + expectKinematicsSwitch(events, line, kinstype, testCase.label); } - } finally { - module._free(switchkinsProgram.ptr); - } - - for (const [line, kinstype] of testCase.expected) { - expectKinematicsSwitch(events, line, kinstype, testCase.label); - } + }); } + await runStep("5axiskins RTCP parse", async () => { reset(handle); events.length = 0; - const fiveaxisConfig = writeString( - module, - JSON.stringify({ - backend: "linuxcnc-rs274", - rtcp: { enabled: true, toolLength: 250 }, - kinematics: "5axiskins", - pivotLength: 250, - }), - ); - try { - if (loadConfig(handle, fiveaxisConfig.ptr, fiveaxisConfig.len) !== 0) { - throw new Error(module.UTF8ToString(lastError(handle))); - } - } finally { - module._free(fiveaxisConfig.ptr); - } - - const fiveaxisProgram = writeString(module, "M428\nG0 X260 Y20 Z280 B90 C0\n"); - try { - if (parseProgram(handle, fiveaxisProgram.ptr, fiveaxisProgram.len) !== 0) { - throw new Error(module.UTF8ToString(lastError(handle))); - } - } finally { - module._free(fiveaxisProgram.ptr); - } - + loadJsonConfig(handle, { + backend: "linuxcnc-rs274", + rtcp: { enabled: true, toolLength: 250 }, + kinematics: "5axiskins", + pivotLength: 250, + }); + parseText(handle, "M428\nG0 X260 Y20 Z280 B90 C0\n"); + }); + await runStep("5axiskins RTCP kinematics switch", async () => { expectKinematicsSwitch(events, 1, 0, "5axiskins"); - if (!events.some((event) => event.type === "rtcp-pivot" && - event.line === 2 && - event.reserved === 0 && - near(event.dwellSeconds, 250) && - near(event.endX, 10) && - near(event.endY, 20) && - near(event.endZ, 30) && - near(event.endB, 90) && - near(event.endC, 0))) { - throw new Error("expected LinuxCNC WASM 5axiskins RTCP pivot from LinuxCNC 5axiskins inverse"); - } + }); + await runStep("5axiskins RTCP pivot event", async () => { + expectRtcpPivot(events, { + line: 2, + reserved: 0, + dwellSeconds: 250, + endX: 10, + endY: 20, + endZ: 30, + endB: 90, + endC: 0, + }, "expected LinuxCNC WASM 5axiskins RTCP pivot from LinuxCNC 5axiskins inverse"); + }); + await runStep("userk RTCP parse", async () => { reset(handle); events.length = 0; - const userkConfig = writeString( - module, - JSON.stringify({ - backend: "linuxcnc-rs274", - rtcp: { enabled: true, toolLength: 250 }, - }), - ); - try { - if (loadConfig(handle, userkConfig.ptr, userkConfig.len) !== 0) { - throw new Error(module.UTF8ToString(lastError(handle))); - } - } finally { - module._free(userkConfig.ptr); - } - - const userkProgram = writeString(module, "M430\nG0 X260 Y20 Z280 B90 C45\n"); - try { - if (parseProgram(handle, userkProgram.ptr, userkProgram.len) !== 0) { - throw new Error(module.UTF8ToString(lastError(handle))); - } - } finally { - module._free(userkProgram.ptr); - } - + loadJsonConfig(handle, { + backend: "linuxcnc-rs274", + rtcp: { enabled: true, toolLength: 250 }, + }); + parseText(handle, "M430\nG0 X260 Y20 Z280 B90 C45\n"); + }); + await runStep("userk RTCP kinematics switch", async () => { expectKinematicsSwitch(events, 1, 2, "userk"); - if (!events.some((event) => event.type === "rtcp-pivot" && - event.line === 2 && - event.reserved === 2 && - near(event.dwellSeconds, 250) && - near(event.endX, 260) && - near(event.endY, 20) && - near(event.endZ, 280) && - near(event.endB, 90) && - near(event.endC, 45))) { - throw new Error("expected LinuxCNC WASM userk RTCP pivot from LinuxCNC userk identity inverse"); - } + }); + await runStep("userk RTCP pivot event", async () => { + expectRtcpPivot(events, { + line: 2, + reserved: 2, + dwellSeconds: 250, + endX: 260, + endY: 20, + endZ: 280, + endB: 90, + endC: 45, + }, "expected LinuxCNC WASM userk RTCP pivot from LinuxCNC userk identity inverse"); + }); - events.length = 0; - const configuredTrtHandle = create(); - if (!configuredTrtHandle) { - throw new Error("cnc_sim_create returned null for configured xyzbc-trt case"); - } - const configuredTrtConfig = writeString( - module, - JSON.stringify({ + await runStep("configured xyzbc-trt RTCP parse", async () => { + await withConfiguredHandle( + "configured xyzbc-trt", + { backend: "linuxcnc-rs274", rtcp: { enabled: false, toolLength: 11 }, xyzbcTrt: { @@ -350,54 +354,32 @@ function near(actual, expected) { zOffset: 7, conventionalDirections: true, }, - }), + }, + "M428\nG43.4\nG0 X30 Y40 Z50 B20 C-30\n", + async () => { + }, ); - try { - reset(configuredTrtHandle); - if (setDialect(configuredTrtHandle, 0) !== 0) { - throw new Error(module.UTF8ToString(lastError(configuredTrtHandle))); - } - if (setCallback(configuredTrtHandle, callbackPtr, 0) !== 0) { - throw new Error(module.UTF8ToString(lastError(configuredTrtHandle))); - } - if (loadConfig(configuredTrtHandle, configuredTrtConfig.ptr, configuredTrtConfig.len) !== 0) { - throw new Error(module.UTF8ToString(lastError(configuredTrtHandle))); - } - } finally { - module._free(configuredTrtConfig.ptr); - } + }); + await runStep("configured xyzbc-trt kinematics switch", async () => { + expectKinematicsSwitch(events, 1, 1, "configured xyzbc-trt"); + }); + await runStep("configured xyzbc-trt RTCP pivot event", async () => { + expectRtcpPivot(events, { + line: 3, + reserved: 1, + dwellSeconds: 11, + endX: -4.814629, + endY: 47.605118, + endZ: 48.160567, + endB: 20, + endC: -30, + }, "expected LinuxCNC WASM configured xyzbc-trt RTCP pivot from LinuxCNC trtfuncs inverse"); + }); - const configuredTrtProgram = writeString(module, "M428\nG43.4\nG0 X30 Y40 Z50 B20 C-30\n"); - try { - if (parseProgram(configuredTrtHandle, configuredTrtProgram.ptr, configuredTrtProgram.len) !== 0) { - throw new Error(module.UTF8ToString(lastError(configuredTrtHandle))); - } - } finally { - module._free(configuredTrtProgram.ptr); - destroy(configuredTrtHandle); - } - - expectKinematicsSwitch(events, 1, 1, "configured xyzbc-trt"); - if (!events.some((event) => event.type === "rtcp-pivot" && - event.line === 3 && - event.reserved === 1 && - near(event.dwellSeconds, 11) && - near(event.endX, -4.814629) && - near(event.endY, 47.605118) && - near(event.endZ, 48.160567) && - near(event.endB, 20) && - near(event.endC, -30))) { - throw new Error("expected LinuxCNC WASM configured xyzbc-trt RTCP pivot from LinuxCNC trtfuncs inverse"); - } - - events.length = 0; - const configuredXyzacHandle = create(); - if (!configuredXyzacHandle) { - throw new Error("cnc_sim_create returned null for configured xyzac-trt case"); - } - const configuredXyzacConfig = writeString( - module, - JSON.stringify({ + await runStep("configured xyzac-trt RTCP parse", async () => { + await withConfiguredHandle( + "configured xyzac-trt", + { backend: "linuxcnc-rs274", rtcp: { enabled: false, toolLength: 7 }, switchkins: "xyzac-trt", @@ -405,47 +387,30 @@ function near(actual, expected) { yOffset: 20, zOffset: 10, }, - }), + }, + "M428\nG43.4\nG0 X12 Y-8 Z42 A35 C-25\n", + async () => { + }, ); - try { - reset(configuredXyzacHandle); - if (setDialect(configuredXyzacHandle, 0) !== 0) { - throw new Error(module.UTF8ToString(lastError(configuredXyzacHandle))); - } - if (setCallback(configuredXyzacHandle, callbackPtr, 0) !== 0) { - throw new Error(module.UTF8ToString(lastError(configuredXyzacHandle))); - } - if (loadConfig(configuredXyzacHandle, configuredXyzacConfig.ptr, configuredXyzacConfig.len) !== 0) { - throw new Error(module.UTF8ToString(lastError(configuredXyzacHandle))); - } - } finally { - module._free(configuredXyzacConfig.ptr); - } - - const configuredXyzacProgram = writeString(module, "M428\nG43.4\nG0 X12 Y-8 Z42 A35 C-25\n"); - try { - if (parseProgram(configuredXyzacHandle, configuredXyzacProgram.ptr, configuredXyzacProgram.len) !== 0) { - throw new Error(module.UTF8ToString(lastError(configuredXyzacHandle))); - } - } finally { - module._free(configuredXyzacProgram.ptr); - destroy(configuredXyzacHandle); - } - - expectKinematicsSwitch(events, 1, 1, "configured xyzac-trt"); - if (!events.some((event) => event.type === "rtcp-pivot" && - event.line === 3 && - event.reserved === 1 && - near(event.dwellSeconds, 7) && - near(event.endX, 7.494747) && - near(event.endY, -20.815946) && - near(event.endZ, 18.939732) && - near(event.endA, 35) && - near(event.endB, 0) && - near(event.endC, -25))) { - throw new Error("expected LinuxCNC WASM configured xyzac-trt RTCP pivot from LinuxCNC xyzab_tdr inverse"); - } + }); + await runStep("configured xyzac-trt kinematics switch", async () => { + expectKinematicsSwitch(events, 1, 1, "configured xyzac-trt"); + }); + await runStep("configured xyzac-trt RTCP pivot event", async () => { + expectRtcpPivot(events, { + line: 3, + reserved: 1, + dwellSeconds: 7, + endX: 7.494747, + endY: -20.815946, + endZ: 18.939732, + endA: 35, + endB: 0, + endC: -25, + }, "expected LinuxCNC WASM configured xyzac-trt RTCP pivot from LinuxCNC xyzab_tdr inverse"); + }); + await runStep("web wasm-core wrapper defaults and options", async () => { globalThis.createCncSimModule = createCncSimModule; const wasmCoreUrl = pathToFileURL(path.join(__dirname, "web", "src", "wasm-core.js")).href; const { createWasmSimulator } = await import(wasmCoreUrl); @@ -461,6 +426,31 @@ function near(actual, expected) { near(event.end.x, 3))) { throw new Error("expected web wasm-core default backend to use LinuxCNC RS274"); } + if (webSimulator.fs.opfs !== null) { + throw new Error("expected Node wasm-core wrapper to leave OPFS disabled"); + } + await expectErrorContaining( + () => webSimulator.parseFile("programs/node-smoke.ngc", "linuxcnc", { backend: "linuxcnc-rs274" }), + "OPFS workspace is not available", + "expected Node wasm-core parseFile to require OPFS", + ); + await expectErrorContaining( + () => webSimulator.parseWithParameterFile("G21 G90\nM30\n", "parameters/node.var", "linuxcnc", { + backend: "linuxcnc-rs274", + }), + "OPFS workspace is not available", + "expected Node wasm-core parseWithParameterFile to require OPFS", + ); + await expectErrorContaining( + () => webSimulator.parseFileWithParameterFile( + "programs/node-smoke.ngc", + "parameters/node.var", + "linuxcnc", + { backend: "linuxcnc-rs274" }, + ), + "OPFS workspace is not available", + "expected Node wasm-core parseFileWithParameterFile to require OPFS", + ); const webEvents = webSimulator.parse( "M428\nG43.4\nG0 X12 Y-8 Z42 A35 C-25\n", @@ -475,22 +465,22 @@ function near(actual, expected) { }, }, ); - if (!webEvents.some((event) => event.type === "rtcp-pivot" && - event.line === 3 && - event.reserved === 1 && - near(event.dwellSeconds, 7) && - near(event.end.x, 7.494747) && - near(event.end.y, -20.815946) && - near(event.end.z, 18.939732) && - near(event.end.a, 35) && - near(event.end.b, 0) && - near(event.end.c, -25))) { - throw new Error("expected web wasm-core options to pass LinuxCNC xyzac-trt RTCP config into WASM"); - } + expectEvent(webEvents, (event) => event.type === "rtcp-pivot" && + event.line === 3 && + event.reserved === 1 && + near(event.dwellSeconds, 7) && + near(event.end.x, 7.494747) && + near(event.end.y, -20.815946) && + near(event.end.z, 18.939732) && + near(event.end.a, 35) && + near(event.end.b, 0) && + near(event.end.c, -25), + "expected web wasm-core options to pass LinuxCNC xyzac-trt RTCP config into WASM"); } finally { webSimulator.dispose(); delete globalThis.createCncSimModule; } + }); } finally { if (callbackPtr) { module.removeFunction(callbackPtr); @@ -498,7 +488,7 @@ function near(actual, expected) { destroy(handle); } - console.log("web wasm node smoke passed"); + console.log(`web wasm node smoke passed (${completedSteps} steps)`); })().catch((error) => { console.error(error && error.stack ? error.stack : error); process.exit(1); diff --git a/test-web-wasm-node-smoke.sh b/test-web-wasm-node-smoke.sh index 1125bf6..9a901ad 100755 --- a/test-web-wasm-node-smoke.sh +++ b/test-web-wasm-node-smoke.sh @@ -4,20 +4,116 @@ set -euo pipefail cd "$(dirname "$0")" missing=() -for required in node web/public/cnc_sim.js web/public/cnc_sim.wasm; do +minimum_steps=${WASM_NODE_SMOKE_MIN_STEPS:-50} +if ! [[ "$minimum_steps" =~ ^[1-9][0-9]*$ ]]; then + echo "WASM_NODE_SMOKE_MIN_STEPS must be a positive integer: $minimum_steps" >&2 + exit 1 +fi + +require_command() { + local command_name=$1 + if ! command -v "$command_name" >/dev/null 2>&1; then + missing+=("$command_name") + fi +} + +require_file() { + local file_path=$1 + if [[ ! -f "$file_path" ]]; then + missing+=("$file_path") + elif [[ ! -r "$file_path" || ! -s "$file_path" ]]; then + missing+=("readable non-empty $file_path") + fi +} + +report_missing_prerequisites() { + echo "WASM Node smoke prerequisites are missing: ${missing[*]}." >&2 + echo "Run ./build-wasm.sh after activating emsdk to generate web/public/cnc_sim.js and web/public/cnc_sim.wasm." >&2 +} + +required_files=( + test-web-wasm-node-smoke.cjs + web/public/cnc_sim.js + web/public/cnc_sim.wasm + web/public/linuxcnc_switchkins_remap_config_cases.json + web/src/app.js + web/src/wasm-core.js +) + +for required in \ + node \ + "${required_files[@]}" +do if [[ "$required" == node ]]; then - if ! command -v node >/dev/null 2>&1; then - missing+=("node") - fi - elif [[ ! -f "$required" ]]; then - missing+=("$required") + require_command node + else + require_file "$required" fi done if ((${#missing[@]} > 0)); then - echo "WASM Node smoke prerequisites are missing: ${missing[*]}." >&2 - echo "Run ./build-wasm.sh after activating emsdk to generate web/public/cnc_sim.js and web/public/cnc_sim.wasm." >&2 + report_missing_prerequisites exit 1 fi -node test-web-wasm-node-smoke.cjs +if ! node - <<'JS' +const fs = require("fs"); +const cases = JSON.parse(fs.readFileSync("web/public/linuxcnc_switchkins_remap_config_cases.json", "utf8")); +if (!Array.isArray(cases) || cases.length === 0) { + throw new Error("expected non-empty LinuxCNC switchkins config case JSON"); +} +for (const [index, configCase] of cases.entries()) { + if ( + !configCase || + typeof configCase.field !== "string" || + typeof configCase.value !== "string" || + !Number.isInteger(configCase.m428) || + !Number.isInteger(configCase.m429) || + !Number.isInteger(configCase.m430) + ) { + throw new Error(`invalid LinuxCNC switchkins config case at index ${index}`); + } +} +JS +then + echo "WASM Node smoke switchkins config case JSON check failed" >&2 + exit 1 +fi + +node_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_node_smoke.XXXXXX.log") +cleanup() { + rm -f "$node_log" +} +trap cleanup EXIT + +echo "running WASM Node smoke" +echo "WASM Node smoke node version: $(node --version)" +if ! node --check test-web-wasm-node-smoke.cjs >"$node_log" 2>&1; then + echo "WASM Node smoke syntax check failed" >&2 + sed -n '1,80p' "$node_log" >&2 + exit 1 +fi +for web_module in web/public/cnc_sim.js web/src/wasm-core.js web/src/app.js; do + if ! node --check "$web_module" >"$node_log" 2>&1; then + echo "WASM Node smoke web module syntax check failed for $web_module" >&2 + sed -n '1,80p' "$node_log" >&2 + exit 1 + fi +done +if ! node test-web-wasm-node-smoke.cjs >"$node_log" 2>&1; then + echo "WASM Node smoke failed" >&2 + sed -n '1,120p' "$node_log" >&2 + exit 1 +fi +passed_steps=$(grep -o 'web wasm node smoke passed ([0-9][0-9]* steps)' "$node_log" | tail -n 1 | grep -o '[0-9][0-9]*' || true) +if [[ -z "$passed_steps" ]]; then + echo "WASM Node smoke did not report a completed step count" >&2 + sed -n '1,120p' "$node_log" >&2 + exit 1 +fi +if ((passed_steps < minimum_steps)); then + echo "WASM Node smoke covered $passed_steps steps, expected at least $minimum_steps" >&2 + sed -n '1,120p' "$node_log" >&2 + exit 1 +fi +cat "$node_log" diff --git a/web/index.html b/web/index.html index 908e40b..b9e2a92 100644 --- a/web/index.html +++ b/web/index.html @@ -12,6 +12,7 @@
CNC SIM
WASM OFFLINE + OPFS INIT MEM MM RESET @@ -65,7 +66,6 @@ BACKEND 0 LINES diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..cda3005 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,994 @@ +{ + "name": "cnc-wasm-simulator-web", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cnc-wasm-simulator-web", + "dependencies": { + "three": "^0.164.0" + }, + "devDependencies": { + "@types/three": "^0.164.0", + "typescript": "^5.4.5", + "vite": "^5.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "dev": true + }, + "node_modules/@types/three": { + "version": "0.164.1", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.164.1.tgz", + "integrity": "sha512-dR/trWDhyaNqJV38rl1TonlCA9DpnX7OPYDWD81bmBGn/+uEc3+zNalFxQcV4FlPTeDBhCY3SFWKvK6EJwL88g==", + "dev": true, + "dependencies": { + "@tweenjs/tween.js": "~23.1.1", + "@types/stats.js": "*", + "@types/webxr": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~0.18.1" + } + }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/meshoptimizer": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz", + "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/three": { + "version": "0.164.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.164.1.tgz", + "integrity": "sha512-iC/hUBbl1vzFny7f5GtqzVXYjMJKaTPxiCxXfrvVdBi1Sf+jhd1CAkitiFwC7mIBFCo3MrDLJG97yisoaWig0w==" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + } +} diff --git a/web/public/cnc_sim.js b/web/public/cnc_sim.js index 44a8294..706598d 100644 --- a/web/public/cnc_sim.js +++ b/web/public/cnc_sim.js @@ -6,7 +6,7 @@ var createCncSimModule = (() => { function(moduleArg = {}) { var moduleRtn; -var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof importScripts=="function";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string"&&process.type!="renderer";if(ENVIRONMENT_IS_NODE){}var moduleOverrides=Object.assign({},Module);var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("fs");var nodePath=require("path");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);var ret=fs.readFileSync(filename);return ret};readAsync=(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);return new Promise((resolve,reject)=>{fs.readFile(filename,binary?undefined:"utf8",(err,data)=>{if(err)reject(err);else resolve(binary?data.buffer:data)})})};if(!Module["thisProgram"]&&process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=url=>{if(isFileURI(url)){return new Promise((resolve,reject)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){resolve(xhr.response);return}reject(xhr.status)};xhr.onerror=reject;xhr.send(null)})}return fetch(url,{credentials:"same-origin"}).then(response=>{if(response.ok){return response.arrayBuffer()}return Promise.reject(new Error(response.status+" : "+response.url))})}}}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var wasmBinary=Module["wasmBinary"];var wasmMemory;var ABORT=false;var EXITSTATUS;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b)}var __ATPRERUN__=[];var __ATINIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;function preRun(){var preRuns=Module["preRun"];if(preRuns){if(typeof preRuns=="function")preRuns=[preRuns];preRuns.forEach(addOnPreRun)}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!FS.initialized)FS.init();FS.ignorePermissions=false;TTY.init();callRuntimeCallbacks(__ATINIT__)}function postRun(){var postRuns=Module["postRun"];if(postRuns){if(typeof postRuns=="function")postRuns=[postRuns];postRuns.forEach(addOnPostRun)}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function getUniqueRunDependency(id){return id}function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var dataURIPrefix="data:application/octet-stream;base64,";var isDataURI=filename=>filename.startsWith(dataURIPrefix);var isFileURI=filename=>filename.startsWith("file://");function findWasmBinary(){var f="cnc_sim.wasm";if(!isDataURI(f)){return locateFile(f)}return f}var wasmBinaryFile;function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}function getBinaryPromise(binaryFile){if(!wasmBinary){return readAsync(binaryFile).then(response=>new Uint8Array(response),()=>getBinarySync(binaryFile))}return Promise.resolve().then(()=>getBinarySync(binaryFile))}function instantiateArrayBuffer(binaryFile,imports,receiver){return getBinaryPromise(binaryFile).then(binary=>WebAssembly.instantiate(binary,imports)).then(receiver,reason=>{err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)})}function instantiateAsync(binary,binaryFile,imports,callback){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!isDataURI(binaryFile)&&!isFileURI(binaryFile)&&!ENVIRONMENT_IS_NODE&&typeof fetch=="function"){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{var result=WebAssembly.instantiateStreaming(response,imports);return result.then(callback,function(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(binaryFile,imports,callback)})})}return instantiateArrayBuffer(binaryFile,imports,callback)}function getWasmImports(){return{a:wasmImports}}function createWasm(){var info=getWasmImports();function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["B"];updateMemoryViews();wasmTable=wasmExports["F"];addOnInit(wasmExports["C"]);removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){receiveInstance(result["instance"])}if(Module["instantiateWasm"]){try{return Module["instantiateWasm"](info,receiveInstance)}catch(e){err(`Module.instantiateWasm callback failed with error: ${e}`);readyPromiseReject(e)}}wasmBinaryFile??=findWasmBinary();instantiateAsync(wasmBinary,wasmBinaryFile,info,receiveInstantiationResult).catch(readyPromiseReject);return{}}var tempDouble;var tempI64;function ExitStatus(status){this.name="ExitStatus";this.message=`Program terminated with exit(${status})`;this.status=status}var callRuntimeCallbacks=callbacks=>{callbacks.forEach(f=>f(Module))};var noExitRuntime=Module["noExitRuntime"]||true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var exceptionLast=0;var uncaughtExceptionCount=0;var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.substr(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.substr(0,dir.length-1)}return root+dir},basename:path=>{if(path==="/")return"/";path=PATH.normalize(path);path=path.replace(/\/$/,"");var lastSlash=path.lastIndexOf("/");if(lastSlash===-1)return path;return path.substr(lastSlash+1)},join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>{if(typeof crypto=="object"&&typeof crypto["getRandomValues"]=="function"){return view=>crypto.getRandomValues(view)}else if(ENVIRONMENT_IS_NODE){try{var crypto_module=require("crypto");var randomFillSync=crypto_module["randomFillSync"];if(randomFillSync){return view=>crypto_module["randomFillSync"](view)}var randomBytes=crypto_module["randomBytes"];return view=>(view.set(randomBytes(view.byteLength)),view)}catch(e){}}abort("initRandomDevice")};var randomFill=view=>(randomFill=initRandomFill())(view);var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).substr(1);to=PATH_FS.resolve(to).substr(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var FS_stdin_getChar_buffer=[];var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx};function intArrayFromString(stringy,dontAddNull,length){var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array}var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(ENVIRONMENT_IS_NODE){var BUFSIZE=256;var buf=Buffer.alloc(BUFSIZE);var bytesRead=0;var fd=process.stdin.fd;try{bytesRead=fs.readSync(fd,buf,0,BUFSIZE)}catch(e){if(e.toString().includes("EOF"))bytesRead=0;else throw e}if(bytesRead>0){result=buf.slice(0,bytesRead).toString("utf-8")}}else if(typeof window!="undefined"&&typeof window.prompt=="function"){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output&&tty.output.length>0){err(UTF8ArrayToString(tty.output));tty.output=[]}}}};var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var mmapAlloc=size=>{abort()};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16384|511,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,allocate:MEMFS.stream_ops.allocate,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.timestamp=Date.now();if(parent){parent.contents[name]=node;parent.timestamp=node.timestamp}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.timestamp);attr.mtime=new Date(node.timestamp);attr.ctime=new Date(node.timestamp);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){if(attr.mode!==undefined){node.mode=attr.mode}if(attr.timestamp!==undefined){node.timestamp=attr.timestamp}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){throw FS.genericErrors[44]},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){if(FS.isDir(old_node.mode)){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}}delete old_node.parent.contents[old_node.name];old_node.parent.timestamp=Date.now();old_node.name=new_name;new_dir.contents[new_name]=old_node;new_dir.timestamp=old_node.parent.timestamp},unlink(parent,name){delete parent.contents[name];parent.timestamp=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.timestamp=Date.now()},readdir(node){var entries=[".",".."];for(var key of Object.keys(node.contents)){entries.push(key)}return entries},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{var dep=!noRunDep?getUniqueRunDependency(`al ${url}`):"";readAsync(url).then(arrayBuffer=>{onload(new Uint8Array(arrayBuffer));if(dep)removeRunDependency(dep)},err=>{if(onerror){onerror()}else{throw`Loading data file "${url}" failed.`}});if(dep)addRunDependency(dep)};var FS_createDataFile=(parent,name,fileData,canRead,canWrite,canOwn)=>{FS.createDataFile(parent,name,fileData,canRead,canWrite,canOwn)};var preloadPlugins=Module["preloadPlugins"]||[];var FS_handledByPreloadPlugin=(byteArray,fullname,finish,onerror)=>{if(typeof Browser!="undefined")Browser.init();var handled=false;preloadPlugins.forEach(plugin=>{if(handled)return;if(plugin["canHandle"](fullname)){plugin["handle"](byteArray,fullname,finish,onerror);handled=true}});return handled};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);function processData(byteArray){function finish(byteArray){preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}onload?.();removeRunDependency(dep)}if(FS_handledByPreloadPlugin(byteArray,fullname,finish,()=>{onerror?.();removeRunDependency(dep)})){return}finish(byteArray)}addRunDependency(dep);if(typeof url=="string"){asyncLoad(url,processData,onerror)}else{processData(url)}};var FS_modeStringToFlags=str=>{var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,ErrnoError:class{constructor(errno){this.name="ErrnoError";this.errno=errno}},genericErrors:{},filesystems:null,syncFSRequests:0,readFiles:{},FSStream:class{constructor(){this.shared={}}get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.mounted=null;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.node_ops={};this.stream_ops={};this.rdev=rdev;this.readMode=292|73;this.writeMode=146}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){path=PATH_FS.resolve(path);if(!path)return{path:"",node:null};var defaults={follow_mount:true,recurse_count:0};opts=Object.assign(defaults,opts);if(opts.recurse_count>8){throw new FS.ErrnoError(32)}var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i40){throw new FS.ErrnoError(32)}}}}return{path:current_path,node:current}},getPath(node){var path;while(true){if(FS.isRoot(node)){var mount=node.mount.mountpoint;if(!path)return mount;return mount[mount.length-1]!=="/"?`${mount}/${path}`:mount+path}path=path?`${node.name}/${path}`:node.name;node=node.parent}},hashName(parentid,name){var hash=0;for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&512){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}mounts.forEach(mount=>{if(!mount.type.syncfs){return done(null)}mount.type.syncfs(mount,populate,done)})},mount(type,opts,mountpoint){var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type,opts,mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);Object.keys(FS.nameTable).forEach(hash=>{var current=FS.nameTable[hash];while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}});node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name||name==="."||name===".."){throw new FS.ErrnoError(28)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},create(path,mode){mode=mode!==undefined?mode:438;mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode){mode=mode!==undefined?mode:511;mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var i=0;iFS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomLeft=randomFill(randomBuffer).byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16384|511,73);node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path}};ret.parent=ret;return ret}};return node}},{},"/proc/self/fd")},createStandardStreams(input,output,error){if(input){FS.createDevice("/dev","stdin",input)}else{FS.symlink("/dev/tty","/dev/stdin")}if(output){FS.createDevice("/dev","stdout",null,output)}else{FS.symlink("/dev/tty","/dev/stdout")}if(error){FS.createDevice("/dev","stderr",null,error)}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1)},staticInit(){[44].forEach(code=>{FS.genericErrors[code]=new FS.ErrnoError(code);FS.genericErrors[code].stack=""});FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS}},init(input,output,error){FS.initialized=true;input??=Module["stdin"];output??=Module["stdout"];error??=Module["stderr"];FS.createStandardStreams(input,output,error)},quit(){FS.initialized=false;for(var i=0;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)throw new Error("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)throw new Error("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")throw new Error("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(typeof XMLHttpRequest!="undefined"){if(!ENVIRONMENT_IS_WORKER)throw"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc";var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};var keys=Object.keys(node.stream_ops);keys.forEach(key=>{var fn=node.stream_ops[key];stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}});function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}};node.stream_ops=stream_ops;return node}};var UTF8ToString=(ptr,maxBytesToRead)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";var SYSCALLS={DEFAULT_POLLMASK:5,calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return PATH.join2(dir,path)},doStat(func,path,buf){var stat=func(path);HEAP32[buf>>2]=stat.dev;HEAP32[buf+4>>2]=stat.mode;HEAPU32[buf+8>>2]=stat.nlink;HEAP32[buf+12>>2]=stat.uid;HEAP32[buf+16>>2]=stat.gid;HEAP32[buf+20>>2]=stat.rdev;tempI64=[stat.size>>>0,(tempDouble=stat.size,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+24>>2]=tempI64[0],HEAP32[buf+28>>2]=tempI64[1];HEAP32[buf+32>>2]=4096;HEAP32[buf+36>>2]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();tempI64=[Math.floor(atime/1e3)>>>0,(tempDouble=Math.floor(atime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+40>>2]=tempI64[0],HEAP32[buf+44>>2]=tempI64[1];HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;tempI64=[Math.floor(mtime/1e3)>>>0,(tempDouble=Math.floor(mtime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+56>>2]=tempI64[0],HEAP32[buf+60>>2]=tempI64[1];HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;tempI64=[Math.floor(ctime/1e3)>>>0,(tempDouble=Math.floor(ctime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+72>>2]=tempI64[0],HEAP32[buf+76>>2]=tempI64[1];HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;tempI64=[stat.ino>>>0,(tempDouble=stat.ino,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+88>>2]=tempI64[0],HEAP32[buf+92>>2]=tempI64[1];return 0},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_faccessat(dirfd,path,amode,flags){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);if(amode&~7){return-28}var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;if(!node){return-44}var perms="";if(amode&4)perms+="r";if(amode&2)perms+="w";if(amode&1)perms+="x";if(perms&&FS.nodePermissions(node,perms)){return-2}return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function syscallGetVarargI(){var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret}var syscallGetVarargP=syscallGetVarargI;function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fdatasync(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fstat64(fd,buf){try{var stream=SYSCALLS.getStreamFromFD(fd);return SYSCALLS.doStat(FS.stat,stream.path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);function ___syscall_getcwd(buf,size){try{if(size===0)return-28;var cwd=FS.cwd();var cwdLengthInBytes=lengthBytesUTF8(cwd)+1;if(size>>0,(tempDouble=id,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[dirp+pos>>2]=tempI64[0],HEAP32[dirp+pos+4>>2]=tempI64[1];tempI64=[(idx+1)*struct_size>>>0,(tempDouble=(idx+1)*struct_size,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[dirp+pos+8>>2]=tempI64[0],HEAP32[dirp+pos+12>>2]=tempI64[1];HEAP16[dirp+pos+16>>1]=280;HEAP8[dirp+pos+18]=type;stringToUTF8(name,dirp+pos+19,256);pos+=struct_size;idx+=1}FS.llseek(stream,idx*struct_size,0);return pos}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(op){case 21509:{if(!stream.tty)return-59;return 0}case 21505:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcgets){var termios=stream.tty.ops.ioctl_tcgets(stream);var argp=syscallGetVarargP();HEAP32[argp>>2]=termios.c_iflag||0;HEAP32[argp+4>>2]=termios.c_oflag||0;HEAP32[argp+8>>2]=termios.c_cflag||0;HEAP32[argp+12>>2]=termios.c_lflag||0;for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag,c_oflag,c_cflag,c_lflag,c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];HEAP16[argp+2>>1]=winsize[1]}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_newfstatat(dirfd,path,buf,flags){try{path=SYSCALLS.getStr(path);var nofollow=flags&256;var allowEmpty=flags&4096;flags=flags&~6400;path=SYSCALLS.calculateAt(dirfd,path,allowEmpty);return SYSCALLS.doStat(nofollow?FS.lstat:FS.stat,path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_readlinkat(dirfd,path,buf,bufsize){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);if(bufsize<=0)return-28;var ret=FS.readlink(path);var len=Math.min(bufsize,lengthBytesUTF8(ret));var endChar=HEAP8[buf+len];stringToUTF8(ret,buf,bufsize+1);HEAP8[buf+len]=endChar;return len}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_renameat(olddirfd,oldpath,newdirfd,newpath){try{oldpath=SYSCALLS.getStr(oldpath);newpath=SYSCALLS.getStr(newpath);oldpath=SYSCALLS.calculateAt(olddirfd,oldpath);newpath=SYSCALLS.calculateAt(newdirfd,newpath);FS.rename(oldpath,newpath);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_stat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.doStat(FS.stat,path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_unlinkat(dirfd,path,flags){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);if(flags===0){FS.unlink(path)}else if(flags===512){FS.rmdir(path)}else{abort("Invalid flags passed to unlinkat")}return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __abort_js=()=>{abort("")};var __emscripten_memcpy_js=(dest,src,num)=>HEAPU8.copyWithin(dest,src,src+num);var isLeapYear=year=>year%4===0&&(year%100!==0||year%400===0);var MONTH_DAYS_LEAP_CUMULATIVE=[0,31,60,91,121,152,182,213,244,274,305,335];var MONTH_DAYS_REGULAR_CUMULATIVE=[0,31,59,90,120,151,181,212,243,273,304,334];var ydayFromDate=date=>{var leap=isLeapYear(date.getFullYear());var monthDaysCumulative=leap?MONTH_DAYS_LEAP_CUMULATIVE:MONTH_DAYS_REGULAR_CUMULATIVE;var yday=monthDaysCumulative[date.getMonth()]+date.getDate()-1;return yday};var convertI32PairToI53Checked=(lo,hi)=>hi+2097152>>>0<4194305-!!lo?(lo>>>0)+hi*4294967296:NaN;function __localtime_js(time_low,time_high,tmPtr){var time=convertI32PairToI53Checked(time_low,time_high);var date=new Date(time*1e3);HEAP32[tmPtr>>2]=date.getSeconds();HEAP32[tmPtr+4>>2]=date.getMinutes();HEAP32[tmPtr+8>>2]=date.getHours();HEAP32[tmPtr+12>>2]=date.getDate();HEAP32[tmPtr+16>>2]=date.getMonth();HEAP32[tmPtr+20>>2]=date.getFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getDay();var yday=ydayFromDate(date)|0;HEAP32[tmPtr+28>>2]=yday;HEAP32[tmPtr+36>>2]=-(date.getTimezoneOffset()*60);var start=new Date(date.getFullYear(),0,1);var summerOffset=new Date(date.getFullYear(),6,1).getTimezoneOffset();var winterOffset=start.getTimezoneOffset();var dst=(summerOffset!=winterOffset&&date.getTimezoneOffset()==Math.min(winterOffset,summerOffset))|0;HEAP32[tmPtr+32>>2]=dst}var __tzset_js=(timezone,daylight,std_name,dst_name)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);var extractZone=timezoneOffset=>{var sign=timezoneOffset>=0?"-":"+";var absOffset=Math.abs(timezoneOffset);var hours=String(Math.floor(absOffset/60)).padStart(2,"0");var minutes=String(absOffset%60).padStart(2,"0");return`UTC${sign}${hours}${minutes}`};var winterName=extractZone(winterOffset);var summerName=extractZone(summerOffset);if(summerOffsetDate.now();var getHeapMax=()=>2147483648;var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(typeof navigator=="object"&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var stringToAscii=(str,buffer)=>{for(var i=0;i{var bufSize=0;getEnvStrings().forEach((string,i)=>{var ptr=environ_buf+bufSize;HEAPU32[__environ+i*4>>2]=ptr;stringToAscii(string,ptr);bufSize+=string.length+1});return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;strings.forEach(string=>bufSize+=string.length+1);HEAPU32[penviron_buf_size>>2]=bufSize;return 0};var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_seek(fd,offset_low,offset_high,whence,newOffset){var offset=convertI32PairToI53Checked(offset_low,offset_high);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);tempI64=[stream.position>>>0,(tempDouble=stream.position,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[newOffset>>2]=tempI64[0],HEAP32[newOffset+4>>2]=tempI64[1];if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};var uleb128Encode=(n,target)=>{if(n<128){target.push(n)}else{target.push(n%128|128,n>>7)}};var sigToWasmTypes=sig=>{var typeNames={i:"i32",j:"i64",f:"f32",d:"f64",e:"externref",p:"i32"};var type={parameters:[],results:sig[0]=="v"?[]:[typeNames[sig[0]]]};for(var i=1;i{var sigRet=sig.slice(0,1);var sigParam=sig.slice(1);var typeCodes={i:127,p:127,j:126,f:125,d:124,e:111};target.push(96);uleb128Encode(sigParam.length,target);for(var i=0;i{if(typeof WebAssembly.Function=="function"){return new WebAssembly.Function(sigToWasmTypes(sig),func)}var typeSectionBody=[1];generateFuncType(sig,typeSectionBody);var bytes=[0,97,115,109,1,0,0,0,1];uleb128Encode(typeSectionBody.length,bytes);bytes.push(...typeSectionBody);bytes.push(2,7,1,1,101,1,102,0,0,7,5,1,1,102,0,0);var module=new WebAssembly.Module(new Uint8Array(bytes));var instance=new WebAssembly.Instance(module,{e:{f:func}});var wrappedFunc=instance.exports["f"];return wrappedFunc};var wasmTableMirror=[];var wasmTable;var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){if(funcPtr>=wasmTableMirror.length)wasmTableMirror.length=funcPtr+1;wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var updateTableMap=(offset,count)=>{if(functionsInTableMap){for(var i=offset;i{if(!functionsInTableMap){functionsInTableMap=new WeakMap;updateTableMap(0,wasmTable.length)}return functionsInTableMap.get(func)||0};var freeTableIndexes=[];var getEmptyTableSlot=()=>{if(freeTableIndexes.length){return freeTableIndexes.pop()}try{wasmTable.grow(1)}catch(err){if(!(err instanceof RangeError)){throw err}throw"Unable to grow wasm table. Set ALLOW_TABLE_GROWTH."}return wasmTable.length-1};var setWasmTableEntry=(idx,func)=>{wasmTable.set(idx,func);wasmTableMirror[idx]=wasmTable.get(idx)};var addFunction=(func,sig)=>{var rtn=getFunctionAddress(func);if(rtn){return rtn}var ret=getEmptyTableSlot();try{setWasmTableEntry(ret,func)}catch(err){if(!(err instanceof TypeError)){throw err}var wrapped=convertJsFunctionToWasm(func,sig);setWasmTableEntry(ret,wrapped)}functionsInTableMap.set(func,ret);return ret};var removeFunction=index=>{functionsInTableMap.delete(getWasmTableEntry(index));setWasmTableEntry(index,null);freeTableIndexes.push(index)};FS.createPreloadedFile=FS_createPreloadedFile;FS.staticInit();var wasmImports={a:___cxa_throw,s:___syscall_faccessat,c:___syscall_fcntl64,i:___syscall_fdatasync,A:___syscall_fstat64,x:___syscall_getcwd,t:___syscall_getdents64,h:___syscall_ioctl,y:___syscall_newfstatat,d:___syscall_openat,r:___syscall_readlinkat,q:___syscall_renameat,z:___syscall_stat64,p:___syscall_unlinkat,n:__abort_js,k:__emscripten_memcpy_js,l:__localtime_js,u:__tzset_js,j:_emscripten_date_now,o:_emscripten_resize_heap,v:_environ_get,w:_environ_sizes_get,g:_exit,b:_fd_close,e:_fd_read,m:_fd_seek,f:_fd_write};var wasmExports=createWasm();var ___wasm_call_ctors=()=>(___wasm_call_ctors=wasmExports["C"])();var _malloc=Module["_malloc"]=a0=>(_malloc=Module["_malloc"]=wasmExports["D"])(a0);var _free=Module["_free"]=a0=>(_free=Module["_free"]=wasmExports["E"])(a0);var _cnc_sim_create=Module["_cnc_sim_create"]=()=>(_cnc_sim_create=Module["_cnc_sim_create"]=wasmExports["G"])();var _cnc_sim_destroy=Module["_cnc_sim_destroy"]=a0=>(_cnc_sim_destroy=Module["_cnc_sim_destroy"]=wasmExports["H"])(a0);var _cnc_sim_reset=Module["_cnc_sim_reset"]=a0=>(_cnc_sim_reset=Module["_cnc_sim_reset"]=wasmExports["I"])(a0);var _cnc_sim_set_dialect=Module["_cnc_sim_set_dialect"]=(a0,a1)=>(_cnc_sim_set_dialect=Module["_cnc_sim_set_dialect"]=wasmExports["J"])(a0,a1);var _cnc_sim_set_event_callback=Module["_cnc_sim_set_event_callback"]=(a0,a1,a2)=>(_cnc_sim_set_event_callback=Module["_cnc_sim_set_event_callback"]=wasmExports["K"])(a0,a1,a2);var _cnc_sim_load_config_json=Module["_cnc_sim_load_config_json"]=(a0,a1,a2)=>(_cnc_sim_load_config_json=Module["_cnc_sim_load_config_json"]=wasmExports["L"])(a0,a1,a2);var _cnc_sim_parse_program=Module["_cnc_sim_parse_program"]=(a0,a1,a2)=>(_cnc_sim_parse_program=Module["_cnc_sim_parse_program"]=wasmExports["M"])(a0,a1,a2);var _cnc_sim_last_error=Module["_cnc_sim_last_error"]=a0=>(_cnc_sim_last_error=Module["_cnc_sim_last_error"]=wasmExports["N"])(a0);var __emscripten_stack_restore=a0=>(__emscripten_stack_restore=wasmExports["O"])(a0);var __emscripten_stack_alloc=a0=>(__emscripten_stack_alloc=wasmExports["P"])(a0);var _emscripten_stack_get_current=()=>(_emscripten_stack_get_current=wasmExports["Q"])();Module["ccall"]=ccall;Module["cwrap"]=cwrap;Module["addFunction"]=addFunction;Module["removeFunction"]=removeFunction;Module["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;Module["lengthBytesUTF8"]=lengthBytesUTF8;var calledRun;var calledPrerun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function run(){if(runDependencies>0){return}if(!calledPrerun){calledPrerun=1;preRun();if(runDependencies>0){return}}function doRun(){if(calledRun)return;calledRun=1;Module["calledRun"]=1;if(ABORT)return;initRuntime();readyPromiseResolve(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run();moduleRtn=readyPromise; +var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof importScripts=="function";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string"&&process.type!="renderer";if(ENVIRONMENT_IS_NODE){}var moduleOverrides=Object.assign({},Module);var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("fs");var nodePath=require("path");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);var ret=fs.readFileSync(filename);return ret};readAsync=(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);return new Promise((resolve,reject)=>{fs.readFile(filename,binary?undefined:"utf8",(err,data)=>{if(err)reject(err);else resolve(binary?data.buffer:data)})})};if(!Module["thisProgram"]&&process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=url=>{if(isFileURI(url)){return new Promise((resolve,reject)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){resolve(xhr.response);return}reject(xhr.status)};xhr.onerror=reject;xhr.send(null)})}return fetch(url,{credentials:"same-origin"}).then(response=>{if(response.ok){return response.arrayBuffer()}return Promise.reject(new Error(response.status+" : "+response.url))})}}}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var wasmBinary=Module["wasmBinary"];var wasmMemory;var ABORT=false;var EXITSTATUS;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b)}var __ATPRERUN__=[];var __ATINIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;function preRun(){var preRuns=Module["preRun"];if(preRuns){if(typeof preRuns=="function")preRuns=[preRuns];preRuns.forEach(addOnPreRun)}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!FS.initialized)FS.init();FS.ignorePermissions=false;TTY.init();callRuntimeCallbacks(__ATINIT__)}function postRun(){var postRuns=Module["postRun"];if(postRuns){if(typeof postRuns=="function")postRuns=[postRuns];postRuns.forEach(addOnPostRun)}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function getUniqueRunDependency(id){return id}function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var dataURIPrefix="data:application/octet-stream;base64,";var isDataURI=filename=>filename.startsWith(dataURIPrefix);var isFileURI=filename=>filename.startsWith("file://");function findWasmBinary(){var f="cnc_sim.wasm";if(!isDataURI(f)){return locateFile(f)}return f}var wasmBinaryFile;function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}function getBinaryPromise(binaryFile){if(!wasmBinary){return readAsync(binaryFile).then(response=>new Uint8Array(response),()=>getBinarySync(binaryFile))}return Promise.resolve().then(()=>getBinarySync(binaryFile))}function instantiateArrayBuffer(binaryFile,imports,receiver){return getBinaryPromise(binaryFile).then(binary=>WebAssembly.instantiate(binary,imports)).then(receiver,reason=>{err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)})}function instantiateAsync(binary,binaryFile,imports,callback){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!isDataURI(binaryFile)&&!isFileURI(binaryFile)&&!ENVIRONMENT_IS_NODE&&typeof fetch=="function"){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{var result=WebAssembly.instantiateStreaming(response,imports);return result.then(callback,function(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(binaryFile,imports,callback)})})}return instantiateArrayBuffer(binaryFile,imports,callback)}function getWasmImports(){return{a:wasmImports}}function createWasm(){var info=getWasmImports();function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["B"];updateMemoryViews();wasmTable=wasmExports["F"];addOnInit(wasmExports["C"]);removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){receiveInstance(result["instance"])}if(Module["instantiateWasm"]){try{return Module["instantiateWasm"](info,receiveInstance)}catch(e){err(`Module.instantiateWasm callback failed with error: ${e}`);readyPromiseReject(e)}}wasmBinaryFile??=findWasmBinary();instantiateAsync(wasmBinary,wasmBinaryFile,info,receiveInstantiationResult).catch(readyPromiseReject);return{}}var tempDouble;var tempI64;function ExitStatus(status){this.name="ExitStatus";this.message=`Program terminated with exit(${status})`;this.status=status}var callRuntimeCallbacks=callbacks=>{callbacks.forEach(f=>f(Module))};var noExitRuntime=Module["noExitRuntime"]||true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var exceptionLast=0;var uncaughtExceptionCount=0;var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.substr(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.substr(0,dir.length-1)}return root+dir},basename:path=>{if(path==="/")return"/";path=PATH.normalize(path);path=path.replace(/\/$/,"");var lastSlash=path.lastIndexOf("/");if(lastSlash===-1)return path;return path.substr(lastSlash+1)},join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>{if(typeof crypto=="object"&&typeof crypto["getRandomValues"]=="function"){return view=>crypto.getRandomValues(view)}else if(ENVIRONMENT_IS_NODE){try{var crypto_module=require("crypto");var randomFillSync=crypto_module["randomFillSync"];if(randomFillSync){return view=>crypto_module["randomFillSync"](view)}var randomBytes=crypto_module["randomBytes"];return view=>(view.set(randomBytes(view.byteLength)),view)}catch(e){}}abort("initRandomDevice")};var randomFill=view=>(randomFill=initRandomFill())(view);var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).substr(1);to=PATH_FS.resolve(to).substr(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var FS_stdin_getChar_buffer=[];var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx};function intArrayFromString(stringy,dontAddNull,length){var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array}var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(ENVIRONMENT_IS_NODE){var BUFSIZE=256;var buf=Buffer.alloc(BUFSIZE);var bytesRead=0;var fd=process.stdin.fd;try{bytesRead=fs.readSync(fd,buf,0,BUFSIZE)}catch(e){if(e.toString().includes("EOF"))bytesRead=0;else throw e}if(bytesRead>0){result=buf.slice(0,bytesRead).toString("utf-8")}}else if(typeof window!="undefined"&&typeof window.prompt=="function"){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output&&tty.output.length>0){err(UTF8ArrayToString(tty.output));tty.output=[]}}}};var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var mmapAlloc=size=>{abort()};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16384|511,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,allocate:MEMFS.stream_ops.allocate,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.timestamp=Date.now();if(parent){parent.contents[name]=node;parent.timestamp=node.timestamp}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.timestamp);attr.mtime=new Date(node.timestamp);attr.ctime=new Date(node.timestamp);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){if(attr.mode!==undefined){node.mode=attr.mode}if(attr.timestamp!==undefined){node.timestamp=attr.timestamp}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){throw FS.genericErrors[44]},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){if(FS.isDir(old_node.mode)){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}}delete old_node.parent.contents[old_node.name];old_node.parent.timestamp=Date.now();old_node.name=new_name;new_dir.contents[new_name]=old_node;new_dir.timestamp=old_node.parent.timestamp},unlink(parent,name){delete parent.contents[name];parent.timestamp=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.timestamp=Date.now()},readdir(node){var entries=[".",".."];for(var key of Object.keys(node.contents)){entries.push(key)}return entries},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{var dep=!noRunDep?getUniqueRunDependency(`al ${url}`):"";readAsync(url).then(arrayBuffer=>{onload(new Uint8Array(arrayBuffer));if(dep)removeRunDependency(dep)},err=>{if(onerror){onerror()}else{throw`Loading data file "${url}" failed.`}});if(dep)addRunDependency(dep)};var FS_createDataFile=(parent,name,fileData,canRead,canWrite,canOwn)=>{FS.createDataFile(parent,name,fileData,canRead,canWrite,canOwn)};var preloadPlugins=Module["preloadPlugins"]||[];var FS_handledByPreloadPlugin=(byteArray,fullname,finish,onerror)=>{if(typeof Browser!="undefined")Browser.init();var handled=false;preloadPlugins.forEach(plugin=>{if(handled)return;if(plugin["canHandle"](fullname)){plugin["handle"](byteArray,fullname,finish,onerror);handled=true}});return handled};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);function processData(byteArray){function finish(byteArray){preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}onload?.();removeRunDependency(dep)}if(FS_handledByPreloadPlugin(byteArray,fullname,finish,()=>{onerror?.();removeRunDependency(dep)})){return}finish(byteArray)}addRunDependency(dep);if(typeof url=="string"){asyncLoad(url,processData,onerror)}else{processData(url)}};var FS_modeStringToFlags=str=>{var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,ErrnoError:class{constructor(errno){this.name="ErrnoError";this.errno=errno}},genericErrors:{},filesystems:null,syncFSRequests:0,readFiles:{},FSStream:class{constructor(){this.shared={}}get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.mounted=null;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.node_ops={};this.stream_ops={};this.rdev=rdev;this.readMode=292|73;this.writeMode=146}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){path=PATH_FS.resolve(path);if(!path)return{path:"",node:null};var defaults={follow_mount:true,recurse_count:0};opts=Object.assign(defaults,opts);if(opts.recurse_count>8){throw new FS.ErrnoError(32)}var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i40){throw new FS.ErrnoError(32)}}}}return{path:current_path,node:current}},getPath(node){var path;while(true){if(FS.isRoot(node)){var mount=node.mount.mountpoint;if(!path)return mount;return mount[mount.length-1]!=="/"?`${mount}/${path}`:mount+path}path=path?`${node.name}/${path}`:node.name;node=node.parent}},hashName(parentid,name){var hash=0;for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&512){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}mounts.forEach(mount=>{if(!mount.type.syncfs){return done(null)}mount.type.syncfs(mount,populate,done)})},mount(type,opts,mountpoint){var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type,opts,mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);Object.keys(FS.nameTable).forEach(hash=>{var current=FS.nameTable[hash];while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}});node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name||name==="."||name===".."){throw new FS.ErrnoError(28)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},create(path,mode){mode=mode!==undefined?mode:438;mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode){mode=mode!==undefined?mode:511;mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var i=0;iFS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomLeft=randomFill(randomBuffer).byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16384|511,73);node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path}};ret.parent=ret;return ret}};return node}},{},"/proc/self/fd")},createStandardStreams(input,output,error){if(input){FS.createDevice("/dev","stdin",input)}else{FS.symlink("/dev/tty","/dev/stdin")}if(output){FS.createDevice("/dev","stdout",null,output)}else{FS.symlink("/dev/tty","/dev/stdout")}if(error){FS.createDevice("/dev","stderr",null,error)}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1)},staticInit(){[44].forEach(code=>{FS.genericErrors[code]=new FS.ErrnoError(code);FS.genericErrors[code].stack=""});FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS}},init(input,output,error){FS.initialized=true;input??=Module["stdin"];output??=Module["stdout"];error??=Module["stderr"];FS.createStandardStreams(input,output,error)},quit(){FS.initialized=false;for(var i=0;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)throw new Error("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)throw new Error("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")throw new Error("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(typeof XMLHttpRequest!="undefined"){if(!ENVIRONMENT_IS_WORKER)throw"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc";var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};var keys=Object.keys(node.stream_ops);keys.forEach(key=>{var fn=node.stream_ops[key];stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}});function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}};node.stream_ops=stream_ops;return node}};var UTF8ToString=(ptr,maxBytesToRead)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";var SYSCALLS={DEFAULT_POLLMASK:5,calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return PATH.join2(dir,path)},doStat(func,path,buf){var stat=func(path);HEAP32[buf>>2]=stat.dev;HEAP32[buf+4>>2]=stat.mode;HEAPU32[buf+8>>2]=stat.nlink;HEAP32[buf+12>>2]=stat.uid;HEAP32[buf+16>>2]=stat.gid;HEAP32[buf+20>>2]=stat.rdev;tempI64=[stat.size>>>0,(tempDouble=stat.size,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+24>>2]=tempI64[0],HEAP32[buf+28>>2]=tempI64[1];HEAP32[buf+32>>2]=4096;HEAP32[buf+36>>2]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();tempI64=[Math.floor(atime/1e3)>>>0,(tempDouble=Math.floor(atime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+40>>2]=tempI64[0],HEAP32[buf+44>>2]=tempI64[1];HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;tempI64=[Math.floor(mtime/1e3)>>>0,(tempDouble=Math.floor(mtime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+56>>2]=tempI64[0],HEAP32[buf+60>>2]=tempI64[1];HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;tempI64=[Math.floor(ctime/1e3)>>>0,(tempDouble=Math.floor(ctime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+72>>2]=tempI64[0],HEAP32[buf+76>>2]=tempI64[1];HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;tempI64=[stat.ino>>>0,(tempDouble=stat.ino,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+88>>2]=tempI64[0],HEAP32[buf+92>>2]=tempI64[1];return 0},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_faccessat(dirfd,path,amode,flags){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);if(amode&~7){return-28}var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;if(!node){return-44}var perms="";if(amode&4)perms+="r";if(amode&2)perms+="w";if(amode&1)perms+="x";if(perms&&FS.nodePermissions(node,perms)){return-2}return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function syscallGetVarargI(){var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret}var syscallGetVarargP=syscallGetVarargI;function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fdatasync(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fstat64(fd,buf){try{var stream=SYSCALLS.getStreamFromFD(fd);return SYSCALLS.doStat(FS.stat,stream.path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);function ___syscall_getcwd(buf,size){try{if(size===0)return-28;var cwd=FS.cwd();var cwdLengthInBytes=lengthBytesUTF8(cwd)+1;if(size>>0,(tempDouble=id,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[dirp+pos>>2]=tempI64[0],HEAP32[dirp+pos+4>>2]=tempI64[1];tempI64=[(idx+1)*struct_size>>>0,(tempDouble=(idx+1)*struct_size,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[dirp+pos+8>>2]=tempI64[0],HEAP32[dirp+pos+12>>2]=tempI64[1];HEAP16[dirp+pos+16>>1]=280;HEAP8[dirp+pos+18]=type;stringToUTF8(name,dirp+pos+19,256);pos+=struct_size;idx+=1}FS.llseek(stream,idx*struct_size,0);return pos}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(op){case 21509:{if(!stream.tty)return-59;return 0}case 21505:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcgets){var termios=stream.tty.ops.ioctl_tcgets(stream);var argp=syscallGetVarargP();HEAP32[argp>>2]=termios.c_iflag||0;HEAP32[argp+4>>2]=termios.c_oflag||0;HEAP32[argp+8>>2]=termios.c_cflag||0;HEAP32[argp+12>>2]=termios.c_lflag||0;for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag,c_oflag,c_cflag,c_lflag,c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];HEAP16[argp+2>>1]=winsize[1]}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_newfstatat(dirfd,path,buf,flags){try{path=SYSCALLS.getStr(path);var nofollow=flags&256;var allowEmpty=flags&4096;flags=flags&~6400;path=SYSCALLS.calculateAt(dirfd,path,allowEmpty);return SYSCALLS.doStat(nofollow?FS.lstat:FS.stat,path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_readlinkat(dirfd,path,buf,bufsize){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);if(bufsize<=0)return-28;var ret=FS.readlink(path);var len=Math.min(bufsize,lengthBytesUTF8(ret));var endChar=HEAP8[buf+len];stringToUTF8(ret,buf,bufsize+1);HEAP8[buf+len]=endChar;return len}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_renameat(olddirfd,oldpath,newdirfd,newpath){try{oldpath=SYSCALLS.getStr(oldpath);newpath=SYSCALLS.getStr(newpath);oldpath=SYSCALLS.calculateAt(olddirfd,oldpath);newpath=SYSCALLS.calculateAt(newdirfd,newpath);FS.rename(oldpath,newpath);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_stat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.doStat(FS.stat,path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_unlinkat(dirfd,path,flags){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);if(flags===0){FS.unlink(path)}else if(flags===512){FS.rmdir(path)}else{abort("Invalid flags passed to unlinkat")}return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __abort_js=()=>{abort("")};var __emscripten_memcpy_js=(dest,src,num)=>HEAPU8.copyWithin(dest,src,src+num);var isLeapYear=year=>year%4===0&&(year%100!==0||year%400===0);var MONTH_DAYS_LEAP_CUMULATIVE=[0,31,60,91,121,152,182,213,244,274,305,335];var MONTH_DAYS_REGULAR_CUMULATIVE=[0,31,59,90,120,151,181,212,243,273,304,334];var ydayFromDate=date=>{var leap=isLeapYear(date.getFullYear());var monthDaysCumulative=leap?MONTH_DAYS_LEAP_CUMULATIVE:MONTH_DAYS_REGULAR_CUMULATIVE;var yday=monthDaysCumulative[date.getMonth()]+date.getDate()-1;return yday};var convertI32PairToI53Checked=(lo,hi)=>hi+2097152>>>0<4194305-!!lo?(lo>>>0)+hi*4294967296:NaN;function __localtime_js(time_low,time_high,tmPtr){var time=convertI32PairToI53Checked(time_low,time_high);var date=new Date(time*1e3);HEAP32[tmPtr>>2]=date.getSeconds();HEAP32[tmPtr+4>>2]=date.getMinutes();HEAP32[tmPtr+8>>2]=date.getHours();HEAP32[tmPtr+12>>2]=date.getDate();HEAP32[tmPtr+16>>2]=date.getMonth();HEAP32[tmPtr+20>>2]=date.getFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getDay();var yday=ydayFromDate(date)|0;HEAP32[tmPtr+28>>2]=yday;HEAP32[tmPtr+36>>2]=-(date.getTimezoneOffset()*60);var start=new Date(date.getFullYear(),0,1);var summerOffset=new Date(date.getFullYear(),6,1).getTimezoneOffset();var winterOffset=start.getTimezoneOffset();var dst=(summerOffset!=winterOffset&&date.getTimezoneOffset()==Math.min(winterOffset,summerOffset))|0;HEAP32[tmPtr+32>>2]=dst}var __tzset_js=(timezone,daylight,std_name,dst_name)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);var extractZone=timezoneOffset=>{var sign=timezoneOffset>=0?"-":"+";var absOffset=Math.abs(timezoneOffset);var hours=String(Math.floor(absOffset/60)).padStart(2,"0");var minutes=String(absOffset%60).padStart(2,"0");return`UTC${sign}${hours}${minutes}`};var winterName=extractZone(winterOffset);var summerName=extractZone(summerOffset);if(summerOffsetDate.now();var getHeapMax=()=>2147483648;var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(typeof navigator=="object"&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var stringToAscii=(str,buffer)=>{for(var i=0;i{var bufSize=0;getEnvStrings().forEach((string,i)=>{var ptr=environ_buf+bufSize;HEAPU32[__environ+i*4>>2]=ptr;stringToAscii(string,ptr);bufSize+=string.length+1});return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;strings.forEach(string=>bufSize+=string.length+1);HEAPU32[penviron_buf_size>>2]=bufSize;return 0};var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_seek(fd,offset_low,offset_high,whence,newOffset){var offset=convertI32PairToI53Checked(offset_low,offset_high);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);tempI64=[stream.position>>>0,(tempDouble=stream.position,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[newOffset>>2]=tempI64[0],HEAP32[newOffset+4>>2]=tempI64[1];if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};var uleb128Encode=(n,target)=>{if(n<128){target.push(n)}else{target.push(n%128|128,n>>7)}};var sigToWasmTypes=sig=>{var typeNames={i:"i32",j:"i64",f:"f32",d:"f64",e:"externref",p:"i32"};var type={parameters:[],results:sig[0]=="v"?[]:[typeNames[sig[0]]]};for(var i=1;i{var sigRet=sig.slice(0,1);var sigParam=sig.slice(1);var typeCodes={i:127,p:127,j:126,f:125,d:124,e:111};target.push(96);uleb128Encode(sigParam.length,target);for(var i=0;i{if(typeof WebAssembly.Function=="function"){return new WebAssembly.Function(sigToWasmTypes(sig),func)}var typeSectionBody=[1];generateFuncType(sig,typeSectionBody);var bytes=[0,97,115,109,1,0,0,0,1];uleb128Encode(typeSectionBody.length,bytes);bytes.push(...typeSectionBody);bytes.push(2,7,1,1,101,1,102,0,0,7,5,1,1,102,0,0);var module=new WebAssembly.Module(new Uint8Array(bytes));var instance=new WebAssembly.Instance(module,{e:{f:func}});var wrappedFunc=instance.exports["f"];return wrappedFunc};var wasmTableMirror=[];var wasmTable;var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){if(funcPtr>=wasmTableMirror.length)wasmTableMirror.length=funcPtr+1;wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var updateTableMap=(offset,count)=>{if(functionsInTableMap){for(var i=offset;i{if(!functionsInTableMap){functionsInTableMap=new WeakMap;updateTableMap(0,wasmTable.length)}return functionsInTableMap.get(func)||0};var freeTableIndexes=[];var getEmptyTableSlot=()=>{if(freeTableIndexes.length){return freeTableIndexes.pop()}try{wasmTable.grow(1)}catch(err){if(!(err instanceof RangeError)){throw err}throw"Unable to grow wasm table. Set ALLOW_TABLE_GROWTH."}return wasmTable.length-1};var setWasmTableEntry=(idx,func)=>{wasmTable.set(idx,func);wasmTableMirror[idx]=wasmTable.get(idx)};var addFunction=(func,sig)=>{var rtn=getFunctionAddress(func);if(rtn){return rtn}var ret=getEmptyTableSlot();try{setWasmTableEntry(ret,func)}catch(err){if(!(err instanceof TypeError)){throw err}var wrapped=convertJsFunctionToWasm(func,sig);setWasmTableEntry(ret,wrapped)}functionsInTableMap.set(func,ret);return ret};var removeFunction=index=>{functionsInTableMap.delete(getWasmTableEntry(index));setWasmTableEntry(index,null);freeTableIndexes.push(index)};var FS_createPath=FS.createPath;var FS_unlink=path=>FS.unlink(path);var FS_createLazyFile=FS.createLazyFile;var FS_createDevice=FS.createDevice;FS.createPreloadedFile=FS_createPreloadedFile;FS.staticInit();Module["FS_createPath"]=FS.createPath;Module["FS_createDataFile"]=FS.createDataFile;Module["FS_createPreloadedFile"]=FS.createPreloadedFile;Module["FS_unlink"]=FS.unlink;Module["FS_createLazyFile"]=FS.createLazyFile;Module["FS_createDevice"]=FS.createDevice;var wasmImports={a:___cxa_throw,s:___syscall_faccessat,c:___syscall_fcntl64,i:___syscall_fdatasync,A:___syscall_fstat64,x:___syscall_getcwd,t:___syscall_getdents64,h:___syscall_ioctl,y:___syscall_newfstatat,d:___syscall_openat,r:___syscall_readlinkat,q:___syscall_renameat,z:___syscall_stat64,p:___syscall_unlinkat,n:__abort_js,k:__emscripten_memcpy_js,l:__localtime_js,u:__tzset_js,j:_emscripten_date_now,o:_emscripten_resize_heap,v:_environ_get,w:_environ_sizes_get,g:_exit,b:_fd_close,e:_fd_read,m:_fd_seek,f:_fd_write};var wasmExports=createWasm();var ___wasm_call_ctors=()=>(___wasm_call_ctors=wasmExports["C"])();var _malloc=Module["_malloc"]=a0=>(_malloc=Module["_malloc"]=wasmExports["D"])(a0);var _free=Module["_free"]=a0=>(_free=Module["_free"]=wasmExports["E"])(a0);var _cnc_sim_create=Module["_cnc_sim_create"]=()=>(_cnc_sim_create=Module["_cnc_sim_create"]=wasmExports["G"])();var _cnc_sim_destroy=Module["_cnc_sim_destroy"]=a0=>(_cnc_sim_destroy=Module["_cnc_sim_destroy"]=wasmExports["H"])(a0);var _cnc_sim_reset=Module["_cnc_sim_reset"]=a0=>(_cnc_sim_reset=Module["_cnc_sim_reset"]=wasmExports["I"])(a0);var _cnc_sim_set_dialect=Module["_cnc_sim_set_dialect"]=(a0,a1)=>(_cnc_sim_set_dialect=Module["_cnc_sim_set_dialect"]=wasmExports["J"])(a0,a1);var _cnc_sim_set_event_callback=Module["_cnc_sim_set_event_callback"]=(a0,a1,a2)=>(_cnc_sim_set_event_callback=Module["_cnc_sim_set_event_callback"]=wasmExports["K"])(a0,a1,a2);var _cnc_sim_load_config_json=Module["_cnc_sim_load_config_json"]=(a0,a1,a2)=>(_cnc_sim_load_config_json=Module["_cnc_sim_load_config_json"]=wasmExports["L"])(a0,a1,a2);var _cnc_sim_parse_program=Module["_cnc_sim_parse_program"]=(a0,a1,a2)=>(_cnc_sim_parse_program=Module["_cnc_sim_parse_program"]=wasmExports["M"])(a0,a1,a2);var _cnc_sim_last_error=Module["_cnc_sim_last_error"]=a0=>(_cnc_sim_last_error=Module["_cnc_sim_last_error"]=wasmExports["N"])(a0);var __emscripten_stack_restore=a0=>(__emscripten_stack_restore=wasmExports["O"])(a0);var __emscripten_stack_alloc=a0=>(__emscripten_stack_alloc=wasmExports["P"])(a0);var _emscripten_stack_get_current=()=>(_emscripten_stack_get_current=wasmExports["Q"])();Module["addRunDependency"]=addRunDependency;Module["removeRunDependency"]=removeRunDependency;Module["ccall"]=ccall;Module["cwrap"]=cwrap;Module["addFunction"]=addFunction;Module["removeFunction"]=removeFunction;Module["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;Module["lengthBytesUTF8"]=lengthBytesUTF8;Module["FS_createPreloadedFile"]=FS_createPreloadedFile;Module["FS_unlink"]=FS_unlink;Module["FS_createPath"]=FS_createPath;Module["FS_createDevice"]=FS_createDevice;Module["FS"]=FS;Module["FS_createDataFile"]=FS_createDataFile;Module["FS_createLazyFile"]=FS_createLazyFile;var calledRun;var calledPrerun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function run(){if(runDependencies>0){return}if(!calledPrerun){calledPrerun=1;preRun();if(runDependencies>0){return}}function doRun(){if(calledRun)return;calledRun=1;Module["calledRun"]=1;if(ABORT)return;initRuntime();readyPromiseResolve(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run();moduleRtn=readyPromise; return moduleRtn; diff --git a/web/public/linuxcnc_switchkins_remap_config_cases.json b/web/public/linuxcnc_switchkins_remap_config_cases.json index 0d2c67f..b2500e6 100644 --- a/web/public/linuxcnc_switchkins_remap_config_cases.json +++ b/web/public/linuxcnc_switchkins_remap_config_cases.json @@ -1,66 +1,383 @@ [ {"field":"config","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2}, + {"field":"configPath","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2}, + {"field":"config_path","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2}, + {"field":"iniFile","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2}, + {"field":"iniFileName","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini_file","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini_file_name","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2}, + {"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2}, {"field":"machine","value":"Sim-5Axis Bridge Mill (xyzbcw)","m428":0,"m429":1,"m430":2}, {"field":"kinematics","value":"5axiskins coordinates=xyzbcwy","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"bridgemill","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"5axis","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"Sim-5Axis Bridge Mill (xyzbcw)","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"Sim-5Axis Bridge Mill","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"5axiskins coordinates=xyzbcwy","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"5axiskins","m428":0,"m429":1,"m430":2}, + {"field":"halFile","value":"5axisgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"halFile","value":"5axisgui","m428":0,"m429":1,"m430":2}, {"field":"halfile","value":"5axisgui.hal","m428":0,"m429":1,"m430":2}, {"field":"halfile","value":"5axisgui","m428":0,"m429":1,"m430":2}, {"field":"hal_file","value":"5axisgui.hal","m428":0,"m429":1,"m430":2}, {"field":"hal_file","value":"5axisgui","m428":0,"m429":1,"m430":2}, + {"field":"HALFILE","value":"5axisgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"HALFILE","value":"5axisgui","m428":0,"m429":1,"m430":2}, + {"field":"postguiHalFile","value":"5axis_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"postguiHalFile","value":"5axis_postgui","m428":0,"m429":1,"m430":2}, + {"field":"postgui_halfile","value":"5axis_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"postgui_halfile","value":"5axis_postgui","m428":0,"m429":1,"m430":2}, + {"field":"postgui_hal_file","value":"5axis_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"postgui_hal_file","value":"5axis_postgui","m428":0,"m429":1,"m430":2}, + {"field":"POSTGUI_HALFILE","value":"5axis_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"POSTGUI_HALFILE","value":"5axis_postgui","m428":0,"m429":1,"m430":2}, + {"field":"remap","value":"configs/sim/axis/vismach/5axis/bridgemill","m428":0,"m429":1,"m430":2}, + {"field":"remap","value":"bridgemill","m428":0,"m429":1,"m430":2}, {"field":"config","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1}, + {"field":"configPath","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1}, + {"field":"config_path","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1}, + {"field":"ini","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1}, + {"field":"iniFile","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1}, + {"field":"iniFileName","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1}, + {"field":"ini_file","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1}, + {"field":"ini_file_name","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1}, + {"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1}, {"field":"machine","value":"sim-xyzab-tdr-kins (switchkins)","m428":1,"m429":0,"m430":-1}, {"field":"kinematics","value":"xyzab_tdr_kins","m428":1,"m429":0,"m430":-1}, + {"field":"switchkins","value":"table-dual-rotary","m428":1,"m429":0,"m430":-1}, + {"field":"switchkins","value":"xyzab-tdr","m428":1,"m429":0,"m430":-1}, + {"field":"switchkins","value":"sim-xyzab-tdr-kins (switchkins)","m428":1,"m429":0,"m430":-1}, + {"field":"switchkins","value":"sim-xyzab-tdr-kins","m428":1,"m429":0,"m430":-1}, + {"field":"switchkins","value":"xyzab_tdr_kins","m428":1,"m429":0,"m430":-1}, + {"field":"switchkins","value":"xyzab_tdr","m428":1,"m429":0,"m430":-1}, + {"field":"switchkins","value":"xyzab-tdr-kins","m428":1,"m429":0,"m430":-1}, + {"field":"postguiHalFile","value":"xyzab-tdr-postgui.hal","m428":1,"m429":0,"m430":-1}, + {"field":"postguiHalFile","value":"xyzab-tdr-postgui","m428":1,"m429":0,"m430":-1}, + {"field":"postgui_halfile","value":"xyzab-tdr-postgui.hal","m428":1,"m429":0,"m430":-1}, + {"field":"postgui_halfile","value":"xyzab-tdr-postgui","m428":1,"m429":0,"m430":-1}, + {"field":"postgui_hal_file","value":"xyzab-tdr-postgui.hal","m428":1,"m429":0,"m430":-1}, + {"field":"postgui_hal_file","value":"xyzab-tdr-postgui","m428":1,"m429":0,"m430":-1}, + {"field":"POSTGUI_HALFILE","value":"xyzab-tdr-postgui.hal","m428":1,"m429":0,"m430":-1}, + {"field":"POSTGUI_HALFILE","value":"xyzab-tdr-postgui","m428":1,"m429":0,"m430":-1}, + {"field":"remap","value":"configs/sim/axis/vismach/5axis/table-dual-rotary","m428":1,"m429":0,"m430":-1}, + {"field":"remap","value":"table-dual-rotary","m428":1,"m429":0,"m430":-1}, {"field":"config","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2}, + {"field":"configPath","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2}, + {"field":"config_path","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2}, + {"field":"ini","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2}, + {"field":"iniFile","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2}, + {"field":"iniFileName","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2}, + {"field":"ini_file","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2}, + {"field":"ini_file_name","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2}, + {"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2}, {"field":"machine","value":"sim-xyzac-trt-kins (switchkins)","m428":1,"m429":0,"m430":2}, {"field":"kinematics","value":"xyzac-trt-kins sparm=identityfirst","m428":1,"m429":0,"m430":2}, + {"field":"switchkins","value":"table-rotary-tilting","m428":1,"m429":0,"m430":2}, + {"field":"switchkins","value":"xyzac-trt","m428":1,"m429":0,"m430":2}, + {"field":"switchkins","value":"sim-xyzac-trt-kins (switchkins)","m428":1,"m429":0,"m430":2}, + {"field":"switchkins","value":"sim-xyzac-trt-kins","m428":1,"m429":0,"m430":2}, + {"field":"switchkins","value":"xyzac-trt-kins sparm=identityfirst","m428":1,"m429":0,"m430":2}, + {"field":"switchkins","value":"xyzac-trt-kins","m428":1,"m429":0,"m430":2}, + {"field":"postguiHalFile","value":"switchkins_postgui.hal","m428":1,"m429":0,"m430":2}, + {"field":"postguiHalFile","value":"switchkins_postgui","m428":1,"m429":0,"m430":2}, + {"field":"postgui_halfile","value":"switchkins_postgui.hal","m428":1,"m429":0,"m430":2}, + {"field":"postgui_halfile","value":"switchkins_postgui","m428":1,"m429":0,"m430":2}, + {"field":"postgui_hal_file","value":"switchkins_postgui.hal","m428":1,"m429":0,"m430":2}, + {"field":"postgui_hal_file","value":"switchkins_postgui","m428":1,"m429":0,"m430":2}, + {"field":"POSTGUI_HALFILE","value":"switchkins_postgui.hal","m428":1,"m429":0,"m430":2}, + {"field":"POSTGUI_HALFILE","value":"switchkins_postgui","m428":1,"m429":0,"m430":2}, + {"field":"remap","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting","m428":1,"m429":0,"m430":2}, + {"field":"remap","value":"table-rotary-tilting","m428":1,"m429":0,"m430":2}, {"field":"config","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2}, + {"field":"configPath","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2}, + {"field":"config_path","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2}, + {"field":"ini","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2}, + {"field":"iniFile","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2}, + {"field":"iniFileName","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2}, + {"field":"ini_file","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2}, + {"field":"ini_file_name","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2}, + {"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2}, {"field":"machine","value":"sim-xyzbc-trt-kins (switchkins)","m428":1,"m429":0,"m430":2}, {"field":"kinematics","value":"xyzbc-trt-kins sparm=identityfirst","m428":1,"m429":0,"m430":2}, + {"field":"switchkins","value":"xyzbc-trt","m428":1,"m429":0,"m430":2}, + {"field":"switchkins","value":"sim-xyzbc-trt-kins (switchkins)","m428":1,"m429":0,"m430":2}, + {"field":"switchkins","value":"sim-xyzbc-trt-kins","m428":1,"m429":0,"m430":2}, + {"field":"switchkins","value":"xyzbc-trt-kins sparm=identityfirst","m428":1,"m429":0,"m430":2}, + {"field":"switchkins","value":"xyzbc-trt-kins","m428":1,"m429":0,"m430":2}, {"field":"config","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2}, + {"field":"configPath","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2}, + {"field":"config_path","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2}, + {"field":"iniFile","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2}, + {"field":"iniFileName","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini_file","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini_file_name","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2}, + {"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2}, {"field":"machine","value":"xyzacb-trsrn (switchkins)","m428":0,"m429":1,"m430":2}, {"field":"kinematics","value":"xyzacb_trsrn","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"xyzacb-trsrn_twp","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"xyzacb-trsrn","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"xyzacb-trsrn (switchkins)","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"xyzacb_trsrn","m428":0,"m429":1,"m430":2}, + {"field":"postguiHalFile","value":"xyzacb-trsrn_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"postguiHalFile","value":"xyzacb-trsrn_postgui","m428":0,"m429":1,"m430":2}, + {"field":"postgui_halfile","value":"xyzacb-trsrn_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"postgui_halfile","value":"xyzacb-trsrn_postgui","m428":0,"m429":1,"m430":2}, + {"field":"postgui_hal_file","value":"xyzacb-trsrn_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"postgui_hal_file","value":"xyzacb-trsrn_postgui","m428":0,"m429":1,"m430":2}, + {"field":"POSTGUI_HALFILE","value":"xyzacb-trsrn_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"POSTGUI_HALFILE","value":"xyzacb-trsrn_postgui","m428":0,"m429":1,"m430":2}, + {"field":"remap","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating","m428":0,"m429":1,"m430":2}, + {"field":"remap","value":"table-rotary_spindle-rotary-nutating","m428":0,"m429":1,"m430":2}, {"field":"config","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2}, + {"field":"configPath","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2}, + {"field":"config_path","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2}, + {"field":"iniFile","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2}, + {"field":"iniFileName","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini_file","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini_file_name","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2}, + {"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2}, {"field":"machine","value":"xyzbca-trsrn (switchkins)","m428":0,"m429":1,"m430":2}, {"field":"kinematics","value":"xyzbca_trsrn","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"xyzbca-trsrn_twp","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"xyzbca-trsrn","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"xyzbca-trsrn (switchkins)","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"xyzbca_trsrn","m428":0,"m429":1,"m430":2}, + {"field":"postguiHalFile","value":"xyzbca-trsrn_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"postguiHalFile","value":"xyzbca-trsrn_postgui","m428":0,"m429":1,"m430":2}, + {"field":"postgui_halfile","value":"xyzbca-trsrn_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"postgui_halfile","value":"xyzbca-trsrn_postgui","m428":0,"m429":1,"m430":2}, + {"field":"postgui_hal_file","value":"xyzbca-trsrn_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"postgui_hal_file","value":"xyzbca-trsrn_postgui","m428":0,"m429":1,"m430":2}, + {"field":"POSTGUI_HALFILE","value":"xyzbca-trsrn_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"POSTGUI_HALFILE","value":"xyzbca-trsrn_postgui","m428":0,"m429":1,"m430":2}, {"field":"config","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2}, + {"field":"configPath","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2}, + {"field":"config_path","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2}, + {"field":"iniFile","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2}, + {"field":"iniFileName","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini_file","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini_file_name","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2}, + {"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2}, {"field":"machine","value":"hexapod (switchkins)","m428":0,"m429":1,"m430":2}, {"field":"kinematics","value":"genhexkins","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"hexapod-sim","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"hexapod","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"hexapod (switchkins)","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"genhexkins","m428":0,"m429":1,"m430":2}, + {"field":"halFile","value":"kinematics.hal","m428":0,"m429":1,"m430":2}, + {"field":"halFile","value":"kinematics","m428":0,"m429":1,"m430":2}, {"field":"halfile","value":"kinematics.hal","m428":0,"m429":1,"m430":2}, {"field":"halfile","value":"kinematics","m428":0,"m429":1,"m430":2}, {"field":"hal_file","value":"kinematics.hal","m428":0,"m429":1,"m430":2}, {"field":"hal_file","value":"kinematics","m428":0,"m429":1,"m430":2}, + {"field":"HALFILE","value":"kinematics.hal","m428":0,"m429":1,"m430":2}, + {"field":"HALFILE","value":"kinematics","m428":0,"m429":1,"m430":2}, + {"field":"postguiHalFile","value":"hexapod_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"postguiHalFile","value":"hexapod_postgui","m428":0,"m429":1,"m430":2}, + {"field":"postgui_halfile","value":"hexapod_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"postgui_halfile","value":"hexapod_postgui","m428":0,"m429":1,"m430":2}, + {"field":"postgui_hal_file","value":"hexapod_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"postgui_hal_file","value":"hexapod_postgui","m428":0,"m429":1,"m430":2}, + {"field":"POSTGUI_HALFILE","value":"hexapod_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"POSTGUI_HALFILE","value":"hexapod_postgui","m428":0,"m429":1,"m430":2}, + {"field":"remap","value":"configs/sim/axis/vismach/hexapod-sim","m428":0,"m429":1,"m430":2}, + {"field":"remap","value":"hexapod-sim","m428":0,"m429":1,"m430":2}, {"field":"config","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2}, + {"field":"configPath","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2}, + {"field":"config_path","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2}, + {"field":"iniFile","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2}, + {"field":"iniFileName","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini_file","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini_file_name","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2}, + {"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2}, {"field":"machine","value":"melfa (mm)","m428":0,"m429":1,"m430":2}, {"field":"kinematics","value":"genserkins","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"melfa-sim","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"melfa","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"melfa (mm)","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"genserkins","m428":0,"m429":1,"m430":2}, + {"field":"halFile","value":"melfa_dh.hal","m428":0,"m429":1,"m430":2}, + {"field":"halFile","value":"melfa_dh","m428":0,"m429":1,"m430":2}, {"field":"halfile","value":"melfa_dh.hal","m428":0,"m429":1,"m430":2}, {"field":"halfile","value":"melfa_dh","m428":0,"m429":1,"m430":2}, {"field":"hal_file","value":"melfa_dh.hal","m428":0,"m429":1,"m430":2}, {"field":"hal_file","value":"melfa_dh","m428":0,"m429":1,"m430":2}, + {"field":"HALFILE","value":"melfa_dh.hal","m428":0,"m429":1,"m430":2}, + {"field":"HALFILE","value":"melfa_dh","m428":0,"m429":1,"m430":2}, + {"field":"postguiHalFile","value":"melfa-postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"postguiHalFile","value":"melfa-postgui","m428":0,"m429":1,"m430":2}, + {"field":"postgui_halfile","value":"melfa-postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"postgui_halfile","value":"melfa-postgui","m428":0,"m429":1,"m430":2}, + {"field":"postgui_hal_file","value":"melfa-postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"postgui_hal_file","value":"melfa-postgui","m428":0,"m429":1,"m430":2}, + {"field":"POSTGUI_HALFILE","value":"melfa-postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"POSTGUI_HALFILE","value":"melfa-postgui","m428":0,"m429":1,"m430":2}, + {"field":"remap","value":"configs/sim/axis/vismach/melfa-sim","m428":0,"m429":1,"m430":2}, + {"field":"remap","value":"melfa-sim","m428":0,"m429":1,"m430":2}, {"field":"config","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1}, + {"field":"configPath","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1}, + {"field":"config_path","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1}, + {"field":"ini","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1}, + {"field":"iniFile","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1}, + {"field":"iniFileName","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1}, + {"field":"ini_file","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1}, + {"field":"ini_file_name","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1}, + {"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1}, {"field":"machine","value":"millturn (mm)","m428":0,"m429":1,"m430":-1}, {"field":"kinematics","value":"millturn","m428":0,"m429":1,"m430":-1}, + {"field":"switchkins","value":"millturn","m428":0,"m429":1,"m430":-1}, + {"field":"switchkins","value":"millturn (mm)","m428":0,"m429":1,"m430":-1}, + {"field":"halFile","value":"millturn.hal","m428":0,"m429":1,"m430":-1}, + {"field":"halFile","value":"millturn","m428":0,"m429":1,"m430":-1}, {"field":"halfile","value":"millturn.hal","m428":0,"m429":1,"m430":-1}, {"field":"halfile","value":"millturn","m428":0,"m429":1,"m430":-1}, {"field":"hal_file","value":"millturn.hal","m428":0,"m429":1,"m430":-1}, {"field":"hal_file","value":"millturn","m428":0,"m429":1,"m430":-1}, + {"field":"HALFILE","value":"millturn.hal","m428":0,"m429":1,"m430":-1}, + {"field":"HALFILE","value":"millturn","m428":0,"m429":1,"m430":-1}, + {"field":"postguiHalFile","value":"millturn-postgui.hal","m428":0,"m429":1,"m430":-1}, + {"field":"postguiHalFile","value":"millturn-postgui","m428":0,"m429":1,"m430":-1}, + {"field":"postgui_halfile","value":"millturn-postgui.hal","m428":0,"m429":1,"m430":-1}, + {"field":"postgui_halfile","value":"millturn-postgui","m428":0,"m429":1,"m430":-1}, + {"field":"postgui_hal_file","value":"millturn-postgui.hal","m428":0,"m429":1,"m430":-1}, + {"field":"postgui_hal_file","value":"millturn-postgui","m428":0,"m429":1,"m430":-1}, + {"field":"POSTGUI_HALFILE","value":"millturn-postgui.hal","m428":0,"m429":1,"m430":-1}, + {"field":"POSTGUI_HALFILE","value":"millturn-postgui","m428":0,"m429":1,"m430":-1}, + {"field":"remap","value":"configs/sim/axis/vismach/millturn","m428":0,"m429":1,"m430":-1}, + {"field":"remap","value":"millturn","m428":0,"m429":1,"m430":-1}, {"field":"config","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2}, + {"field":"configPath","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2}, + {"field":"config_path","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2}, + {"field":"iniFile","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2}, + {"field":"iniFileName","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini_file","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini_file_name","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2}, + {"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2}, {"field":"machine","value":"PUMA (pumakins,switchkins)","m428":0,"m429":1,"m430":2}, {"field":"kinematics","value":"pumakins","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"puma","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"PUMA (pumakins,switchkins)","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"PUMA","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"pumakins","m428":0,"m429":1,"m430":2}, + {"field":"halFile","value":"puma_dh.hal","m428":0,"m429":1,"m430":2}, + {"field":"halFile","value":"puma_dh","m428":0,"m429":1,"m430":2}, {"field":"halfile","value":"puma_dh.hal","m428":0,"m429":1,"m430":2}, {"field":"halfile","value":"puma_dh","m428":0,"m429":1,"m430":2}, {"field":"hal_file","value":"puma_dh.hal","m428":0,"m429":1,"m430":2}, {"field":"hal_file","value":"puma_dh","m428":0,"m429":1,"m430":2}, + {"field":"HALFILE","value":"puma_dh.hal","m428":0,"m429":1,"m430":2}, + {"field":"HALFILE","value":"puma_dh","m428":0,"m429":1,"m430":2}, + {"field":"postguiHalFile","value":"puma_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"postguiHalFile","value":"puma_postgui","m428":0,"m429":1,"m430":2}, + {"field":"postgui_halfile","value":"puma_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"postgui_halfile","value":"puma_postgui","m428":0,"m429":1,"m430":2}, + {"field":"postgui_hal_file","value":"puma_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"postgui_hal_file","value":"puma_postgui","m428":0,"m429":1,"m430":2}, + {"field":"POSTGUI_HALFILE","value":"puma_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"POSTGUI_HALFILE","value":"puma_postgui","m428":0,"m429":1,"m430":2}, + {"field":"remap","value":"configs/sim/axis/vismach/puma","m428":0,"m429":1,"m430":2}, + {"field":"remap","value":"puma","m428":0,"m429":1,"m430":2}, {"field":"config","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2}, + {"field":"configPath","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2}, + {"field":"config_path","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2}, + {"field":"iniFile","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2}, + {"field":"iniFileName","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini_file","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini_file_name","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2}, + {"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2}, {"field":"machine","value":"puma_cube.ini (pumakins)","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"puma_cube","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"puma_cube.ini (pumakins)","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"puma_cube.ini","m428":0,"m429":1,"m430":2}, {"field":"config","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2}, + {"field":"configPath","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2}, + {"field":"config_path","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2}, + {"field":"iniFile","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2}, + {"field":"iniFileName","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini_file","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini_file_name","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2}, + {"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2}, {"field":"machine","value":"puma560 (switchkins) (inch)","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"puma560","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"puma560 (switchkins) (inch)","m428":0,"m429":1,"m430":2}, + {"field":"halFile","value":"puma560_dh.hal","m428":0,"m429":1,"m430":2}, + {"field":"halFile","value":"puma560_dh","m428":0,"m429":1,"m430":2}, {"field":"halfile","value":"puma560_dh.hal","m428":0,"m429":1,"m430":2}, {"field":"halfile","value":"puma560_dh","m428":0,"m429":1,"m430":2}, {"field":"hal_file","value":"puma560_dh.hal","m428":0,"m429":1,"m430":2}, {"field":"hal_file","value":"puma560_dh","m428":0,"m429":1,"m430":2}, + {"field":"HALFILE","value":"puma560_dh.hal","m428":0,"m429":1,"m430":2}, + {"field":"HALFILE","value":"puma560_dh","m428":0,"m429":1,"m430":2}, + {"field":"postguiHalFile","value":"puma560_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"postguiHalFile","value":"puma560_postgui","m428":0,"m429":1,"m430":2}, + {"field":"postgui_halfile","value":"puma560_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"postgui_halfile","value":"puma560_postgui","m428":0,"m429":1,"m430":2}, + {"field":"postgui_hal_file","value":"puma560_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"postgui_hal_file","value":"puma560_postgui","m428":0,"m429":1,"m430":2}, + {"field":"POSTGUI_HALFILE","value":"puma560_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"POSTGUI_HALFILE","value":"puma560_postgui","m428":0,"m429":1,"m430":2}, {"field":"config","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2}, + {"field":"configPath","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2}, + {"field":"config_path","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2}, + {"field":"iniFile","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2}, + {"field":"iniFileName","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini_file","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini_file_name","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2}, + {"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"puma560_uvw","m428":0,"m429":1,"m430":2}, {"field":"config","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2}, + {"field":"configPath","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2}, + {"field":"config_path","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2}, + {"field":"iniFile","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2}, + {"field":"iniFileName","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini_file","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini_file_name","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2}, + {"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2}, {"field":"machine","value":"SCARA (genserkins,switchkins)","m428":0,"m429":1,"m430":2}, {"field":"kinematics","value":"scarakins coordinates=xyzcab","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"scara","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"SCARA (genserkins,switchkins)","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"SCARA","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"scarakins coordinates=xyzcab","m428":0,"m429":1,"m430":2}, + {"field":"switchkins","value":"scarakins","m428":0,"m429":1,"m430":2}, + {"field":"postguiHalFile","value":"scara_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"postguiHalFile","value":"scara_postgui","m428":0,"m429":1,"m430":2}, + {"field":"postgui_halfile","value":"scara_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"postgui_halfile","value":"scara_postgui","m428":0,"m429":1,"m430":2}, + {"field":"postgui_hal_file","value":"scara_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"postgui_hal_file","value":"scara_postgui","m428":0,"m429":1,"m430":2}, + {"field":"POSTGUI_HALFILE","value":"scara_postgui.hal","m428":0,"m429":1,"m430":2}, + {"field":"POSTGUI_HALFILE","value":"scara_postgui","m428":0,"m429":1,"m430":2}, + {"field":"remap","value":"configs/sim/axis/vismach/scara","m428":0,"m429":1,"m430":2}, + {"field":"remap","value":"scara","m428":0,"m429":1,"m430":2}, {"field":"config","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2}, - {"field":"config","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2} + {"field":"configPath","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2}, + {"field":"config_path","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2}, + {"field":"iniFile","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2}, + {"field":"iniFileName","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini_file","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini_file_name","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2}, + {"field":"INI_FILE_NAME","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2}, + {"field":"remap","value":"configs/sim/qtaxis/non-trivial/scara","m428":0,"m429":1,"m430":2}, + {"field":"config","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2}, + {"field":"configPath","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2}, + {"field":"config_path","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2}, + {"field":"iniFile","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2}, + {"field":"iniFileName","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini_file","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2}, + {"field":"ini_file_name","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2}, + {"field":"INI_FILE_NAME","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2}, + {"field":"remap","value":"configs/sim/qtvcp_screens/non-trivial/scara","m428":0,"m429":1,"m430":2} ] diff --git a/web/src/app.js b/web/src/app.js index 59853bd..c6995a5 100644 --- a/web/src/app.js +++ b/web/src/app.js @@ -2,6 +2,7 @@ import { createWasmSimulator } from "./wasm-core.js"; const elements = { wasmState: document.querySelector("#wasmState"), + storageState: document.querySelector("#storageState"), modeState: document.querySelector("#modeState"), unitState: document.querySelector("#unitState"), programState: document.querySelector("#programState"), @@ -26,12 +27,82 @@ const elements = { let simulatorPromise = null; let lastEvents = []; +let isParsing = false; +let hasUserEditedProgram = false; +let saveProgramTimer = 0; +let saveProgramSequence = 0; +let saveProgramPromise = Promise.resolve(); +let startupPromise = Promise.resolve(); +const programPath = "programs/current.ngc"; +// LinuxCNC source basis: interp_internal.hh defines RS274NGC_PARAMETER_FILE_NAME_DEFAULT. +const linuxCncParameterFileName = "rs274ngc.var"; +const parameterPath = `parameters/${linuxCncParameterFileName}`; +const linuxCncBackend = "linuxcnc-rs274"; +const opfsOptions = { required: true, mountPoint: "/cnc", workspacePath: "cnc-simulator" }; function setStatus(node, text, alarm = false) { node.textContent = text; node.classList.toggle("alarm", alarm); } +function setRuntimeReady(simulator) { + setStatus(elements.wasmState, "WASM ONLINE"); + if (simulator.fs?.opfs) { + const storageState = elements.storageState.textContent; + if (storageState === "OPFS SAVING" || storageState.startsWith("OPFS SAVED")) { + setStatus(elements.storageState, storageState); + } else { + setStatus(elements.storageState, "OPFS READY"); + } + } else { + setStatus(elements.storageState, "OPFS OFF", true); + } +} + +function setRuntimeUnavailable() { + setStatus(elements.wasmState, "WASM OFFLINE", true); + setStatus(elements.storageState, "OPFS OFF", true); +} + +function setControlsEnabled(enabled) { + elements.parseBtn.disabled = !enabled; + elements.resetBtn.disabled = !enabled; + elements.holdBtn.disabled = !enabled; + elements.stopBtn.disabled = !enabled; + elements.backendSelect.disabled = !enabled; +} + +function setStorageSaved(size) { + setStatus(elements.storageState, Number.isFinite(size) ? `OPFS SAVED ${size}B` : "OPFS SAVED"); +} + +function setProgramReady() { + if (elements.programState.textContent !== "ALARM") { + setStatus(elements.programState, "READY"); + } +} + +async function statIfExists(opfs, path) { + if (!(await opfs.exists(path))) { + return null; + } + return opfs.stat(path); +} + +async function logWorkspaceState(opfs) { + const programStat = await statIfExists(opfs, programPath); + if (programStat?.kind === "file") { + log(`OPFS PROGRAM ${programStat.size}B`); + } + + const parameterStat = await statIfExists(opfs, parameterPath); + if (parameterStat?.kind === "file") { + log(`OPFS PARAMETERS ${parameterStat.size}B`); + } else { + log("OPFS PARAMETERS PENDING"); + } +} + function log(message, error = false) { const line = document.createElement("div"); line.className = `alarm-line${error ? " error" : ""}`; @@ -163,21 +234,63 @@ function updateLineCount() { elements.lineCount.textContent = `${count} LINES`; } +async function getOpfsWorkspace() { + const simulator = await getSimulator(); + setRuntimeReady(simulator); + if (!simulator.fs.opfs) { + throw new Error("OPFS workspace is required for browser program parsing"); + } + return simulator.fs.opfs; +} + async function getSimulator() { if (!simulatorPromise) { - simulatorPromise = createWasmSimulator(); + simulatorPromise = createWasmSimulator({ + opfs: opfsOptions, + }).then((simulator) => { + setRuntimeReady(simulator); + return simulator; + }).catch((error) => { + simulatorPromise = null; + globalThis.__cncSimulatorForTest = null; + setRuntimeUnavailable(); + throw error; + }); + globalThis.__cncSimulatorForTest = simulatorPromise; } return simulatorPromise; } async function parseProgram() { + if (isParsing) { + return; + } + isParsing = true; + setControlsEnabled(false); setStatus(elements.programState, "RUN"); + let runtimeReady = false; + let savedProgramSize = null; try { + await startupPromise.catch(() => {}); const simulator = await getSimulator(); - setStatus(elements.wasmState, "WASM ONLINE"); - const events = simulator.parse(elements.input.value, "linuxcnc", { - backend: elements.backendSelect.value, - }); + runtimeReady = true; + setRuntimeReady(simulator); + const parseOptions = { + backend: linuxCncBackend, + }; + if (!simulator.fs.opfs) { + throw new Error("OPFS workspace is required for browser program parsing"); + } + elements.backendSelect.value = linuxCncBackend; + await saveProgramPromise.catch(() => {}); + await simulator.fs.opfs.writeFile(programPath, elements.input.value); + const programStat = await simulator.fs.opfs.stat(programPath); + savedProgramSize = programStat.kind === "file" ? programStat.size : null; + setStorageSaved(savedProgramSize); + const events = await simulator.parseFileWithParameterFile(programPath, parameterPath, "linuxcnc", parseOptions); + const parameterStat = await simulator.fs.opfs.stat(parameterPath); + setStorageSaved(parameterStat.size); + log(`OPFS PARAMETERS ${parameterStat.size}B`); lastEvents = events; drawToolpath(events); @@ -193,9 +306,82 @@ async function parseProgram() { setStatus(elements.programState, "END"); log(`OK ${events.length} EVENTS`); } catch (error) { - setStatus(elements.wasmState, "WASM OFFLINE", true); + if (!runtimeReady) { + setRuntimeUnavailable(); + } + if (savedProgramSize !== null) { + log(`OPFS PROGRAM SAVED ${savedProgramSize}B`); + } setStatus(elements.programState, "ALARM", true); log(error.message, true); + } finally { + isParsing = false; + setControlsEnabled(true); + } +} + +async function saveProgramDraft() { + const sequence = ++saveProgramSequence; + saveProgramPromise = saveProgramPromise.catch(() => {}).then(async () => { + const opfs = await getOpfsWorkspace(); + const program = elements.input.value; + setStatus(elements.storageState, "OPFS SAVING"); + await opfs.writeFile(programPath, program); + if (sequence === saveProgramSequence) { + const stat = await opfs.stat(programPath); + setStorageSaved(stat.size); + setProgramReady(); + } + }); + try { + await saveProgramPromise; + } catch (error) { + setStatus(elements.storageState, "OPFS ALARM", true); + log(error.message, true); + } +} + +function scheduleProgramSave() { + window.clearTimeout(saveProgramTimer); + saveProgramTimer = window.setTimeout(() => { + void saveProgramDraft(); + }, 350); +} + +function flushProgramSave() { + window.clearTimeout(saveProgramTimer); + if (hasUserEditedProgram) { + void saveProgramDraft(); + } +} + +async function restoreProgram() { + try { + const opfs = await getOpfsWorkspace(); + if (!(await opfs.exists(programPath))) { + await opfs.writeFile(programPath, elements.input.value); + const stat = await opfs.stat(programPath); + setStorageSaved(stat.size); + setProgramReady(); + log(`OPFS PROGRAM CREATED ${stat.size}B`); + await logWorkspaceState(opfs); + return; + } + const program = new TextDecoder().decode(await opfs.readFile(programPath)); + if (hasUserEditedProgram) { + return; + } + elements.input.value = program; + updateLineCount(); + const stat = await opfs.stat(programPath); + setStorageSaved(stat.size); + setProgramReady(); + log("OPFS PROGRAM RESTORED"); + await logWorkspaceState(opfs); + } catch (error) { + if (error?.name !== "NotFoundError") { + log(error.message, true); + } } } @@ -205,6 +391,7 @@ document.querySelectorAll("[data-mode]").forEach((button) => { }); }); +elements.backendSelect.value = linuxCncBackend; elements.parseBtn.addEventListener("click", parseProgram); elements.resetBtn.addEventListener("click", () => { lastEvents = []; @@ -214,8 +401,32 @@ elements.resetBtn.addEventListener("click", () => { }); elements.holdBtn.addEventListener("click", () => setStatus(elements.programState, "HOLD")); elements.stopBtn.addEventListener("click", () => setStatus(elements.programState, "STOP", true)); -elements.input.addEventListener("input", updateLineCount); +elements.input.addEventListener("input", () => { + updateLineCount(); + hasUserEditedProgram = true; + setStatus(elements.programState, "EDIT"); + scheduleProgramSave(); +}); +document.addEventListener("keydown", (event) => { + if (!(event.ctrlKey || event.metaKey) || event.altKey) { + return; + } + if (event.key === "s") { + event.preventDefault(); + void saveProgramDraft(); + } else if (event.key === "Enter") { + event.preventDefault(); + void parseProgram(); + } +}); window.addEventListener("resize", resizeCanvas); +window.addEventListener("pagehide", flushProgramSave); +document.addEventListener("visibilitychange", () => { + if (document.visibilityState === "hidden") { + flushProgramSave(); + } +}); updateLineCount(); resizeCanvas(); +startupPromise = restoreProgram(); diff --git a/web/src/index.ts b/web/src/index.ts index 385ff54..27c04ba 100644 --- a/web/src/index.ts +++ b/web/src/index.ts @@ -84,8 +84,19 @@ export type CncParseOptions = { config?: string; configPath?: string; config_path?: string; + ini?: string; + iniFile?: string; + iniFileName?: string; + ini_file?: string; + ini_file_name?: string; + INI_FILE_NAME?: string; + halFile?: string; halfile?: string; hal_file?: string; + postguiHalFile?: string; + postgui_halfile?: string; + postgui_hal_file?: string; + POSTGUI_HALFILE?: string; kinematics?: string; trt?: "xyzbc" | "xyzac" | string; pivotLength?: number; diff --git a/web/src/wasm-core.d.ts b/web/src/wasm-core.d.ts index 007f69e..5f28b90 100644 --- a/web/src/wasm-core.d.ts +++ b/web/src/wasm-core.d.ts @@ -2,13 +2,65 @@ import type { CncDialect, CncEvent, CncParseOptions } from "./index"; export type WasmSimulator = { parse(program: string, dialect?: CncDialect, options?: CncParseOptions): CncEvent[]; + parseFile(path: string, dialect?: CncDialect, options?: CncParseOptions): Promise; + parseWithParameterFile( + program: string, + parameterPath: string, + dialect?: CncDialect, + options?: CncParseOptions, + ): Promise; + parseFileWithParameterFile( + path: string, + parameterPath: string, + dialect?: CncDialect, + options?: CncParseOptions, + ): Promise; dispose(): void; + fs: { + opfs: WasmOpfsWorkspace | null; + module: unknown; + }; }; +export type WasmOpfsWorkspace = { + mountPoint: string; + workspacePath: string; + resolvePath(path: string): string; + readFile(path: string): Promise; + readDirectory(path: string): Promise; + persistFile(path: string): Promise; + persistDirectory(path: string): Promise; + writeFile(path: string, data: Uint8Array | string): Promise; + copyFile(fromPath: string, toPath: string): Promise; + moveFile(fromPath: string, toPath: string): Promise; + exists(path: string): Promise; + stat(path: string): Promise; + loadParameterFile(path: string): Promise; + persistParameterFile(path: string): Promise; + removeFile(path: string): Promise; + removeDirectory(path: string): Promise; + clear(): Promise; +}; + +export type WasmOpfsEntryStat = + | { + kind: "file"; + size: number; + } + | { + kind: "directory"; + size: null; + }; + export type WasmModuleOptions = { locateFile?: (file: string) => string; print?: (text: string) => void; printErr?: (text: string) => void; + opfs?: false | { + required?: boolean; + mountPoint?: string; + workspacePath?: string; + }; }; export function createWasmSimulator(moduleOptions?: WasmModuleOptions): Promise; diff --git a/web/src/wasm-core.js b/web/src/wasm-core.js index 34f71ef..745767a 100644 --- a/web/src/wasm-core.js +++ b/web/src/wasm-core.js @@ -41,6 +41,486 @@ const EVENT_OFFSETS = { reserved: 268, }; +const DEFAULT_OPFS_MOUNT_POINT = "/cnc"; +const LINUXCNC_DEFAULT_PARAMETER_FILE = "rs274ngc.var"; +const LINUXCNC_PARAMETER_SCRATCH_FILES = [ + LINUXCNC_DEFAULT_PARAMETER_FILE, + `${LINUXCNC_DEFAULT_PARAMETER_FILE}.new`, + `${LINUXCNC_DEFAULT_PARAMETER_FILE}.bak`, +]; + +function isOpfsAvailable() { + return Boolean(globalThis.navigator?.storage?.getDirectory); +} + +function assertRelativePath(path) { + if (!path || typeof path !== "string") { + throw new Error("OPFS path must be a non-empty relative path"); + } + if ( + path.startsWith("/") || + path.includes("\\") || + path.includes("\0") || + path.split("/").some((part) => part === "" || part === "." || part === "..") + ) { + throw new Error(`OPFS path must stay inside the CNC workspace: ${path}`); + } +} + +function assertWorkspacePath(path) { + assertRelativePath(path); +} + +function assertMountPoint(path) { + if (!path || typeof path !== "string") { + throw new Error("OPFS mount point must be a non-empty absolute path"); + } + const parts = path.split("/").slice(1); + if ( + !path.startsWith("/") || + path.includes("\\") || + path.includes("\0") || + parts.some((part) => part === "" || part === "." || part === "..") + ) { + throw new Error(`OPFS mount point must stay inside the Emscripten filesystem: ${path}`); + } +} + +async function getOpfsDirectoryHandle(path, create) { + if (!isOpfsAvailable()) { + throw new Error("OPFS is not available in this browser context"); + } + let directory = await globalThis.navigator.storage.getDirectory(); + for (const part of path.split("/").filter(Boolean)) { + directory = await directory.getDirectoryHandle(part, { create }); + } + return directory; +} + +async function readOpfsFile(workspacePath, relativePath) { + assertRelativePath(relativePath); + const parts = relativePath.split("/"); + const fileName = parts.pop(); + const directory = await getOpfsDirectoryHandle([workspacePath, ...parts].filter(Boolean).join("/"), false); + const file = await directory.getFileHandle(fileName, { create: false }); + return new Uint8Array(await (await file.getFile()).arrayBuffer()); +} + +async function writeOpfsFile(workspacePath, relativePath, data) { + assertRelativePath(relativePath); + const parts = relativePath.split("/"); + const fileName = parts.pop(); + const directory = await getOpfsDirectoryHandle([workspacePath, ...parts].filter(Boolean).join("/"), true); + const file = await directory.getFileHandle(fileName, { create: true }); + const writable = await file.createWritable(); + try { + await writable.truncate(0); + await writable.write(data); + await writable.close(); + } catch (error) { + try { + await writable.abort(); + } catch (_abortError) { + // Preserve the original OPFS write failure. + } + throw error; + } +} + +async function removeOpfsEntry(workspacePath, relativePath, recursive) { + assertRelativePath(relativePath); + const parts = relativePath.split("/"); + const entryName = parts.pop(); + try { + const directory = await getOpfsDirectoryHandle([workspacePath, ...parts].filter(Boolean).join("/"), false); + await directory.removeEntry(entryName, { recursive }); + } catch (error) { + if (error?.name !== "NotFoundError") { + throw error; + } + } +} + +async function opfsEntryExists(workspacePath, relativePath) { + assertRelativePath(relativePath); + const parts = relativePath.split("/"); + const entryName = parts.pop(); + try { + const directory = await getOpfsDirectoryHandle([workspacePath, ...parts].filter(Boolean).join("/"), false); + try { + await directory.getFileHandle(entryName, { create: false }); + return true; + } catch (fileError) { + if (fileError?.name !== "NotFoundError" && fileError?.name !== "TypeMismatchError") { + throw fileError; + } + } + try { + await directory.getDirectoryHandle(entryName, { create: false }); + return true; + } catch (directoryError) { + if (directoryError?.name !== "NotFoundError" && directoryError?.name !== "TypeMismatchError") { + throw directoryError; + } + } + return false; + } catch (error) { + if (error?.name === "NotFoundError") { + return false; + } + throw error; + } +} + +async function statOpfsEntry(workspacePath, relativePath) { + assertRelativePath(relativePath); + const parts = relativePath.split("/"); + const entryName = parts.pop(); + const directory = await getOpfsDirectoryHandle([workspacePath, ...parts].filter(Boolean).join("/"), false); + try { + const file = await directory.getFileHandle(entryName, { create: false }); + return { kind: "file", size: (await file.getFile()).size }; + } catch (fileError) { + if (fileError?.name !== "TypeMismatchError") { + throw fileError; + } + } + await directory.getDirectoryHandle(entryName, { create: false }); + return { kind: "directory", size: null }; +} + +async function clearOpfsWorkspace(workspacePath) { + const parts = workspacePath.split("/").filter(Boolean); + if (parts.length === 0) { + return; + } + const entryName = parts.pop(); + const directory = await getOpfsDirectoryHandle(parts.join("/"), true); + try { + await directory.removeEntry(entryName, { recursive: true }); + } catch (error) { + if (error?.name !== "NotFoundError") { + throw error; + } + } + await getOpfsDirectoryHandle(workspacePath, true); +} + +async function replaceOpfsDirectory(workspacePath, relativePath) { + assertRelativePath(relativePath); + await removeOpfsEntry(workspacePath, relativePath, true); + const directory = await getOpfsDirectoryHandle([workspacePath, relativePath].filter(Boolean).join("/"), true); + for await (const [name] of directory.entries()) { + await directory.removeEntry(name, { recursive: true }); + } +} + +async function readOpfsDirectoryTree(workspacePath, relativePath, relativeRoot = "") { + assertRelativePath(relativePath); + const directory = await getOpfsDirectoryHandle([workspacePath, relativePath, relativeRoot].filter(Boolean).join("/"), false); + const files = []; + const directories = []; + for await (const [name, handle] of directory.entries()) { + const path = relativeRoot ? `${relativeRoot}/${name}` : name; + if (handle.kind === "directory") { + directories.push(path); + const child = await readOpfsDirectoryTree(workspacePath, relativePath, path); + directories.push(...child.directories); + files.push(...child.files); + } else if (handle.kind === "file") { + files.push(path); + } + } + return { + directories: directories.sort(), + files: files.sort(), + }; +} + +function ensureWasmDirectory(module, path) { + const parts = path.split("/").filter(Boolean); + let current = ""; + for (const part of parts) { + current += `/${part}`; + try { + module.FS.mkdir(current); + } catch (error) { + if (error?.errno !== 20) { + throw error; + } + } + } +} + +function readWasmDirectoryTree(module, rootPath, relativeRoot = "") { + const files = []; + const directories = []; + for (const entry of module.FS.readdir(rootPath)) { + if (entry === "." || entry === "..") { + continue; + } + const path = `${rootPath}/${entry}`; + const relativePath = relativeRoot ? `${relativeRoot}/${entry}` : entry; + const mode = module.FS.stat(path).mode; + if (module.FS.isDir(mode)) { + directories.push(relativePath); + const child = readWasmDirectoryTree(module, path, relativePath); + directories.push(...child.directories); + files.push(...child.files); + } else if (module.FS.isFile(mode)) { + files.push(relativePath); + } + } + return { + directories: directories.sort(), + files: files.sort(), + }; +} + +function removeWasmPath(module, path, recursive) { + let mode; + try { + mode = module.FS.stat(path).mode; + } catch (error) { + if (error?.errno !== 44) { + throw error; + } + return; + } + if (module.FS.isDir(mode)) { + if (!recursive) { + module.FS.rmdir(path); + return; + } + for (const entry of module.FS.readdir(path)) { + if (entry !== "." && entry !== "..") { + removeWasmPath(module, `${path}/${entry}`, true); + } + } + module.FS.rmdir(path); + } else { + module.FS.unlink(path); + } +} + +function wasmPathExists(module, path) { + try { + module.FS.stat(path); + return true; + } catch (error) { + if (error?.errno !== 44) { + throw error; + } + return false; + } +} + +function readFirstExistingWasmFile(module, paths) { + for (const path of paths) { + try { + return module.FS.readFile(path); + } catch (error) { + if (error?.errno !== 44) { + throw error; + } + } + } + return null; +} + +function writeWasmFileReplacingPath(module, path, data) { + removeWasmPath(module, path, true); + const parentIndex = path.lastIndexOf("/"); + if (parentIndex > 0) { + ensureWasmDirectory(module, path.slice(0, parentIndex)); + } + module.FS.writeFile(path, data); +} + +function cleanupLinuxCncParameterFiles(module) { + for (const path of LINUXCNC_PARAMETER_SCRATCH_FILES) { + removeWasmPath(module, path, true); + removeWasmPath(module, `/${path}`, true); + } +} + +function installOpfsWorkspace(module, options) { + const opfsOptions = options.opfs ?? (isOpfsAvailable() ? {} : false); + if (opfsOptions === false) { + if (isOpfsAvailable()) { + throw new Error("OPFS cannot be disabled in browser contexts with OPFS support"); + } + return null; + } + if (!module.FS) { + throw new Error("cnc_sim.js was not built with Emscripten FS export"); + } + if (!isOpfsAvailable()) { + if (opfsOptions.required) { + throw new Error("OPFS is only available in browser contexts"); + } + return null; + } + const mountPoint = opfsOptions.mountPoint ?? DEFAULT_OPFS_MOUNT_POINT; + const workspacePath = opfsOptions.workspacePath ?? "cnc-simulator"; + assertMountPoint(mountPoint); + assertWorkspacePath(workspacePath); + ensureWasmDirectory(module, mountPoint); + return { + mountPoint, + workspacePath, + resolvePath(path) { + assertRelativePath(path); + return `${mountPoint}/${path}`; + }, + async readFile(path) { + const data = await readOpfsFile(workspacePath, path); + const wasmPath = this.resolvePath(path); + writeWasmFileReplacingPath(module, wasmPath, data); + return data; + }, + async readDirectory(path) { + assertRelativePath(path); + let tree; + try { + tree = await readOpfsDirectoryTree(workspacePath, path); + } catch (error) { + if (error?.name === "NotFoundError") { + removeWasmPath(module, this.resolvePath(path), true); + } + throw error; + } + const wasmPath = this.resolvePath(path); + removeWasmPath(module, wasmPath, true); + ensureWasmDirectory(module, wasmPath); + for (const directory of tree.directories) { + ensureWasmDirectory(module, `${wasmPath}/${directory}`); + } + for (const file of tree.files) { + await this.readFile(`${path}/${file}`); + } + return tree.files; + }, + async persistFile(path) { + const wasmPath = this.resolvePath(path); + const data = module.FS.readFile(wasmPath); + await writeOpfsFile(workspacePath, path, data); + return data; + }, + async persistDirectory(path) { + assertRelativePath(path); + const wasmPath = this.resolvePath(path); + const tree = readWasmDirectoryTree(module, wasmPath); + await replaceOpfsDirectory(workspacePath, path); + for (const directory of tree.directories) { + await getOpfsDirectoryHandle([workspacePath, path, directory].filter(Boolean).join("/"), true); + } + for (const file of tree.files) { + await this.persistFile(`${path}/${file}`); + } + return tree.files; + }, + async writeFile(path, data) { + await writeOpfsFile(workspacePath, path, data); + const wasmPath = this.resolvePath(path); + writeWasmFileReplacingPath(module, wasmPath, data); + }, + // LinuxCNC source basis: rs274ngc_pre.cc save_parameters() links the + // current parameter file to filename + ".bak" before replacing it. + async copyFile(fromPath, toPath) { + assertRelativePath(fromPath); + assertRelativePath(toPath); + const data = await readOpfsFile(workspacePath, fromPath); + await writeOpfsFile(workspacePath, toPath, data); + + const fromWasmPath = this.resolvePath(fromPath); + const toWasmPath = this.resolvePath(toPath); + writeWasmFileReplacingPath(module, fromWasmPath, data); + writeWasmFileReplacingPath(module, toWasmPath, data); + return data; + }, + // LinuxCNC source basis: rs274ngc_pre.cc save_parameters() writes a + // temporary filename + ".new" and renames it over the parameter file. + async moveFile(fromPath, toPath) { + assertRelativePath(fromPath); + assertRelativePath(toPath); + if (fromPath === toPath) { + return; + } + const data = await readOpfsFile(workspacePath, fromPath); + await writeOpfsFile(workspacePath, toPath, data); + await removeOpfsEntry(workspacePath, fromPath, false); + + const fromWasmPath = this.resolvePath(fromPath); + const toWasmPath = this.resolvePath(toPath); + writeWasmFileReplacingPath(module, toWasmPath, data); + removeWasmPath(module, fromWasmPath, false); + }, + async exists(path) { + return opfsEntryExists(workspacePath, path); + }, + async stat(path) { + return statOpfsEntry(workspacePath, path); + }, + // LinuxCNC source basis: rs274ngc_pre.cc restore_parameters() reads the + // configured parameter file, and save_parameters() writes filename + ".new" + // before replacing the main file and managing filename + ".bak". + async loadParameterFile(path) { + assertRelativePath(path); + try { + const data = await readOpfsFile(workspacePath, path); + writeWasmFileReplacingPath(module, LINUXCNC_DEFAULT_PARAMETER_FILE, data); + writeWasmFileReplacingPath(module, `/${LINUXCNC_DEFAULT_PARAMETER_FILE}`, data); + return data; + } catch (error) { + if (error?.name !== "NotFoundError") { + throw error; + } + cleanupLinuxCncParameterFiles(module); + return null; + } + }, + async persistParameterFile(path) { + assertRelativePath(path); + const data = readFirstExistingWasmFile(module, [ + LINUXCNC_DEFAULT_PARAMETER_FILE, + `/${LINUXCNC_DEFAULT_PARAMETER_FILE}`, + ]); + if (data === null) { + throw new Error("LinuxCNC parameter file was not produced by the interpreter"); + } + await writeOpfsFile(workspacePath, path, data); + await removeOpfsEntry(workspacePath, `${path}.new`, false); + removeWasmPath(module, `${LINUXCNC_DEFAULT_PARAMETER_FILE}.new`, false); + removeWasmPath(module, `/${LINUXCNC_DEFAULT_PARAMETER_FILE}.new`, false); + const backupPath = `${LINUXCNC_DEFAULT_PARAMETER_FILE}.bak`; + const backupData = readFirstExistingWasmFile(module, [backupPath, `/${backupPath}`]); + if (backupData !== null) { + await writeOpfsFile(workspacePath, `${path}.bak`, backupData); + } else { + await removeOpfsEntry(workspacePath, `${path}.bak`, false); + } + return data; + }, + async removeFile(path) { + await removeOpfsEntry(workspacePath, path, false); + removeWasmPath(module, this.resolvePath(path), false); + }, + async removeDirectory(path) { + await removeOpfsEntry(workspacePath, path, true); + removeWasmPath(module, this.resolvePath(path), true); + }, + async clear() { + await clearOpfsWorkspace(workspacePath); + for (const entry of module.FS.readdir(mountPoint)) { + if (entry !== "." && entry !== "..") { + removeWasmPath(module, `${mountPoint}/${entry}`, true); + } + } + }, + }; +} + function readPose(view, offset) { return { x: view.getFloat64(offset + 0, true), @@ -82,12 +562,21 @@ async function loadModuleFactory() { } export async function createWasmSimulator(moduleOptions = {}) { + if (moduleOptions.opfs === false && isOpfsAvailable()) { + throw new Error("OPFS cannot be disabled in browser contexts with OPFS support"); + } + const hasOpfsOptions = Object.prototype.hasOwnProperty.call(moduleOptions, "opfs"); + const emscriptenModuleOptions = hasOpfsOptions + ? (({ opfs: _opfs, ...rest }) => rest)(moduleOptions) + : moduleOptions; + const createModule = await loadModuleFactory(); if (!createModule) { throw new Error("cnc_sim.js did not export createCncSimModule"); } - const module = await createModule(moduleOptions); + const module = hasOpfsOptions ? await createModule(emscriptenModuleOptions) : await createModule(moduleOptions); + const opfs = installOpfsWorkspace(module, moduleOptions); const create = module.cwrap("cnc_sim_create", "number", []); const destroy = module.cwrap("cnc_sim_destroy", null, ["number"]); const reset = module.cwrap("cnc_sim_reset", null, ["number"]); @@ -100,9 +589,9 @@ export async function createWasmSimulator(moduleOptions = {}) { const handle = create(); let callbackPtr = 0; - return { - parse(program, dialect = "linuxcnc", options = {}) { - const events = []; + const parseText = (program, dialect = "linuxcnc", options = {}, parseOptions = {}) => { + const events = []; + try { reset(handle); setDialect(handle, DIALECT[dialect] ?? DIALECT.linuxcnc); @@ -117,8 +606,19 @@ export async function createWasmSimulator(moduleOptions = {}) { ...(options.config ? { config: options.config } : {}), ...(options.configPath ? { configPath: options.configPath } : {}), ...(options.config_path ? { config_path: options.config_path } : {}), + ...(options.ini ? { ini: options.ini } : {}), + ...(options.iniFile ? { iniFile: options.iniFile } : {}), + ...(options.iniFileName ? { iniFileName: options.iniFileName } : {}), + ...(options.ini_file ? { ini_file: options.ini_file } : {}), + ...(options.ini_file_name ? { ini_file_name: options.ini_file_name } : {}), + ...(options.INI_FILE_NAME ? { INI_FILE_NAME: options.INI_FILE_NAME } : {}), + ...(options.halFile ? { halFile: options.halFile } : {}), ...(options.halfile ? { halfile: options.halfile } : {}), ...(options.hal_file ? { hal_file: options.hal_file } : {}), + ...(options.postguiHalFile ? { postguiHalFile: options.postguiHalFile } : {}), + ...(options.postgui_halfile ? { postgui_halfile: options.postgui_halfile } : {}), + ...(options.postgui_hal_file ? { postgui_hal_file: options.postgui_hal_file } : {}), + ...(options.POSTGUI_HALFILE ? { POSTGUI_HALFILE: options.POSTGUI_HALFILE } : {}), ...(options.kinematics ? { kinematics: options.kinematics } : {}), ...(options.trt ? { trt: options.trt } : {}), ...(options.pivotLength !== undefined ? { pivotLength: options.pivotLength } : {}), @@ -135,9 +635,13 @@ export async function createWasmSimulator(moduleOptions = {}) { }); const configBytes = module.lengthBytesUTF8(config) + 1; const configPtr = module._malloc(configBytes); - module.stringToUTF8(config, configPtr, configBytes); - const configRc = loadConfig(handle, configPtr, configBytes - 1); - module._free(configPtr); + let configRc; + try { + module.stringToUTF8(config, configPtr, configBytes); + configRc = loadConfig(handle, configPtr, configBytes - 1); + } finally { + module._free(configPtr); + } if (configRc !== 0) { throw new Error(module.UTF8ToString(lastError(handle))); } @@ -153,14 +657,59 @@ export async function createWasmSimulator(moduleOptions = {}) { const bytes = module.lengthBytesUTF8(program) + 1; const ptr = module._malloc(bytes); - module.stringToUTF8(program, ptr, bytes); - const rc = parseProgram(handle, ptr, bytes - 1); - module._free(ptr); + let rc; + try { + module.stringToUTF8(program, ptr, bytes); + rc = parseProgram(handle, ptr, bytes - 1); + } finally { + module._free(ptr); + } if (rc !== 0) { throw new Error(module.UTF8ToString(lastError(handle))); } return events; + } finally { + if (opfs && !parseOptions.keepLinuxCncParameterFiles) { + cleanupLinuxCncParameterFiles(module); + } + } + }; + + return { + parse(program, dialect = "linuxcnc", options = {}) { + return parseText(program, dialect, options); + }, + + async parseFile(path, dialect = "linuxcnc", options = {}) { + if (!opfs) { + throw new Error("OPFS workspace is not available for program file parsing"); + } + const program = new TextDecoder().decode(await opfs.readFile(path)); + return parseText(program, dialect, options); + }, + + async parseWithParameterFile(program, parameterPath, dialect = "linuxcnc", options = {}) { + if (!opfs) { + throw new Error("OPFS workspace is not available for parameter file persistence"); + } + cleanupLinuxCncParameterFiles(module); + await opfs.loadParameterFile(parameterPath); + try { + const events = parseText(program, dialect, options, { keepLinuxCncParameterFiles: true }); + await opfs.persistParameterFile(parameterPath); + return events; + } finally { + cleanupLinuxCncParameterFiles(module); + } + }, + + async parseFileWithParameterFile(path, parameterPath, dialect = "linuxcnc", options = {}) { + if (!opfs) { + throw new Error("OPFS workspace is not available for program and parameter file parsing"); + } + const program = new TextDecoder().decode(await opfs.readFile(path)); + return this.parseWithParameterFile(program, parameterPath, dialect, options); }, dispose() { @@ -170,5 +719,10 @@ export async function createWasmSimulator(moduleOptions = {}) { } destroy(handle); }, + + fs: { + opfs, + module, + }, }; } diff --git a/web/test-browser-wasm-smoke-app-sections.js b/web/test-browser-wasm-smoke-app-sections.js new file mode 100644 index 0000000..1a9569e --- /dev/null +++ b/web/test-browser-wasm-smoke-app-sections.js @@ -0,0 +1,66 @@ +export async function runBrowserAppOpfsSections(context) { + const { + browserHasOpfs, + createHiddenAppFrame, + disposeAppFrame, + expectIncludes, + expectText, + getAppFrameSimulator, + parseAppFrameProgram, + runSection, + waitFor, + waitForAppAlarm, + waitForAppControls, + } = context; + if (!browserHasOpfs()) { + return; + } + await runSection("browser app opfs save", async () => { + const appFrame = await createHiddenAppFrame(); + try { + await waitForAppControls(appFrame, "app frame did not load CNC controls"); + parseAppFrameProgram(appFrame, "G21 G90\nG0 X1\nG28.1\nM30\n"); + await waitForAppAlarm(appFrame, "OK ", "app frame did not parse program"); + const appSimulator = await getAppFrameSimulator(appFrame, "app frame did not create OPFS simulator workspace"); + const savedProgram = new TextDecoder().decode(await appSimulator.fs.opfs.readFile("programs/current.ngc")); + expectText(savedProgram, "G21 G90\nG0 X1\nG28.1\nM30\n", + "app frame did not persist current program into OPFS"); + const savedParameterFile = new TextDecoder().decode(await appSimulator.fs.opfs.readFile("parameters/rs274ngc.var")); + expectIncludes(savedParameterFile, "5161\t1.000000", + "app frame did not persist LinuxCNC parameter state into OPFS"); + } finally { + await disposeAppFrame(appFrame); + } + }); + await runSection("browser app opfs restore", async () => { + const restoredAppFrame = await createHiddenAppFrame(); + try { + await waitForAppControls(restoredAppFrame, "restored app frame did not load CNC controls"); + await waitFor(() => { + const restoredInput = restoredAppFrame.contentDocument?.querySelector("#programInput"); + return restoredInput?.value === "G21 G90\nG0 X1\nG28.1\nM30\n"; + }, "app frame did not restore current program from OPFS after reload"); + await waitForAppAlarm( + restoredAppFrame, + "OPFS PROGRAM RESTORED", + "app frame did not report OPFS program restore after reload", + ); + await getAppFrameSimulator(restoredAppFrame, "restored app frame did not create OPFS simulator workspace"); + parseAppFrameProgram( + restoredAppFrame, + "G21 G90\nF100\nO10 if [#5161 EQ 1]\nG1 X12\nO10 endif\nM30\n", + ); + await waitForAppAlarm( + restoredAppFrame, + "OK ", + "restored app frame did not parse program with reloaded parameter state", + ); + await waitFor( + () => restoredAppFrame.contentDocument?.querySelector("#axisX")?.value === "12.000", + "app frame did not reload LinuxCNC parameter state from OPFS after reload", + ); + } finally { + await disposeAppFrame(restoredAppFrame); + } + }); +} diff --git a/web/test-browser-wasm-smoke-helpers.js b/web/test-browser-wasm-smoke-helpers.js new file mode 100644 index 0000000..ada42fb --- /dev/null +++ b/web/test-browser-wasm-smoke-helpers.js @@ -0,0 +1,176 @@ +const textDecoder = new TextDecoder(); +const textEncoder = new TextEncoder(); +const linuxCncParameterScratchFiles = ["rs274ngc.var", "rs274ngc.var.new", "rs274ngc.var.bak"]; + +export function createBrowserSmokeHarness(result) { + const sectionResults = []; + const near = (actual, expected) => Math.abs(actual - expected) < 1e-6; + const waitFor = async (predicate, message) => { + for (let i = 0; i < 50; i += 1) { + if (predicate()) { + return; + } + await new Promise((resolve) => setTimeout(resolve, 100)); + } + throw new Error(message); + }; + const runSection = async (name, action) => { + const sectionNumber = sectionResults.length + 1; + result.textContent = `running: ${sectionNumber}: ${name}`; + try { + await action(); + sectionResults.push(name); + } catch (error) { + const detail = error && error.stack ? error.stack : error; + throw new Error(`section "${name}" failed: ${detail}`); + } + }; + const completedMessage = () => `browser wasm smoke passed (${sectionResults.length} sections)`; + const browserHasOpfs = () => navigator.storage && navigator.storage.getDirectory; + const expectEvent = (events, predicate, message) => { + if (!events.some(predicate)) { + throw new Error(message); + } + }; + const expectNoEvent = (events, predicate, message) => { + if (events.some(predicate)) { + throw new Error(message); + } + }; + const expectText = (actual, expected, message) => { + if (actual !== expected) { + throw new Error(message); + } + }; + const expectIncludes = (actual, expected, message) => { + if (!actual.includes(expected)) { + throw new Error(message); + } + }; + const expectFileList = (actual, expected, message) => { + if ( + actual.length !== expected.length || + expected.some((entry) => !actual.includes(entry)) + ) { + throw new Error(`${message}: ${actual.join(",")}`); + } + }; + const expectErrorContaining = async (action, expectedText, message) => { + try { + await action(); + throw new Error(message); + } catch (error) { + if (!String(error?.message ?? error).includes(expectedText)) { + throw error; + } + } + }; + const expectMissingOpfsFile = async (action, message) => { + try { + await action(); + throw new Error(message); + } catch (error) { + if (error?.name !== "NotFoundError") { + throw error; + } + } + }; + const expectMissingWasmPath = (action, message) => { + try { + action(); + throw new Error(message); + } catch (error) { + if (error?.errno !== 44) { + throw error; + } + } + }; + const expectWasmFilesMissing = (fs, paths, messageForPath) => { + for (const wasmPath of paths) { + expectMissingWasmPath( + () => fs.readFile(wasmPath), + messageForPath(wasmPath), + ); + } + }; + const decodeBytes = (bytes) => textDecoder.decode(bytes); + const encodeText = (text) => textEncoder.encode(text); + const readOpfsText = async (simulator, path) => decodeBytes(await simulator.fs.opfs.readFile(path)); + const readWasmText = (simulator, path) => decodeBytes(simulator.fs.module.FS.readFile(path)); + const writeWasmText = (simulator, path, text) => simulator.fs.module.FS.writeFile(path, encodeText(text)); + const removeWasmPathIfExists = (simulator, path) => { + try { + simulator.fs.module.FS.unlink(path); + } catch (error) { + if (error?.errno !== 44) { + throw error; + } + } + }; + const expectOpfsText = async (simulator, path, expected, message) => { + expectText(await readOpfsText(simulator, path), expected, message); + }; + const expectWasmText = (simulator, path, expected, message) => { + expectText(readWasmText(simulator, path), expected, message); + }; + const expectOpfsExists = async (simulator, path, message) => { + if (!(await simulator.fs.opfs.exists(path))) { + throw new Error(message); + } + }; + const expectOpfsMissing = async (simulator, path, message) => { + if (await simulator.fs.opfs.exists(path)) { + throw new Error(message); + } + }; + const expectOpfsFileStat = async (simulator, path, expectedSize, message) => { + const stat = await simulator.fs.opfs.stat(path); + if (stat.kind !== "file" || stat.size !== expectedSize) { + throw new Error(message); + } + }; + const expectOpfsDirectoryStat = async (simulator, path, message) => { + const stat = await simulator.fs.opfs.stat(path); + if (stat.kind !== "directory" || stat.size !== null) { + throw new Error(message); + } + }; + const expectLinuxCncParameterScratchMissing = (simulator, messageForPath) => { + for (const parameterPath of linuxCncParameterScratchFiles) { + expectMissingWasmPath( + () => simulator.fs.module.FS.stat(parameterPath), + messageForPath(parameterPath), + ); + } + }; + + return { + browserHasOpfs, + completedMessage, + decodeBytes, + encodeText, + expectErrorContaining, + expectEvent, + expectFileList, + expectIncludes, + expectLinuxCncParameterScratchMissing, + expectMissingOpfsFile, + expectMissingWasmPath, + expectNoEvent, + expectOpfsDirectoryStat, + expectOpfsExists, + expectOpfsFileStat, + expectOpfsMissing, + expectOpfsText, + expectText, + expectWasmFilesMissing, + expectWasmText, + near, + readOpfsText, + readWasmText, + removeWasmPathIfExists, + runSection, + waitFor, + writeWasmText, + }; +} diff --git a/web/test-browser-wasm-smoke-linuxcnc-sections.js b/web/test-browser-wasm-smoke-linuxcnc-sections.js new file mode 100644 index 0000000..6078b13 --- /dev/null +++ b/web/test-browser-wasm-smoke-linuxcnc-sections.js @@ -0,0 +1,72 @@ +export async function runLinuxCncBrowserSections(context, simulator) { + const { + expectEvent, + expectMissingWasmPath, + near, + runSection, + } = context; + await runSection("linuxcnc browser basic parse", async () => { + const events = simulator.parse("G21 G90\nG0 X0\nG1 X5 F100\nM30\n", "linuxcnc", { + backend: "linuxcnc-rs274", + }); + expectEvent(events, (event) => event.type === "linear-feed" && near(event.end.x, 5), + "missing expected LinuxCNC WASM browser linear-feed event"); + expectEvent(events, (event) => event.type === "program-end", + "missing expected LinuxCNC WASM browser program-end event"); + }); + let configCases = []; + await runSection("linuxcnc generated switchkins cases load", async () => { + const configCaseResponse = await fetch("/public/linuxcnc_switchkins_remap_config_cases.json"); + if (!configCaseResponse.ok) { + throw new Error(`failed to load LinuxCNC switchkins config cases: ${configCaseResponse.status}`); + } + configCases = await configCaseResponse.json(); + if (!Array.isArray(configCases) || configCases.length === 0) { + throw new Error("missing LinuxCNC switchkins config cases"); + } + }); + for (const configCase of configCases) { + await runSection(`linuxcnc generated switchkins ${configCase.field}=${configCase.value}`, async () => { + const options = { + backend: "linuxcnc-rs274", + [configCase.field]: configCase.value, + }; + const program = configCase.m430 >= 0 ? "M428\nM429\nM430\n" : "M428\nM429\n"; + const configEvents = simulator.parse(program, "linuxcnc", options); + for (const [line, kinstype] of [ + [1, configCase.m428], + [2, configCase.m429], + ...(configCase.m430 >= 0 ? [[3, configCase.m430]] : []), + ]) { + expectEvent(configEvents, + (event) => event.type === "kinematics-switch" && event.line === line && event.reserved === kinstype, + `missing LinuxCNC WASM browser generated ${configCase.field}=${configCase.value} line ${line} switchkins type ${kinstype}`, + ); + } + }); + } + await runSection("linuxcnc browser explicit scara switchkins", async () => { + const switchEvents = simulator.parse("M428\nM429\nM430\n", "linuxcnc", { + backend: "linuxcnc-rs274", + switchkins: "scara", + }); + for (const [line, kinstype] of [ + [1, 0], + [2, 1], + [3, 2], + ]) { + expectEvent(switchEvents, + (event) => event.type === "kinematics-switch" && event.line === line && event.reserved === kinstype, + `missing LinuxCNC WASM browser M${427 + line} switchkins type ${kinstype}`, + ); + } + }); + await runSection("linuxcnc browser parse parameter scratch cleanup", async () => { + for (const parameterPath of ["rs274ngc.var", "rs274ngc.var.new", "rs274ngc.var.bak"]) { + expectMissingWasmPath( + () => simulator.fs.module.FS.stat(parameterPath), + `browser parse left LinuxCNC parameter state outside OPFS: ${parameterPath}`, + ); + } + }); +} diff --git a/web/test-browser-wasm-smoke-opfs-basic-sections.js b/web/test-browser-wasm-smoke-opfs-basic-sections.js new file mode 100644 index 0000000..6dbc2e1 --- /dev/null +++ b/web/test-browser-wasm-smoke-opfs-basic-sections.js @@ -0,0 +1,114 @@ +export async function runBrowserOpfsBasicSections(context, simulator) { + const { + expectErrorContaining, + expectEvent, + expectMissingWasmPath, + expectText, + near, + runSection, + withSmokeSimulator, + } = context; + + await runSection("opfs workspace clear and healthcheck", async () => { + if (!simulator.fs.opfs) { + throw new Error("expected OPFS workspace to be available in browser smoke"); + } + await simulator.fs.opfs.clear(); + if (simulator.fs.opfs.workspacePath !== "cnc-simulator-smoke") { + throw new Error("unexpected OPFS workspace path"); + } + await simulator.fs.opfs.writeFile("healthcheck.ngc", "G21 G90\n"); + if (!(await simulator.fs.opfs.exists("healthcheck.ngc"))) { + throw new Error("OPFS exists did not find a persisted file"); + } + if (await simulator.fs.opfs.exists("missing-healthcheck.ngc")) { + throw new Error("OPFS exists reported a missing file"); + } + const healthcheckStat = await simulator.fs.opfs.stat("healthcheck.ngc"); + if (healthcheckStat.kind !== "file" || healthcheckStat.size !== 8) { + throw new Error("OPFS stat did not report the persisted healthcheck file size"); + } + const clearCheck = new TextDecoder().decode(await simulator.fs.opfs.readFile("healthcheck.ngc")); + expectText(clearCheck, "G21 G90\n", "OPFS workspace did not survive clear/write round trip"); + }); + + await runSection("opfs default mount check", async () => { + await withSmokeSimulator((defaultMountSimulator) => { + if (!defaultMountSimulator.fs.opfs || defaultMountSimulator.fs.opfs.mountPoint !== "/cnc") { + throw new Error("default OPFS mount point was not preserved"); + } + }); + }); + + await runSection("opfs program write and read", async () => { + const encoded = new TextEncoder().encode("G21 G90\nG0 X1\n"); + await simulator.fs.opfs.writeFile("programs/browser-smoke.ngc", encoded); + const decoded = new TextDecoder().decode( + await simulator.fs.opfs.readFile("programs/browser-smoke.ngc"), + ); + expectText(decoded, "G21 G90\nG0 X1\n", "OPFS round trip did not preserve browser smoke program"); + }); + + await runSection("opfs unsafe path rejection", async () => { + for (const unsafePath of ["../escape.ngc", "programs/../escape.ngc", "/escape.ngc", "programs//escape.ngc"]) { + await expectErrorContaining( + () => simulator.fs.opfs.writeFile(unsafePath, "G21\n"), + "inside the CNC workspace", + `OPFS accepted unsafe path ${unsafePath}`, + ); + } + for (const [operation, action] of [ + ["readFile", () => simulator.fs.opfs.readFile("../escape.ngc")], + ["readDirectory", () => simulator.fs.opfs.readDirectory("../escape")], + ["persistFile", () => simulator.fs.opfs.persistFile("../escape.ngc")], + ["persistDirectory", () => simulator.fs.opfs.persistDirectory("../escape")], + ["copyFile", () => simulator.fs.opfs.copyFile("../escape.ngc", "programs/escape.ngc")], + ["copyFileTarget", () => simulator.fs.opfs.copyFile("healthcheck.ngc", "../escape.ngc")], + ["exists", () => simulator.fs.opfs.exists("../escape.ngc")], + ["stat", () => simulator.fs.opfs.stat("../escape.ngc")], + ["moveFile", () => simulator.fs.opfs.moveFile("../escape.ngc", "programs/escape.ngc")], + ["moveFileTarget", () => simulator.fs.opfs.moveFile("healthcheck.ngc", "../escape.ngc")], + ["removeFile", () => simulator.fs.opfs.removeFile("../escape.ngc")], + ["removeDirectory", () => simulator.fs.opfs.removeDirectory("../escape")], + ["parseFile", () => simulator.parseFile("../escape.ngc", "linuxcnc", { backend: "linuxcnc-rs274" })], + [ + "parseWithParameterFile", + () => + simulator.parseWithParameterFile("G21 G90\nM30\n", "../escape.var", "linuxcnc", { + backend: "linuxcnc-rs274", + }), + ], + ]) { + await expectErrorContaining( + action, + "inside the CNC workspace", + `OPFS ${operation} accepted an unsafe path`, + ); + } + }); + + await runSection("opfs program parse and restore", async () => { + const opfsProgramEvents = await simulator.parseFile("programs/browser-smoke.ngc", "linuxcnc", { + backend: "linuxcnc-rs274", + }); + expectEvent(opfsProgramEvents, (event) => event.type === "rapid" && near(event.end.x, 1), + "OPFS program file was not parsed through LinuxCNC WASM"); + for (const parameterPath of ["rs274ngc.var", "rs274ngc.var.new", "rs274ngc.var.bak"]) { + expectMissingWasmPath( + () => simulator.fs.module.FS.stat(parameterPath), + `browser parseFile left LinuxCNC parameter state outside OPFS: ${parameterPath}`, + ); + } + await withSmokeSimulator(async (restoredProgramSimulator) => { + // LinuxCNC source basis: rs274ngc_pre.cc read_text() reads + // blocks from the program stream after the OPFS mirror is loaded. + const restoredProgramEvents = await restoredProgramSimulator.parseFile( + "programs/browser-smoke.ngc", + "linuxcnc", + { backend: "linuxcnc-rs274" }, + ); + expectEvent(restoredProgramEvents, (event) => event.type === "rapid" && near(event.end.x, 1), + "LinuxCNC OPFS program file was not restored across WASM simulator instances"); + }); + }); +} diff --git a/web/test-browser-wasm-smoke-opfs-directory-sections.js b/web/test-browser-wasm-smoke-opfs-directory-sections.js new file mode 100644 index 0000000..d033321 --- /dev/null +++ b/web/test-browser-wasm-smoke-opfs-directory-sections.js @@ -0,0 +1,174 @@ +export async function runBrowserOpfsDirectorySections(context, simulator) { + const { + expectFileList, + expectMissingOpfsFile, + expectMissingWasmPath, + expectText, + runSection, + withSmokeSimulator, + } = context; + + await runSection("opfs directory persist and load", async () => { + const fixturePath = simulator.fs.opfs.resolvePath("fixtures"); + simulator.fs.module.FS.mkdirTree(`${fixturePath}/nested`); + simulator.fs.module.FS.mkdirTree(`${fixturePath}/empty/nested`); + simulator.fs.module.FS.writeFile(`${fixturePath}/nested/fixture.ngc`, "G21 G90\nG0 X3\n"); + simulator.fs.module.FS.writeFile(`${fixturePath}/root.ngc`, "G21 G90\nG0 X4\n"); + const persistedFiles = await simulator.fs.opfs.persistDirectory("fixtures"); + const fixtureStat = await simulator.fs.opfs.stat("fixtures"); + if (fixtureStat.kind !== "directory" || fixtureStat.size !== null) { + throw new Error("OPFS stat did not report a persisted directory"); + } + expectFileList(persistedFiles, ["nested/fixture.ngc", "root.ngc"], + "unexpected OPFS directory persist file list"); + const fixtureDecoded = new TextDecoder().decode( + await simulator.fs.opfs.readFile("fixtures/nested/fixture.ngc"), + ); + expectText(fixtureDecoded, "G21 G90\nG0 X3\n", + "persisted WASM filesystem directory file was not readable from OPFS"); + const emptyFixtureStat = await simulator.fs.opfs.stat("fixtures/empty/nested"); + if (emptyFixtureStat.kind !== "directory" || emptyFixtureStat.size !== null) { + throw new Error("OPFS persistDirectory did not preserve an empty directory"); + } + await withSmokeSimulator(async (staleDirectorySimulator) => { + await staleDirectorySimulator.fs.opfs.writeFile("fixtures/stale.ngc", "G21 G90\nG0 X6\n"); + }); + simulator.fs.module.FS.unlink(`${fixturePath}/nested/fixture.ngc`); + const refreshedPersistedFiles = await simulator.fs.opfs.persistDirectory("fixtures"); + expectFileList(refreshedPersistedFiles, ["root.ngc"], + "unexpected refreshed OPFS directory persist file list"); + await expectMissingOpfsFile( + () => simulator.fs.opfs.readFile("fixtures/stale.ngc"), + "stale OPFS directory file remained after directory persist", + ); + await expectMissingOpfsFile( + () => simulator.fs.opfs.readFile("fixtures/nested/fixture.ngc"), + "removed WASM directory file remained in OPFS after directory persist", + ); + simulator.fs.module.FS.writeFile(`${fixturePath}/nested/fixture.ngc`, "G21 G90\nG0 X3\n"); + const restoredPersistedFiles = await simulator.fs.opfs.persistDirectory("fixtures"); + expectFileList(restoredPersistedFiles, ["nested/fixture.ngc", "root.ngc"], + "unexpected restored OPFS directory persist file list"); + const loadedFiles = await simulator.fs.opfs.readDirectory("fixtures"); + expectFileList(loadedFiles, ["nested/fixture.ngc", "root.ngc"], + "unexpected OPFS directory load file list"); + const loadedRootPath = simulator.fs.opfs.resolvePath("fixtures/root.ngc"); + const loadedRootDecoded = new TextDecoder().decode(simulator.fs.module.FS.readFile(loadedRootPath)); + expectText(loadedRootDecoded, "G21 G90\nG0 X4\n", + "OPFS directory file was not mirrored into the WASM filesystem"); + const loadedEmptyPath = simulator.fs.opfs.resolvePath("fixtures/empty/nested"); + const loadedEmptyMode = simulator.fs.module.FS.stat(loadedEmptyPath).mode; + if (!simulator.fs.module.FS.isDir(loadedEmptyMode)) { + throw new Error("OPFS readDirectory did not mirror an empty directory into WASM FS"); + } + }); + + await runSection("opfs directory refresh and type conflicts", async () => { + const loadedRootPath = simulator.fs.opfs.resolvePath("fixtures/root.ngc"); + await withSmokeSimulator(async (updateDirectorySimulator) => { + await updateDirectorySimulator.fs.opfs.removeFile("fixtures/root.ngc"); + await updateDirectorySimulator.fs.opfs.writeFile("fixtures/nested/fixture.ngc", "G21 G90\nG0 X5\n"); + const refreshedFiles = await simulator.fs.opfs.readDirectory("fixtures"); + expectFileList(refreshedFiles, ["nested/fixture.ngc"], + "unexpected refreshed OPFS directory file list"); + expectMissingWasmPath( + () => simulator.fs.module.FS.stat(loadedRootPath), + "stale OPFS directory mirror file remained in WASM FS", + ); + const refreshedFixturePath = simulator.fs.opfs.resolvePath("fixtures/nested/fixture.ngc"); + const refreshedFixture = new TextDecoder().decode( + simulator.fs.module.FS.readFile(refreshedFixturePath), + ); + expectText(refreshedFixture, "G21 G90\nG0 X5\n", + "OPFS directory refresh did not replace the WASM mirror file"); + await simulator.fs.opfs.writeFile("fixtures/type-conflict/original.ngc", "G21 G90\n"); + await updateDirectorySimulator.fs.opfs.removeDirectory("fixtures/type-conflict"); + await updateDirectorySimulator.fs.opfs.writeFile("fixtures/type-conflict", "G21 G90\nG0 X11\n"); + const replacedConflictFile = new TextDecoder().decode( + await simulator.fs.opfs.readFile("fixtures/type-conflict"), + ); + expectText(replacedConflictFile, "G21 G90\nG0 X11\n", + "OPFS readFile did not load a file that replaced a directory"); + const conflictMirrorPath = simulator.fs.opfs.resolvePath("fixtures/type-conflict"); + const conflictMirrorMode = simulator.fs.module.FS.stat(conflictMirrorPath).mode; + if (!simulator.fs.module.FS.isFile(conflictMirrorMode)) { + throw new Error("OPFS readFile did not replace the WASM mirror directory with a file"); + } + await updateDirectorySimulator.fs.opfs.removeFile("fixtures/type-conflict"); + await updateDirectorySimulator.fs.opfs.writeFile( + "fixtures/type-conflict/original.ngc", + "G21 G90\n", + ); + await updateDirectorySimulator.fs.opfs.removeDirectory("fixtures/type-conflict"); + await updateDirectorySimulator.fs.opfs.writeFile("fixtures/root.ngc", "G21 G90\nG0 X4\n"); + await updateDirectorySimulator.fs.opfs.writeFile("fixtures/nested/fixture.ngc", "G21 G90\nG0 X3\n"); + const restoredFiles = await simulator.fs.opfs.readDirectory("fixtures"); + expectFileList(restoredFiles, ["nested/fixture.ngc", "root.ngc"], + "unexpected reloaded OPFS directory file list"); + }); + }); + + await runSection("opfs directory restore in new instance", async () => { + await withSmokeSimulator(async (restoredDirectorySimulator) => { + const restoredDirectoryFiles = await restoredDirectorySimulator.fs.opfs.readDirectory("fixtures"); + expectFileList(restoredDirectoryFiles, ["nested/fixture.ngc", "root.ngc"], + "unexpected restored OPFS directory file list"); + const restoredFixtureRootPath = restoredDirectorySimulator.fs.opfs.resolvePath("fixtures/root.ngc"); + const restoredFixtureRoot = new TextDecoder().decode( + restoredDirectorySimulator.fs.module.FS.readFile(restoredFixtureRootPath), + ); + expectText(restoredFixtureRoot, "G21 G90\nG0 X4\n", + "OPFS directory was not restored into a new WASM filesystem instance"); + const restoredEmptyFixturePath = restoredDirectorySimulator.fs.opfs.resolvePath("fixtures/empty/nested"); + const restoredEmptyFixtureMode = + restoredDirectorySimulator.fs.module.FS.stat(restoredEmptyFixturePath).mode; + if (!restoredDirectorySimulator.fs.module.FS.isDir(restoredEmptyFixtureMode)) { + throw new Error("OPFS empty directory was not restored into a new WASM filesystem instance"); + } + }); + }); + + await runSection("opfs metadata queries avoid wasm mirror pollution", async () => { + await withSmokeSimulator(async (existsOnlySimulator) => { + await existsOnlySimulator.fs.opfs.writeFile("programs/exists-only.ngc", "G21 G90\nG0 X10\n"); + const existsOnlyPath = simulator.fs.opfs.resolvePath("programs/exists-only.ngc"); + if (!(await simulator.fs.opfs.exists("programs/exists-only.ngc"))) { + throw new Error("OPFS exists did not see a file written by another simulator instance"); + } + const existsOnlyStat = await simulator.fs.opfs.stat("programs/exists-only.ngc"); + if (existsOnlyStat.kind !== "file" || existsOnlyStat.size !== 15) { + throw new Error("OPFS stat did not see a file written by another simulator instance"); + } + expectMissingWasmPath( + () => simulator.fs.module.FS.stat(existsOnlyPath), + "OPFS metadata queries polluted the WASM filesystem mirror", + ); + }); + }); + + await runSection("opfs workspace clear and mirror removal", async () => { + await simulator.fs.opfs.clear(); + await withSmokeSimulator(async (clearedWorkspaceSimulator) => { + await expectMissingOpfsFile( + () => clearedWorkspaceSimulator.fs.opfs.readDirectory("fixtures"), + "cleared OPFS workspace still exposed a persisted directory", + ); + }); + await simulator.fs.opfs.removeFile("programs/browser-smoke.ngc"); + if (await simulator.fs.opfs.exists("programs/browser-smoke.ngc")) { + throw new Error("OPFS exists reported a removed file"); + } + const removedProgramPath = simulator.fs.opfs.resolvePath("programs/browser-smoke.ngc"); + expectMissingWasmPath( + () => simulator.fs.module.FS.stat(removedProgramPath), + "OPFS removeFile did not remove the WASM mirror", + ); + const fixturePath = simulator.fs.opfs.resolvePath("fixtures"); + await simulator.fs.opfs.removeDirectory("fixtures"); + await simulator.fs.opfs.removeDirectory("fixtures"); + expectMissingWasmPath( + () => simulator.fs.module.FS.stat(fixturePath), + "OPFS removeDirectory did not remove the WASM mirror", + ); + }); +} diff --git a/web/test-browser-wasm-smoke-opfs-mirror-sections.js b/web/test-browser-wasm-smoke-opfs-mirror-sections.js new file mode 100644 index 0000000..d36a6be --- /dev/null +++ b/web/test-browser-wasm-smoke-opfs-mirror-sections.js @@ -0,0 +1,76 @@ +export async function runBrowserOpfsMirrorSections(context, simulator) { + const { + expectMissingWasmPath, + expectText, + runSection, + } = context; + + await runSection("opfs wasm mirror persist file", async () => { + const wasmPath = simulator.fs.opfs.resolvePath("programs/browser-smoke.ngc"); + const wasmDecoded = new TextDecoder().decode(simulator.fs.module.FS.readFile(wasmPath)); + expectText(wasmDecoded, "G21 G90\nG0 X1\n", "OPFS file was not mirrored into the WASM filesystem"); + const persistedPath = simulator.fs.opfs.resolvePath("programs/browser-persisted.ngc"); + simulator.fs.module.FS.writeFile(persistedPath, new TextEncoder().encode("G21 G90\nG0 X2\n")); + const persisted = new TextDecoder().decode(await simulator.fs.opfs.persistFile("programs/browser-persisted.ngc")); + expectText(persisted, "G21 G90\nG0 X2\n", "WASM filesystem file was not persisted into OPFS"); + const persistedDecoded = new TextDecoder().decode( + await simulator.fs.opfs.readFile("programs/browser-persisted.ngc"), + ); + expectText(persistedDecoded, "G21 G90\nG0 X2\n", + "persisted WASM filesystem file was not readable from OPFS"); + }); + + await runSection("opfs copy and move mirror files", async () => { + // LinuxCNC source basis: rs274ngc_pre.cc save_parameters() + // links the current parameter file to filename + ".bak", then + // writes filename + ".new" and renames it over the parameter file. + await simulator.fs.opfs.writeFile("parameters/move-main.var", "5161\t1.000000\n"); + const copiedParameterBackup = new TextDecoder().decode( + await simulator.fs.opfs.copyFile("parameters/move-main.var", "parameters/move-main.var.bak"), + ); + expectText(copiedParameterBackup, "5161\t1.000000\n", + "OPFS copyFile did not return the copied file contents"); + const copiedParameterBackupFile = new TextDecoder().decode( + await simulator.fs.opfs.readFile("parameters/move-main.var.bak"), + ); + expectText(copiedParameterBackupFile, "5161\t1.000000\n", + "OPFS copyFile did not create the backup file"); + const copiedParameterBackupWasmPath = simulator.fs.opfs.resolvePath("parameters/move-main.var.bak"); + const copiedParameterBackupWasm = new TextDecoder().decode( + simulator.fs.module.FS.readFile(copiedParameterBackupWasmPath), + ); + expectText(copiedParameterBackupWasm, "5161\t1.000000\n", + "OPFS copyFile did not update the WASM mirror destination"); + await simulator.fs.opfs.writeFile("parameters/move-main.var.new", "5161\t2.000000\n"); + await simulator.fs.opfs.moveFile("parameters/move-main.var.new", "parameters/move-main.var"); + const movedParameterFile = new TextDecoder().decode( + await simulator.fs.opfs.readFile("parameters/move-main.var"), + ); + expectText(movedParameterFile, "5161\t2.000000\n", + "OPFS moveFile did not replace the destination file"); + if (await simulator.fs.opfs.exists("parameters/move-main.var.new")) { + throw new Error("OPFS moveFile left the source file behind"); + } + const movedParameterWasmPath = simulator.fs.opfs.resolvePath("parameters/move-main.var"); + const movedParameterWasm = new TextDecoder().decode( + simulator.fs.module.FS.readFile(movedParameterWasmPath), + ); + expectText(movedParameterWasm, "5161\t2.000000\n", + "OPFS moveFile did not update the WASM mirror destination"); + const movedTemporaryWasmPath = simulator.fs.opfs.resolvePath("parameters/move-main.var.new"); + expectMissingWasmPath( + () => simulator.fs.module.FS.stat(movedTemporaryWasmPath), + "OPFS moveFile left the WASM mirror source behind", + ); + }); + + await runSection("opfs write truncates wasm mirror", async () => { + await simulator.fs.opfs.writeFile("programs/truncate.ngc", "G21 G90\nG0 X12345\nM30\n"); + await simulator.fs.opfs.writeFile("programs/truncate.ngc", "G21\n"); + const truncatedProgram = new TextDecoder().decode(await simulator.fs.opfs.readFile("programs/truncate.ngc")); + expectText(truncatedProgram, "G21\n", "OPFS writeFile did not truncate stale program bytes"); + const truncatedWasmPath = simulator.fs.opfs.resolvePath("programs/truncate.ngc"); + const truncatedWasmProgram = new TextDecoder().decode(simulator.fs.module.FS.readFile(truncatedWasmPath)); + expectText(truncatedWasmProgram, "G21\n", "OPFS writeFile did not truncate the WASM mirror"); + }); +} diff --git a/web/test-browser-wasm-smoke-opfs-parameter-sections.js b/web/test-browser-wasm-smoke-opfs-parameter-sections.js new file mode 100644 index 0000000..5947b27 --- /dev/null +++ b/web/test-browser-wasm-smoke-opfs-parameter-sections.js @@ -0,0 +1,255 @@ +export async function runBrowserOpfsParameterSections(context, simulator) { + const { + expectErrorContaining, + expectEvent, + expectIncludes, + expectMissingOpfsFile, + expectMissingWasmPath, + expectNoEvent, + expectText, + expectWasmFilesMissing, + near, + removeWasmPathIfExists, + runSection, + withSmokeSimulator, + } = context; + + await runSection("opfs parameter save", async () => { + await simulator.parseWithParameterFile( + "G21 G90\nG0 X1\nG28.1\nM30\n", + "parameters/rs274ngc.var", + "linuxcnc", + { backend: "linuxcnc-rs274" }, + ); + const opfsParameterFile = new TextDecoder().decode( + await simulator.fs.opfs.readFile("parameters/rs274ngc.var"), + ); + expectIncludes(opfsParameterFile, "5161\t1.000000", "LinuxCNC parameter file was not persisted into OPFS"); + }); + + await runSection("opfs parameter restore in new instance", async () => { + await withSmokeSimulator(async (restoredWorkspaceSimulator) => { + // LinuxCNC source basis: rs274ngc_pre.cc restore_parameters() + // loads persisted numbered parameters before interpreting blocks. + const restoredWorkspaceEvents = await restoredWorkspaceSimulator.parseWithParameterFile( + "G21 G90\nF100\nO10 if [#5161 EQ 1]\nG1 X8\nO10 endif\n", + "parameters/rs274ngc.var", + "linuxcnc", + { backend: "linuxcnc-rs274" }, + ); + expectEvent(restoredWorkspaceEvents, (event) => event.type === "linear-feed" && near(event.end.x, 8), + "LinuxCNC OPFS parameter file was not restored across WASM simulator instances"); + }); + }); + + await runSection("opfs parameter temporary cleanup", async () => { + await simulator.fs.opfs.writeFile("parameters/rs274ngc.var.new", "stale temporary parameter file\n"); + // LinuxCNC source basis: rs274ngc_pre.cc save_parameters() writes + // filename + ".new", then renames it over the parameter file. + simulator.fs.module.FS.writeFile("rs274ngc.var.new", "stale wasm temporary parameter file\n"); + const restoredParameterEvents = await simulator.parseWithParameterFile( + "G21 G90\nF100\nO10 if [#5161 EQ 1]\nG1 X9\nO10 endif\n", + "parameters/rs274ngc.var", + "linuxcnc", + { backend: "linuxcnc-rs274" }, + ); + expectEvent(restoredParameterEvents, (event) => event.type === "linear-feed" && near(event.end.x, 9), + "LinuxCNC parameter file was not restored from OPFS"); + await expectMissingOpfsFile( + () => simulator.fs.opfs.readFile("parameters/rs274ngc.var.new"), + "stale LinuxCNC temporary parameter file remained in OPFS", + ); + expectMissingWasmPath( + () => simulator.fs.module.FS.readFile("rs274ngc.var.new"), + "stale LinuxCNC temporary parameter file remained in WASM FS", + ); + }); + + await runSection("opfs parameter backup bridge", async () => { + // LinuxCNC source basis: rs274ngc_pre.cc save_parameters() + // attempts link(filename, filename + ".bak") but treats link + // failure as non-fatal. The browser bridge must persist a backup + // when the WASM filesystem exposes one. + simulator.fs.module.FS.writeFile("rs274ngc.var", "5161\t2.000000\n"); + simulator.fs.module.FS.writeFile("rs274ngc.var.bak", "5161\t1.000000\n"); + await simulator.fs.opfs.persistParameterFile("parameters/backup-bridge.var"); + const opfsParameterBackup = new TextDecoder().decode( + await simulator.fs.opfs.readFile("parameters/backup-bridge.var.bak"), + ); + expectIncludes(opfsParameterBackup, "5161\t1.000000", + "LinuxCNC parameter backup file was not bridged into OPFS"); + }); + + await runSection("opfs parameter stale backup cleanup", async () => { + removeWasmPathIfExists(simulator, "rs274ngc.var.bak"); + removeWasmPathIfExists(simulator, "/rs274ngc.var.bak"); + await simulator.parseWithParameterFile( + "G21 G90\nM30\n", + "parameters/rs274ngc.var", + "linuxcnc", + { backend: "linuxcnc-rs274" }, + ); + await expectMissingOpfsFile( + () => simulator.fs.opfs.readFile("parameters/rs274ngc.var.bak"), + "stale LinuxCNC parameter backup file remained in OPFS", + ); + // LinuxCNC source basis: rs274ngc_pre.cc save_parameters() + // unlinks stale backup files before replacing the parameter file. + await simulator.fs.opfs.removeFile("parameters/missing-cleanup/rs274ngc.var.bak"); + }); + + await runSection("opfs program file with parameter persistence", async () => { + await simulator.fs.opfs.writeFile("programs/parameter-file-smoke.ngc", "G21 G90\nF100\nG0 X1\nG28.1\nM30\n"); + const fileParameterEvents = await simulator.parseFileWithParameterFile( + "programs/parameter-file-smoke.ngc", + "parameters/rs274ngc.var", + "linuxcnc", + { backend: "linuxcnc-rs274" }, + ); + expectEvent(fileParameterEvents, (event) => event.type === "program-end", + "OPFS program file with LinuxCNC parameter persistence did not parse"); + }); + + await runSection("opfs missing parameter file creation", async () => { + await simulator.fs.opfs.writeFile("parameters/missing-main.var.bak", "stale backup without main file\n"); + await simulator.fs.opfs.writeFile("parameters/missing-main.var.new", "stale temporary without main file\n"); + // LinuxCNC source basis: rs274ngc_pre.cc save_parameters() unlinks + // filename + RS274NGC_PARAMETER_FILE_BACKUP_SUFFIX before linking, + // then renames filename + ".new" over the parameter file. + simulator.fs.module.FS.writeFile("rs274ngc.var.bak", "stale wasm backup without main file\n"); + simulator.fs.module.FS.writeFile("rs274ngc.var.new", "stale wasm temporary without main file\n"); + await simulator.parseWithParameterFile( + "G21 G90\nM30\n", + "parameters/missing-main.var", + "linuxcnc", + { backend: "linuxcnc-rs274" }, + ); + const missingMainParameterFile = new TextDecoder().decode( + await simulator.fs.opfs.readFile("parameters/missing-main.var"), + ); + expectIncludes(missingMainParameterFile, "5220\t1.000000", + "LinuxCNC missing parameter file was not created through OPFS"); + expectIncludes(missingMainParameterFile, "5161\t0.000000", + "LinuxCNC missing parameter file did not include required G28 parameter"); + const missingMainRestoreEvents = await simulator.parseWithParameterFile( + "G21 G90\nF100\nO10 if [#5220 EQ 1]\nG1 X6\nO10 endif\n", + "parameters/missing-main.var", + "linuxcnc", + { backend: "linuxcnc-rs274" }, + ); + expectEvent(missingMainRestoreEvents, (event) => event.type === "linear-feed" && near(event.end.x, 6), + "LinuxCNC-created missing OPFS parameter file was not restored on the next parse"); + await expectMissingOpfsFile( + () => simulator.fs.opfs.readFile("parameters/missing-main.var.bak"), + "stale LinuxCNC missing-parameter backup remained in OPFS", + ); + expectMissingWasmPath( + () => simulator.fs.module.FS.readFile("rs274ngc.var.bak"), + "stale LinuxCNC missing-parameter backup remained in WASM FS", + ); + await expectMissingOpfsFile( + () => simulator.fs.opfs.readFile("parameters/missing-main.var.new"), + "stale LinuxCNC missing-parameter temporary file remained in OPFS", + ); + expectMissingWasmPath( + () => simulator.fs.module.FS.readFile("rs274ngc.var.new"), + "stale LinuxCNC missing-parameter temporary file remained in WASM FS", + ); + }); + + await runSection("opfs invalid parameter restore failures", async () => { + await simulator.fs.opfs.writeFile("parameters/out-of-order.var", "5162\t1.000000\n5161\t1.000000\n"); + await expectErrorContaining( + () => simulator.parseWithParameterFile( + "G21 G90\nM30\n", + "parameters/out-of-order.var", + "linuxcnc", + { backend: "linuxcnc-rs274" }, + ), + "Parameter file out of order", + "LinuxCNC accepted an out-of-order OPFS parameter file", + ); + const outOfOrderParameterFile = new TextDecoder().decode( + await simulator.fs.opfs.readFile("parameters/out-of-order.var"), + ); + expectText(outOfOrderParameterFile, "5162\t1.000000\n5161\t1.000000\n", + "LinuxCNC persisted an out-of-order OPFS parameter file after restore failure"); + expectMissingWasmPath( + () => simulator.fs.module.FS.readFile("rs274ngc.var"), + "LinuxCNC out-of-order parameter file remained in WASM FS after restore failure", + ); + await simulator.fs.opfs.writeFile("parameters/out-of-range.var", "0\t1.000000\n"); + await expectErrorContaining( + () => simulator.parseWithParameterFile( + "G21 G90\nM30\n", + "parameters/out-of-range.var", + "linuxcnc", + { backend: "linuxcnc-rs274" }, + ), + "Parameter number out of range", + "LinuxCNC accepted an out-of-range OPFS parameter file", + ); + const outOfRangeParameterFile = new TextDecoder().decode( + await simulator.fs.opfs.readFile("parameters/out-of-range.var"), + ); + expectText(outOfRangeParameterFile, "0\t1.000000\n", + "LinuxCNC persisted an out-of-range OPFS parameter file after restore failure"); + expectMissingWasmPath( + () => simulator.fs.module.FS.readFile("rs274ngc.var"), + "LinuxCNC out-of-range parameter file remained in WASM FS after restore failure", + ); + await simulator.fs.opfs.writeFile("parameters/failed-parse.var", "1\t0.000000\n"); + await expectErrorContaining( + () => simulator.parseWithParameterFile( + "G21 G90\nG28.1\n#0 = 1\n", + "parameters/failed-parse.var", + "linuxcnc", + { backend: "linuxcnc-rs274" }, + ), + "", + "LinuxCNC accepted a failing parse that should not persist parameters", + ); + const failedParseParameterFile = new TextDecoder().decode( + await simulator.fs.opfs.readFile("parameters/failed-parse.var"), + ); + if (failedParseParameterFile.includes("5161\t1.000000")) { + throw new Error("LinuxCNC persisted failed parse parameter updates into OPFS"); + } + }); + + await runSection("opfs stale parameter state cleanup", async () => { + await simulator.fs.opfs.writeFile("parameters/stale-state.var", "5161\t1.000000\n"); + const staleStateEvents = await simulator.parseWithParameterFile( + "G21 G90\nF100\nO10 if [#5161 EQ 1]\nG1 X9\nO10 endif\n", + "parameters/stale-state.var", + "linuxcnc", + { backend: "linuxcnc-rs274" }, + ); + expectEvent(staleStateEvents, (event) => event.type === "linear-feed" && near(event.end.x, 9), + "LinuxCNC did not honor the loaded OPFS parameter state"); + await simulator.fs.opfs.removeFile("parameters/stale-state.var"); + const missingStateEvents = await simulator.parseWithParameterFile( + "G21 G90\nF100\nO10 if [#5161 EQ 1]\nG1 X9\nO10 endif\n", + "parameters/stale-state.var", + "linuxcnc", + { backend: "linuxcnc-rs274" }, + ); + expectNoEvent(missingStateEvents, (event) => event.type === "linear-feed" && near(event.end.x, 9), + "LinuxCNC stale OPFS parameter state was not cleared when the file was missing"); + // LinuxCNC source basis: rs274ngc_pre.cc restore_parameters() + // treats a missing parameter file as OK; the browser OPFS bridge + // must not leave stale default parameter files in the WASM mirror. + simulator.fs.module.FS.writeFile("rs274ngc.var", "5161\t1.000000\n"); + simulator.fs.module.FS.writeFile("rs274ngc.var.new", "stale temporary parameter file\n"); + simulator.fs.module.FS.writeFile("rs274ngc.var.bak", "stale backup parameter file\n"); + const missingLoad = await simulator.fs.opfs.loadParameterFile("parameters/load-missing.var"); + if (missingLoad !== null) { + throw new Error("missing OPFS parameter file did not return null"); + } + expectWasmFilesMissing( + simulator.fs.module.FS, + ["rs274ngc.var", "rs274ngc.var.new", "rs274ngc.var.bak"], + (parameterPath) => `missing OPFS parameter load left stale WASM file ${parameterPath}`, + ); + }); +} diff --git a/web/test-browser-wasm-smoke-opfs-policy-sections.js b/web/test-browser-wasm-smoke-opfs-policy-sections.js new file mode 100644 index 0000000..42dcc2f --- /dev/null +++ b/web/test-browser-wasm-smoke-opfs-policy-sections.js @@ -0,0 +1,49 @@ +export async function runBrowserOpfsPolicySections(context) { + const { + browserHasOpfs, + createDefaultBrowserSimulator, + createSmokeSimulator, + expectErrorContaining, + runSection, + } = context; + if (!browserHasOpfs()) { + return; + } + await runSection("browser opfs policy", async () => { + const defaultOpfsSimulator = await createDefaultBrowserSimulator(); + try { + if (!defaultOpfsSimulator.fs.opfs) { + throw new Error("default browser simulator did not create an OPFS workspace"); + } + if (defaultOpfsSimulator.fs.opfs.workspacePath !== "cnc-simulator") { + throw new Error("default browser simulator used an unexpected OPFS workspace path"); + } + } finally { + defaultOpfsSimulator.dispose(); + } + await expectErrorContaining( + () => createSmokeSimulator(false), + "OPFS cannot be disabled", + "browser simulator allowed OPFS to be disabled", + ); + for (const unsafeWorkspacePath of ["../bad-workspace", "/bad-workspace", "bad//workspace"]) { + await expectErrorContaining( + () => createSmokeSimulator({ workspacePath: unsafeWorkspacePath }), + "inside the CNC workspace", + `browser simulator accepted unsafe OPFS workspace ${unsafeWorkspacePath}`, + ); + } + for (const unsafeMountPoint of ["cnc", "/cnc/../escape", "/cnc//escape"]) { + await expectErrorContaining( + () => createSmokeSimulator({ mountPoint: unsafeMountPoint, workspacePath: "cnc-simulator-smoke" }), + "Emscripten filesystem", + `browser simulator accepted unsafe OPFS mount ${unsafeMountPoint}`, + ); + } + await expectErrorContaining( + () => createSmokeSimulator({ mountPoint: "", workspacePath: "cnc-simulator-smoke" }), + "non-empty absolute path", + "browser simulator accepted empty OPFS mount", + ); + }); +} diff --git a/web/test-browser-wasm-smoke-opfs-workspace-sections.js b/web/test-browser-wasm-smoke-opfs-workspace-sections.js new file mode 100644 index 0000000..f62538f --- /dev/null +++ b/web/test-browser-wasm-smoke-opfs-workspace-sections.js @@ -0,0 +1,14 @@ +import { runBrowserOpfsBasicSections } from "/test-browser-wasm-smoke-opfs-basic-sections.js"; +import { runBrowserOpfsDirectorySections } from "/test-browser-wasm-smoke-opfs-directory-sections.js"; +import { runBrowserOpfsMirrorSections } from "/test-browser-wasm-smoke-opfs-mirror-sections.js"; +import { runBrowserOpfsParameterSections } from "/test-browser-wasm-smoke-opfs-parameter-sections.js"; + +export async function runBrowserOpfsWorkspaceSections(context, simulator) { + if (!context.browserHasOpfs()) { + return; + } + await runBrowserOpfsBasicSections(context, simulator); + await runBrowserOpfsParameterSections(context, simulator); + await runBrowserOpfsMirrorSections(context, simulator); + await runBrowserOpfsDirectorySections(context, simulator); +} diff --git a/web/test-browser-wasm-smoke-sections.js b/web/test-browser-wasm-smoke-sections.js new file mode 100644 index 0000000..a91b506 --- /dev/null +++ b/web/test-browser-wasm-smoke-sections.js @@ -0,0 +1,99 @@ +import { createBrowserSmokeHarness } from "/test-browser-wasm-smoke-helpers.js"; +import { runBrowserAppOpfsSections } from "/test-browser-wasm-smoke-app-sections.js"; +import { runLinuxCncBrowserSections } from "/test-browser-wasm-smoke-linuxcnc-sections.js"; +import { runBrowserOpfsPolicySections } from "/test-browser-wasm-smoke-opfs-policy-sections.js"; +import { runBrowserOpfsWorkspaceSections } from "/test-browser-wasm-smoke-opfs-workspace-sections.js"; + +export async function runBrowserWasmSmoke({ createWasmSimulator, result }) { + const harness = createBrowserSmokeHarness(result); + const createSmokeSimulator = (opfs = { mountPoint: "/cnc", workspacePath: "cnc-simulator-smoke" }) => + createWasmSimulator({ + locateFile: (file) => `/public/${file}`, + print: () => {}, + printErr: () => {}, + ...(opfs === undefined ? {} : { opfs }), + }); + const createDefaultBrowserSimulator = () => + createWasmSimulator({ + locateFile: (file) => `/public/${file}`, + print: () => {}, + printErr: () => {}, + }); + const withSmokeSimulator = async (action, opfs) => { + const nestedSimulator = await createSmokeSimulator(opfs); + try { + return await action(nestedSimulator); + } finally { + nestedSimulator.dispose(); + } + }; + const createHiddenAppFrame = async () => { + const appFrame = document.createElement("iframe"); + const appHtml = await (await fetch("/")).text(); + appFrame.srcdoc = appHtml.replace( + "", + '