diff --git a/docs/setup.md b/docs/setup.md index 8f03529..81dc44e 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -35,14 +35,14 @@ apt update apt install -y clang build-essential libpcre2-dev mariadb-client libmariadb-dev curl rsync ca-certificates ``` -UCE also requires two toolchains that are not vendored in this repository: +UCE also requires two non-vendored dependencies. WASI SDK is load-bearing at runtime because UCE compiles units on demand during requests and during proactive startup scans. - **Wasmtime C API / C++ headers** at `/opt/wasmtime` by default. `scripts/build_linux.sh` expects: - `/opt/wasmtime/include/wasmtime.hh` - `/opt/wasmtime/include/wasmtime/*.h` - `/opt/wasmtime/lib/libwasmtime.so` -- **WASI SDK** at `/opt/wasi-sdk` by default. `scripts/build_core_wasm.sh` and `scripts/compile_wasm_unit` expect: +- **Pinned WASI SDK** at `/opt/wasi-sdk` by default. `scripts/build_core_wasm.sh` and request-time `scripts/compile_wasm_unit` expect: - `/opt/wasi-sdk/bin/clang++` - `/opt/wasi-sdk/bin/wasm-ld` - `/opt/wasi-sdk/bin/llvm-objcopy` @@ -85,11 +85,11 @@ bin/uce_fastcgi.linux.bin ### Installing Wasmtime and WASI SDK -The UCE build does not download these dependencies for you. Install them through your distro if it provides compatible development packages, or unpack pinned release archives under `/opt/wasmtime` and `/opt/wasi-sdk`. +The UCE build does not download these dependencies for you. Install Wasmtime through a compatible distro package or a pinned upstream C API archive. Install WASI SDK with UCE's pinned installer, or unpack the same pinned archive under `/opt/wasi-sdk`. Do not use `curl | sh` installers in production setup scripts. Download archives from the upstream release pages, verify checksums/signatures when available, and record the exact versions in your deployment notes. Avoid installing a release published in the last few days unless you have reviewed it separately. -The expected directory shape is: +The expected directories are: ```text /opt/wasmtime/include/wasmtime.hh @@ -101,7 +101,17 @@ The expected directory shape is: /opt/wasi-sdk/bin/llvm-objcopy ``` -Example install flow using pinned archive files you have already chosen and verified: +Install the WASI SDK: + +```bash +cd /opt/uce +scripts/install_wasi_sdk.sh +scripts/install_wasi_sdk.sh --check-only +``` + +The current pin is documented in `docs/wasi-sdk-toolchain.md`. The script verifies the archive SHA256 before installing and updates `/opt/wasi-sdk` to point at the pinned versioned directory. + +For Wasmtime, use a compatible distro package or an upstream C API archive. Example flow using an archive you have already chosen and verified: ```bash mkdir -p /opt /tmp/uce-deps @@ -113,12 +123,6 @@ cd /tmp/uce-deps sha256sum -c wasmtime-c-api.sha256 mkdir -p /opt/wasmtime tar -xf wasmtime-*-c-api*.tar.* -C /opt/wasmtime --strip-components=1 - -# Download the WASI SDK archive for your architecture from the upstream release -# page, then verify its checksum before unpacking. -sha256sum -c wasi-sdk.sha256 -mkdir -p /opt/wasi-sdk -tar -xf wasi-sdk-*.tar.* -C /opt/wasi-sdk --strip-components=1 ``` After unpacking, verify the tools UCE needs. Also record the exact Wasmtime and WASI SDK versions used. The native build embeds an rpath for `$WASMTIME_HOME/lib`, so the service environment should use the same `WASMTIME_HOME` value used during build. @@ -200,7 +204,7 @@ Important settings: - `BIN_DIRECTORY` stores generated C++, wasm artifacts, compile output, and runtime caches. - `TMP_UPLOAD_PATH` and `SESSION_PATH` must be writable by the runtime. - `HTTP_PORT` is the built-in HTTP/WebSocket listener used for WebSocket upgrade traffic and direct local probes. Bind/firewall it for local access only; nginx/Apache should be the public entry point. -- `WASM_COMPILE_SCRIPT` must point to `scripts/compile_wasm_unit` unless you provide an equivalent compiler. Relative paths are resolved from the runtime root/`COMPILER_SYS_PATH`. That script calls `scripts/check_unit_wasm.py` after linking each unit. +- `WASM_COMPILE_SCRIPT` must point to `scripts/compile_wasm_unit` unless you provide an equivalent compiler. Relative paths are resolved from the runtime root/`COMPILER_SYS_PATH`. That script calls `scripts/check_unit_wasm.py` after linking each unit and uses the pinned WASI SDK on every deployment host. - `WASM_CORE_PATH` must point at the built `core.wasm` file. After editing settings, restart UCE: @@ -265,6 +269,34 @@ systemctl daemon-reload systemctl enable --now uce.service ``` +### Debian package build + +To build a Debian package from the repository root: + +```bash +bash scripts/make_deb.sh 0.1.2 +``` + +The Debian package creator bundles WASI SDK and Wasmtime by default when `/opt/wasi-sdk` and `/opt/wasmtime` are present. Verify the pinned SDK before building: + +```bash +scripts/install_wasi_sdk.sh --check-only +bash scripts/make_deb.sh 0.1.2 +``` + +This includes the resolved `/opt/wasi-sdk-...` tree, `/opt/wasi-sdk` symlink, resolved `/opt/wasmtime-...` tree, and `/opt/wasmtime` symlink in the package. It makes the package large, but keeps request-time unit compilation and runtime linking tied to the toolchain versions that passed the test suite. Set `UCE_DEB_BUNDLE_WASI_SDK=0` or `UCE_DEB_BUNDLE_WASMTIME=0` only if your deployment provides those exact dependencies separately. + +### RPM package build + +To build an RPM package from the repository root, install `rpmbuild` on the packaging host, verify the pinned SDK, then run: + +```bash +scripts/install_wasi_sdk.sh --check-only +bash scripts/make_rpm.sh 0.1.2 +``` + +The RPM creator mirrors the Debian package layout: runtime files under `/usr/lib/uce`, public files under `/var/www/html`, config under `/etc/uce/settings.cfg`, systemd unit under `/usr/lib/systemd/system/uce.service`, and bundled `/opt/wasi-sdk` plus `/opt/wasmtime` trees by default. Set `UCE_RPM_BUNDLE_WASI_SDK=0` or `UCE_RPM_BUNDLE_WASMTIME=0` only if your deployment provides those exact dependencies separately. + ## How request routing works ### Static files @@ -585,6 +617,7 @@ PY - Set `HTTP_DOCUMENT_ROOT` when the web root is outside the runtime working directory. The built-in HTTP/WebSocket listener resolves upgrade paths from this setting. - Do not expose `CLI_SOCKET_PATH` or `HTTP_PORT` as public entry points. The public path should be nginx/Apache. - Do not trust `Script-Filename` request headers from direct HTTP clients. The built-in HTTP listener resolves from `HTTP_DOCUMENT_ROOT` and rejects `..` path segments. +- WASI SDK is a deployment/runtime dependency, not just a developer build tool. UCE compiles units to wasm on demand during requests and during proactive startup scans, so each host must use the pinned SDK version documented in `docs/wasi-sdk-toolchain.md`. - After toolchain or compile-script fixes, clear stale failed artifacts under `BIN_DIRECTORY`; otherwise a later request may report an old compile failure. ## Troubleshooting @@ -625,7 +658,7 @@ Common compile footguns: - `WASM_COMPILE_SCRIPT` is unset or points at a removed script such as `scripts/compile`; set it to `scripts/compile_wasm_unit`. - `scripts/check_unit_wasm.py` is missing or not executable; `scripts/compile_wasm_unit` calls it after linking each unit. -- `WASI_SDK` does not point at a tree with `clang++`, `wasm-ld`, and `llvm-objcopy`. +- `WASI_SDK` does not point at the pinned tree with `clang++`, `wasm-ld`, `llvm-objcopy`, and `llvm-nm`; run `scripts/install_wasi_sdk.sh --check-only`. - `WASMTIME_HOME` does not point at a tree with Wasmtime headers and `libwasmtime.so`. - A previous failed compile left stale `.compile.txt`, `.wasm-check.txt`, or partial `.wasm` files under `BIN_DIRECTORY`. diff --git a/docs/wasi-sdk-toolchain.md b/docs/wasi-sdk-toolchain.md new file mode 100644 index 0000000..37a328a --- /dev/null +++ b/docs/wasi-sdk-toolchain.md @@ -0,0 +1,55 @@ +# WASI SDK Toolchain Pin + +UCE treats WASI SDK as a deployment/runtime dependency, not just a developer build tool. + +The runtime compiles `.uce` units to wasm on demand during requests and during the proactive compiler scan. That means every deployment host must have the same compiler/linker toolchain available, and the generated `.wasm`/`.cwasm` artifacts are tied to that toolchain version and UCE unit ABI version. + +## Current pin + +- Upstream: +- Release tag: `wasi-sdk-33` +- Version: `33.0` +- Linux x86_64 archive: `wasi-sdk-33.0-x86_64-linux.tar.gz` +- URL: `https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-33/wasi-sdk-33.0-x86_64-linux.tar.gz` +- SHA256: `0ba8b5bfaeb2adf3f29bab5841d76cf5318ab8e1642ea195f88baba1abd47bce` +- Expected install symlink: `/opt/wasi-sdk` +- Expected resolved path: `/opt/wasi-sdk-33.0-x86_64-linux` + +Install or verify with: + +```bash +scripts/install_wasi_sdk.sh +scripts/install_wasi_sdk.sh --check-only +``` + +## Required tools + +UCE expects these executables on each deployment host: + +```text +/opt/wasi-sdk/bin/clang++ +/opt/wasi-sdk/bin/wasm-ld +/opt/wasi-sdk/bin/llvm-objcopy +/opt/wasi-sdk/bin/llvm-nm +``` + +`llvm-nm` is used by `scripts/check_unit_wasm.py`, which is called by `scripts/compile_wasm_unit` after linking each unit. + +## Upgrade policy + +Treat WASI SDK upgrades like runtime dependency upgrades: + +1. Update `scripts/install_wasi_sdk.sh` version, URL, and SHA256. +2. Record the new release and checksum here. +3. Rebuild `bin/wasm/core.wasm` with `scripts/build_core_wasm.sh`. +4. Rebuild the native runtime with `scripts/build_linux.sh`. +5. Clear or invalidate stale unit wasm artifacts by bumping `UCE_UNIT_ABI_VERSION` when required, or by removing affected generated artifacts under `BIN_DIRECTORY`. +6. Run the full CLI suite including wasm kill tests: + +```bash +scripts/run_cli_tests.sh --include-wasm-kill +``` + +## Known footgun + +WASI SDK 33's `llvm-nm` was observed to crash on a degenerate but valid unit module with no exported handlers. `scripts/check_unit_wasm.py` treats that specific validator-tool crash as a skipped allocator-definition scan while still rejecting forbidden allocator exports and other ABI violations. This is one reason the toolchain is pinned instead of relying on whatever `/opt/wasi-sdk` happens to contain. diff --git a/scripts/deb/control.in b/scripts/deb/control.in index 7c1a31a..27af6b4 100644 --- a/scripts/deb/control.in +++ b/scripts/deb/control.in @@ -3,7 +3,7 @@ Version: @VERSION@ Section: web Priority: optional Architecture: @ARCH@ -Depends: bash, clang, libmariadb-dev | default-libmysqlclient-dev, systemd +Depends: bash, python3, curl, systemd, libpcre2-8-0, zlib1g, libssl3, libstdc++6, libgcc-s1, libmariadb3 | libmysqlclient21 | libmysqlclient24 Maintainer: Udo Schroeter Installed-Size: @INSTALLED_SIZE@ Description: UCE FastCGI runtime and live-compiling C++ web environment diff --git a/scripts/deb/postinst b/scripts/deb/postinst index b825238..aa75b1d 100644 --- a/scripts/deb/postinst +++ b/scripts/deb/postinst @@ -5,6 +5,7 @@ if command -v systemctl >/dev/null 2>&1; then systemctl daemon-reload >/dev/null 2>&1 || true if [ "$1" = "configure" ]; then systemctl enable uce.service >/dev/null 2>&1 || true + systemctl restart uce.service >/dev/null 2>&1 || true fi fi diff --git a/scripts/deb/uce.service b/scripts/deb/uce.service index f17d30d..bea986a 100644 --- a/scripts/deb/uce.service +++ b/scripts/deb/uce.service @@ -11,7 +11,6 @@ StateDirectory=uce CacheDirectory=uce ExecStartPre=/usr/bin/mkdir -p /var/cache/uce/work /var/lib/uce/uploads /var/lib/uce/sessions ExecStartPre=/usr/bin/rm -f /run/uce/fastcgi.sock -ExecStartPre=/usr/bin/bash /usr/lib/uce/scripts/build_linux.sh ExecStart=/usr/lib/uce/bin/uce_fastcgi.linux.bin ExecStopPost=/usr/bin/rm -f /run/uce/fastcgi.sock Restart=always diff --git a/scripts/install_wasi_sdk.sh b/scripts/install_wasi_sdk.sh new file mode 100755 index 0000000..98d57f8 --- /dev/null +++ b/scripts/install_wasi_sdk.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Install the pinned WASI SDK used by UCE's request-time wasm compiler. +# This is a runtime dependency: UCE compiles .uce units on demand and during +# proactive startup scans, so every deployment host needs the same toolchain. + +WASI_SDK_VERSION="${WASI_SDK_VERSION:-33.0}" +WASI_SDK_RELEASE_TAG="${WASI_SDK_RELEASE_TAG:-wasi-sdk-33}" +WASI_SDK_ARCHIVE="${WASI_SDK_ARCHIVE:-wasi-sdk-33.0-x86_64-linux.tar.gz}" +WASI_SDK_SHA256="${WASI_SDK_SHA256:-0ba8b5bfaeb2adf3f29bab5841d76cf5318ab8e1642ea195f88baba1abd47bce}" +WASI_SDK_URL="${WASI_SDK_URL:-https://github.com/WebAssembly/wasi-sdk/releases/download/${WASI_SDK_RELEASE_TAG}/${WASI_SDK_ARCHIVE}}" +INSTALL_BASE="${INSTALL_BASE:-/opt}" +INSTALL_DIR="${WASI_SDK_INSTALL_DIR:-${INSTALL_BASE}/wasi-sdk-${WASI_SDK_VERSION}-x86_64-linux}" +SYMLINK_PATH="${WASI_SDK_SYMLINK:-${INSTALL_BASE}/wasi-sdk}" +CACHE_DIR="${WASI_SDK_CACHE_DIR:-/tmp/uce-deps}" + +usage() { + cat <&2 + exit 2 +fi + +require_command() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "Required command not found: $1" >&2 + exit 1 + fi +} + +verify_tree() { + local root="$1" + for tool in clang++ wasm-ld llvm-objcopy llvm-nm; do + if [[ ! -x "$root/bin/$tool" ]]; then + echo "Missing WASI SDK tool: $root/bin/$tool" >&2 + return 1 + fi + done + "$root/bin/clang++" --version | head -n 1 +} + +if [[ $check_only -eq 1 ]]; then + verify_tree "${WASI_SDK:-$SYMLINK_PATH}" + exit 0 +fi + +require_command curl +require_command sha256sum +require_command tar +require_command mkdir +require_command ln + +mkdir -p "$CACHE_DIR" "$INSTALL_BASE" +archive_path="$CACHE_DIR/$WASI_SDK_ARCHIVE" + +if [[ ! -f "$archive_path" ]]; then + echo "Downloading $WASI_SDK_URL" + curl -fL --proto '=https' --tlsv1.2 -o "$archive_path.tmp" "$WASI_SDK_URL" + mv "$archive_path.tmp" "$archive_path" +fi + +printf '%s %s\n' "$WASI_SDK_SHA256" "$archive_path" | sha256sum -c - + +rm -rf -- "$INSTALL_DIR.tmp" +mkdir -p "$INSTALL_DIR.tmp" +tar -xf "$archive_path" -C "$INSTALL_DIR.tmp" --strip-components=1 +verify_tree "$INSTALL_DIR.tmp" + +rm -rf -- "$INSTALL_DIR" +mv "$INSTALL_DIR.tmp" "$INSTALL_DIR" +ln -sfn "$INSTALL_DIR" "$SYMLINK_PATH" + +echo "Installed WASI SDK at $INSTALL_DIR" +echo "Updated symlink $SYMLINK_PATH -> $INSTALL_DIR" +verify_tree "$SYMLINK_PATH" diff --git a/scripts/make_deb.sh b/scripts/make_deb.sh index f7da601..c62652f 100755 --- a/scripts/make_deb.sh +++ b/scripts/make_deb.sh @@ -13,8 +13,11 @@ Usage: scripts/make_deb.sh VERSION Environment: - UCE_DEB_REVISION Debian package revision suffix (default: 1) - UCE_DEB_ARCH Override package architecture + UCE_DEB_REVISION Debian package revision suffix (default: 1) + UCE_DEB_ARCH Override package architecture + UCE_DEB_WEBROOT Public web root staged into the package (default: /var/www/html) + UCE_DEB_BUNDLE_WASI_SDK Bundle pinned /opt/wasi-sdk into the package (default: 1) + UCE_DEB_BUNDLE_WASMTIME Bundle /opt/wasmtime into the package (default: 1) EOF } @@ -47,12 +50,37 @@ validate_version() { copy_payload() { local destination="$1" + local webroot="$2" + local stage_dir="$3" local path - for path in LICENSE README.md codesearch bin scripts site src; do + for path in LICENSE README.md codesearch bin scripts src docs; do cp -a "$REPO_ROOT/$path" "$destination/" done - mkdir -p "$destination/etc" + mkdir -p "$destination/etc" "$stage_dir$webroot" cp -a "$REPO_ROOT/etc/uce" "$destination/etc/" + cp -a "$REPO_ROOT/site/." "$stage_dir$webroot/" +} + +write_packaged_settings() { + local output_file="$1" + local webroot="$2" + python3 - "$REPO_ROOT/etc/uce/settings.cfg" "$output_file" "$webroot" <<'PY' +from pathlib import Path +import sys +src, dst, webroot = sys.argv[1:4] +s = Path(src).read_text() +replacements = { + "SITE_DIRECTORY=site": f"SITE_DIRECTORY={webroot}", + "WASM_CORE_PATH=/Code/uce.openfu.com/uce/bin/wasm/core.wasm": "WASM_CORE_PATH=/usr/lib/uce/bin/wasm/core.wasm", + "HTTP_DOCUMENT_ROOT=": f"HTTP_DOCUMENT_ROOT={webroot}", +} +for old, new in replacements.items(): + if old in s: + s = s.replace(old, new) +if "WASM_COMPILE_SCRIPT=" not in s: + s += "\nWASM_COMPILE_SCRIPT=scripts/compile_wasm_unit\n" +Path(dst).write_text(s) +PY } write_control_file() { @@ -69,11 +97,49 @@ write_control_file() { "$DEB_ASSET_DIR/control.in" > "$output_file" } +bundle_wasi_sdk() { + local stage_dir="$1" + local wasi_root="${WASI_SDK:-/opt/wasi-sdk}" + if [[ "${UCE_DEB_BUNDLE_WASI_SDK:-1}" != "1" ]]; then + return + fi + if [[ ! -x "$wasi_root/bin/clang++" || ! -x "$wasi_root/bin/wasm-ld" || ! -x "$wasi_root/bin/llvm-nm" ]]; then + echo "UCE_DEB_BUNDLE_WASI_SDK=1 but WASI_SDK does not point at a complete SDK: $wasi_root" >&2 + exit 1 + fi + local resolved + resolved="$(readlink -f "$wasi_root")" + local base + base="$(basename "$resolved")" + mkdir -p "$stage_dir/opt" + cp -a "$resolved" "$stage_dir/opt/$base" + ln -sfn "$base" "$stage_dir/opt/wasi-sdk" +} + +bundle_wasmtime() { + local stage_dir="$1" + local wasmtime_root="${WASMTIME_HOME:-/opt/wasmtime}" + if [[ "${UCE_DEB_BUNDLE_WASMTIME:-1}" != "1" ]]; then + return + fi + if [[ ! -f "$wasmtime_root/include/wasmtime.hh" || ! -f "$wasmtime_root/lib/libwasmtime.so" ]]; then + echo "UCE_DEB_BUNDLE_WASMTIME=1 but WASMTIME_HOME does not point at a complete C API tree: $wasmtime_root" >&2 + exit 1 + fi + local resolved + resolved="$(readlink -f "$wasmtime_root")" + local base + base="$(basename "$resolved")" + mkdir -p "$stage_dir/opt" + cp -a "$resolved" "$stage_dir/opt/$base" + ln -sfn "$base" "$stage_dir/opt/wasmtime" +} + write_md5sums() { local stage_dir="$1" ( cd "$stage_dir" - find usr etc lib -type f -print0 | sort -z | xargs -0 md5sum > DEBIAN/md5sums + find usr etc lib opt -type f -print0 2>/dev/null | sort -z | xargs -0 --no-run-if-empty md5sum > DEBIAN/md5sums ) } @@ -106,6 +172,7 @@ PACKAGE_BASENAME="${PACKAGE_NAME}_${PACKAGE_VERSION}_${ARCH}" STAGE_DIR="$REPO_ROOT/pkg/$PACKAGE_BASENAME" DEBIAN_DIR="$STAGE_DIR/DEBIAN" INSTALL_ROOT="$STAGE_DIR/usr/lib/uce" +WEBROOT="${UCE_DEB_WEBROOT:-/var/www/html}" DIST_DIR="$REPO_ROOT/dist" OUTPUT_DEB="$DIST_DIR/$PACKAGE_BASENAME.deb" @@ -117,9 +184,11 @@ bash "$REPO_ROOT/scripts/build_linux.sh" rm -rf -- "$STAGE_DIR" mkdir -p "$DEBIAN_DIR" "$INSTALL_ROOT" "$STAGE_DIR/etc/uce" "$STAGE_DIR/lib/systemd/system" "$DIST_DIR" -copy_payload "$INSTALL_ROOT" +copy_payload "$INSTALL_ROOT" "$WEBROOT" "$STAGE_DIR" +bundle_wasi_sdk "$STAGE_DIR" +bundle_wasmtime "$STAGE_DIR" -install -m 0644 "$REPO_ROOT/etc/uce/settings.cfg" "$STAGE_DIR/etc/uce/settings.cfg" +write_packaged_settings "$STAGE_DIR/etc/uce/settings.cfg" "$WEBROOT" install -m 0644 "$DEB_ASSET_DIR/uce.service" "$STAGE_DIR/lib/systemd/system/uce.service" install -m 0644 "$DEB_ASSET_DIR/conffiles" "$DEBIAN_DIR/conffiles" install -m 0755 "$DEB_ASSET_DIR/postinst" "$DEBIAN_DIR/postinst" diff --git a/scripts/make_rpm.sh b/scripts/make_rpm.sh new file mode 100755 index 0000000..bb78990 --- /dev/null +++ b/scripts/make_rpm.sh @@ -0,0 +1,230 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +PACKAGE_NAME="uce" +RELEASE="${UCE_RPM_RELEASE:-1}" +WEBROOT="${UCE_RPM_WEBROOT:-/var/www/html}" + +usage() { + cat <<'EOF' +Usage: + scripts/make_rpm.sh VERSION + +Environment: + UCE_RPM_RELEASE RPM release suffix (default: 1) + UCE_RPM_ARCH Override RPM architecture + UCE_RPM_WEBROOT Public web root staged into the package (default: /var/www/html) + UCE_RPM_BUNDLE_WASI_SDK Bundle pinned /opt/wasi-sdk into the package (default: 1) + UCE_RPM_BUNDLE_WASMTIME Bundle /opt/wasmtime into the package (default: 1) +EOF +} + +require_command() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "Required command not found: $1" >&2 + exit 1 + fi +} + +validate_version() { + local version="$1" + if [[ ! "$version" =~ ^[0-9][A-Za-z0-9._+~]*$ ]]; then + echo "Invalid RPM version string: $version" >&2 + exit 1 + fi +} + +resolve_arch() { + if [[ -n "${UCE_RPM_ARCH:-}" ]]; then + printf '%s\n' "$UCE_RPM_ARCH" + return + fi + uname -m +} + +copy_payload() { + local destination="$1" + local webroot="$2" + local stage_dir="$3" + local path + for path in LICENSE README.md codesearch bin scripts src docs; do + cp -a "$REPO_ROOT/$path" "$destination/" + done + mkdir -p "$destination/etc" "$stage_dir$webroot" + cp -a "$REPO_ROOT/etc/uce" "$destination/etc/" + cp -a "$REPO_ROOT/site/." "$stage_dir$webroot/" +} + +write_packaged_settings() { + local output_file="$1" + local webroot="$2" + python3 - "$REPO_ROOT/etc/uce/settings.cfg" "$output_file" "$webroot" <<'PY' +from pathlib import Path +import sys +src, dst, webroot = sys.argv[1:4] +s = Path(src).read_text() +replacements = { + "SITE_DIRECTORY=site": f"SITE_DIRECTORY={webroot}", + "WASM_CORE_PATH=/Code/uce.openfu.com/uce/bin/wasm/core.wasm": "WASM_CORE_PATH=/usr/lib/uce/bin/wasm/core.wasm", + "HTTP_DOCUMENT_ROOT=": f"HTTP_DOCUMENT_ROOT={webroot}", +} +for old, new in replacements.items(): + if old in s: + s = s.replace(old, new) +if "WASM_COMPILE_SCRIPT=" not in s: + s += "\nWASM_COMPILE_SCRIPT=scripts/compile_wasm_unit\n" +Path(dst).write_text(s) +PY +} + +bundle_wasi_sdk() { + local stage_dir="$1" + local wasi_root="${WASI_SDK:-/opt/wasi-sdk}" + if [[ "${UCE_RPM_BUNDLE_WASI_SDK:-1}" != "1" ]]; then + return + fi + if [[ ! -x "$wasi_root/bin/clang++" || ! -x "$wasi_root/bin/wasm-ld" || ! -x "$wasi_root/bin/llvm-nm" ]]; then + echo "UCE_RPM_BUNDLE_WASI_SDK=1 but WASI_SDK does not point at a complete SDK: $wasi_root" >&2 + exit 1 + fi + local resolved base + resolved="$(readlink -f "$wasi_root")" + base="$(basename "$resolved")" + mkdir -p "$stage_dir/opt" + cp -a "$resolved" "$stage_dir/opt/$base" + ln -sfn "$base" "$stage_dir/opt/wasi-sdk" +} + +bundle_wasmtime() { + local stage_dir="$1" + local wasmtime_root="${WASMTIME_HOME:-/opt/wasmtime}" + if [[ "${UCE_RPM_BUNDLE_WASMTIME:-1}" != "1" ]]; then + return + fi + if [[ ! -f "$wasmtime_root/include/wasmtime.hh" || ! -f "$wasmtime_root/lib/libwasmtime.so" ]]; then + echo "UCE_RPM_BUNDLE_WASMTIME=1 but WASMTIME_HOME does not point at a complete C API tree: $wasmtime_root" >&2 + exit 1 + fi + local resolved base + resolved="$(readlink -f "$wasmtime_root")" + base="$(basename "$resolved")" + mkdir -p "$stage_dir/opt" + cp -a "$resolved" "$stage_dir/opt/$base" + ln -sfn "$base" "$stage_dir/opt/wasmtime" +} + +if [[ $# -ne 1 ]]; then + usage >&2 + exit 1 +fi + +VERSION="$1" +validate_version "$VERSION" + +require_command bash +require_command rpmbuild +require_command find +require_command cp +require_command tar +require_command sed +require_command python3 +require_command readlink + +ARCH="$(resolve_arch)" +PACKAGE_VERSION="${VERSION}-${RELEASE}" +BUILD_ROOT="$REPO_ROOT/pkg/rpm-build" +STAGE_DIR="$BUILD_ROOT/stage" +RPMBUILD_DIR="$BUILD_ROOT/rpmbuild" +INSTALL_ROOT="$STAGE_DIR/usr/lib/uce" +SPEC_FILE="$RPMBUILD_DIR/SPECS/$PACKAGE_NAME.spec" +DIST_DIR="$REPO_ROOT/dist" + +bash "$REPO_ROOT/scripts/build_linux.sh" + +rm -rf -- "$BUILD_ROOT" +mkdir -p "$INSTALL_ROOT" "$STAGE_DIR/etc/uce" "$STAGE_DIR/usr/lib/systemd/system" "$RPMBUILD_DIR"/{BUILD,RPMS,SOURCES,SPECS,SRPMS} "$DIST_DIR" + +copy_payload "$INSTALL_ROOT" "$WEBROOT" "$STAGE_DIR" +bundle_wasi_sdk "$STAGE_DIR" +bundle_wasmtime "$STAGE_DIR" +write_packaged_settings "$STAGE_DIR/etc/uce/settings.cfg" "$WEBROOT" +install -m 0644 "$REPO_ROOT/scripts/deb/uce.service" "$STAGE_DIR/usr/lib/systemd/system/uce.service" + +( + cd "$STAGE_DIR" + tar --sort=name --mtime='UTC 2026-01-01' --owner=0 --group=0 --numeric-owner -czf "$RPMBUILD_DIR/SOURCES/$PACKAGE_NAME-$VERSION.tar.gz" . +) + +cat > "$SPEC_FILE" </dev/null 2>&1; then + systemctl daemon-reload >/dev/null 2>&1 || true + systemctl enable uce.service >/dev/null 2>&1 || true + systemctl restart uce.service >/dev/null 2>&1 || true +fi + +%preun +if [ "\$1" = "0" ] && command -v systemctl >/dev/null 2>&1; then + systemctl disable --now uce.service >/dev/null 2>&1 || true +fi + +%postun +if command -v systemctl >/dev/null 2>&1; then + systemctl daemon-reload >/dev/null 2>&1 || true +fi + +%files +%license /usr/lib/uce/LICENSE +%doc /usr/lib/uce/README.md +%config(noreplace) /etc/uce/settings.cfg +/usr/lib/uce +/usr/lib/systemd/system/uce.service +$WEBROOT +%dir /var/cache/uce +%dir /var/lib/uce +/opt/* + +%changelog +* Mon Jun 15 2026 UCE Packager - $PACKAGE_VERSION +- Package UCE runtime with pinned deployment toolchains. +EOF + +rpmbuild --define "_topdir $RPMBUILD_DIR" -bb "$SPEC_FILE" +find "$RPMBUILD_DIR/RPMS" -type f -name '*.rpm' -print -exec cp -a {} "$DIST_DIR/" \; +find "$DIST_DIR" -maxdepth 1 -type f -name "${PACKAGE_NAME}-${VERSION}-${RELEASE}*.rpm" -print