Expand WASM sim-config staging coverage

This commit is contained in:
2026-06-09 07:32:15 +08:00
parent 2658ee3b72
commit 5116b9894d
26 changed files with 5989 additions and 141 deletions

View File

@@ -1,2 +1,3 @@
export { createLinuxCncIniSdk } from "./linuxcnc-ini.js";
export { createLinuxCncInterpSdk } from "./linuxcnc-interp.js";
export { planSimConfigStaging } from "./sim-config-staging.js";

View File

@@ -0,0 +1,252 @@
function cleanManifest(manifestText) {
return manifestText
.split("\n")
.map((line) => line.trim())
.filter((line) => line && !line.startsWith("#"));
}
function normalizeRel(path) {
const parts = [];
for (const part of path.split("/")) {
if (!part || part === ".") {
continue;
}
if (part === "..") {
parts.pop();
continue;
}
parts.push(part);
}
return parts.join("/");
}
function dirname(path) {
const clean = normalizeRel(path);
const index = clean.lastIndexOf("/");
return index === -1 ? "" : clean.slice(0, index);
}
function basename(path) {
const clean = normalizeRel(path);
const index = clean.lastIndexOf("/");
return index === -1 ? clean : clean.slice(index + 1);
}
function stripIniComment(line) {
const hash = line.indexOf("#");
const semicolon = line.indexOf(";");
const indexes = [hash, semicolon].filter((index) => index >= 0);
if (indexes.length === 0) {
return line;
}
return line.slice(0, Math.min(...indexes));
}
function parseIni(iniText) {
const values = new Map();
let section = "";
for (const rawLine of iniText.split("\n")) {
const sectionMatch = rawLine.match(/^\s*\[([^\]]+)\]/);
if (sectionMatch) {
section = sectionMatch[1].trim().toUpperCase();
continue;
}
const line = stripIniComment(rawLine);
const equals = line.indexOf("=");
if (equals === -1 || !section) {
continue;
}
const key = line.slice(0, equals).trim().toUpperCase();
const value = line.slice(equals + 1).trim();
if (!key || !value) {
continue;
}
const mapKey = `${section}.${key}`;
const existing = values.get(mapKey) ?? [];
existing.push(value);
values.set(mapKey, existing);
}
return values;
}
function firstIniValue(values, section, key) {
return values.get(`${section.toUpperCase()}.${key.toUpperCase()}`)?.[0] ?? null;
}
function allIniValues(values, section, key) {
return values.get(`${section.toUpperCase()}.${key.toUpperCase()}`) ?? [];
}
function splitSearchPath(value) {
return value.split(":").map((entry) => entry.trim()).filter(Boolean);
}
function sourceRelFor(machineRel, path) {
return normalizeRel(`configs/sim/${machineRel}/${path}`);
}
function targetPathFor(wasmDir, targetRel) {
return `${wasmDir}/${normalizeRel(targetRel)}`;
}
function isUserMCodePath(path) {
return /^M1\d\d$/i.test(basename(path));
}
function isSubroutinePath(path) {
return path.toLowerCase().endsWith(".ngc");
}
function findUpwardByBasename(manifestSet, sourceDir, fileName) {
let current = sourceDir;
while (current.startsWith("configs/sim")) {
const candidate = normalizeRel(`${current}/${fileName}`);
if (manifestSet.has(candidate)) {
return candidate;
}
if (current === "configs/sim") {
break;
}
current = dirname(current);
}
return null;
}
function sourceForReference(manifestSet, sourceDir, reference) {
if (!reference || reference.startsWith("/")) {
return null;
}
const sourceRel = normalizeRel(`${sourceDir}/${reference}`);
if (manifestSet.has(sourceRel)) {
return sourceRel;
}
return findUpwardByBasename(manifestSet, sourceDir, basename(reference));
}
function addPlannedFile(plan, manifestSet, sourceRel, targetRel, options = {}) {
if (!sourceRel || !manifestSet.has(sourceRel)) {
if (options.required) {
throw new Error(`missing vendored sim-config file: ${sourceRel}`);
}
return;
}
const existing = plan.get(sourceRel);
const executable = options.executable || isUserMCodePath(sourceRel);
if (existing) {
existing.executable ||= executable;
return;
}
plan.set(sourceRel, {
sourceRel,
path: targetPathFor(options.wasmDir, targetRel),
executable,
});
}
function addDirectoryFiles(plan, manifestEntries, sourceDir, targetDir, predicate, options) {
const prefix = sourceDir ? `${sourceDir}/` : "";
for (const sourceRel of manifestEntries) {
if (!sourceRel.startsWith(prefix)) {
continue;
}
const childRel = sourceRel.slice(prefix.length);
if (childRel.includes("/") || !predicate(sourceRel)) {
continue;
}
addPlannedFile(plan, new Set(manifestEntries), sourceRel, normalizeRel(`${targetDir}/${childRel}`), options);
}
}
function remapNgcNames(iniValues) {
const names = [];
for (const remap of allIniValues(iniValues, "RS274NGC", "REMAP")) {
const match = remap.match(/(?:^|\s)ngc=([^\s]+)/i);
if (match) {
names.push(`${match[1]}.ngc`);
}
}
return names;
}
export function planSimConfigStaging({
manifestText,
machineRel,
iniFile,
iniText,
programFile = null,
wasmDir = `/work/sim/${machineRel}`,
}) {
const manifestEntries = cleanManifest(manifestText);
const manifestSet = new Set(manifestEntries);
const iniValues = parseIni(iniText);
const plan = new Map();
const sourceDir = sourceRelFor(machineRel, dirname(iniFile));
const iniSourceRel = sourceRelFor(machineRel, iniFile);
const programReference = programFile ?? firstIniValue(iniValues, "DISPLAY", "OPEN_FILE");
addPlannedFile(plan, manifestSet, iniSourceRel, iniFile, { wasmDir, required: true });
if (programReference) {
const programSourceRel = sourceForReference(manifestSet, sourceDir, programReference);
addPlannedFile(plan, manifestSet, programSourceRel, programReference, {
wasmDir,
required: true,
});
}
for (const [section, key] of [
["EMCIO", "TOOL_TABLE"],
["RS274NGC", "PARAMETER_FILE"],
]) {
const reference = firstIniValue(iniValues, section, key);
if (!reference) {
continue;
}
const sourceRel = sourceForReference(manifestSet, sourceDir, reference);
addPlannedFile(plan, manifestSet, sourceRel, reference, { wasmDir });
}
const subroutineDirs = splitSearchPath(
firstIniValue(iniValues, "RS274NGC", "SUBROUTINE_PATH") ?? "",
);
for (const dirEntry of subroutineDirs) {
if (dirEntry.startsWith("/")) {
continue;
}
const sourceSubdir = normalizeRel(`${sourceDir}/${dirEntry}`);
const targetSubdir = normalizeRel(dirEntry);
addDirectoryFiles(plan, manifestEntries, sourceSubdir, targetSubdir, isSubroutinePath, {
wasmDir,
});
}
for (const dirEntry of splitSearchPath(firstIniValue(iniValues, "RS274NGC", "USER_M_PATH") ?? "")) {
if (dirEntry.startsWith("/")) {
continue;
}
const sourceUserMDir = normalizeRel(`${sourceDir}/${dirEntry}`);
const targetUserMDir = normalizeRel(dirEntry);
addDirectoryFiles(plan, manifestEntries, sourceUserMDir, targetUserMDir, isUserMCodePath, {
wasmDir,
executable: true,
});
}
for (const remapName of remapNgcNames(iniValues)) {
for (const dirEntry of subroutineDirs) {
const sourceRel = sourceForReference(manifestSet, normalizeRel(`${sourceDir}/${dirEntry}`), remapName);
addPlannedFile(plan, manifestSet, sourceRel, normalizeRel(`${dirEntry}/${remapName}`), {
wasmDir,
});
}
}
return {
wasmDir,
iniPath: targetPathFor(wasmDir, iniFile),
programPath: programReference ? targetPathFor(wasmDir, programReference) : null,
files: [...plan.values()],
};
}