Complete sim config boundary coverage

This commit is contained in:
2026-06-11 06:12:06 +08:00
parent 5bd8f12872
commit 1ab5571ae1
76 changed files with 18975 additions and 146 deletions

View File

@@ -0,0 +1,215 @@
#include <fstream>
#include <iostream>
#include <iterator>
#include <string>
#include "emc/ini/inifile.hh"
#ifndef LINUXCNC_SOURCE_CONFIG_DIR
#error "LINUXCNC_SOURCE_CONFIG_DIR must point at the LinuxCNC configs/sim tree"
#endif
namespace {
struct UserMCase {
const char *code;
const char *remap_code;
const char *script_rel;
const char *remap_rel;
const char *state_mode;
const char *guard_pin;
const char *switchkins_target;
const char *active_g5x;
const char *work_offset_pocket;
const char *work_offset_words;
const char *min_limit_key;
const char *max_limit_key;
};
std::string source_config_path(const std::string &rel)
{
return std::string(LINUXCNC_SOURCE_CONFIG_DIR) + "/" + rel;
}
std::string read_text(const std::string &path)
{
std::ifstream input(path);
return std::string(
std::istreambuf_iterator<char>(input),
std::istreambuf_iterator<char>());
}
bool contains(const std::string &text, const std::string &needle)
{
return text.find(needle) != std::string::npos;
}
void print_bool(const std::string &name, bool value)
{
std::cout << name << "=" << (value ? 1 : 0) << "\n";
}
bool check_ini_value(
const linuxcnc::IniFile &ini,
const std::string &label,
const std::string &section,
const std::string &tag,
const std::string &expected)
{
const auto actual = ini.findString(tag, section);
std::cout << label << "_" << section << "." << tag << "="
<< (actual ? *actual : "<missing>") << "\n";
const bool ok = actual && *actual == expected;
print_bool(label + "_" + section + "." + tag + "_ok", ok);
return ok;
}
bool check_axis_targets(
const linuxcnc::IniFile &ini,
const UserMCase &user_m,
const char *axis,
const char *expected_min,
const char *expected_max,
const char *expected_velocity,
const char *expected_acceleration)
{
const std::string section = std::string("AXIS_") + axis;
const std::string label = std::string(user_m.code) + "_" + axis;
bool ok = true;
ok &= check_ini_value(ini, label, section, user_m.min_limit_key, expected_min);
ok &= check_ini_value(ini, label, section, user_m.max_limit_key, expected_max);
ok &= check_ini_value(ini, label, section, "MAX_VELOCITY", expected_velocity);
ok &= check_ini_value(ini, label, section, "MAX_ACCELERATION", expected_acceleration);
return ok;
}
bool check_script(const UserMCase &user_m)
{
const std::string text = read_text(source_config_path(user_m.script_rel));
bool ok = true;
ok &= contains(text, "#!/usr/bin/tclsh");
ok &= contains(text, "package require Linuxcnc");
ok &= contains(text, "package require Hal");
ok &= contains(text, "parse_ini $::env(INI_FILE_NAME)");
ok &= contains(text, std::string("hal getp ") + user_m.guard_pin);
ok &= contains(text, "hal setp ini.$l.min_limit");
ok &= contains(text, "hal setp ini.$l.max_limit");
ok &= contains(text, "hal setp ini.$l.min_velocity");
ok &= contains(text, "hal setp ini.$l.max_acceleration");
ok &= contains(text, user_m.min_limit_key);
ok &= contains(text, user_m.max_limit_key);
print_bool(std::string(user_m.code) + "_script_tcl_hal_runtime", ok);
return ok;
}
bool check_remap_caller(const UserMCase &user_m)
{
const std::string text = read_text(source_config_path(user_m.remap_rel));
const std::string remap_code(user_m.remap_code);
const bool ok =
contains(text, user_m.code) &&
contains(text, "#<SWITCHKINS_PIN> = 3") &&
contains(text, std::string("#<kinstype> = ") + user_m.switchkins_target) &&
contains(text, "M68") &&
contains(text, "M68 E#<SWITCHKINS_PIN> Q#<kinstype>") &&
contains(text, "M66") &&
contains(text, "M66 E0 L0") &&
contains(text, std::string("G10 L2 ") + user_m.work_offset_pocket + " " + user_m.work_offset_words) &&
contains(text, user_m.active_g5x) &&
contains(text, "#<_hal[motion.switchkins-type]>") &&
contains(text, std::string("[#<_hal[motion.switchkins-type]> NE ") + user_m.switchkins_target + "]");
std::cout << remap_code << "_switchkins_output_pin=motion.analog-out-03\n";
std::cout << remap_code << "_switchkins_target=" << user_m.switchkins_target << "\n";
std::cout << remap_code << "_work_offset_pocket=" << user_m.work_offset_pocket << "\n";
std::cout << remap_code << "_work_offset_words=" << user_m.work_offset_words << "\n";
std::cout << remap_code << "_active_g5x=" << user_m.active_g5x << "\n";
print_bool(remap_code + "_calls_" + user_m.code, contains(text, user_m.code));
print_bool(remap_code + "_source_transition_from_linuxcnc", ok);
print_bool(std::string(user_m.code) + "_remap_calls_user_m_process", ok);
return ok;
}
bool check_user_m_case(const linuxcnc::IniFile &ini, const UserMCase &user_m)
{
bool ok = true;
ok &= check_script(user_m);
ok &= check_remap_caller(user_m);
if (std::string(user_m.code) == "M128") {
ok &= check_axis_targets(ini, user_m, "X", "-300", "300", "60.0", "400.0");
ok &= check_axis_targets(ini, user_m, "Y", "-100", "100", "60.0", "400.0");
ok &= check_axis_targets(ini, user_m, "Z", "-240", "0", "60.0", "400.0");
} else {
ok &= check_axis_targets(ini, user_m, "X", "-240", "0", "60.0", "400.0");
ok &= check_axis_targets(ini, user_m, "Y", "-100", "100", "60.0", "400.0");
ok &= check_axis_targets(ini, user_m, "Z", "-300", "300", "60.0", "400.0");
}
print_bool(std::string(user_m.code) + "_source_state_targets_from_linuxcnc", ok);
return ok;
}
} // namespace
int main()
{
const std::string ini_path = source_config_path("axis/vismach/millturn/millturn.ini");
linuxcnc::IniFile ini(ini_path);
const bool opened = static_cast<bool>(ini);
print_bool("millturn_ini_open", opened);
if (!opened) {
return 1;
}
bool ok = true;
ok &= check_ini_value(ini, "millturn", "RS274NGC", "USER_M_PATH", "./mcodes");
ok &= check_ini_value(ini, "millturn", "RS274NGC", "SUBROUTINE_PATH", "./remap_subs");
ok &= check_ini_value(ini, "millturn", "DISPLAY", "PYVCP", "millturn.xml");
ok &= check_ini_value(ini, "millturn", "HAL", "HALUI", "halui");
ok &= check_ini_value(
ini,
"millturn",
"HAL",
"HALCMD",
"net :kinstype-select <= motion.analog-out-03 => motion.switchkins-type");
const UserMCase cases[] = {
{
"M128",
"M428",
"axis/vismach/millturn/mcodes/M128",
"axis/vismach/millturn/remap_subs/428remap.ngc",
"mill",
"kinstype.is-0",
"0",
"G59.1",
"P7",
"X-290 Y0 Z-160 A0",
"MIN_LIMIT",
"MAX_LIMIT",
},
{
"M129",
"M429",
"axis/vismach/millturn/mcodes/M129",
"axis/vismach/millturn/remap_subs/429remap.ngc",
"turn",
"kinstype.is-1",
"1",
"G59.2",
"P8",
"X-160 Y0 Z-290 A0",
"MIN_LIMIT_TURN",
"MAX_LIMIT_TURN",
},
};
for (const auto &user_m : cases) {
std::cout << user_m.code << "_state_mode=" << user_m.state_mode << "\n";
std::cout << user_m.code << "_guard_pin=" << user_m.guard_pin << "\n";
ok &= check_user_m_case(ini, user_m);
}
print_bool("millturn_user_m_native_source_state_proof", ok);
print_bool("millturn_user_m_execution_enabled", false);
print_bool("millturn_user_m_promotion_allowed", false);
return ok ? 0 : 1;
}

View File

@@ -0,0 +1,189 @@
#include <fstream>
#include <iostream>
#include <iterator>
#include <string>
#ifndef LINUXCNC_SOURCE_DIR
#error "LINUXCNC_SOURCE_DIR must point at the LinuxCNC source tree"
#endif
#ifndef LINUXCNC_SOURCE_CONFIG_DIR
#error "LINUXCNC_SOURCE_CONFIG_DIR must point at the LinuxCNC configs/sim tree"
#endif
namespace {
std::string source_path(const std::string &rel)
{
return std::string(LINUXCNC_SOURCE_DIR) + "/" + rel;
}
std::string source_config_path(const std::string &rel)
{
return std::string(LINUXCNC_SOURCE_CONFIG_DIR) + "/" + rel;
}
std::string read_text(const std::string &path)
{
std::ifstream input(path);
return std::string(
std::istreambuf_iterator<char>(input),
std::istreambuf_iterator<char>());
}
bool contains(const std::string &text, const std::string &needle)
{
return text.find(needle) != std::string::npos;
}
void print_bool(const std::string &name, bool value)
{
std::cout << name << "=" << (value ? 1 : 0) << "\n";
}
bool check_python_runtime_owner()
{
const std::string interp_python = read_text(source_path("src/emc/rs274ngc/interp_python.cc"));
const std::string python_plugin = read_text(source_path("src/emc/pythonplugin/python_plugin.cc"));
const bool pycall_dispatch =
contains(interp_python, "PythonPlugin *python_plugin") &&
contains(interp_python, "Interp::pycall") &&
contains(interp_python, "python_plugin->call(module,funcname");
const bool remap_phases =
contains(interp_python, "PY_PROLOG") &&
contains(interp_python, "PY_BODY") &&
contains(interp_python, "PY_EPILOG");
const bool generator_finish =
contains(interp_python, "PyGen_Check(retval.ptr())") &&
contains(interp_python, "PY_FINISH_PROLOG") &&
contains(interp_python, "PY_FINISH_EPILOG");
const bool callable_lookup =
contains(interp_python, "Interp::is_pycallable") &&
contains(interp_python, "python_plugin->is_callable(module,funcname)");
const bool execute_runtime =
contains(interp_python, "Interp::py_execute") &&
contains(interp_python, "python_plugin->run_string(cmd, retval, as_file)");
const bool initializes_python =
contains(python_plugin, "PyConfig_InitPythonConfig(&config)") &&
contains(python_plugin, "Py_InitializeFromConfig(&config)");
const bool toplevel_exec =
contains(python_plugin, "findString(\"TOPLEVEL\", section)") &&
contains(python_plugin, "bp::exec_file(abs_path, main_namespace, main_namespace)");
const bool ini_python_path =
contains(python_plugin, "findString(n, \"PATH_PREPEND\", \"PYTHON\")") &&
contains(python_plugin, "findString(n, \"PATH_APPEND\", \"PYTHON\")") &&
contains(python_plugin, "PyRun_SimpleString(pycmd)");
const bool callable_invoke =
contains(python_plugin, "PythonPlugin::call") &&
contains(python_plugin, "PyObject_Call(function.ptr(), tupleargs.ptr(), kwargs.ptr())");
const bool reload_on_change =
contains(python_plugin, "PythonPlugin::reload") &&
contains(python_plugin, "reload_on_change");
print_bool("python_runtime_pycall_dispatch", pycall_dispatch);
print_bool("python_runtime_remap_phases", remap_phases);
print_bool("python_runtime_generator_finish", generator_finish);
print_bool("python_runtime_callable_lookup", callable_lookup);
print_bool("python_runtime_execute_runtime", execute_runtime);
print_bool("python_plugin_initializes_python", initializes_python);
print_bool("python_plugin_toplevel_exec_file", toplevel_exec);
print_bool("python_plugin_ini_python_path", ini_python_path);
print_bool("python_plugin_callable_invoke", callable_invoke);
print_bool("python_plugin_reload_on_change", reload_on_change);
return pycall_dispatch && remap_phases && generator_finish && callable_lookup &&
execute_runtime && initializes_python && toplevel_exec && ini_python_path &&
callable_invoke && reload_on_change;
}
bool check_axis_laser_family()
{
const std::string ini = read_text(source_config_path("axis/laser/laser.ini"));
const std::string remap = read_text(source_config_path("axis/laser/python/remap.py"));
const bool ok =
contains(ini, "REMAP= M10 modalgroup=10 python=rasterStop") &&
contains(ini, "REMAP= M11 modalgroup=10 python=rasterBegin") &&
contains(ini, "REMAP= M12 modalgroup=10 python=rasterData") &&
contains(ini, "REMAP= M13 modalgroup=10 python=rasterStart") &&
contains(ini, "TOPLEVEL=python/toplevel.py") &&
contains(ini, "PATH_APPEND=python") &&
contains(remap, "def rasterBegin(self, **words):") &&
contains(remap, "def rasterData(self, **words):") &&
contains(remap, "def rasterStart(self, **words):") &&
contains(remap, "def rasterStop(self, **words):");
print_bool("python_family_axis_laser_inventory", ok);
return ok;
}
bool check_twp_nutating_family()
{
const std::string ini = read_text(source_config_path(
"axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini"));
const std::string remap = read_text(source_config_path(
"axis/vismach/5axis/table-rotary_spindle-rotary-nutating/python/remap.py"));
const bool ok =
contains(ini, "REMAP = G68.2") &&
contains(ini, "python=g682") &&
contains(ini, "REMAP = G68.3") &&
contains(ini, "python=g683") &&
contains(ini, "REMAP = G68.4") &&
contains(ini, "python=g684") &&
contains(ini, "python=g53x_core") &&
contains(ini, "python=g69_core") &&
contains(ini, "PATH_APPEND = ../python") &&
contains(ini, "TOPLEVEL = ../python/toplevel.py") &&
contains(remap, "def g53x_core(self):") &&
contains(remap, "def g69_core(self):") &&
contains(remap, "def g682(self, **words):") &&
contains(remap, "def g683(self, **words):") &&
contains(remap, "def g684(self, **words):");
print_bool("python_family_twp_nutating_inventory", ok);
return ok;
}
bool check_cycle_and_stdglue_families()
{
const std::string cycle_ini = read_text(source_config_path("axis/remap/cycle/cycle.ini"));
const std::string cycle_remap = read_text(source_config_path("axis/remap/cycle/python/remap.py"));
const std::string stdglue = read_text(source_config_path(
"axis/nc_files/remap_lib/python-stdglue/stdglue.py"));
const std::string gmoccapy_stdglue = read_text(source_config_path("gmoccapy/python/stdglue.py"));
const bool cycle_ok =
contains(cycle_ini, "python=g842") &&
contains(cycle_ini, "prolog=cycle_prolog") &&
contains(cycle_ini, "epilog=cycle_epilog") &&
contains(cycle_ini, "PATH_PREPEND=./python") &&
contains(cycle_ini, "PATH_APPEND=../../nc_files/remap_lib/python-stdglue") &&
contains(cycle_remap, "def g842(self,**words):") &&
contains(stdglue, "def cycle_prolog(self,**words):") &&
contains(stdglue, "def cycle_epilog(self,**words):");
const bool gmoccapy_ok =
contains(gmoccapy_stdglue, "def settool_prolog(self,**words):") &&
contains(gmoccapy_stdglue, "def settool_epilog(self,**words):") &&
contains(gmoccapy_stdglue, "def change_prolog(self, **words):") &&
contains(gmoccapy_stdglue, "def change_epilog(self, **words):");
print_bool("python_family_axis_remap_cycle_inventory", cycle_ok);
print_bool("python_family_gmoccapy_stdglue_inventory", gmoccapy_ok);
return cycle_ok && gmoccapy_ok;
}
} // namespace
int main()
{
bool ok = true;
ok &= check_python_runtime_owner();
ok &= check_axis_laser_family();
ok &= check_twp_nutating_family();
ok &= check_cycle_and_stdglue_families();
print_bool("python_remap_native_source_inventory_proof", ok);
print_bool("python_remap_execution_enabled", false);
print_bool("python_remap_promotion_allowed", false);
return ok ? 0 : 1;
}

View File

@@ -0,0 +1,177 @@
#include <fstream>
#include <iostream>
#include <iterator>
#include <string>
#include "emc/ini/inifile.hh"
#ifndef LINUXCNC_SOURCE_DIR
#error "LINUXCNC_SOURCE_DIR must point at the LinuxCNC source tree"
#endif
#ifndef LINUXCNC_SOURCE_CONFIG_DIR
#error "LINUXCNC_SOURCE_CONFIG_DIR must point at the LinuxCNC configs/sim tree"
#endif
namespace {
std::string source_path(const std::string &rel)
{
return std::string(LINUXCNC_SOURCE_DIR) + "/" + rel;
}
std::string source_config_path(const std::string &rel)
{
return std::string(LINUXCNC_SOURCE_CONFIG_DIR) + "/" + rel;
}
std::string read_text(const std::string &path)
{
std::ifstream input(path);
return std::string(
std::istreambuf_iterator<char>(input),
std::istreambuf_iterator<char>());
}
bool contains(const std::string &text, const std::string &needle)
{
return text.find(needle) != std::string::npos;
}
void print_bool(const std::string &name, bool value)
{
std::cout << name << "=" << (value ? 1 : 0) << "\n";
}
bool check_ini_value(
const linuxcnc::IniFile &ini,
const std::string &label,
const std::string &section,
const std::string &tag,
const std::string &expected)
{
const auto actual = ini.findString(tag, section);
std::cout << label << "_" << section << "." << tag << "="
<< (actual ? *actual : "<missing>") << "\n";
const bool ok = actual && *actual == expected;
print_bool(label + "_" + section + "." + tag + "_ok", ok);
return ok;
}
bool check_task_owner()
{
const std::string text = read_text(source_path("src/emc/task/taskclass.cc"));
const bool db_init =
contains(text, "findString(\"DB_PROGRAM\", \"EMCIO\")") &&
contains(text, "tooldata_set_db(db_mode)") &&
contains(text, "tooldata_db_init(db_program, random_toolchanger)");
const bool tbl_ignored = contains(text, "DB_PROGRAM active: IGNORING tool table file");
const bool load_unload_notify =
contains(text, "tooldata_db_notify(SPINDLE_UNLOAD") &&
contains(text, "tooldata_db_notify(SPINDLE_LOAD");
print_bool("tool_db_task_db_mode_init", db_init);
print_bool("tool_db_task_ignores_tool_table_with_db_program", tbl_ignored);
print_bool("tool_db_task_load_unload_notify", load_unload_notify);
return db_init && tbl_ignored && load_unload_notify;
}
bool check_tooldata_db_owner()
{
const std::string text = read_text(source_path("src/emc/tooldata/tooldata_db.cc"));
const bool child_process =
contains(text, "pipe2(pipes[PARENT_READ_PIPE]") &&
contains(text, "pipe2(pipes[PARENT_WRITE_PIPE]") &&
contains(text, "fork()") &&
contains(text, "execv(myargv[0], myargv)");
const bool executable_check =
contains(text, "access(child_argv[0],X_OK)") &&
contains(text, "not executable");
const bool version_handshake =
contains(text, "#define DB_VERSION \"v2.1\"") &&
contains(text, "read_reply(reply,sizeof(reply))") &&
contains(text, "strncmp(reply,DB_VERSION");
const bool getall =
contains(text, "send_request((char*)\"g\\n\")") &&
contains(text, "tooldata_reset()") &&
contains(text, "strstr(reply,\"FINI\")") &&
contains(text, "tooldata_read_entry(reply)");
const bool notify =
contains(text, "snprintf(msg,sizeof(msg),\"l %s\\n\",buffer)") &&
contains(text, "snprintf(msg,sizeof(msg),\"u %s\\n\",buffer)") &&
contains(text, "snprintf(msg,sizeof(msg),\"p %s\\n\",buffer)") &&
contains(text, "send_and_verify(msg)");
print_bool("tool_db_child_process_boundary", child_process);
print_bool("tool_db_program_executable_check", executable_check);
print_bool("tool_db_v2_1_handshake", version_handshake);
print_bool("tool_db_getall_g_until_fini", getall);
print_bool("tool_db_notify_l_u_p_protocol", notify);
return child_process && executable_check && version_handshake && getall && notify;
}
bool check_db_program_owner()
{
const std::string text = read_text(source_config_path("axis/db_demo/db.py"));
const bool imports =
contains(text, "from tooldb import tooldb_callbacks") &&
contains(text, "from tooldb import tooldb_tools") &&
contains(text, "from tooldb import tooldb_loop");
const bool callbacks =
contains(text, "def user_get_tool(tno):") &&
contains(text, "def user_put_tool(tno,params):") &&
contains(text, "def user_load_spindle_nonran_tc(tno,params):") &&
contains(text, "def user_unload_spindle_nonran_tc(tno,params):") &&
contains(text, "tooldb_callbacks(user_get_tool") &&
contains(text, "tooldb_tools(toollist)") &&
contains(text, "tooldb_loop()");
const bool state_targets =
contains(text, "db_nonran_savefile = \"/tmp/db_nonran_file\"") &&
contains(text, "toolno_min = 10") &&
contains(text, "toolno_max = 19") &&
contains(text, "pocket_offset = 100") &&
contains(text, "return tno+pocket_offset") &&
contains(text, "D['P'] = \"0\"") &&
contains(text, "nonran_restore_pocket(spindle_tool)") &&
contains(text, "linuxcnc.command().load_tool_table") &&
contains(text, "save_tools_to_file(db_savefile");
const bool reload_rules =
contains(text, "apply_db_rules()") &&
contains(text, "G10L0");
print_bool("tool_db_program_tooldb_module", imports);
print_bool("tool_db_program_nonran_callbacks", callbacks);
print_bool("tool_db_program_nonran_state_targets", state_targets);
print_bool("tool_db_program_reload_rules", reload_rules);
return imports && callbacks && state_targets && reload_rules;
}
} // namespace
int main()
{
const std::string ini_path = source_config_path("axis/db_demo/db_nonran.ini");
const std::string ini_text = read_text(ini_path);
linuxcnc::IniFile ini(ini_path);
const bool opened = static_cast<bool>(ini);
print_bool("db_nonran_ini_open", opened);
if (!opened) {
return 1;
}
bool ok = true;
ok &= check_ini_value(ini, "db_nonran", "EMCIO", "RANDOM_TOOLCHANGER", "0");
ok &= check_ini_value(ini, "db_nonran", "EMCIO", "DB_PROGRAM", "./db_nonran.py");
const bool tool_table_ignored = contains(ini_text, "TOOL_TABLE= is not used with DB_PROGRAM");
print_bool("db_nonran_tool_table_ignored_with_db_program", tool_table_ignored);
ok &= tool_table_ignored;
ok &= check_task_owner();
ok &= check_tooldata_db_owner();
ok &= check_db_program_owner();
print_bool("tool_db_native_source_protocol_proof", ok);
print_bool("tool_db_execution_enabled", false);
print_bool("tool_db_promotion_allowed", false);
return ok ? 0 : 1;
}

View File

@@ -23,6 +23,7 @@ Use `src/index.js` for stable imports:
```js
import {
analyzeIniRuntimeBoundaries,
createLinuxCncIniSdk,
createLinuxCncInterpSdk,
planIniFileContextStaging,
@@ -104,6 +105,20 @@ source manifest text, machine relative path, INI file name, and INI text. It
uses `planIniFileContextStaging()` with a `configs/sim/<machine>` source root
and `configs/sim` upward-search boundary.
`analyzeIniRuntimeBoundaries()` is a host-boundary classifier for LinuxCNC
INI-driven runs. It reads INI text plus optional execution text and manifest
text, then reports declared HAL, UI, HALUI MDI, Python, tool-database, and
external user-M process dependencies. User-M accounting is per execution code:
`executionCodes` lists `M100..M199` codes seen in the supplied execution text,
while `unstagedExecutionCodes` lists the subset not backed by vendored
`USER_M_PATH` files. Python accounting keeps UI/DB references separate from
Python remap runtime references, so a UI handler or DB program does not imply
Python-remap coverage. It also returns the currently recommended Layer 4
blocked kind for hard process boundaries such as `L4-TOOL-DB` and
`L4-USER-M-PROCESS`. The classifier is policy/accounting only: it does not
execute HAL, task, UI, Python, user-M, or tool-database behavior and does not
change interpreter semantics.
`runFileWithIniContinueOnError()` uses the same LinuxCNC-backed file execution
path but keeps the runner loop going after LinuxCNC reports an error, matching
upstream `rs274 -n 0` regression tests such as `tests/interp/oword-unwind`.

View File

@@ -1,6 +1,7 @@
export { createLinuxCncIniSdk } from "./linuxcnc-ini.js";
export { createLinuxCncInterpSdk } from "./linuxcnc-interp.js";
export {
analyzeIniRuntimeBoundaries,
planIniFileContextStaging,
planSimConfigStaging,
} from "./sim-config-staging.js";

View File

@@ -172,6 +172,197 @@ function remapNgcNames(iniValues) {
return names;
}
function sectionValues(iniValues, section) {
const prefix = `${section.toUpperCase()}.`;
const values = [];
for (const [key, entries] of iniValues.entries()) {
if (!key.startsWith(prefix)) {
continue;
}
for (const value of entries) {
values.push({ key: key.slice(prefix.length), value });
}
}
return values;
}
function valuesMatching(iniValues, section, keys) {
const wanted = new Set(keys.map((key) => key.toUpperCase()));
return sectionValues(iniValues, section)
.filter((entry) => wanted.has(entry.key))
.map((entry) => entry.value);
}
function looksLikePythonReference(value) {
return /(^|\s|=|:)["']?[^"'\s]*\.py(["'\s]|$)/i.test(value);
}
function userMCodesInText(text) {
const codes = new Set();
for (const match of text.matchAll(/(?<![A-Za-z0-9_])M\s*(1\d\d)(?![0-9])/gi)) {
codes.add(`M${match[1]}`);
}
return [...codes].sort();
}
function vendoredUserMCodes({
manifestEntries,
sourceDir,
normalizedSearchRoot,
userMPathValues,
}) {
const manifestSet = new Set(manifestEntries);
const executableCodes = new Set();
for (const dirEntry of userMPathValues.flatMap(splitSearchPath)) {
if (dirEntry.startsWith("/")) {
continue;
}
const sourceUserMDir = normalizeRel(`${sourceDir}/${dirEntry}`);
const prefix = sourceUserMDir ? `${sourceUserMDir}/` : "";
for (const sourceRel of manifestEntries) {
if (
sourceRel.startsWith(prefix) &&
!sourceRel.slice(prefix.length).includes("/") &&
isUserMCodePath(sourceRel)
) {
executableCodes.add(basename(sourceRel).toUpperCase());
}
}
}
for (const dirEntry of userMPathValues.flatMap(splitSearchPath)) {
if (dirEntry.startsWith("/")) {
continue;
}
const sourceUserMDir = normalizeRel(`${sourceDir}/${dirEntry}`);
for (let code = 100; code <= 199; code += 1) {
const candidate = findUpwardByBasename(
manifestSet,
sourceUserMDir,
`M${code}`,
normalizedSearchRoot,
);
if (candidate) {
executableCodes.add(`M${code}`);
}
}
}
return [...executableCodes].sort();
}
export function analyzeIniRuntimeBoundaries({
manifestText = "",
sourceRootRel,
sourceSearchRootRel = sourceRootRel,
iniFile = "",
iniText,
executionTexts = [],
}) {
const manifestEntries = cleanManifest(manifestText);
const iniValues = parseIni(iniText);
const normalizedSourceRoot = normalizeRel(sourceRootRel);
const normalizedSearchRoot = normalizeRel(sourceSearchRootRel);
const sourceDir = normalizeRel(`${normalizedSourceRoot}/${dirname(iniFile)}`);
const userMPathValues = allIniValues(iniValues, "RS274NGC", "USER_M_PATH");
const halValues = valuesMatching(iniValues, "HAL", [
"HALFILE",
"HALCMD",
"POSTGUI_HALFILE",
"HALUI",
]);
const displayValues = valuesMatching(iniValues, "DISPLAY", [
"DISPLAY",
"PYVCP",
"GLADEVCP",
"EMBED_TAB_COMMAND",
]);
const halUiMdiCommands = allIniValues(iniValues, "HALUI", "MDI_COMMAND");
const dbProgram = firstIniValue(iniValues, "EMCIO", "DB_PROGRAM");
const remapValues = allIniValues(iniValues, "RS274NGC", "REMAP");
const pythonRemapReferences = [
...sectionValues(iniValues, "PYTHON").map((entry) => entry.value),
...remapValues.filter((value) => /(?:^|\s)python=/i.test(value)),
];
const pythonUiReferences = [
...displayValues.filter(looksLikePythonReference),
...(dbProgram && looksLikePythonReference(dbProgram) ? [dbProgram] : []),
];
const pythonReferences = [...pythonRemapReferences, ...pythonUiReferences];
const vendoredUserMCodeList = vendoredUserMCodes({
manifestEntries,
sourceDir,
normalizedSearchRoot,
userMPathValues,
});
const vendoredUserMCodeSet = new Set(vendoredUserMCodeList);
const hasUserMPath = userMPathValues.length > 0;
const executionUserMCodes = [...new Set(executionTexts.flatMap(userMCodesInText))].sort();
const unstagedExecutionUserMCodes = executionUserMCodes
.filter((code) => !vendoredUserMCodeSet.has(code));
const hasExternalUserMUse = hasUserMPath && unstagedExecutionUserMCodes.length > 0;
const dependencies = [];
if (dbProgram) {
dependencies.push("tool_database_process");
}
if (halValues.length > 0) {
dependencies.push("hal_process");
}
if (displayValues.some((value) => !/^axis$/i.test(value))) {
dependencies.push("ui_process");
}
if (halUiMdiCommands.length > 0) {
dependencies.push("halui_mdi_process");
}
if (pythonReferences.length > 0) {
dependencies.push("python_runtime");
}
if (hasExternalUserMUse) {
dependencies.push("external_user_m_process");
}
let recommendedBlockedKind = "-";
if (dbProgram) {
recommendedBlockedKind = "L4-TOOL-DB";
} else if (hasExternalUserMUse) {
recommendedBlockedKind = "L4-USER-M-PROCESS";
} else if (pythonRemapReferences.length > 0) {
recommendedBlockedKind = "L4-PYTHON-REMAP";
}
return {
dependencies: [...new Set(dependencies)].sort(),
recommendedBlockedKind,
toolDatabaseProgram: dbProgram ?? "",
halRuntime: {
values: halValues,
requiresProcess: halValues.length > 0,
},
uiRuntime: {
values: displayValues,
requiresProcess: displayValues.some((value) => !/^axis$/i.test(value)),
},
haluiRuntime: {
mdiCommands: halUiMdiCommands,
requiresProcess: halUiMdiCommands.length > 0,
},
userMRuntime: {
paths: userMPathValues,
vendoredExecutableCount: vendoredUserMCodeList.length,
executionCodes: executionUserMCodes,
unstagedExecutionCodes: unstagedExecutionUserMCodes,
requiresExternalProcess: hasExternalUserMUse,
},
pythonRuntime: {
references: pythonReferences,
remapReferences: pythonRemapReferences,
uiReferences: pythonUiReferences,
requiresProcess: pythonReferences.length > 0,
},
};
}
export function planSimConfigStaging({
manifestText,
machineRel,