Skip to content

Development Workflow: Local Coding to Remote Deployment

The Portal project spans two hardware architectures — your x86_64 development machine and the ARM64 Spaceboard (Snapdragon X Elite) or Orange Pi 5 Max target — connected by a multi-stage pipeline that moves code from your editor through automated quality gates, cross-compilation, remote smoke testing, cryptographic signing, and finally systemd-based deployment on the target device. This page walks you through every stage of that journey, showing you exactly which commands to run, what each script does, and where the automated guardrails protect you from mistakes.

Sources: justfile#L1-L67, Cargo.toml#L1-L272, rust-toolchain.toml#L1-L4

Before diving into individual steps, here is the complete pipeline from the moment you write code to the moment it runs on the Spaceboard:

flowchart TD
    A["Write Code<br/>(macOS / Linux x86_64)"] --> B["Pre-commit Hooks<br/>Gitleaks + TruffleHog"]
    B --> C["Local Checks<br/>cargo fmt, clippy, test"]
    C --> D["Push Feature Branch"]
    D --> E["CI Quality Gates<br/>fmt · clippy · docs · coverage<br/>test · miri · semver · security"]
    E --> F{"All Checks Pass?"}
    F -->|No| A
    F -->|Yes| G["Pull Request to<br/>x-elite-spaceboard"]
    G --> H{"Guard Main:<br/>Approved + CI Green?"}
    H -->|No| A
    H -->|Yes| I["Merge to x-elite-spaceboard"]
    I --> J["Sync to Spaceboard<br/>sync-to-hp.sh"]
    J --> K["Native Build + Smoke Test<br/>hp_smoke_test.sh"]
    K --> L{"Smoke Pass?"}
    L -->|No| A
    L -->|Yes| M["Tag Release v*.*.*"]
    M --> N["Release CI:<br/>Cross-compile + Sign + SBOM"]
    N --> O["GitHub Release<br/>with Signed Artifacts"]
    O --> P["Deploy to Spaceboard<br/>deploy-spaceboard.sh"]
    P --> Q["Systemd Services<br/>Live on Hardware"]

Each box in this diagram corresponds to a concrete file, script, or workflow in the repository. The sections below walk through them in order.

Sources: .github/workflows/check.yml#L1-L87, scripts/sync-to-hp.sh#L1-L22, scripts/hp_smoke_test.sh#L1-L126

Stage 1: Setting Up Your Local Environment

Section titled “Stage 1: Setting Up Your Local Environment”

The project pins Rust 1.97.0 with rustfmt, clippy, and rust-src as mandatory components. This version is declared in a rust-toolchain.toml file, which rustup reads automatically — you do not need to manually run rustup install.

Sources: rust-toolchain.toml#L1-L4

Portal uses a single Cargo workspace containing over 40 crates organized by subsystem. The root Cargo.toml defines all member crates and shared dependencies. The key architectural groups are:

Group Representative Crates Purpose
Compositor & Spatial portal-spatial, portal-spatial-plugin, portal-warp, portal-wm Wayland compositor integration and spatial rendering
Input portal-input Touch fusion, keyboard daemon, virtual input devices
Voice & AI portal-voice, portal-llm, portal-npu-runtime VAD, STT, TTS, on-device LLM, NPU wrappers
Streaming portal-stream H.265 hardware encode → RTP/UDP pipeline
Application Framework portal-context, portal-launcher, portal-shell, portal-style Context engine, universal launcher, design tokens
PCP portal/pcp/* (18 crates) Portal Capability Protocol — IPC, registry, daemon
Shared portal-common Common types, utilities

Sources: Cargo.toml#L3-L45

The workspace enforces a strict clippy policy at the workspace level. Production code (libraries and binaries) is held to the highest bar: correctness, suspicious, style, complexity, perf, and pedantic categories are all set to deny. Additionally, unwrap_used, expect_used, and panic_in_result_fn are denied to force proper error handling via Result. Test code receives a deliberately permissive policy because tests should panic on setup failure as the test signal.

Sources: Cargo.toml#L166-L214, .github/workflows/check.yml#L18-L46

Before any change enters git history, two independent secret-scanning tools run in parallel:

Layer Tool Mode Speed Strategy
Layer 1 Gitleaks v8.30.1 Pattern + entropy Milliseconds Scans only staged changes
Layer 2 TruffleHog v3.95.3 Deep verification Slower Scans diff from HEAD, verifies secrets against live APIs

If either layer detects a secret, the commit is blocked. This dual-layer approach catches both fast false-positive-prone patterns (Layer 1) and slow but near-zero-false-positive verified secrets (Layer 2).

Sources: .pre-commit-config.yaml#L1-L35

The justfile provides shorthand commands for the most common development tasks. Install just with cargo install just, then run just (with no arguments) to see all available recipes.

Command What It Does
just build-rust Cross-compile all Rust crates for ARM64 (aarch64-unknown-linux-gnu) in release mode
just build-crate <name> Build a specific crate for ARM64
just build-cpp Build Wayfire C++ plugins via CMake
just build-voiced Build the voice daemon with ALSA fallback features
just build-all Run all build targets in sequence
just test Run cargo test --workspace --all-features
just clean Remove all build artifacts

For day-to-day development, you will typically run these commands natively on your x86_64 machine before pushing:

Terminal window
# Format check
cargo fmt --all -- --check
# Lint (production-grade strict)
cargo clippy --lib --bins --workspace --all-features -- -D warnings
# Tests
cargo test --lib --workspace --all-features

Sources: justfile#L1-L67, .github/workflows/check.yml#L8-L16

Since the target hardware is ARM64, you can test cross-compiled binaries locally using a Docker-based QEMU emulation sandbox. The run_sandbox.sh script spins up an arm64v8/debian:bookworm container that matches the Spaceboard’s OS environment exactly:

Terminal window
# Test a cross-compiled binary locally before deploying
./run_sandbox.sh ./target/aarch64-unknown-linux-gnu/release/portal-wm --help

The sandbox sets IS_SANDBOX=1 and TARGET_ARCH=arm64 environment variables so your code can detect it is running under emulation.

Sources: run_sandbox.sh#L1-L29, docs/ARM64_Sandbox_Environment_Setup.md#L5-L11

When you push a branch or open a pull request, nine automated workflows run in parallel. Every workflow must pass before code reaches the main branch.

flowchart LR
    PR["Push / Pull Request"] --> W1["Check<br/>fmt + clippy + docs<br/>+ coverage + clippy-arm64"]
    PR --> W2["Test<br/>lib + integration tests"]
    PR --> W3["Cross-Compile<br/>ARM64 release build"]
    PR --> W4["Security<br/>cargo-audit + cargo-deny"]
    PR --> W5["Semver<br/>API compatibility"]
    PR --> W6["Miri<br/>UB scan on FFI crates"]
    PR --> W7["Gitleaks<br/>Secret scan"]
    PR --> W8["Guard Main<br/>Branch protection"]
CI Job Gate Failure Condition Notes
fmt cargo fmt --all -- --check Any unformatted code Pinned to stable toolchain
clippy (production) cargo clippy --lib --bins -- -D warnings Any clippy warning in production code Strict: zero tolerance for unwrap/expect/panic
clippy (tests) cargo clippy --tests -- -D warnings -A ... Any clippy warning in tests (except allowed) Permissive on test-specific patterns
docs cargo doc --no-deps --all-features with RUSTDOCFLAGS: -D warnings Any rustdoc warning All features enabled
coverage cargo llvm-cov --fail-under-lines 85 Line coverage below 85% Floor accounts for FFI boundary gaps
clippy-arm64 cross clippy --target aarch64-unknown-linux-gnu Any clippy warning on ARM64 Currently continue-on-error: true (advisory)

Sources: .github/workflows/check.yml#L1-L87

The test pipeline runs in three stages: library unit tests (--lib), integration tests (--test '*'), and documentation tests (--doc). Doc tests currently run with continue-on-error: true while intra-doc links are being stabilized.

Sources: .github/workflows/test.yml#L1-L26

Workflow Tool Frequency Purpose
Security cargo-audit Push + PR + Daily cron RustSec advisory database scan
Security cargo-deny Push + PR + Daily cron Advisories + licenses + bans + sources
Miri cargo +nightly miri test Push + PR Undefined Behavior scan on FFI-heavy crates (npu-runtime, spatial, input, voice)
Gitleaks Gitleaks Action Push + PR Full-history secret scan with fetch-depth: 0
Semver cargo-semver-checks Push + PR API compatibility regression detection (advisory, non-blocking)

Sources: .github/workflows/security.yml#L1-L45, .github/workflows/miri.yml#L1-L51, .github/workflows/gitleaks-scan.yml#L1-L26, .github/workflows/semver.yml#L1-L24

The deny.toml file controls cargo-deny’s behavior. It denies all advisory classes (vulnerabilities, unmaintained, unsound, yanked) by default. Each ignored advisory carries a documented reason and a re-verification timestamp. License compliance is enforced with a confidence threshold of 0.93 and an explicit allow-list of 14 approved license types.

Sources: deny.toml#L1-L21, deny.toml#L23-L41

Stage 4: Branch Protection and Pull Request Flow

Section titled “Stage 4: Branch Protection and Pull Request Flow”

Since the repository runs on GitHub Free (which does not support native branch protection for private repos), a software-enforced Guard Main workflow provides equivalent protection.

flowchart TD
    PUSH["Push to x-elite-spaceboard"] --> IS_BOT{"Is bot push?"}
    IS_BOT -->|Yes| SKIP["Skip (prevent loops)"]
    IS_BOT -->|No| IS_MERGE{"Is PR merge?"}
    IS_MERGE -->|No, direct push| REVERT["Auto-revert commit<br/>Create security issue"]
    IS_MERGE -->|Yes| APPROVED{"Was PR approved?<br/>(repo owner exempt)"}
    APPROVED -->|No| ISSUE["Create violation issue"]
    APPROVED -->|Yes| CI_PASSED{"Did CI checks pass?"}
    CI_PASSED -->|No| ISSUE
    CI_PASSED -->|Yes| ALLOW["Allow commit"]
    CI_PASSED -->|Unknown| ALLOW

The rules enforced are:

  1. Direct pushes to x-elite-spaceboard are automatically reverted and a security issue is filed with the security-violation label.
  2. PR merges require at least one approval — except PRs authored by the repo owner (single-developer allowance).
  3. CI checks must pass — any GitHub Actions check suite with conclusion: failure blocks the merge.
  4. Bot pushes and revert commits are skipped to prevent infinite guard-revert loops.

Sources: .github/workflows/guard-main.yml#L1-L227

The project targets aarch64-unknown-linux-gnu (ARM64 Linux). Cross-compilation uses the cross tool (v0.2.5) with Docker-based toolchain images.

The cross-compilation setup spans three files:

File Role
Cross.toml#L1-L13 Defines Docker images for ARM64 and ARMv7 targets, passes through RUSTFLAGS and PKG_CONFIG variables
.cargo/config.toml#L1-L3 Sets aarch64-linux-gnu-gcc as the linker for the aarch64-unknown-linux-gnu target
scripts/deploy/aarch64-toolchain.cmake#L1-L14 CMake cross-compilation toolchain for C++ Wayfire plugins

The Cross-Compile workflow builds the entire workspace in release mode for ARM64 and uploads the resulting binaries as GitHub Actions artifacts:

Artifact Source Crate Type
libportal_spatial.so portal-spatial Shared library (cdylib)
portal-wm portal-wm Binary
portal-voiced portal-voice Binary
portal-llmd portal-llm Binary
portal-keyboardd portal-input Binary

Sources: .github/workflows/cross-compile.yml#L1-L28, Cross.toml#L1-L13, .cargo/config.toml#L1-L3

After merging to x-elite-spaceboard, the sync-to-hp.sh script uses rsync to push the source tree from your Mac worktree to the HP Spaceboard. It excludes build artifacts, git metadata, and editor-specific directories:

Terminal window
# From your Mac worktree:
./scripts/sync-to-hp.sh

The script syncs two things: the portal/ directory (all Rust source) and the workspace root files (Cargo.toml, Cargo.lock, deny.toml, rust-toolchain.toml).

Sources: scripts/sync-to-hp.sh#L1-L22

The Spaceboard (HP EliteBook Ultra G1q with Snapdragon X Elite) builds the project natively — no cross-compilation needed. This is critical because the Hexagon DSP, Qualcomm IRIS encoder, and Adreno GPU libraries are only available on the target device. The hp_smoke_test.sh script orchestrates this:

Terminal window
# Full build + smoke test (run from Mac, SSHs into Spaceboard):
./scripts/hp_smoke_test.sh
# Smoke test only (assumes already built):
./scripts/hp_smoke_test.sh --skip-build

The smoke test verifies each binary in sequence:

Check Binary Verification Method
Workspace build All crates cargo build --workspace --release
Voice daemon build portal-voiced Build with 5 QNN features enabled
LLM daemon build portal-llmd Build with hexagon feature
Keyboard daemon build portal-keyboardd Build with keyboardd feature
Binary existence All 5 binaries test -x on each
Startup probe portal-wm, portal-voiced, portal-llmd timeout 5 <bin> --help (clap exits 0)
Unit tests npu-runtime, spatial, input, voice cargo test -p <crate> --lib
C tests portal-stream (C) make test in portal/streaming
Systemd probe portal-llm systemctl is-active or is-activating

Sources: scripts/hp_smoke_test.sh#L1-L126, docs/x-elite-deployment-state.md#L1-L23

For production verification, the hp-smoke-suite.sh script performs live hardware checks that go beyond binary buildability:

  1. Six services active — verifies portal, portal-input, portal-stream, portal-voice, portal-llm, portal-pcp are all active (running)
  2. Voice UDP probe — sends a 4-byte probe to 127.0.0.1:5601 to confirm the voice daemon’s audio socket is bound
  3. Keyboard DRM scanout — checks /sys/class/drm/card0-eDP-1/modes is non-empty (panel initialized)
  4. Spatial IPC handshake — connects to /run/portal/spatial.sock and sends a QueryStatus message
  5. Service restart resilience — cycles portal-input three times; each must return to active within 10 seconds (catches DSP/DRM fd leaks)

Sources: scripts/hp-smoke-suite.sh#L1-L200

When you are ready to publish a release, the workflow follows a strict cryptographic chain.

Releases are triggered by pushing a git tag matching v* (e.g., git tag v0.2.1 && git push origin v0.2.1). This activates the Release workflow.

Sources: .github/workflows/release.yml#L1-L5

flowchart TD
    TAG["Tag Push: v*.*.*"] --> BUILD["Cross-compile<br/>ARM64 release build"]
    BUILD --> SBOM["Generate SBOM<br/>CycloneDX JSON + XML<br/>(cargo-cyclonedx 0.5.9)"]
    SBOM --> COSIGN["Cosign Signing<br/>Keyless Ed25519 via GitHub OIDC<br/>.sig + .pem + .bundle"]
    COSIGN --> STAGE["Stage Artifacts<br/>binaries + SBOMs + signatures"]
    STAGE --> MINISIGN{"MINISIGN_SECRET_KEY<br/>set in CI?"}
    MINISIGN -->|Yes| SIGN_MIN["Minisign Signing<br/>Ed25519 signatures<br/>on all artifacts"]
    SIGN_MIN --> VERIFY["Round-trip verify<br/>verify_release.sh"]
    MINISIGN -->|No| SKIP_MIN["Skip minisign<br/>(cosign signatures sufficient)"]
    VERIFY --> GH_REL["Publish GitHub Release<br/>with all assets"]
    SKIP_MIN --> GH_REL

Every release binary receives two independent signature types:

Signature System Algorithm Identity Transparency Artifacts Produced
Cosign (Sigstore) Ed25519 GitHub OIDC (keyless) Rekor transparency log .sig, .pem, .bundle
Minisign Ed25519 Offline secret key None (trust-on-first-use) .minisig

The dual-signing approach provides defense in depth: cosign provides automated, auditable keyless signing in CI, while minisign provides offline signing with a key generated on an air-gapped machine. The minisign step is optional in CI — if the MINISIGN_SECRET_KEY secret is not configured, cosign signatures alone are produced.

Sources: .github/workflows/release.yml#L52-L107, scripts/sign_release.sh#L1-L168, scripts/verify_release.sh#L1-L143

Software Bills of Materials are generated using cargo-cyclonedx 0.5.9 in both CycloneDX JSON and XML formats (spec version 1.5). One SBOM file is produced per crate. The scripts/generate_sbom.sh script collects them into target/sbom/ for clean release uploads.

Sources: .github/workflows/release.yml#L34-L39, scripts/generate_sbom.sh#L1-L127

Each GitHub Release includes five production binaries, each with up to five sidecar files:

Binary Purpose Cosign Files Minisign File
portal-voiced Voice pipeline daemon (VAD + STT + TTS + NLU) .sig, .pem, .bundle .minisig
portal-llmd On-device LLM daemon (GenieX llama.cpp on Hexagon) .sig, .pem, .bundle .minisig
portal-keyboardd DRM/KMS keyboard daemon with Cairo rendering .sig, .pem, .bundle .minisig
portal-wm Window manager daemon (Wayland toplevel tracking) .sig, .pem, .bundle .minisig
portal-stream H.265 streaming C binary .sig, .pem, .bundle .minisig

Sources: .github/workflows/release.yml#L109-L136, scripts/sign_release.sh#L109-L115

The release signing keypair follows a strict offline policy:

Item Rule
Public keys (*.pub) Safe to commit — tracked in git at keys/portal-release.pub
Secret keys (*.sec) Must never be committed — stored offline (1Password, hardware token, air-gapped media)
Key password Entered interactively when signing — never piped via stdin, env var, or CLI flag
Key algorithm minisign Ed25519
Key rotation On compromise or annual cadence, whichever is sooner

Sources: keys/README.md#L1-L60

The deploy-spaceboard.sh script is a 12-step idempotent installer that sets up a fresh Spaceboard from scratch. Run it as root on the target device:

Terminal window
sudo ./portal/scripts/deploy-spaceboard.sh
Step Action Details
1 Create directories /run/portal, /var/lib/portal, user data dirs
2 NetworkManager config dns=none, uap0 interface
3 Mask conflicting services dnsmasq, getty@tty1
4 Set default target multi-user.target (no display manager)
5 Portal config Wayfire init at /etc/portal/wayfire.ini
6 Network configs hostapd (5GHz AP) + dnsmasq DHCP
7 Systemd services 13 Portal services + 2 watchdogs
8 Utility scripts File watcher, Wine monitor, health monitor
9 Patched hostapd ath11k EALREADY/EBUSY fix
10 System software box64, LibreOffice, inotify-tools, Wine
11 Log files /var/log/portal-*.log
12 Enable services systemctl daemon-reload + enable all

Sources: portal/scripts/deploy-spaceboard.sh#L1-L100

For the legacy Orange Pi 5 Max target, the deployment process is more manual and documented in scripts/deploy/README.md. The key difference is that Rust libraries are cross-compiled on the build machine, while C++ shims must be built on the Orange Pi itself:

Terminal window
# 1. Transfer files
rsync -avz scripts/deploy/ orangepi@<ip>:/tmp/portal-deploy/
# 2. Install libraries on Orange Pi
ssh orangepi@<ip>
sudo cp /tmp/portal-deploy/libportal_spatial.so /usr/local/lib/
sudo ldconfig
# 3. Build C++ shims on the Orange Pi
cd /tmp/portal-deploy/../wayfire-plugins
cmake -B build .
cmake --build build
# 4. Install plugins
sudo cp build/portal-spatial.so /usr/lib/wayfire/
sudo cp build/portal-spatial-warp.so /usr/lib/wayfire/

Sources: scripts/deploy/README.md#L72-L116, scripts/deploy/deploy.sh#L1-L39

The Portal compositor requires a non-standard 2560×1080 resolution on HDMI-A-2 for the glasses output. The ensure-hdmi-a2-resolution.sh script idempotently patches /boot/orangepiEnv.txt with the kernel cmdline parameter video=HDMI-A-2:2560x1080@60e (the e suffix forces the mode even when EDID does not advertise it). A reboot is required after applying.

Sources: scripts/deploy/ensure-hdmi-a2-resolution.sh#L1-L82, scripts/deploy/README.md#L165-L187

Once a release is published, two synchronization pipelines keep downstream repositories updated:

The sync-to-public.sh script creates a squashed single-commit tree of the current release and force-pushes it to Demain-Technology/Portal-OS. The public repo gets exactly one commit per release — clean history with no intermediate work visible.

Sources: scripts/sync-to-public.sh#L1-L79

Two GitHub Actions workflows provide event-driven synchronization:

  • Sync to demain-deenson (sync-to-demain.yml): When a PR is opened or merged, the workflow extracts mirror issue references from the PR body (e.g., closes #123) and dispatches events to the demain-deenson/portal repository.
  • Notify Release (notify-release.yml): When a GitHub Release is published, the workflow dispatches a release-published event to the board repository with the tag name, body, and URL.

Sources: .github/workflows/sync-to-demain.yml#L1-L90, .github/workflows/notify-release.yml#L1-L34

Quick Reference: Complete Command Sequence

Section titled “Quick Reference: Complete Command Sequence”

For a developer making a change end-to-end, here is the full sequence of commands:

Terminal window
# 1. Create feature branch
git checkout -b feat/my-change
# 2. Write code, then run local checks
cargo fmt --all
cargo clippy --lib --bins --workspace --all-features -- -D warnings
cargo test --lib --workspace --all-features
# 3. Commit (pre-commit hooks run automatically)
git add -A && git commit -m "feat: my change"
# 4. Push and open PR
git push origin feat/my-change
# CI workflows run: check, test, cross-compile, security, miri, semver, gitleaks
# 5. After CI passes and PR is approved, merge to x-elite-spaceboard
# Guard Main verifies approval + CI green
# 6. Sync to Spaceboard and smoke test
./scripts/sync-to-hp.sh
./scripts/hp_smoke_test.sh
# 7. Tag release
git tag v0.2.X && git push origin v0.2.X
# Release CI builds, signs (cosign + minisign), generates SBOM, publishes
# 8. Sync to public repo
./scripts/sync-to-public.sh v0.2.X
# 9. Deploy to Spaceboard (on the device)
sudo ./portal/scripts/deploy-spaceboard.sh

Sources: justfile#L1-L67, scripts/sync-to-hp.sh#L1-L22, .github/workflows/release.yml#L1-L5, portal/scripts/deploy-spaceboard.sh#L1-L100

Now that you understand the complete development pipeline, these pages dive deeper into specific stages: