#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" LINUXCNC_ROOT="${LINUXCNC_ROOT:-$ROOT_DIR/../linuxcnc}" FIXTURE_FAMILY="axis/remap/stop-lookahead/nc_files" FIXTURE_DIR="$LINUXCNC_ROOT/configs/sim/axis/remap/stop-lookahead" FIXTURE_INI="$FIXTURE_DIR/demo.ini" RUN_DIR="$ROOT_DIR/build/native/python-remap-runtime" PYTHON_RUNTIME_STDOUT="$RUN_DIR/python_lifecycle.stdout.log" PYTHON_RUNTIME_STDERR="$RUN_DIR/python_lifecycle.stderr.log" required_commands=(python3 linuxcnc) missing_requirements=() command_path() { command -v "$1" 2>/dev/null || true } csv_join() { local IFS=, printf '%s' "$*" } print_kv() { printf '%s=%s\n' "$1" "$2" } runtime_requirements=() for command_name in "${required_commands[@]}"; do path="$(command_path "$command_name")" if [[ -n "$path" ]]; then runtime_requirements+=("$command_name:1") print_kv "python_remap_runtime_${command_name}_path" "$path" else runtime_requirements+=("$command_name:0") missing_requirements+=("$command_name") fi done source_ready=1 for rel in \ src/emc/rs274ngc/interp_python.cc \ src/emc/pythonplugin/python_plugin.cc \ configs/sim/axis/remap/stop-lookahead/demo.ini \ configs/sim/axis/remap/stop-lookahead/python/remap.py \ configs/sim/axis/remap/stop-lookahead/python/toplevel.py; do if [[ ! -f "$LINUXCNC_ROOT/$rel" ]]; then source_ready=0 missing_requirements+=("$rel") print_kv "python_remap_missing_source_${rel//[^A-Za-z0-9]/_}" "$LINUXCNC_ROOT/$rel" fi done runtime_ready=0 if [[ "${#missing_requirements[@]}" -eq 0 ]]; then runtime_ready=1 fi print_kv python_remap_runtime_fixture_family "$FIXTURE_FAMILY" print_kv python_remap_runtime_fixture_id "stop_lookahead_python_runtime_lifecycle" print_kv python_remap_runtime_fixture_scope "no_python_callables_no_ngc_only_subpaths" print_kv python_remap_runtime_phases "initialize_python,apply_ini_python_path,execute_toplevel,callable_lookup,pycall_dispatch,callable_invoke,remap_phase_dispatch,generator_finish,execute_python_runtime,reload_on_change" print_kv python_remap_runtime_modules "axis/remap/stop-lookahead/python/remap.py,axis/remap/stop-lookahead/python/toplevel.py" print_kv python_remap_runtime_requirements "$(csv_join "${runtime_requirements[@]}")" if [[ "${#missing_requirements[@]}" -eq 0 ]]; then print_kv python_remap_missing_requirements "-" else print_kv python_remap_missing_requirements "$(csv_join "${missing_requirements[@]}")" fi print_kv python_remap_source_proof_ready "$source_ready" print_kv python_remap_runtime_ready "$runtime_ready" print_kv python_remap_execution_enabled 0 print_kv python_remap_promotion_allowed 0 if [[ "$source_ready" != "1" ]]; then print_kv python_remap_runtime_probe_status "blocked_missing_source" print_kv python_remap_runtime_probe_note "missing_source_requirements_keep_python_remap_blocked" exit 0 fi if [[ "$runtime_ready" != "1" ]]; then print_kv python_remap_runtime_probe_status "skipped_missing_host_runtime" print_kv python_remap_runtime_probe_note "missing_host_runtime_requirements_keep_python_remap_blocked" exit 0 fi if [[ "${ENABLE_PYTHON_REMAP_RUNTIME_PROBE:-0}" != "1" ]]; then print_kv python_remap_runtime_probe_status "ready_disabled_by_default" print_kv python_remap_runtime_probe_note "set_ENABLE_PYTHON_REMAP_RUNTIME_PROBE_1_to_run_python_lifecycle_probe" exit 0 fi mkdir -p "$RUN_DIR" rm -f "$PYTHON_RUNTIME_STDOUT" "$PYTHON_RUNTIME_STDERR" print_kv python_remap_runtime_probe_fixture "$FIXTURE_DIR" print_kv python_remap_runtime_probe_ini "$FIXTURE_INI" print_kv python_remap_runtime_probe_stdout "$PYTHON_RUNTIME_STDOUT" print_kv python_remap_runtime_probe_stderr "$PYTHON_RUNTIME_STDERR" if python3 - "$LINUXCNC_ROOT" "$FIXTURE_DIR" "$FIXTURE_INI" "$RUN_DIR" \ >"$PYTHON_RUNTIME_STDOUT" 2>"$PYTHON_RUNTIME_STDERR" <<'PY' import configparser import importlib import inspect import os from pathlib import Path import re import sys import tempfile linuxcnc_root = Path(sys.argv[1]) fixture_dir = Path(sys.argv[2]) fixture_ini = Path(sys.argv[3]) run_dir = Path(sys.argv[4]) def emit(name, value): print(f"{name}={value}") def require(condition, message): if not condition: raise RuntimeError(message) interp_return = linuxcnc_root / "src/emc/nml_intf/interp_return.hh" match = re.search( r"INTERP_EXECUTE_FINISH\s*=\s*([0-9]+)", interp_return.read_text(encoding="utf-8"), ) require(match, "missing_INTERP_EXECUTE_FINISH_source_value") execute_finish = int(match.group(1)) emit("python_remap_lifecycle_execute_finish_source_value", execute_finish) stub_dir = Path(tempfile.mkdtemp(prefix="python-remap-interpreter-", dir=run_dir)) (stub_dir / "interpreter.py").write_text( f"INTERP_EXECUTE_FINISH = {execute_finish}\n", encoding="utf-8", ) parser = configparser.ConfigParser(strict=False) parser.optionxform = str parser.read(fixture_ini) require(parser.has_section("PYTHON"), "missing_PYTHON_section") path_prepend = parser.get("PYTHON", "PATH_PREPEND", fallback="") toplevel = parser.get("PYTHON", "TOPLEVEL", fallback="") require(path_prepend == "python", f"PATH_PREPEND_drift:{path_prepend}") require(toplevel == "python/toplevel.py", f"TOPLEVEL_drift:{toplevel}") emit("python_remap_lifecycle_ini_path_prepend", path_prepend) emit("python_remap_lifecycle_ini_toplevel", toplevel) sys.path.insert(0, str(stub_dir)) sys.path.insert(0, str(fixture_dir / path_prepend)) sys.path.insert(0, str(linuxcnc_root / "lib/python")) import interpreter # noqa: E402 require(interpreter.INTERP_EXECUTE_FINISH == execute_finish, "interpreter_sentinel_drift") emit("python_remap_lifecycle_interpreter_sentinel_ok", 1) toplevel_module = importlib.import_module(Path(toplevel).stem) emit("python_remap_lifecycle_toplevel_imported", int(toplevel_module.__name__ == "toplevel")) remap_module = importlib.import_module("remap") emit("python_remap_lifecycle_remap_imported", int(remap_module.__name__ == "remap")) require(hasattr(remap_module, "queuebuster"), "missing_queuebuster_callable") require(callable(remap_module.queuebuster), "queuebuster_not_callable") emit("python_remap_lifecycle_callable_lookup_ok", 1) result = remap_module.queuebuster(object()) require(inspect.isgenerator(result), "queuebuster_not_generator") emit("python_remap_lifecycle_generator_returned", 1) first_yield = next(result) require(first_yield == execute_finish, f"queuebuster_yield_drift:{first_yield}") emit("python_remap_lifecycle_generator_first_yield", first_yield) try: next(result) raise RuntimeError("queuebuster_generator_did_not_finish") except StopIteration: emit("python_remap_lifecycle_generator_finish_ok", 1) emit("python_remap_runtime_lifecycle_probe_ok", 1) PY then cat "$PYTHON_RUNTIME_STDOUT" print_kv python_remap_runtime_probe_status "runtime_lifecycle_probe_passed" print_kv python_remap_runtime_probe_note "linuxcnc_stop_lookahead_python_lifecycle_probe_passed_without_promotion" exit 0 fi cat "$PYTHON_RUNTIME_STDOUT" || true print_kv python_remap_runtime_probe_status "runtime_lifecycle_probe_failed" print_kv python_remap_runtime_probe_note "linuxcnc_stop_lookahead_python_lifecycle_probe_failed" exit 1