Initial LinuxCNC WebAssembly workspace snapshot

This commit is contained in:
2026-08-20 18:21:15 +08:00
commit 5dd614c335
92 changed files with 200203 additions and 0 deletions

46
python-runtime/compat.mjs Normal file
View File

@@ -0,0 +1,46 @@
import { createHash } from "node:crypto";
import manifest from "./manifest.json" with { type: "json" };
const allowedModules = new Set(Object.keys(manifest.virtual_modules));
const denied = new Set(manifest.denied_capabilities);
export function manifestHash(value = manifest) {
return `sha256:${createHash("sha256").update(JSON.stringify(value, Object.keys(value).sort())).digest("hex")}`;
}
export function runtimeCapabilities() {
return {
runtime: manifest.runtime,
version: manifest.version,
python_version: manifest.python_version,
abi_version: manifest.abi_version,
platform: manifest.platform,
worker_only: manifest.worker_only,
offline_required: manifest.offline_required,
manifest_sha256: manifestHash(),
virtual_modules: structuredClone(manifest.virtual_modules),
denied_capabilities: [...manifest.denied_capabilities],
};
}
export function validateImport(moduleName) {
if (typeof moduleName !== "string" || moduleName.length === 0) return { status: "blocked", code: "python_import_invalid", message_key: "python.import_invalid" };
if (allowedModules.has(moduleName)) return { status: "accepted", module: moduleName, symbols: [...manifest.virtual_modules[moduleName]] };
return { status: "blocked", code: "python_unsupported", message_key: "python.unsupported_module", module: moduleName, retryable: false };
}
export function validateCapability(capability) {
if (denied.has(capability)) return { status: "REAL-EXCLUDED", code: "real_hardware_excluded", message_key: "python.host_capability_denied", capability, retryable: false };
if (typeof capability !== "string" || capability.length === 0) return { status: "blocked", code: "python_capability_invalid", message_key: "python.capability_invalid" };
return { status: "accepted", capability };
}
export function createVirtualEnvironment() {
return Object.freeze({
importModule: validateImport,
checkCapability: validateCapability,
capabilities: runtimeCapabilities(),
});
}
export { manifest };

View File

@@ -0,0 +1,51 @@
{
"schema": "python-runtime-manifest.v1",
"runtime": "Pyodide",
"version": "0.29.4",
"python_version": "3.13.2",
"abi_version": "2025_0",
"platform": "emscripten_4_0_9",
"index_url": "https://cdn.jsdelivr.net/pyodide/v0.29.4/full/",
"origin_release": "https://github.com/pyodide/pyodide/releases/tag/0.29.4",
"worker_only": true,
"offline_required": true,
"package_policy": {
"mode": "allowlist",
"allowed_packages": [],
"reason": "LinuxCNC compatibility modules are virtual and core Python packages are not accepted until individually mapped and hashed."
},
"virtual_modules": {
"linuxcnc": ["command", "stat", "error", "ini"],
"hal": ["component", "pin", "param", "signal", "thread"],
"gstat": ["state", "task", "motion"],
"INI": ["find", "findall", "sections"]
},
"denied_capabilities": [
"os.system", "subprocess", "socket", "select", "multiprocessing", "threading",
"ctypes", "fcntl", "pty", "termios", "device_paths", "arbitrary_host_paths"
],
"artifacts": [
{
"name": "pyodide-lock.json",
"version": "0.29.4",
"source_url": "https://cdn.jsdelivr.net/pyodide/v0.29.4/full/pyodide-lock.json",
"origin_url": "https://github.com/pyodide/pyodide/releases/tag/0.29.4",
"sha256": "14d2c2dba101277999e17135e653d8f15389ad1437f53eae213bf0c3cdff723d",
"bytes": 122027,
"license": "MPL-2.0",
"mirror_kind": "official",
"role": "package_lock"
},
{
"name": "pyodide.js",
"version": "0.29.4",
"source_url": "https://cdn.jsdelivr.net/pyodide/v0.29.4/full/pyodide.js",
"origin_url": "https://github.com/pyodide/pyodide/releases/tag/0.29.4",
"sha256": "7be9e63eddf6bd8786a5ab71c8689c35731d187e3415fe58d7cb95423b50421a",
"bytes": 18597,
"license": "MPL-2.0",
"mirror_kind": "official",
"role": "loader_metadata"
}
]
}

View File

@@ -0,0 +1,41 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { createVirtualEnvironment, manifest, manifestHash, runtimeCapabilities, validateCapability, validateImport } from "./compat.mjs";
assert.equal(manifest.schema, "python-runtime-manifest.v1");
assert.equal(manifest.runtime, "Pyodide");
assert.equal(manifest.version, "0.29.4");
assert.equal(manifest.python_version, "3.13.2");
assert.equal(manifest.abi_version, "2025_0");
assert.equal(manifest.platform, "emscripten_4_0_9");
assert.equal(manifest.artifacts.length, 2);
assert.equal(new Set(manifest.artifacts.map((artifact) => `${artifact.name}@${artifact.version}`)).size, 2);
for (const artifact of manifest.artifacts) {
assert.match(artifact.sha256, /^[0-9a-f]{64}$/);
assert.ok(artifact.bytes > 0);
assert.equal(artifact.mirror_kind, "official");
}
assert.equal(validateImport("linuxcnc").status, "accepted");
assert.equal(validateImport("os").code, "python_unsupported");
assert.equal(validateImport("").code, "python_import_invalid");
assert.equal(validateCapability("subprocess").status, "REAL-EXCLUDED");
assert.equal(validateCapability("arbitrary_host_paths").status, "REAL-EXCLUDED");
assert.equal(validateCapability("math").status, "accepted");
const environment = createVirtualEnvironment();
assert.equal(environment.capabilities.manifest_sha256, manifestHash());
assert.equal(environment.importModule("hal").symbols.includes("component"), true);
assert.equal(runtimeCapabilities().denied_capabilities.includes("socket"), true);
const report = {
schema: "python-runtime-report.v1",
status: "passed",
runtime: manifest.runtime,
version: manifest.version,
manifest_sha256: manifestHash(),
artifacts: manifest.artifacts.map(({ name, version, bytes, sha256 }) => ({ name, version, bytes, sha256 })),
virtual_modules: Object.keys(manifest.virtual_modules).length,
denied_capabilities: manifest.denied_capabilities.length,
};
await writeFile(new URL("../coverage/python-runtime.report.json", import.meta.url), `${JSON.stringify(report, null, 2)}\n`);
console.log(`python_runtime=passed version=${manifest.version} modules=${report.virtual_modules}`);