147 lines
5.1 KiB
Python
147 lines
5.1 KiB
Python
"""Chip compilation service — turns user-supplied C source into a WASM binary
|
|
using clang from the WASI-SDK toolchain. Mirrors the patterns used by
|
|
`arduino_cli.py` (subprocess in a thread, tempdir, structured result dict).
|
|
|
|
Toolchain layout:
|
|
- WASI-SDK installed in the Docker image at /opt/wasi-sdk (Apache-2.0).
|
|
- velxio-chip.h shipped at /app/sdk/velxio-chip.h (or backend/sdk/ in dev).
|
|
|
|
The compile command mirrors the sandbox script verbatim:
|
|
clang --target=wasm32-unknown-wasip1 -O2 -nostartfiles
|
|
-Wl,--import-memory -Wl,--export-table -Wl,--no-entry
|
|
-Wl,--export=chip_setup -Wl,--allow-undefined
|
|
-I <sdk> chip.c -o chip.wasm
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import base64
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
|
|
def _resolve_wasi_sdk() -> Path | None:
|
|
"""Find a wasi-sdk installation. Honours WASI_SDK env var first."""
|
|
candidates: list[Path] = []
|
|
env = os.environ.get("WASI_SDK")
|
|
if env:
|
|
candidates.append(Path(env))
|
|
# Linux/macOS conventional installs (Docker image lands here)
|
|
candidates.extend([
|
|
Path("/opt/wasi-sdk"),
|
|
Path("/usr/local/wasi-sdk"),
|
|
Path.home() / "wasi-sdk",
|
|
])
|
|
# Windows dev convenience (not used in production Docker)
|
|
candidates.extend([Path("C:/wasi-sdk"), Path("C:/Program Files/wasi-sdk")])
|
|
|
|
for c in candidates:
|
|
clang = c / "bin" / ("clang.exe" if os.name == "nt" else "clang")
|
|
if clang.is_file():
|
|
return c
|
|
return None
|
|
|
|
|
|
def _resolve_sdk_include() -> Path | None:
|
|
"""Locate the directory containing velxio-chip.h."""
|
|
here = Path(__file__).resolve()
|
|
# Production: backend/sdk/
|
|
backend_sdk = here.parents[2] / "sdk"
|
|
if (backend_sdk / "velxio-chip.h").is_file():
|
|
return backend_sdk
|
|
# In Docker the WORKDIR is /app, so /app/sdk:
|
|
docker_sdk = Path("/app/sdk")
|
|
if (docker_sdk / "velxio-chip.h").is_file():
|
|
return docker_sdk
|
|
# Dev fallback: the sandbox copy
|
|
sandbox_sdk = here.parents[3] / "test" / "test_custom_chips" / "sdk" / "include"
|
|
if (sandbox_sdk / "velxio-chip.h").is_file():
|
|
return sandbox_sdk
|
|
return None
|
|
|
|
|
|
class ChipCompileService:
|
|
"""Compiles a single Velxio custom-chip C source file to a WASM blob."""
|
|
|
|
def __init__(self) -> None:
|
|
self.wasi_sdk = _resolve_wasi_sdk()
|
|
self.sdk_include = _resolve_sdk_include()
|
|
self.available = self.wasi_sdk is not None and self.sdk_include is not None
|
|
|
|
def status(self) -> dict:
|
|
return {
|
|
"available": self.available,
|
|
"wasi_sdk": str(self.wasi_sdk) if self.wasi_sdk else None,
|
|
"sdk_include": str(self.sdk_include) if self.sdk_include else None,
|
|
}
|
|
|
|
async def compile(self, source: str) -> dict:
|
|
"""Compile a single .c source into a .wasm. Returns:
|
|
{ success, wasm_base64, stdout, stderr, error, byte_size }
|
|
"""
|
|
if not self.available or self.wasi_sdk is None or self.sdk_include is None:
|
|
return {
|
|
"success": False,
|
|
"wasm_base64": None,
|
|
"stdout": "",
|
|
"stderr": "",
|
|
"error": (
|
|
"wasi-sdk not installed on this host. The backend Docker image "
|
|
"needs WASI-SDK 22+ at /opt/wasi-sdk."
|
|
),
|
|
"byte_size": 0,
|
|
}
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
tmp = Path(temp_dir)
|
|
(tmp / "chip.c").write_text(source, encoding="utf-8")
|
|
wasm_path = tmp / "chip.wasm"
|
|
|
|
clang = self.wasi_sdk / "bin" / ("clang.exe" if os.name == "nt" else "clang")
|
|
cmd = [
|
|
str(clang),
|
|
"--target=wasm32-unknown-wasip1",
|
|
"-O2",
|
|
"-nostartfiles",
|
|
"-Wl,--import-memory",
|
|
"-Wl,--export-table",
|
|
"-Wl,--no-entry",
|
|
"-Wl,--export=chip_setup",
|
|
"-Wl,--allow-undefined",
|
|
"-I",
|
|
str(self.sdk_include),
|
|
str(tmp / "chip.c"),
|
|
"-o",
|
|
str(wasm_path),
|
|
]
|
|
|
|
def _run() -> subprocess.CompletedProcess:
|
|
return subprocess.run(cmd, capture_output=True, text=True, cwd=str(tmp))
|
|
|
|
result = await asyncio.to_thread(_run)
|
|
|
|
if result.returncode != 0 or not wasm_path.is_file():
|
|
return {
|
|
"success": False,
|
|
"wasm_base64": None,
|
|
"stdout": result.stdout,
|
|
"stderr": result.stderr,
|
|
"error": "clang exited with a non-zero status",
|
|
"byte_size": 0,
|
|
}
|
|
|
|
wasm_bytes = wasm_path.read_bytes()
|
|
return {
|
|
"success": True,
|
|
"wasm_base64": base64.b64encode(wasm_bytes).decode("ascii"),
|
|
"stdout": result.stdout,
|
|
"stderr": result.stderr,
|
|
"error": None,
|
|
"byte_size": len(wasm_bytes),
|
|
}
|
|
|
|
|
|
chip_compile_service = ChipCompileService()
|