This commit is contained in:
udo
2026-06-13 02:07:38 +00:00
parent eb8f303f94
commit 577aae076e
27 changed files with 2937 additions and 21 deletions
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
# Build the production W1 UCE WASM core from the real runtime carve-out.
# Run on k-uce from any working directory.
set -euo pipefail
cd "$(dirname "$0")/.."
SDK=${WASI_SDK:-/opt/wasi-sdk}
OUT=${UCE_WASM_OUT:-/tmp/uce/wasm-w1}
mkdir -p "$OUT" bin/wasm
if [ ! -x "$SDK/bin/clang++" ]; then
echo "wasi-sdk clang++ not found; set WASI_SDK" >&2
exit 1
fi
"$SDK/bin/clang++" --target=wasm32-wasip1 -mexec-model=reactor \
-O1 -g -std=c++20 -fno-exceptions -fno-rtti \
-D__UCE_WASM_CORE__ \
-I. -Isrc/lib \
src/wasm/core.cpp -o "$OUT/core.wasm" \
-Wl,--export-all \
-Wl,--export=__heap_base \
-Wl,--export=__stack_pointer \
-Wl,--import-table \
-Wl,--allow-undefined-file=src/wasm/core_hostcalls.syms \
-Wl,--no-entry \
$(sed "s/^/-Wl,--export-if-defined=/" src/wasm/core_libc_exports.syms | tr "\n" " ")
cp "$OUT/core.wasm" bin/wasm/core.wasm
ls -lh "$OUT/core.wasm" bin/wasm/core.wasm
+1 -1
View File
@@ -26,7 +26,7 @@ OPT_FLAG="O0"
COMPILER="clang++"
#COMPILER="g++"
FLAGS="-shared -g -rdynamic -w -Wall -$OPT_FLAG -std=c++20 -fpermissive -ffast-math -fPIC"
FLAGS="-shared -g -rdynamic -w -Wall -$OPT_FLAG -std=c++20 -fpermissive -ffast-math -fPIC -Isrc/lib"
LIBS="-ldl -lm -lpthread"
SRCFLAGS="-D PLATFORM_NAME=\"linux\""
+93
View File
@@ -0,0 +1,93 @@
#!/bin/bash
# Compile a preprocessed UCE unit into a PIC WebAssembly side module.
set -euo pipefail
cd "$(dirname "$0")"
cd ..
SRC_DIR="$1"
DEST_DIR="$2"
SRC_FN="$3"
PP_FN="$4"
WASM_FN="$5"
SDK=${WASI_SDK:-/opt/wasi-sdk}
ABI_VERSION=${UCE_UNIT_ABI_VERSION:-6}
ROOT=$(pwd)
OBJ_FN="$DEST_DIR/$PP_FN.wasm.o"
ABI_TMP="$DEST_DIR/$PP_FN.uce-abi.txt"
PCH_ENABLED=${UCE_WASM_UNIT_PCH:-1}
PCH_DIR=${UCE_WASM_PCH_DIR:-/tmp/uce/wasm-w2/pch}
COMMON_FLAGS=(
--target=wasm32-wasip1
-fPIC -fvisibility=default -fvisibility-inlines-hidden
-O1 -g -std=c++20
# must match the core build ABI: units with RTTI/EH enabled import
# typeinfo/unwind symbols the -fno-rtti/-fno-exceptions core cannot provide
-fno-exceptions -fno-rtti
-D__UCE_WASM_UNIT__
-DPLATFORM_NAME=\"wasm32-wasip1\"
)
if [ ! -x "$SDK/bin/clang++" ] || [ ! -x "$SDK/bin/wasm-ld" ] || [ ! -x "$SDK/bin/llvm-objcopy" ]; then
echo "wasi-sdk tools not found; set WASI_SDK" >&2
exit 1
fi
TOOLCHAIN_ID=$(${SDK}/bin/clang++ --version | head -n 1)
HEADER_HASH=$(find src/lib -maxdepth 1 -name '*.h' -type f -print0 | sort -z | xargs -0 sha1sum | sha1sum | cut -c1-16)
FLAGS_HASH=$(printf '%s\0' "${COMMON_FLAGS[@]}" -Isrc/lib | sha1sum | cut -c1-16)
PCH_KEY=$(printf '%s\n%s\n%s\n%s\n' "$ABI_VERSION" "$TOOLCHAIN_ID" "$HEADER_HASH" "$FLAGS_HASH" | sha1sum | cut -c1-16)
PCH_FN="$PCH_DIR/uce_lib-wasm-unit-$PCH_KEY.pch"
mkdir -p "$DEST_DIR" >/dev/null 2>&1
build_pch_if_needed() {
if [ "$PCH_ENABLED" = "0" ]; then
return 0
fi
mkdir -p "$PCH_DIR"
if [ -s "$PCH_FN" ]; then
return 0
fi
"$SDK/bin/clang++" "${COMMON_FLAGS[@]}" \
-Isrc/lib \
-x c++-header src/lib/uce_lib.h -o "$PCH_FN.tmp"
mv "$PCH_FN.tmp" "$PCH_FN"
}
cat > "$ABI_TMP" <<EOF
format=uce-wasm-unit-abi-v1
unit_abi_version=$ABI_VERSION
toolchain=$TOOLCHAIN_ID
source=$SRC_FN
EOF
build_pch_if_needed
PCH_FLAGS=()
if [ "$PCH_ENABLED" != "0" ]; then
PCH_FLAGS=(-include-pch "$PCH_FN")
fi
"$SDK/bin/clang++" "${COMMON_FLAGS[@]}" \
-I"$SRC_DIR" -I"$ROOT/src/lib" \
"${PCH_FLAGS[@]}" \
-c "$DEST_DIR/$PP_FN" -o "$OBJ_FN"
"$SDK/bin/wasm-ld" -shared --experimental-pic \
--unresolved-symbols=import-dynamic \
--Bsymbolic \
"$OBJ_FN" -o "$DEST_DIR/$WASM_FN" \
--export-if-defined=__uce_set_current_request \
--export-if-defined=__uce_render \
--export-if-defined=__uce_component \
--export-if-defined=__uce_websocket \
--export-if-defined=__uce_cli \
--export-if-defined=__uce_serve_http \
--export-if-defined=__uce_once \
--export-if-defined=__uce_init
"$SDK/bin/llvm-objcopy" --add-section=uce.abi="$ABI_TMP" "$DEST_DIR/$WASM_FN"
python3 scripts/wasm/check_unit_wasm.py "$DEST_DIR/$WASM_FN" --abi-version "$ABI_VERSION" --llvm-nm "$SDK/bin/llvm-nm"
rm -f "$OBJ_FN" "$ABI_TMP"
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash
# Build the W1 host smoke driver. Run on k-uce.
set -euo pipefail
cd "$(dirname "$0")/../.."
OUT=${UCE_WASM_OUT:-/tmp/uce/wasm-w1}
WASMTIME_HOME=${WASMTIME_HOME:-/opt/wasmtime}
WASMTIME_INCLUDE=${WASMTIME_INCLUDE:-$WASMTIME_HOME/include}
WASMTIME_LIB=${WASMTIME_LIB:-$WASMTIME_HOME/lib}
mkdir -p "$OUT"
if [ ! -d "$WASMTIME_INCLUDE" ] || [ ! -d "$WASMTIME_LIB" ]; then
echo "Wasmtime C API not found; set WASMTIME_HOME or WASMTIME_INCLUDE/WASMTIME_LIB" >&2
exit 1
fi
g++ -std=c++17 -O2 -Wall -Wextra \
-I"$WASMTIME_INCLUDE" \
src/wasm/w1_smoke.cpp \
-L"$WASMTIME_LIB" \
-Wl,-rpath,"$WASMTIME_LIB" \
-lwasmtime \
-o "$OUT/w1_smoke"
ls -lh "$OUT/w1_smoke"
+62
View File
@@ -0,0 +1,62 @@
#!/bin/bash
# Batch-build W2 wasm side modules for already-known/generated UCE units.
set -euo pipefail
cd "$(dirname "$0")/../.."
BIN_DIR=${UCE_BIN_DIRECTORY:-/tmp/uce/work}
KNOWN_FILE=${UCE_KNOWN_UNITS_FILE:-$BIN_DIR/known-uce-files.txt}
MIN_UNITS=${UCE_W2_MIN_UNITS:-1}
if [ "$#" -gt 0 ]; then
UNITS=("$@")
else
if [ ! -f "$KNOWN_FILE" ]; then
echo "known unit registry not found: $KNOWN_FILE" >&2
exit 1
fi
mapfile -t UNITS < <(grep -v '^[[:space:]]*$' "$KNOWN_FILE")
fi
count=0
checked=0
skipped=0
# Native-only units that cannot be wasm side modules (yet).
# - error-reporting.uce deliberately throws to exercise the native exception
# path; the wasm backend replaces that machinery with traps (§11.1).
# - tests/zip.uce uses try/catch around the zip library, which is carved out
# of the wasm core until it moves behind a hostcall (W4+ membrane work).
SKIP_PATTERN=${UCE_W2_SKIP:-(error-reporting|tests/zip)\.uce$}
for unit in "${UNITS[@]}"; do
case "$unit" in
*.uce|*.ws.uce) ;;
*) continue ;;
esac
if [[ "$unit" =~ $SKIP_PATTERN ]]; then
continue
fi
src_dir=$(dirname "$unit")
base=$(basename "$unit")
dest_dir="$BIN_DIR$src_dir"
pp_fn="$base.cpp"
wasm_fn="$base.wasm"
if [ ! -f "$dest_dir/$pp_fn" ]; then
echo "missing preprocessed unit: $dest_dir/$pp_fn" >&2
exit 1
fi
if [ -s "$dest_dir/$wasm_fn" ] && [ "$dest_dir/$wasm_fn" -nt "$dest_dir/$pp_fn" ] && [ "$dest_dir/$wasm_fn" -nt "$unit" ]; then
scripts/wasm/check_unit_wasm.py "$dest_dir/$wasm_fn"
skipped=$((skipped + 1))
else
scripts/compile_wasm_unit "$src_dir" "$dest_dir" "$unit" "$pp_fn" "$wasm_fn"
count=$((count + 1))
fi
checked=$((checked + 1))
done
if [ "$checked" -lt "$MIN_UNITS" ]; then
echo "checked only $checked wasm units, expected at least $MIN_UNITS" >&2
exit 1
fi
echo "W2 batch wasm units: checked=$checked compiled=$count reused=$skipped"
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
# Build the W3 workspace-runtime CLI driver. Run on k-uce.
set -euo pipefail
cd "$(dirname "$0")/../.."
OUT=${UCE_WASM_OUT:-/tmp/uce/wasm-w3}
WASMTIME_HOME=${WASMTIME_HOME:-/opt/wasmtime}
mkdir -p "$OUT"
g++ -std=c++20 -O1 -g -w \
-Isrc/lib -Isrc/wasm \
-I"$WASMTIME_HOME/include" \
src/wasm/w3_driver.cpp \
-L"$WASMTIME_HOME/lib" \
-Wl,-rpath,"$WASMTIME_HOME/lib" \
-lwasmtime -lpcre2-8 -lpthread -ldl \
-o "$OUT/w3_driver"
ls -lh "$OUT/w3_driver"
+227
View File
@@ -0,0 +1,227 @@
#!/usr/bin/env python3
"""Validate a W2 UCE PIC unit wasm artifact.
Checks intentionally stay small and explicit: section walk for dylink.0,
uce.abi, imports/exports, plus llvm-nm for allocator definitions.
"""
from __future__ import annotations
import argparse
import shutil
import subprocess
import sys
from pathlib import Path
def read_u32leb(data: bytes, pos: int) -> tuple[int, int]:
result = 0
shift = 0
while True:
if pos >= len(data):
raise ValueError("truncated leb128")
b = data[pos]
pos += 1
result |= (b & 0x7F) << shift
if (b & 0x80) == 0:
return result, pos
shift += 7
if shift > 35:
raise ValueError("oversized leb128")
def read_name(data: bytes, pos: int) -> tuple[str, int]:
n, pos = read_u32leb(data, pos)
end = pos + n
if end > len(data):
raise ValueError("truncated name")
return data[pos:end].decode("utf-8", "replace"), end
def walk_sections(data: bytes):
if not data.startswith(b"\0asm\x01\0\0\0"):
raise ValueError("not a wasm v1 module")
pos = 8
while pos < len(data):
section_id = data[pos]
pos += 1
size, pos = read_u32leb(data, pos)
end = pos + size
if end > len(data):
raise ValueError("section extends past EOF")
payload = data[pos:end]
yield section_id, payload
pos = end
def parse_imports(payload: bytes):
pos = 0
count, pos = read_u32leb(payload, pos)
imports = []
for _ in range(count):
module, pos = read_name(payload, pos)
name, pos = read_name(payload, pos)
if pos >= len(payload):
raise ValueError("truncated import kind")
kind = payload[pos]
pos += 1
# Skip type descriptors. We only need module/name/kind for W2 policy.
if kind == 0: # func type index
_, pos = read_u32leb(payload, pos)
elif kind == 1: # table
if pos >= len(payload): raise ValueError("truncated table import")
pos += 1
flags, pos = read_u32leb(payload, pos)
_, pos = read_u32leb(payload, pos)
if flags & 1: _, pos = read_u32leb(payload, pos)
elif kind == 2: # memory
flags, pos = read_u32leb(payload, pos)
_, pos = read_u32leb(payload, pos)
if flags & 1: _, pos = read_u32leb(payload, pos)
elif kind == 3: # global
pos += 2
else:
raise ValueError(f"unknown import kind {kind}")
imports.append((module, name, kind))
return imports
def parse_exports(payload: bytes):
pos = 0
count, pos = read_u32leb(payload, pos)
exports = []
for _ in range(count):
name, pos = read_name(payload, pos)
if pos >= len(payload):
raise ValueError("truncated export kind")
kind = payload[pos]
pos += 1
_, pos = read_u32leb(payload, pos)
exports.append((name, kind))
return exports
def collect(path: Path):
data = path.read_bytes()
customs: dict[str, list[bytes]] = {}
imports = []
exports = []
for section_id, payload in walk_sections(data):
if section_id == 0:
name, pos = read_name(payload, 0)
customs.setdefault(name, []).append(payload[pos:])
elif section_id == 2:
imports = parse_imports(payload)
elif section_id == 7:
exports = parse_exports(payload)
return customs, imports, exports
def dylink_has_valid_mem_info(payload: bytes) -> bool:
pos = 0
while pos < len(payload):
subsection_id = payload[pos]
pos += 1
size, pos = read_u32leb(payload, pos)
end = pos + size
if end > len(payload):
raise ValueError("dylink.0 subsection extends past section")
if subsection_id == 1:
mem_size, p = read_u32leb(payload, pos)
mem_align, p = read_u32leb(payload, p)
table_size, p = read_u32leb(payload, p)
table_align, p = read_u32leb(payload, p)
if p > end:
raise ValueError("truncated dylink.0 mem_info")
return mem_align < 32 and table_align < 32 and mem_size < (1 << 31) and table_size < (1 << 31)
pos = end
return False
def defined_symbols(path: Path, llvm_nm: str) -> list[str]:
proc = subprocess.run([llvm_nm, "--defined-only", str(path)], text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if proc.returncode != 0:
raise RuntimeError(proc.stderr.strip() or "llvm-nm failed")
symbols = []
for line in proc.stdout.splitlines():
parts = line.split()
if parts:
symbols.append(parts[-1])
return symbols
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("wasm", type=Path)
ap.add_argument("--abi-version", default="6")
ap.add_argument("--llvm-nm", default=None)
ap.add_argument("--verbose", action="store_true")
args = ap.parse_args()
try:
customs, imports, exports = collect(args.wasm)
errors = []
dylink_payloads = customs.get("dylink.0", [])
if not dylink_payloads:
errors.append("missing dylink.0 custom section")
elif not any(dylink_has_valid_mem_info(payload) for payload in dylink_payloads):
errors.append("dylink.0 missing valid mem_info subsection")
abi_payloads = customs.get("uce.abi", [])
if not abi_payloads:
errors.append("missing uce.abi custom section")
else:
abi_text = abi_payloads[-1].decode("utf-8", "replace")
required = ["format=uce-wasm-unit-abi-v1", f"unit_abi_version={args.abi_version}", "toolchain="]
for needle in required:
if needle not in abi_text:
errors.append(f"uce.abi missing {needle!r}")
export_names = {name for name, _ in exports}
forbidden_exports = {"uce_alloc", "uce_free"}
for name in sorted(export_names & forbidden_exports):
errors.append(f"forbidden allocator export {name}")
import_map = {(module, name): kind for module, name, kind in imports}
required_imports = {
("env", "memory"): 2,
("env", "__memory_base"): 3,
}
# units without indirect calls / stack spills / table needs
# legitimately omit these; if present, the kind must be right
optional_imports = {
("env", "__indirect_function_table"): 1,
("env", "__stack_pointer"): 3,
("env", "__table_base"): 3,
}
for key, kind in required_imports.items():
if import_map.get(key) != kind:
errors.append(f"missing required import {key[0]}.{key[1]}")
for key, kind in optional_imports.items():
if key in import_map and import_map[key] != kind:
errors.append(f"wrong kind for import {key[0]}.{key[1]}")
for module, name, kind in imports:
if module.startswith("wasi_") or module == "wasi_snapshot_preview1":
errors.append(f"forbidden WASI import {module}.{name}")
if module not in {"env", "GOT.mem", "GOT.func"} and not module.startswith("GOT."):
errors.append(f"unexpected import module {module}.{name}")
if module.startswith("GOT.") and kind != 3:
errors.append(f"GOT import is not a global: {module}.{name}")
llvm_nm = args.llvm_nm or shutil.which("llvm-nm") or "/opt/wasi-sdk/bin/llvm-nm"
if Path(llvm_nm).exists():
bad_prefixes = ("_Znwm", "_Znam", "_ZdlPv", "_ZdaPv", "_ZdlPvm", "_ZdaPvm")
for sym in defined_symbols(args.wasm, llvm_nm):
if sym in {"uce_alloc", "uce_free"} or sym.startswith(bad_prefixes):
errors.append(f"forbidden allocator definition {sym}")
else:
errors.append("llvm-nm not found; cannot verify allocator definitions")
if errors:
for e in errors:
print(f"ERROR: {e}", file=sys.stderr)
return 1
if args.verbose:
print(f"UCE W2 unit check PASS: {args.wasm}")
return 0
except Exception as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())