Initial LinuxCNC WebAssembly workspace snapshot
This commit is contained in:
46
python-runtime/compat.mjs
Normal file
46
python-runtime/compat.mjs
Normal 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 };
|
||||
51
python-runtime/manifest.json
Normal file
51
python-runtime/manifest.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
41
python-runtime/test_runtime.mjs
Normal file
41
python-runtime/test_runtime.mjs
Normal 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}`);
|
||||
Reference in New Issue
Block a user