Quick Start: Building and Deploying on the Spaceboard
Portal is a Wayland compositor stack for AR glasses spatial computing, running on the Spaceboard — an HP EliteBook Ultra G1q powered by the Snapdragon X Elite (X1E80100). This guide walks beginner developers through the end-to-end process: verifying prerequisites, building every component, deploying to the device, and confirming that all 15+ systemd services are healthy. The primary build strategy is native compilation directly on the Spaceboard (not cross-compilation from a Mac), because the device has Rust 1.97.0, gcc 15.3, and all Portal build dependencies installed natively.
Sources: README.md#L1-L150
Prerequisites: What You Need Before You Start
Section titled “Prerequisites: What You Need Before You Start”Before touching any build commands, confirm that your Spaceboard meets the hardware and software requirements. The device runs a custom kernel (7.0.13-iris) on Debian testing (trixie), with Qualcomm’s IRIS hardware video encoder and Adreno X1-85 GPU exposed through Mesa 26.1.2.
| Component | Requirement | How to Verify |
|---|---|---|
| Machine | HP EliteBook Ultra G1q (X1E80100) | ssh hp "uname -m" → aarch64 |
| OS | Debian testing (trixie) | ssh hp "cat /etc/debian_version" |
| Kernel | 7.0.13-iris (custom, IRIS + videocc-sm8550) | ssh hp "uname -r" |
| Rust | 1.97.0 (pinned via rust-toolchain.toml) | ssh hp "rustc --version" |
| Cargo | Cargo 1.97+ | ssh hp "cargo --version" |
| C Compiler | gcc 15.3 | ssh hp "gcc --version" |
| QNN SDK | QAIRT v2.44 at /opt/qairt |
ssh hp "ls /opt/qairt" |
| SSH Access | ssh hp alias configured |
ssh hp "echo ok" |
The Rust toolchain is pinned to exactly 1.97.0 in the repository’s rust-toolchain.toml, ensuring that every developer and the CI pipeline compile with identical semantics.
Sources: rust-toolchain.toml#L1-L4, docs/x-elite-deployment-state.md#L9-L24
Repository Access
Section titled “Repository Access”Clone the repository and SSH into the Spaceboard to begin:
# On your development machinegit clone <repo-url> portal-developmentcd portal-development
# Verify SSH connectivity to the Spaceboardssh hp "rustc --version && cargo --version"# Expected: rustc 1.97.0 / cargo 1.97.0The workspace uses Cargo with a resolver v2 layout containing 30+ crates spanning spatial rendering, streaming, input, voice, NPU, and application framework subsystems.
Sources: Cargo.toml#L1-L45
Build Workflow Overview
Section titled “Build Workflow Overview”Portal produces artifacts across three distinct build systems, each serving a different layer of the stack. Understanding which build to run — and when — is the first architectural decision a new developer faces.
flowchart TD
subgraph "Rust Workspace (cargo)"
RW["cargo build --workspace --release<br/>30+ crates → binaries + cdylibs"]
end
subgraph "C++ Plugins (cmake)"
CP["cmake -B build && cmake --build build<br/>portal-spatial + portal-spatial-warp shims"]
end
subgraph "C Streaming (gcc + Makefile)"
CS["./build.sh<br/>portal_stream binary (GStreamer pipeline)"]
end
RW -->|libportal_spatial.so, portal-wm, portal-voiced...| INSTALL["/usr/local/lib/ + /usr/local/bin/"]
CP -->|portal-spatial.so, portal-spatial-warp.so| WAYFIRE["Wayfire plugin directory"]
CS -->|portal_stream| STREAM["portal-stream.service"]
WAYFIRE --> COMPOSITOR["portal.service (Wayfire)"]
STREAM --> COMPOSITOR
INSTALL --> SERVICES["All portal-*.service units"]
The three build paths are independent but produce interdependent artifacts: the C++ shims load Rust cdylibs at runtime via dlopen, and the C streaming binary captures Wayland surfaces produced by the Rust-driven compositor.
Sources: justfile#L1-L67, README.md#L34-L54
Step 1: Build the Rust Workspace
Section titled “Step 1: Build the Rust Workspace”The Rust workspace is the largest build — it compiles 30+ crates including the spatial domain model, Wayfire plugins (as Rust cdylibs), the window manager daemon, input handling, voice pipeline, NPU runtime, and the PCP application framework.
Individual Crates (Recommended for Iteration)
Section titled “Individual Crates (Recommended for Iteration)”When iterating on a specific component, build only the crate you changed to minimize compile time:
# On the Spaceboard (ssh hp)cargo build -p portal-spatial-plugin --release # Wayfire spatial plugin (Rust cdylib)cargo build -p portal-warp --release # Spatial warp rendering (Rust cdylib)cargo build -p portal-wm --release # Window manager daemoncargo build -p portal-input --release # Input handlingcargo build -p portal-voice --release # Voice daemon (requires QNN features)cargo build -p portal-llm --release # LLM daemon (requires hexagon feature)Full Workspace Build
Section titled “Full Workspace Build”For a clean deploy or first-time setup, build everything at once:
cargo build --workspace --releaseVoice and LLM Daemon — Special Feature Flags
Section titled “Voice and LLM Daemon — Special Feature Flags”The voice and LLM daemons require NPU-specific feature flags to enable QNN (Hexagon) and Sherpa-onnx backends. Without these flags, the STT engine is compiled out entirely:
# Voice daemon: VAD + TTS + STT + network audiocargo build --release \ --features "qnn-tts,qnn-vad,npu-stt,network-audio" \ --bin portal-voiced
# LLM daemon: Hexagon DSP support for GenieX llama.cppcargo build --release \ -p portal-llm --features hexagon \ --bin portal-llmd| Crate | Output Binary | Feature Flags Required | Install Path |
|---|---|---|---|
portal-spatial |
libportal_spatial.so |
— | /usr/local/lib/ |
portal-spatial-plugin |
libportal_spatial_plugin.so |
— | /usr/local/lib/ |
portal-warp |
libportal_spatial_warp.so |
— | /usr/local/lib/ |
portal-wm |
portal-wm |
— | /usr/local/bin/ |
portal-input |
portal-keyboardd |
— | /usr/local/bin/ |
portal-voice |
portal-voiced |
qnn-tts,qnn-vad,npu-stt,network-audio |
/usr/local/bin/ |
portal-llm |
portal-llmd |
hexagon |
/usr/local/bin/ |
portal-launcher |
portal-launcherd |
— | /usr/local/bin/ |
portal-pcp/daemon |
portal-pcpd |
— | /usr/local/bin/ |
Sources: README.md#L43-L54, docs/x-elite-deployment-state.md#L168-L175, Cargo.toml#L1-L45
Step 2: Build the C++ Wayfire Plugin Shims
Section titled “Step 2: Build the C++ Wayfire Plugin Shims”The Wayfire plugins are thin C++ shims that bridge between Wayfire’s C++ plugin API and the Rust cdylib implementations. They use CMake and require the Wayfire development package to be installed.
# On the Spaceboard (ssh hp)cd portal/wayfire-pluginscmake -B buildcmake --build build
# Result: build/portal-spatial.so and build/portal-spatial-warp.so# These are Wayfire plugins that dlopen the Rust cdylibs at runtimeThe CMakeLists.txt discovers the Wayfire plugin directory via pkg-config and builds two plugin targets. The keyboard plugin that previously lived here is deprecated — keyboard rendering is now a standalone DRM/KMS daemon.
Sources: portal/wayfire-plugins/CMakeLists.txt#L1-L34, justfile#L22-L28
Step 3: Build the C Streaming Binary
Section titled “Step 3: Build the C Streaming Binary”The streaming pipeline is written in C and uses GStreamer to capture Wayland DMA-BUF surfaces, encode them to H.265 via the Qualcomm IRIS hardware encoder, and packetize the output as RTP/UDP. This binary is separate from the Rust workspace.
# On the Spaceboard (ssh hp)cd portal/streaming./build.sh
# Result: portal_stream binary in portal/streaming/The build script handles Wayland protocol binding generation (using wayland-scanner against pinned wlr-protocols v0.2.1), applies compiler hardening flags (stack canaries, FORTIFY_SOURCE, full RELRO, PIE), and links against GStreamer, libdrm, GBM, EGL, GLESv2, and OpenSSL. There is also a ./build.sh test subcommand that runs cmocka unit tests for HMAC authentication utilities.
Sources: portal/streaming/build.sh#L1-L91, portal/scripts/start-stream.sh#L1-L35
Step 4: Deploy to the Spaceboard
Section titled “Step 4: Deploy to the Spaceboard”Portal offers two deployment paths depending on whether you are setting up a fresh install or iterating on changes.
Option A: Fresh Install — One-Shot Deployment Script
Section titled “Option A: Fresh Install — One-Shot Deployment Script”For a clean Spaceboard (e.g., after a reflash), use the deployment script which installs all configurations, systemd units, and system software in a single pass:
# Clone the repo on the Spaceboard firstgit clone <repo-url> ~/portal-development
# Run as root (sudo)sudo ./portal/scripts/deploy-spaceboard.shThe script performs 12 ordered steps:
| Step | Action | Details |
|---|---|---|
| 1/12 | Create directories | /run/portal, /var/lib/portal, user home dirs |
| 2/12 | NetworkManager config | dns=none to prevent dnsmasq port 53 conflict |
| 3/12 | Mask conflicting services | dnsmasq.service, getty@tty1.service |
| 4/12 | Set default target | multi-user.target (no graphical login manager) |
| 5/12 | Portal config | Wayfire config to /etc/portal/wayfire.ini |
| 6/12 | Network configs | hostapd (5 GHz AP) + dnsmasq DHCP pool |
| 7/12 | Systemd services | 13 portal service units to /etc/systemd/system/ |
| 8/12 | Helper scripts | File watcher, Wine monitor, health monitor, Wi-Fi watchdog |
| 9/12 | Patched hostapd | ath11k EALREADY/EBUSY tolerance (build from source) |
| 10/12 | System software | box64, LibreOffice, inotify-tools, Wine 10.0 |
| 11/12 | Log files | /var/log/portal-health.log and monitor logs |
| 12/12 | Enable services | systemctl enable all 13 services + daemon-reload |
After the script completes, reboot to verify a clean boot sequence.
Sources: portal/scripts/deploy-spaceboard.sh#L1-L100
Option B: Iterative Deploy — Copy Individual Binaries
Section titled “Option B: Iterative Deploy — Copy Individual Binaries”When you rebuild a single component and need to push it live, use scp and systemctl restart:
# --- Rust binaries ---scp ./target/release/portal-wm hp:~/ssh hp "sudo cp ~/portal-wm /usr/local/bin/ && sudo systemctl restart portal-wm"
# --- Rust cdylibs (spatial plugins) ---scp ./target/release/libportal_spatial.so hp:~/ssh hp "sudo cp ~/libportal_spatial.so /usr/local/lib/ && sudo ldconfig"ssh hp "sudo systemctl restart portal"
# --- C++ Wayfire plugins ---scp portal/wayfire-plugins/build/portal-spatial.so hp:~/ssh hp "sudo cp ~/portal-spatial.so \$(pkg-config --variable=plugindir wayfire)/"ssh hp "sudo systemctl restart portal"
# --- C streaming binary ---scp portal/streaming/portal_stream hp:~/ssh hp "sudo cp ~/portal_stream /usr/local/bin/ && sudo systemctl restart portal-stream"Important: After copying any .so file, run sudo ldconfig on the Spaceboard to update the dynamic linker cache before restarting dependent services.
Sources: README.md#L73-L83, docs/x-elite-deployment-state.md#L137-L151
Service Topology: Understanding Startup Order
Section titled “Service Topology: Understanding Startup Order”Portal manages 15+ systemd services that start in a strict dependency chain. The compositor (portal.service) is the root — every other service waits for the Wayland socket to appear before starting.
flowchart TD
BOOT["multi-user.target"] --> PORTAL["portal.service<br/>(Wayfire DRM backend)"]
BOOT --> HEALTH["portal-health-monitor"]
BOOT --> WATCHDOG["wlo1-watchdog"]
BOOT --> BINFMT["portal-binfmt-fix"]
BOOT --> IRIS["portal-iris-fixup"]
PORTAL -->|"socket: /run/portal/wayland-1"| INPUT["portal-input<br/>(keyboard + touch)"]
PORTAL -->|"socket"| STREAM["portal-stream<br/>(IRIS H.265 → RTP)"]
PORTAL --> WM["portal-wm<br/>(window manager)"]
PORTAL --> LAUNCHER["portal-launcher<br/>(app daemon)"]
PORTAL --> LLM["portal-llm<br/>(on-device LLM)"]
DBUS["portal-dbus<br/>(session bus)"] --> PCP["portal-pcp<br/>(capability daemon)"]
PORTAL --> DBUS
PCP --> INPUT
LLM --> VOICE["portal-voice<br/>(VAD + STT + TTS)"]
PORTAL --> NET["portal-network<br/>(hostapd 5GHz AP)"]
style PORTAL fill:#e1f5fe,stroke:#0288d1,stroke-width:2px
style PCP fill:#fff3e0,stroke:#f57c00
style VOICE fill:#f3e5f5,stroke:#7b1fa2
Each service includes an ExecStartPre bash one-liner that polls for the Wayland socket at /run/portal/wayland-1 with a 30-attempt retry loop (15-second timeout). This ensures robust startup even if Wayfire’s initialization takes a few seconds longer than expected.
Sources: portal/systemd/portal.service#L1-L29, portal/systemd/portal-input.service#L1-L45, portal/systemd/portal-stream.service#L1-L34, docs/x-elite-deployment-state.md#L29-L50
Key Environment Configuration
Section titled “Key Environment Configuration”Services read their runtime parameters from environment files in /etc/portal/. The repository ships .example templates that you must copy and customize:
| Environment File | Copied To | Key Variables | Purpose |
|---|---|---|---|
portal-stream.env.example |
/etc/portal/portal-stream.env |
PORTAL_STREAM_TARGET, STREAM_PORT |
RTP destination IP + port for H.265 stream |
portal-voice.env.example |
/etc/portal/portal-voice.env |
PORTAL_GLASSES_IP, PORTAL_VENV_DIR |
TTS audio destination + GenieX Python venv |
portal-network.env.example |
/etc/portal/portal-network.env |
PORTAL_NETWORK_CIDR, PORTAL_DHCP_RANGE |
Wi-Fi AP subnet + DHCP pool for glasses |
portal-pcp.env |
/etc/default/portal-pcp |
PORTAL_UID |
PCP daemon user override |
Critical hardening note: Since v0.2.1, PORTAL_STREAM_TARGET and PORTAL_VENV_DIR have no hardcoded fallbacks. The binaries exit with a clear error message if these are missing, preventing deployment-specific IPs or paths from being baked into source code.
Sources: portal/systemd/portal-stream.env.example#L1-L22, portal/systemd/portal-voice.env.example#L1-L23, portal/network/portal-network.env.example#L1-L22
Step 5: Verify the Deployment
Section titled “Step 5: Verify the Deployment”After deploying and rebooting (or restarting services), verify that all components are operational.
Check All Services
Section titled “Check All Services”ssh hp "systemctl status portal portal-input portal-stream portal-network \ portal-wm portal-pcp portal-dbus portal-launcher portal-voice portal-llm \ portal-health-monitor wlo1-watchdog portal-binfmt-fix"Verify the Compositor
Section titled “Verify the Compositor”# Wayfire should own both eDP-1 and GLASSES-1 outputsssh hp "WAYLAND_DISPLAY=wayland-1 XDG_RUNTIME_DIR=/run/portal wlr-randr"# Expected: eDP-1 (2240×1400) + GLASSES-1 (2560×1080)
# Confirm streaming is active (60 fps, zero drops)ssh hp "journalctl -u portal-stream --no-pager -n 20 | grep -i fps"Verify the Network AP
Section titled “Verify the Network AP”# Check hostapd is running and glasses can connectssh hp "systemctl status portal-network"# The AP broadcasts as SSID "Spaceboard" on 5 GHz channel 36Verify Voice and LLM (NPU)
Section titled “Verify Voice and LLM (NPU)”# Check that DSP contexts initializedssh hp "journalctl -u portal-voice --no-pager -n 30 | grep -i 'qnn\|vad\|tts'"ssh hp "journalctl -u portal-llm --no-pager -n 20 | grep -i 'geniex\|qwen'"Quick Health Dashboard
Section titled “Quick Health Dashboard”| Check | Command | Expected Result |
|---|---|---|
| All services active | ssh hp "systemctl is-active portal*" |
All return active |
| Wayland socket exists | ssh hp "ls /run/portal/wayland-1" |
Socket file present |
| PCP IPC socket | ssh hp "ls /run/portal/pcp.sock" |
Socket file present |
| LLM IPC socket | ssh hp "ls /run/portal/llm.sock" |
Socket file present |
| Wi-Fi AP up | ssh hp "ip addr show uap0" |
192.168.50.1/24 assigned |
| Streaming at 60fps | journalctl -u portal-stream |
60.0 fps with zero drops |
| No crash loops | journalctl -u portal-health-monitor |
No “3+ restarts” warnings |
Sources: docs/x-elite-deployment-state.md#L29-L50, docs/x-elite-deployment-state.md#L247-L248
Iterative Development Workflow
Section titled “Iterative Development Workflow”The recommended day-to-day loop is to write code on your local machine, then build and deploy on the Spaceboard via SSH. The justfile provides shorthand commands for common build operations.
flowchart LR
CODE["Edit code<br/>(local machine)"] --> SYNC["Sync to Spaceboard<br/>(git push/pull or rsync)"]
SYNC --> BUILD["Build on Spaceboard<br/>ssh hp 'cargo build -p <crate> --release'"]
BUILD --> DEPLOY["Deploy binary<br/>scp + systemctl restart"]
DEPLOY --> TEST["Verify<br/>journalctl + wlr-randr"]
TEST -->|iterate| CODE
Using just for Build Automation
Section titled “Using just for Build Automation”The justfile wraps common build commands with the just task runner:
# On the Spaceboardjust build-rust # Full workspace (ARM64 target)just build-crate portal-wm # Single cratejust build-cpp # C++ Wayfire pluginsjust build-all # Everythingjust test # Run workspace testsjust clean # Remove all build artifactsSources: justfile#L1-L67, README.md#L129-L133
Local Smoke Testing with the ARM64 Sandbox
Section titled “Local Smoke Testing with the ARM64 Sandbox”When you need to quickly test an ARM64 binary on your x86_64 development machine (macOS or Linux), use the QEMU-based Docker sandbox. This is useful for catching crashes or logic errors before deploying to the Spaceboard, though it cannot exercise hardware-specific paths (GPU, NPU, DRM).
# Cross-compile for ARM64 (from your dev machine)cargo build --target aarch64-unknown-linux-gnu --release
# Smoke-test in the ARM64 sandbox./run_sandbox.sh ./target/aarch64-unknown-linux-gnu/release/portal-wm --helpThe sandbox runs arm64v8/debian:bookworm with QEMU user-mode emulation, matching the glibc and libstdc++ versions on the Spaceboard. It is explicitly a smoke test environment — not a substitute for building and running on the actual hardware.
Sources: run_sandbox.sh#L1-L29, docs/ARM64_Sandbox_Environment_Setup.md#L28-L55
Cross-Compilation (CI Only)
Section titled “Cross-Compilation (CI Only)”The repository includes Cross.toml and CI workflows for cross-compilation via cross-rs, but the README explicitly warns against cross-compiling from a Mac for production builds. The Spaceboard’s native toolchain has all the system libraries (Wayland, GStreamer, QNN) that cross-compilation would need to replicate, making native builds simpler and more reliable.
| Build Method | Tool | Use Case |
|---|---|---|
| Native on Spaceboard | cargo build --release |
Production builds, daily development |
Cross-compile via cross |
cross build --target aarch64-unknown-linux-gnu |
CI pipeline only |
| Sandbox smoke test | run_sandbox.sh |
Quick binary validation on x86_64 dev machine |
Sources: Cross.toml#L1-L13, .github/workflows/cross-compile.yml#L1-L28, README.md#L36-L37
Troubleshooting Common Build and Deploy Issues
Section titled “Troubleshooting Common Build and Deploy Issues”| Symptom | Root Cause | Fix |
|---|---|---|
cargo build fails with missing system libs |
Wayland/GStreamer dev packages not installed | sudo apt install libwayland-dev libgstreamer1.0-dev ... |
| Voice daemon silently does nothing | Missing npu-stt feature flag |
Rebuild with --features "qnn-tts,qnn-vad,npu-stt,network-audio" |
portal-stream exits immediately |
PORTAL_STREAM_TARGET unset in env |
Copy portal-stream.env.example to /etc/portal/portal-stream.env |
| Wayfire starts but no windows appear | GLASSES-1 output positioned wrong | Check /etc/portal/wayfire.ini has [output:GLASSES-1] position = 0,0 |
| Wine apps fail with QEMU errors | qemu-x86_64 shadowing box64 in binfmt_misc |
portal-binfmt-fix.service should handle this; verify it’s active |
| Services restart in a loop | systemd-logind deletes /run/user/1000 socket |
All services use /run/portal as XDG_RUNTIME_DIR — verify portal.service ExecStartPre creates it |
| Wi-Fi AP drops under load | ath11k concurrent STA+AP driver bug | wlo1-watchdog.service auto-reconnects every 10s; check it’s enabled |
Sources: docs/x-elite-deployment-state.md#L222-L284, portal/compositor/wayfire.ini#L1-L48, portal/systemd/portal-stream.service#L13-L18
What’s Next
Section titled “What’s Next”Now that you have Portal building and running on the Spaceboard, explore the deeper architecture and development workflows:
- Repository Structure and Workspace Layout — Understand the crate hierarchy, shared dependencies, and how subsystems relate.
- Development Workflow: Local Coding to Remote Deployment — Master the day-to-day SSH-based development loop with advanced rsync and hot-reload patterns.
- ARM64 Sandbox Environment Setup — Configure the QEMU Docker sandbox for faster iteration without hardware.
- Systemd Service Management and Network Topology — Deep dive into all 15 service units, their security hardening, and the Wi-Fi AP network design.
- Dual-Display Wayland Architecture — Understand how Wayfire manages the eDP-1 panel and virtual GLASSES-1 output.