From 038d59f97246d87ad0dbc3e0e8c04ca959fd6b15 Mon Sep 17 00:00:00 2001 From: Hayodea Hekol Date: Thu, 25 Jun 2026 23:01:52 -0400 Subject: [PATCH] Add distro/ubuntuCore for UC26 snap and image builds. Centralize salmanoff snapcraft, dangerous-model image scripts, and QEMU workflow so UC26 can be reproduced from the SMO repo without ubuntu-core-practice. Co-authored-by: Cursor --- distro/ubuntuCore/.gitignore | 30 ++ .../ubuntuCore/config/dev-image.env.example | 16 + distro/ubuntuCore/config/qemu-stock.env | 6 + .../ubuntuCore/config/snap-source.env.example | 3 + .../models/salmanoff-dev-amd64.model.json | 43 +++ distro/ubuntuCore/scripts/build-dev-image.sh | 133 +++++++++ distro/ubuntuCore/scripts/build-snap.sh | 78 +++++ .../ubuntuCore/scripts/build-stock-image.sh | 96 ++++++ distro/ubuntuCore/scripts/fetch-model.sh | 12 + distro/ubuntuCore/scripts/fix-lxd-network.sh | 68 +++++ distro/ubuntuCore/scripts/run-qemu.sh | 105 +++++++ .../ubuntuCore/scripts/setup-dev-signing.sh | 120 ++++++++ .../ubuntuCore/scripts/sign-dev-assertions.sh | 106 +++++++ distro/ubuntuCore/scripts/test-snap.sh | 74 +++++ .../snaps/salmanoff/bin/salmanoff-launch | 20 ++ .../ubuntuCore/snaps/salmanoff/snapcraft.yaml | 109 +++++++ distro/ubuntuCore/ubuntu-core-plan.md | 276 ++++++++++++++++++ 17 files changed, 1295 insertions(+) create mode 100644 distro/ubuntuCore/.gitignore create mode 100644 distro/ubuntuCore/config/dev-image.env.example create mode 100644 distro/ubuntuCore/config/qemu-stock.env create mode 100644 distro/ubuntuCore/config/snap-source.env.example create mode 100644 distro/ubuntuCore/models/salmanoff-dev-amd64.model.json create mode 100755 distro/ubuntuCore/scripts/build-dev-image.sh create mode 100755 distro/ubuntuCore/scripts/build-snap.sh create mode 100755 distro/ubuntuCore/scripts/build-stock-image.sh create mode 100755 distro/ubuntuCore/scripts/fetch-model.sh create mode 100755 distro/ubuntuCore/scripts/fix-lxd-network.sh create mode 100755 distro/ubuntuCore/scripts/run-qemu.sh create mode 100755 distro/ubuntuCore/scripts/setup-dev-signing.sh create mode 100755 distro/ubuntuCore/scripts/sign-dev-assertions.sh create mode 100755 distro/ubuntuCore/scripts/test-snap.sh create mode 100755 distro/ubuntuCore/snaps/salmanoff/bin/salmanoff-launch create mode 100644 distro/ubuntuCore/snaps/salmanoff/snapcraft.yaml create mode 100644 distro/ubuntuCore/ubuntu-core-plan.md diff --git a/distro/ubuntuCore/.gitignore b/distro/ubuntuCore/.gitignore new file mode 100644 index 0000000..8bfd31d --- /dev/null +++ b/distro/ubuntuCore/.gitignore @@ -0,0 +1,30 @@ +# Image build outputs (large) +artifacts/images/ +artifacts/images-dev/ +artifacts/logs/ + +# ubuntu-image scratch +work/ubuntu-image-snap/ +work/ubuntu-image-dev/ + +# Dev signing outputs (account-specific) +models/salmanoff-dev-amd64.model +models/ubuntu-core-26-amd64.model +assertions/ +config/dev-image.env +config/ssh/smo-dev +config/ssh/smo-dev.pub + +# QEMU writable UEFI vars +artifacts/firmware/OVMF_VARS_4M.ms.fd + +# snapcraft working tree +snaps/salmanoff/parts/ +snaps/salmanoff/stage/ +snaps/salmanoff/prime/ +snaps/salmanoff/.snapcraft/ +snaps/salmanoff/*.snap + +# Editor / OS noise +*~ +.DS_Store diff --git a/distro/ubuntuCore/config/dev-image.env.example b/distro/ubuntuCore/config/dev-image.env.example new file mode 100644 index 0000000..a0f2294 --- /dev/null +++ b/distro/ubuntuCore/config/dev-image.env.example @@ -0,0 +1,16 @@ +# Copy to config/dev-image.env and adjust after setup-dev-signing.sh. +# +# ACCOUNT_ID comes from: snapcraft whoami (or snap whoami when logged in) +# SIGN_KEY_NAME comes from: snap keys (after snapcraft create-key + register-key) + +ACCOUNT_ID= +SIGN_KEY_NAME=salmanoff-dev + +# System user created on first boot (no Ubuntu One / console-conf SSO). +SYSTEM_USER_NAME=smo +SYSTEM_USER_EMAIL=smo-dev@salmanoff +# Public key used in the system-user assertion (private key stays local). +SSH_PUBKEY_FILE=config/ssh/smo-dev.pub + +# Dangerous-grade dev model (UC26 amd64). salmanoff is optional until --snap is passed. +MODEL_NAME=salmanoff-dev-amd64 diff --git a/distro/ubuntuCore/config/qemu-stock.env b/distro/ubuntuCore/config/qemu-stock.env new file mode 100644 index 0000000..bb99e2e --- /dev/null +++ b/distro/ubuntuCore/config/qemu-stock.env @@ -0,0 +1,6 @@ +# Defaults for scripts/run-qemu.sh (source manually if you want overrides) +RAM_MB=2048 +SMP=2 +SSH_PORT=8022 +OVMF_CODE=/usr/share/OVMF/OVMF_CODE_4M.secboot.fd +OVMF_VARS_TEMPLATE=/usr/share/OVMF/OVMF_VARS_4M.ms.fd diff --git a/distro/ubuntuCore/config/snap-source.env.example b/distro/ubuntuCore/config/snap-source.env.example new file mode 100644 index 0000000..51a1972 --- /dev/null +++ b/distro/ubuntuCore/config/snap-source.env.example @@ -0,0 +1,3 @@ +# Optional: remote git source if snapcraft.yaml is switched from local tree to git. +SMO_GIT_URL=git@zbz-gitea-as-hayodea:hayodea/salmanoff.git +SMO_GIT_BRANCH=clast diff --git a/distro/ubuntuCore/models/salmanoff-dev-amd64.model.json b/distro/ubuntuCore/models/salmanoff-dev-amd64.model.json new file mode 100644 index 0000000..a6cd008 --- /dev/null +++ b/distro/ubuntuCore/models/salmanoff-dev-amd64.model.json @@ -0,0 +1,43 @@ +{ + "type": "model", + "authority-id": "@ACCOUNT_ID@", + "brand-id": "@ACCOUNT_ID@", + "series": "16", + "model": "salmanoff-dev-amd64", + "architecture": "amd64", + "base": "core26", + "grade": "dangerous", + "system-user-authority": "*", + "timestamp": "@TIMESTAMP@", + "snaps": [ + { + "name": "pc", + "type": "gadget", + "default-channel": "26/stable", + "id": "UqFziVZDHLSyO3TqSWgNBoAdHbLI4dAH" + }, + { + "name": "pc-kernel", + "type": "kernel", + "default-channel": "26/stable", + "id": "pYVQrBcKmBa0mZ4CCN7ExT6jH8rY1hza" + }, + { + "name": "core26", + "type": "base", + "default-channel": "cloud-init/stable", + "id": "cUqM61hRuZAJYmIS898Ux66VY61gBbZf" + }, + { + "name": "snapd", + "type": "snapd", + "default-channel": "latest/stable", + "id": "PMrrV4ml8uWuEUDBT8dSGnKUYbevVhc4" + }, + { + "name": "salmanoff", + "type": "app", + "presence": "optional" + } + ] +} diff --git a/distro/ubuntuCore/scripts/build-dev-image.sh b/distro/ubuntuCore/scripts/build-dev-image.sh new file mode 100755 index 0000000..67708c8 --- /dev/null +++ b/distro/ubuntuCore/scripts/build-dev-image.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# Build dangerous-grade UC26 image with seeded system-user (no Ubuntu One at first boot). +set -euo pipefail + +UC_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ENV_FILE="${UC_ROOT}/config/dev-image.env" + +MODEL="${UC_ROOT}/models/salmanoff-dev-amd64.model" +SYSTEM_USER_ASSERT="${UC_ROOT}/assertions/smo-system-user.assert" +WORKDIR="${UC_ROOT}/work/ubuntu-image-dev" +OUTPUT_DIR="${UC_ROOT}/artifacts/images-dev" +LOG_DIR="${UC_ROOT}/artifacts/logs" + +usage() { + cat <<'EOF' +Usage: build-dev-image.sh [OPTIONS] + +Build a dangerous-grade ubuntu-core dev image with: + - custom model (salmanoff-dev-amd64) + - system-user assertion seeded (SSH login as smo, no Ubuntu One) + +Prerequisites: + scripts/setup-dev-signing.sh (once: Snap Store login + signing key) + scripts/sign-dev-assertions.sh (sign model + system-user) + +Options: + --snap PATH Extra snap to preinstall (repeatable). Requires grade dangerous. + --fresh-workdir Remove work/ubuntu-image-dev before building + --resume Pass --resume to ubuntu-image + --no-sign Skip sign-dev-assertions.sh (use existing assertions) + -h, --help Show this help + +Outputs: + artifacts/images-dev/pc.img + artifacts/logs/build-dev-.log + +After first boot in QEMU: + ssh -i config/ssh/smo-dev smo@localhost -p 8022 +EOF +} + +fresh_workdir=false +resume=false +no_sign=false +extra_snaps=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --snap) extra_snaps+=("$2"); shift 2 ;; + --fresh-workdir) fresh_workdir=true; shift ;; + --resume) resume=true; shift ;; + --no-sign) no_sign=true; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;; + esac +done + +if [[ "$no_sign" == false ]]; then + "${UC_ROOT}/scripts/sign-dev-assertions.sh" +fi + +if [[ ! -f "$MODEL" ]]; then + echo "Missing signed model: $MODEL" >&2 + exit 1 +fi +if [[ ! -f "$SYSTEM_USER_ASSERT" ]]; then + echo "Missing system-user assertion: $SYSTEM_USER_ASSERT" >&2 + exit 1 +fi + +if ! command -v ubuntu-image >/dev/null 2>&1; then + echo "ubuntu-image not found. Install with: sudo snap install ubuntu-image --classic" >&2 + exit 1 +fi + +mkdir -p "$OUTPUT_DIR" "$LOG_DIR" + +if [[ "$fresh_workdir" == true ]] || [[ "$resume" == false ]]; then + if [[ -d "$WORKDIR" ]]; then + echo "Removing workdir: $WORKDIR" + rm -rf "$WORKDIR" + fi +fi +mkdir -p "$WORKDIR" + +timestamp="$(date +%Y%m%d-%H%M%S)" +log_file="${LOG_DIR}/build-dev-${timestamp}.log" + +cmd=(ubuntu-image snap + --workdir "$WORKDIR" + --output-dir "$OUTPUT_DIR" + --image-size 4G + --assertion "$SYSTEM_USER_ASSERT" + "$MODEL") + +for snap_path in "${extra_snaps[@]}"; do + if [[ ! -f "$snap_path" ]]; then + echo "Snap not found: $snap_path" >&2 + exit 1 + fi + cmd+=(--snap "$snap_path") +done + +if [[ "$resume" == true ]]; then + cmd+=(--resume) +fi + +echo "Model: $MODEL" +echo "System user: $SYSTEM_USER_ASSERT" +echo "Workdir: $WORKDIR" +echo "Output dir: $OUTPUT_DIR" +echo "Log: $log_file" +if [[ ${#extra_snaps[@]} -gt 0 ]]; then + echo "Extra snaps: ${extra_snaps[*]}" +fi +echo "Running: ${cmd[*]}" + +"${cmd[@]}" 2>&1 | tee "$log_file" + +img="${OUTPUT_DIR}/pc.img" +if [[ ! -f "$img" ]]; then + echo "Build finished but $img not found" >&2 + exit 1 +fi + +echo "" +echo "Done. Image: $img" +if [[ -f "$ENV_FILE" ]]; then + # shellcheck source=/dev/null + source "$ENV_FILE" + echo "SSH: ssh -i ${UC_ROOT}/config/ssh/smo-dev ${SYSTEM_USER_NAME:-smo}@localhost -p 8022" +fi +ls -lh "$img" "${img}.seed.manifest" 2>/dev/null || ls -lh "$img" diff --git a/distro/ubuntuCore/scripts/build-snap.sh b/distro/ubuntuCore/scripts/build-snap.sh new file mode 100755 index 0000000..8e258aa --- /dev/null +++ b/distro/ubuntuCore/scripts/build-snap.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# Build the salmanoff snap from the enclosing SMO repo tree. +set -euo pipefail + +UC_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SNAP_DIR="${UC_ROOT}/snaps/salmanoff" + +usage() { + cat <<'EOF' +Usage: build-snap.sh [OPTIONS] + +Build salmanoff snap with snapcraft (source: SMO repo root via local snapcraft.yaml). + +Options: + --refetch-source Clean salmanoff part before pack (re-copy tree + submodules) + --clean Remove all snapcraft parts/prime/stage before building + --lxd Use LXD cleanroom instead of --destructive-mode + -h, --help Show this help + +Prerequisites: + snap install snapcraft --classic + For --lxd on core26: LXD outbound internet (run: sudo scripts/fix-lxd-network.sh) + For --destructive-mode: run on Ubuntu 26.04 or use --lxd +EOF +} + +clean=false +refetch_source=false +use_lxd=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --clean) clean=true; shift ;; + --refetch-source) refetch_source=true; shift ;; + --lxd) use_lxd=true; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;; + esac +done + +if ! command -v snapcraft >/dev/null 2>&1; then + echo "snapcraft not found. Install with: sudo snap install snapcraft --classic" >&2 + exit 1 +fi + +if [[ "$clean" == true ]]; then + rm -rf "${SNAP_DIR}/parts" "${SNAP_DIR}/stage" "${SNAP_DIR}/prime" \ + "${SNAP_DIR}/.snapcraft" "${SNAP_DIR}"/*.snap 2>/dev/null || true +fi + +cd "$SNAP_DIR" + +pack_flags=() +if [[ "$use_lxd" != true ]]; then + pack_flags+=(--destructive-mode) +else + pack_flags+=(--use-lxd) +fi + +if [[ "$refetch_source" == true && "$clean" != true ]]; then + echo "Refetching salmanoff source (clean part + pull)..." + snapcraft clean salmanoff "${pack_flags[@]}" +fi + +cmd=(snapcraft pack "${pack_flags[@]}") + +echo "UC root: $UC_ROOT" +echo "Snap dir: $SNAP_DIR" +echo "Running: ${cmd[*]}" + +"${cmd[@]}" + +snap_file="$(ls -1t "${SNAP_DIR}"/*.snap 2>/dev/null | head -1)" +if [[ -n "$snap_file" ]]; then + echo "" + echo "Built: $snap_file" + ls -lh "$snap_file" +fi diff --git a/distro/ubuntuCore/scripts/build-stock-image.sh b/distro/ubuntuCore/scripts/build-stock-image.sh new file mode 100755 index 0000000..6a915d2 --- /dev/null +++ b/distro/ubuntuCore/scripts/build-stock-image.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# Build stock Ubuntu Core 26 amd64 image from Canonical's signed model assertion. +set -euo pipefail + +UC_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +MODEL="${UC_ROOT}/models/ubuntu-core-26-amd64.model" +WORKDIR="${UC_ROOT}/work/ubuntu-image-snap" +OUTPUT_DIR="${UC_ROOT}/artifacts/images" +LOG_DIR="${UC_ROOT}/artifacts/logs" + +usage() { + cat <<'EOF' +Usage: build-stock-image.sh [OPTIONS] + +Build a stock ubuntu-core-26-amd64 disk image with ubuntu-image snap. + +Options: + --fresh-workdir Remove work/ubuntu-image-snap before building (full re-download) + --resume Pass --resume to ubuntu-image (continue partial build) + -h, --help Show this help + +Outputs: + artifacts/images/pc.img + artifacts/images/pc.img.seed.manifest + artifacts/logs/build-.log + +Host prerequisites: + snap install ubuntu-image --classic + scripts/fetch-model.sh (download signed model if missing) +EOF +} + +fresh_workdir=false +resume=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --fresh-workdir) fresh_workdir=true; shift ;; + --resume) resume=true; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;; + esac +done + +if [[ ! -f "$MODEL" ]]; then + echo "Missing model assertion: $MODEL" >&2 + echo "Run: scripts/fetch-model.sh" >&2 + exit 1 +fi + +if ! command -v ubuntu-image >/dev/null 2>&1; then + echo "ubuntu-image not found. Install with: sudo snap install ubuntu-image --classic" >&2 + exit 1 +fi + +mkdir -p "$OUTPUT_DIR" "$LOG_DIR" + +if [[ "$fresh_workdir" == true ]] || [[ "$resume" == false ]]; then + if [[ -d "$WORKDIR" ]]; then + echo "Removing workdir: $WORKDIR" + rm -rf "$WORKDIR" + fi +fi +mkdir -p "$WORKDIR" + +timestamp="$(date +%Y%m%d-%H%M%S)" +log_file="${LOG_DIR}/build-${timestamp}.log" + +cmd=(ubuntu-image snap + --workdir "$WORKDIR" + --output-dir "$OUTPUT_DIR" + --image-size 4G + "$MODEL") + +if [[ "$resume" == true ]]; then + cmd+=(--resume) +fi + +echo "Model: $MODEL" +echo "Workdir: $WORKDIR" +echo "Output dir: $OUTPUT_DIR" +echo "Log: $log_file" +echo "Running: ${cmd[*]}" + +"${cmd[@]}" 2>&1 | tee "$log_file" + +img="${OUTPUT_DIR}/pc.img" +if [[ ! -f "$img" ]]; then + echo "Build finished but $img not found" >&2 + exit 1 +fi + +echo "" +echo "Done. Image: $img" +ls -lh "$img" "${img}.seed.manifest" 2>/dev/null || ls -lh "$img" diff --git a/distro/ubuntuCore/scripts/fetch-model.sh b/distro/ubuntuCore/scripts/fetch-model.sh new file mode 100755 index 0000000..49b5cfc --- /dev/null +++ b/distro/ubuntuCore/scripts/fetch-model.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Refresh the signed reference model from snapcore/models on GitHub. +set -euo pipefail + +UC_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +MODEL="${UC_ROOT}/models/ubuntu-core-26-amd64.model" +URL="https://raw.githubusercontent.com/snapcore/models/master/ubuntu-core-26-amd64.model" + +mkdir -p "$(dirname "$MODEL")" +curl -fsSL -o "$MODEL" "$URL" +echo "Updated: $MODEL" +head -15 "$MODEL" diff --git a/distro/ubuntuCore/scripts/fix-lxd-network.sh b/distro/ubuntuCore/scripts/fix-lxd-network.sh new file mode 100755 index 0000000..7f2c4f2 --- /dev/null +++ b/distro/ubuntuCore/scripts/fix-lxd-network.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# Restore outbound internet for LXD containers (common Docker + LXD conflict on Ubuntu). +set -euo pipefail + +if [[ "${EUID}" -ne 0 ]]; then + echo "Run with sudo: sudo $0" >&2 + exit 1 +fi + +LXD_BRIDGE="${LXD_BRIDGE:-lxdbr0}" +LXD_SUBNET="$(lxc network get "${LXD_BRIDGE}" ipv4.address 2>/dev/null | cut -d/ -f1 | awk -F. '{print $1"."$2"."$3".0/24"}')" +if [[ -z "${LXD_SUBNET}" || "${LXD_SUBNET}" == ".0/24" ]]; then + LXD_SUBNET="10.239.141.0/24" +fi + +echo "LXD bridge: ${LXD_BRIDGE}" +echo "LXD subnet: ${LXD_SUBNET}" + +echo "==> Allow LXD traffic through Docker's DOCKER-USER chain (if present)" +if iptables -L DOCKER-USER -n &>/dev/null; then + iptables -C DOCKER-USER -i "${LXD_BRIDGE}" -j ACCEPT 2>/dev/null \ + || iptables -I DOCKER-USER 1 -i "${LXD_BRIDGE}" -j ACCEPT + iptables -C DOCKER-USER -o "${LXD_BRIDGE}" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null \ + || iptables -I DOCKER-USER 2 -o "${LXD_BRIDGE}" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT + echo " DOCKER-USER rules added" +else + echo " No DOCKER-USER chain (Docker may not be managing iptables)" +fi + +echo "==> Ensure FORWARD accepts ${LXD_BRIDGE}" +iptables -C FORWARD -i "${LXD_BRIDGE}" -j ACCEPT 2>/dev/null \ + || iptables -I FORWARD 1 -i "${LXD_BRIDGE}" -j ACCEPT +iptables -C FORWARD -o "${LXD_BRIDGE}" -j ACCEPT 2>/dev/null \ + || iptables -I FORWARD 1 -o "${LXD_BRIDGE}" -j ACCEPT + +echo "==> Ensure MASQUERADE for ${LXD_SUBNET}" +if ! iptables -t nat -C POSTROUTING -s "${LXD_SUBNET}" ! -d "${LXD_SUBNET}" -j MASQUERADE 2>/dev/null; then + iptables -t nat -A POSTROUTING -s "${LXD_SUBNET}" ! -d "${LXD_SUBNET}" -j MASQUERADE +fi + +echo "==> LXD network: disable per-network firewall, refresh NAT" +lxc network set "${LXD_BRIDGE}" ipv4.firewall false +lxc network set "${LXD_BRIDGE}" ipv6.firewall false +lxc network set "${LXD_BRIDGE}" ipv4.nat false +lxc network set "${LXD_BRIDGE}" ipv4.nat true + +echo "==> Restart LXD daemon" +if command -v snap >/dev/null 2>&1 && snap list lxd &>/dev/null; then + snap restart lxd +else + systemctl restart lxd || systemctl restart snap.lxd.daemon +fi +sleep 2 + +echo "==> Smoke test (ephemeral container in project snapcraft)" +TEST_NAME="lxd-net-test-$$" +lxc launch ubuntu:26.04 "${TEST_NAME}" --project snapcraft +trap 'lxc delete -f --project snapcraft "${TEST_NAME}" 2>/dev/null || true' EXIT +if lxc exec --project snapcraft "${TEST_NAME}" -- curl -fsSI --max-time 15 https://github.com | head -1; then + echo "OK: container outbound HTTPS works" +else + echo "FAIL: container still cannot reach github.com" >&2 + echo "Consider permanent Docker fix: add \"iptables\": false to /etc/docker/daemon.json and restart docker" >&2 + exit 1 +fi + +echo "" +echo "Done. LXD containers should have outbound internet now." diff --git a/distro/ubuntuCore/scripts/run-qemu.sh b/distro/ubuntuCore/scripts/run-qemu.sh new file mode 100755 index 0000000..c0150c8 --- /dev/null +++ b/distro/ubuntuCore/scripts/run-qemu.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# Boot Ubuntu Core 26 amd64 image in QEMU (UEFI, user networking, SSH on localhost:8022). +set -euo pipefail + +UC_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +IMG="${UC_ROOT}/artifacts/images-dev/pc.img" +FIRMWARE_DIR="${UC_ROOT}/artifacts/firmware" + +OVMF_CODE="${OVMF_CODE:-/usr/share/OVMF/OVMF_CODE_4M.secboot.fd}" +OVMF_VARS_TEMPLATE="${OVMF_VARS_TEMPLATE:-/usr/share/OVMF/OVMF_VARS_4M.ms.fd}" +OVMF_VARS="${FIRMWARE_DIR}/OVMF_VARS_4M.ms.fd" + +RAM_MB="${RAM_MB:-2048}" +SMP="${SMP:-2}" +SSH_PORT="${SSH_PORT:-8022}" + +usage() { + cat <<'EOF' +Usage: run-qemu.sh [OPTIONS] + +Boot a UC26 pc.img in QEMU. + +Options: + --img PATH Disk image (default: artifacts/images-dev/pc.img) + --stock Use stock image artifacts/images/pc.img (console-conf / Ubuntu SSO) + --ram MB RAM in MiB (default: 2048) + --smp N vCPU count (default: 2) + --ssh-port PORT Host port forwarded to guest :22 (default: 8022) + --reset-uefi-vars Recopy writable OVMF vars from host template + -h, --help Show this help + +Dev image (default): SSH after first boot without Ubuntu One: + ssh -i config/ssh/smo-dev smo@localhost -p 8022 + +Stock image (--stock): complete console-conf in the serial console first. +EOF +} + +reset_vars=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --stock) IMG="${UC_ROOT}/artifacts/images/pc.img"; shift ;; + --img) IMG="$2"; shift 2 ;; + --ram) RAM_MB="$2"; shift 2 ;; + --smp) SMP="$2"; shift 2 ;; + --ssh-port) SSH_PORT="$2"; shift 2 ;; + --reset-uefi-vars) reset_vars=true; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;; + esac +done + +if [[ ! -f "$IMG" ]]; then + echo "Image not found: $IMG" >&2 + echo "Build dev image: scripts/build-dev-image.sh" >&2 + echo "Or stock image: scripts/build-stock-image.sh (then run-qemu.sh --stock)" >&2 + exit 1 +fi + +for f in "$OVMF_CODE" "$OVMF_VARS_TEMPLATE"; do + if [[ ! -f "$f" ]]; then + echo "Missing firmware file: $f" >&2 + echo "Install with: sudo apt install ovmf qemu-kvm" >&2 + exit 1 + fi +done + +if ! command -v qemu-system-x86_64 >/dev/null 2>&1; then + echo "qemu-system-x86_64 not found. Install with: sudo apt install qemu-kvm" >&2 + exit 1 +fi + +mkdir -p "$FIRMWARE_DIR" + +if [[ "$reset_vars" == true || ! -f "$OVMF_VARS" ]]; then + cp "$OVMF_VARS_TEMPLATE" "$OVMF_VARS" + echo "UEFI vars: $OVMF_VARS (fresh copy from template)" +fi + +kvm_args=() +if [[ -r /dev/kvm ]]; then + kvm_args=(-enable-kvm -cpu host) +else + echo "Warning: /dev/kvm not available; running without KVM" >&2 + kvm_args=(-cpu max) +fi + +echo "Image: $IMG" +echo "RAM: ${RAM_MB}M SMP: $SMP SSH: localhost:${SSH_PORT}" + +exec qemu-system-x86_64 \ + "${kvm_args[@]}" \ + -smp "$SMP" \ + -m "$RAM_MB" \ + -machine q35 \ + -global ICH9-LPC.disable_s3=1 \ + -netdev "user,id=net0,hostfwd=tcp::${SSH_PORT}-:22" \ + -device virtio-net-pci,netdev=net0 \ + -drive "file=${OVMF_CODE},if=pflash,format=raw,unit=0,readonly=on" \ + -drive "file=${OVMF_VARS},if=pflash,format=raw,unit=1" \ + -drive "file=${IMG},if=none,format=raw,id=disk1" \ + -device virtio-blk-pci,drive=disk1,bootindex=1 \ + -serial mon:stdio diff --git a/distro/ubuntuCore/scripts/setup-dev-signing.sh b/distro/ubuntuCore/scripts/setup-dev-signing.sh new file mode 100755 index 0000000..817d8c2 --- /dev/null +++ b/distro/ubuntuCore/scripts/setup-dev-signing.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# One-time setup: Ubuntu One login + GPG signing key for custom UC26 dev models. +set -euo pipefail + +UC_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ENV_FILE="${UC_ROOT}/config/dev-image.env" +EXAMPLE="${UC_ROOT}/config/dev-image.env.example" +KEY_NAME="${SIGN_KEY_NAME:-salmanoff-dev}" +SSH_DIR="${UC_ROOT}/config/ssh" +SSH_PRIV="${SSH_DIR}/smo-dev" +SSH_PUB="${SSH_DIR}/smo-dev.pub" + +usage() { + cat <<'EOF' +Usage: setup-dev-signing.sh [OPTIONS] + +Prepare signing credentials for dangerous-grade salmanoff-dev-amd64 images. + +This script: + 1. Ensures an SSH keypair exists for the seeded system user (smo). + 2. Guides snapcraft login + create-key + register-key (interactive). + 3. Writes config/dev-image.env with your Snap Store account id. + +Options: + --key-name NAME Signing key name (default: salmanoff-dev) + -h, --help Show this help + +After setup, run: + scripts/sign-dev-assertions.sh + scripts/build-dev-image.sh +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --key-name) KEY_NAME="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;; + esac +done + +mkdir -p "$SSH_DIR" + +if [[ ! -f "$SSH_PUB" ]]; then + echo "Generating SSH keypair for system user: $SSH_PRIV" + ssh-keygen -t ed25519 -N "" -f "$SSH_PRIV" -C "smo-dev@salmanoff" +fi + +if ! command -v snapcraft >/dev/null 2>&1; then + echo "snapcraft not found. Install with: sudo snap install snapcraft --classic" >&2 + exit 1 +fi + +echo "" +echo "=== Step 1: log in to the Snap Store (Ubuntu One) ===" +echo "Run: snapcraft login" +echo "" +if ! snapcraft whoami >/dev/null 2>&1; then + echo "Not logged in yet. Complete 'snapcraft login' in this terminal, then re-run this script." >&2 + exit 1 +fi + +ACCOUNT_ID="$(snapcraft whoami 2>/dev/null | awk '/^id:/ {print $2}')" +if [[ -z "$ACCOUNT_ID" ]]; then + echo "Could not read account id from 'snapcraft whoami'" >&2 + exit 1 +fi +echo "Account id: $ACCOUNT_ID" + +echo "" +echo "=== Step 2: create and register a signing key ===" +if ! snap keys 2>/dev/null | awk 'NR>1 {print $1}' | grep -qx "$KEY_NAME"; then + echo "No local key named '$KEY_NAME'." + echo "Run interactively (you will choose a passphrase):" + echo " snapcraft create-key $KEY_NAME" + echo " snapcraft register-key $KEY_NAME" + echo "" + echo "Re-run this script after both commands succeed." >&2 + exit 1 +fi + +KEY_FP="$(snap keys 2>/dev/null | awk -v k="$KEY_NAME" '$1 == k {print $2}')" +if [[ -z "$KEY_FP" ]]; then + echo "Could not read SHA3-384 fingerprint for key '$KEY_NAME'" >&2 + exit 1 +fi + +if ! snap known --remote account-key "public-key-sha3-384=${KEY_FP}" >/dev/null 2>&1; then + echo "Key '$KEY_NAME' exists locally but is not registered in the store." + echo "Run: snapcraft register-key $KEY_NAME" + echo "Then re-run this script." >&2 + exit 1 +fi + +echo "Signing key: $KEY_NAME ($KEY_FP)" + +if [[ ! -f "$ENV_FILE" ]]; then + cp "$EXAMPLE" "$ENV_FILE" +fi + +tmp="$(mktemp)" +while IFS= read -r line || [[ -n "$line" ]]; do + case "$line" in + ACCOUNT_ID=*) echo "ACCOUNT_ID=${ACCOUNT_ID}" ;; + SIGN_KEY_NAME=*) echo "SIGN_KEY_NAME=${KEY_NAME}" ;; + SSH_PUBKEY_FILE=*) echo "SSH_PUBKEY_FILE=config/ssh/smo-dev.pub" ;; + *) echo "$line" ;; + esac +done < "$ENV_FILE" > "$tmp" +mv "$tmp" "$ENV_FILE" + +echo "" +echo "Wrote $ENV_FILE" +echo "" +echo "Next:" +echo " scripts/sign-dev-assertions.sh" +echo " scripts/build-dev-image.sh" +echo "" +echo "SSH to the VM after first boot:" +echo " ssh -i ${SSH_PRIV} smo@localhost -p 8022" diff --git a/distro/ubuntuCore/scripts/sign-dev-assertions.sh b/distro/ubuntuCore/scripts/sign-dev-assertions.sh new file mode 100755 index 0000000..ee6c4f5 --- /dev/null +++ b/distro/ubuntuCore/scripts/sign-dev-assertions.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# Sign dangerous-grade model + system-user assertions for salmanoff-dev-amd64. +set -euo pipefail + +UC_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ENV_FILE="${UC_ROOT}/config/dev-image.env" +MODEL_TEMPLATE="${UC_ROOT}/models/salmanoff-dev-amd64.model.json" +ASSERT_DIR="${UC_ROOT}/assertions" + +usage() { + cat <<'EOF' +Usage: sign-dev-assertions.sh [OPTIONS] + +Sign the dev model assertion and a system-user assertion (SSH key, no Ubuntu One). + +Requires config/dev-image.env (see scripts/setup-dev-signing.sh). + +Outputs: + models/salmanoff-dev-amd64.model + assertions/smo-system-user.assert (account + account-key + system-user chain) +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;; + esac +done + +if [[ ! -f "$ENV_FILE" ]]; then + echo "Missing $ENV_FILE — run scripts/setup-dev-signing.sh first" >&2 + exit 1 +fi +# shellcheck source=/dev/null +source "$ENV_FILE" + +: "${ACCOUNT_ID:?ACCOUNT_ID not set in $ENV_FILE}" +: "${SIGN_KEY_NAME:?SIGN_KEY_NAME not set in $ENV_FILE}" +: "${SYSTEM_USER_NAME:=smo}" +: "${SYSTEM_USER_EMAIL:=smo-dev@salmanoff}" +: "${SSH_PUBKEY_FILE:=config/ssh/smo-dev.pub}" +: "${MODEL_NAME:=salmanoff-dev-amd64}" + +SSH_PUBKEY_PATH="${UC_ROOT}/${SSH_PUBKEY_FILE}" +if [[ ! -f "$SSH_PUBKEY_PATH" ]]; then + echo "SSH public key not found: $SSH_PUBKEY_PATH" >&2 + echo "Run scripts/setup-dev-signing.sh" >&2 + exit 1 +fi + +KEY_FP="$(snap keys 2>/dev/null | awk -v k="$SIGN_KEY_NAME" '$1 == k {print $2}')" +if [[ -z "$KEY_FP" ]]; then + echo "Signing key '$SIGN_KEY_NAME' not found. Run scripts/setup-dev-signing.sh" >&2 + exit 1 +fi + +if ! snap known --remote account-key "public-key-sha3-384=${KEY_FP}" >/dev/null 2>&1; then + echo "Key '$SIGN_KEY_NAME' is not registered in the Snap Store." >&2 + echo "Run: snapcraft register-key $SIGN_KEY_NAME" >&2 + exit 1 +fi + +export GPG_TTY="${GPG_TTY:-$(tty)}" + +mkdir -p "$ASSERT_DIR" "${UC_ROOT}/models" + +TIMESTAMP="$(date -Iseconds --utc)" +MODEL_JSON="$(mktemp)" +MODEL_OUT="${UC_ROOT}/models/${MODEL_NAME}.model" +SYSTEM_USER_JSON="$(mktemp)" +SYSTEM_USER_OUT="${ASSERT_DIR}/smo-system-user.assert" + +sed -e "s/@ACCOUNT_ID@/${ACCOUNT_ID}/g" \ + -e "s/@TIMESTAMP@/${TIMESTAMP}/g" \ + "$MODEL_TEMPLATE" > "$MODEL_JSON" + +echo "Signing model → $MODEL_OUT" +snap sign -k "$SIGN_KEY_NAME" "$MODEL_JSON" > "$MODEL_OUT" + +SSH_PUB="$(tr -d '\n' < "$SSH_PUBKEY_PATH")" +cat > "$SYSTEM_USER_JSON" < "$SYSTEM_USER_OUT" + +rm -f "$MODEL_JSON" "$SYSTEM_USER_JSON" + +echo "" +echo "Model authority/brand: $ACCOUNT_ID" +echo "System user: ${SYSTEM_USER_NAME} (SSH pubkey from ${SSH_PUBKEY_FILE})" +echo "Signing key: ${SIGN_KEY_NAME} (${KEY_FP})" diff --git a/distro/ubuntuCore/scripts/test-snap.sh b/distro/ubuntuCore/scripts/test-snap.sh new file mode 100755 index 0000000..af58674 --- /dev/null +++ b/distro/ubuntuCore/scripts/test-snap.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Install and smoke-test the salmanoff snap (strict by default). +set -euo pipefail + +UC_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SNAP_DIR="${UC_ROOT}/snaps/salmanoff" + +usage() { + cat <<'EOF' +Usage: test-snap.sh [OPTIONS] [-- ARGS_FOR_SALMANOFF...] + +Install the newest salmanoff_*.snap and run a smoke test. + +Options: + --snap PATH Specific .snap file (default: newest in snaps/salmanoff/) + --no-install Skip install; only run snap run (snap already installed) + --devmode Install with --devmode (overrides strict snap metadata) + -- ARGS Passed to salmanoff (default: --help) + -h, --help Show this help +EOF +} + +snap_file="" +do_install=true +use_devmode=false +extra_args=(--help) + +while [[ $# -gt 0 ]]; do + case "$1" in + --snap) snap_file="$2"; shift 2 ;; + --no-install) do_install=false; shift ;; + --devmode) use_devmode=true; shift ;; + --) shift; extra_args=("$@"); break ;; + -h|--help) usage; exit 0 ;; + *) extra_args=("$@"); break ;; + esac +done + +if [[ -z "$snap_file" ]]; then + snap_file="$(ls -1t "${SNAP_DIR}"/*.snap 2>/dev/null | head -1 || true)" +fi + +if [[ -z "$snap_file" || ! -f "$snap_file" ]]; then + if [[ "$do_install" == true ]]; then + echo "No .snap found. Build first: ./scripts/build-snap.sh" >&2 + exit 1 + fi +else + echo "Newest .snap: $snap_file" +fi + +connect_plugs() { + local plug + for plug in network network-bind hardware-observe camera media-control; do + if snap interfaces 2>/dev/null | grep -q "salmanoff:${plug}"; then + if ! snap interfaces 2>/dev/null | grep -q "salmanoff:${plug}.*-"; then + echo "Connecting plug: ${plug}" + sudo snap connect "salmanoff:${plug}" 2>/dev/null || true + fi + fi + done +} + +if [[ "$do_install" == true ]]; then + install_flags=(--dangerous) + if [[ "$use_devmode" == true ]]; then + install_flags+=(--devmode) + fi + sudo snap install "${install_flags[@]}" "$snap_file" + connect_plugs +fi + +echo "Running: snap run salmanoff ${extra_args[*]}" +snap run salmanoff "${extra_args[@]}" diff --git a/distro/ubuntuCore/snaps/salmanoff/bin/salmanoff-launch b/distro/ubuntuCore/snaps/salmanoff/bin/salmanoff-launch new file mode 100755 index 0000000..2a3509b --- /dev/null +++ b/distro/ubuntuCore/snaps/salmanoff/bin/salmanoff-launch @@ -0,0 +1,20 @@ +#!/bin/bash +# Wrapper: plugin search path, OpenCL ICD, Rusticl llvmpipe, Gallium DRI, libcamera IPA. +set -euo pipefail + +shopt -s nullglob +lib_dirs=("$SNAP/usr/lib/"*-linux-gnu) +if ((${#lib_dirs[@]} > 0)); then + lib_dir="${lib_dirs[0]}" +else + lib_dir="$SNAP/usr/lib" +fi + +export RUSTICL_ENABLE="${RUSTICL_ENABLE:-llvmpipe}" +export OCL_ICD_VENDORS="${OCL_ICD_VENDORS:-$SNAP/etc/OpenCL/vendors}" +export LIBGL_DRIVERS_PATH="${LIBGL_DRIVERS_PATH:-${lib_dir}/dri}" +export LIBCAMERA_IPA_MODULE_PATH="${LIBCAMERA_IPA_MODULE_PATH:-${lib_dir}/libcamera}" +export LIBCAMERA_IPA_CONFIG_PATH="${LIBCAMERA_IPA_CONFIG_PATH:-$SNAP/usr/share/libcamera/ipa}" +export LD_LIBRARY_PATH="${lib_dir}:${SNAP}/usr/lib:${LD_LIBRARY_PATH:-}" + +exec "${SNAP}/usr/bin/salmanoff" -p "${lib_dir}" "$@" diff --git a/distro/ubuntuCore/snaps/salmanoff/snapcraft.yaml b/distro/ubuntuCore/snaps/salmanoff/snapcraft.yaml new file mode 100644 index 0000000..f82c53b --- /dev/null +++ b/distro/ubuntuCore/snaps/salmanoff/snapcraft.yaml @@ -0,0 +1,109 @@ +# Salmanoff snap for Ubuntu Core 26 (salmanoff-dev-amd64 dangerous model). +# Build from distro/ubuntuCore: ../../scripts/build-snap.sh +name: salmanoff +version: "0.01.001" +summary: Salmanoff cognitive robotics runtime +description: | + Sensor management runtime (SMO) with Livox LiDAR, libcamera, and OpenCL + stimulus paths. Packaged for iterative snap-first development before seeding + into an Ubuntu Core image. + +grade: devel +confinement: strict +base: core26 + +lint: + ignore: + - unused-library: + - usr/lib/*/libcoreComp.so* + - usr/lib/*/liblcameraBuff.so* + - usr/lib/*/liblcameraDev.so* + - usr/lib/*/liblivoxGen1.so* + - usr/lib/*/liblivoxProto1.so* + - usr/lib/*/libattachmentSupport.so* + - usr/lib/*/libspinscale.so* + - usr/lib/*/libRusticlOpenCL.so* + - usr/lib/*/libboost_log_setup.so* + - usr/lib/*/libunwind*.so* + - usr/lib/*/liburing-ffi.so* + - usr/lib/*/libicu*.so* + - usr/lib/*/liblttng*.so* + +apps: + salmanoff: + command: bin/salmanoff-launch + plugs: + - network + - network-bind + - hardware-observe + - camera + - media-control + environment: + RUSTICL_ENABLE: llvmpipe + OCL_ICD_VENDORS: $SNAP/etc/OpenCL/vendors + +layout: + /etc/OpenCL: + bind: $SNAP/etc/OpenCL + /usr/lib/clc: + bind: $SNAP/usr/lib/clc + /usr/lib/x86_64-linux-gnu/libcamera: + bind: $SNAP/usr/lib/x86_64-linux-gnu/libcamera + /usr/share/libcamera: + bind: $SNAP/usr/share/libcamera + +parts: + launch: + plugin: nil + override-build: | + install -Dm755 "$CRAFT_PROJECT_DIR/bin/salmanoff-launch" \ + "$CRAFT_PART_INSTALL/bin/salmanoff-launch" + + salmanoff: + after: [launch] + plugin: cmake + # SMO repo root (distro/ubuntuCore/snaps/salmanoff -> ../../../..) + source: ../../.. + source-type: local + source-submodules: + - libspinscale + build-packages: + - build-essential + - cmake + - ninja-build + - pkg-config + - flex + - bison + - git + - libboost1.88-dev + - libboost-system1.88-dev + - libboost-log1.88-dev + - liburing-dev + - libcamera-dev + - ocl-icd-opencl-dev + stage-packages: + - libboost-system1.88.0 + - libboost-log1.88.0 + - liburing2 + - libcamera0.7 + - libcamera-ipa + - ocl-icd-libopencl1 + - mesa-opencl-icd + - libclc-20 + - mesa-libgallium + - libgl1-mesa-dri + cmake-parameters: + - -DCMAKE_INSTALL_PREFIX=/usr + - -DCMAKE_BUILD_TYPE=RelWithDebInfo + - -DBoost_DIR=/usr/lib/x86_64-linux-gnu/cmake/Boost-1.88.0 + - -DENABLE_TESTS=OFF + - -DENABLE_LIB_xcbXorg=OFF + - -DENABLE_LIB_lcameraDev=ON + - -DENABLE_STIMBUFFAPI_lcameraBuff=ON + - -DENABLE_LCAMERADEV_TOOLS=OFF + - -DCOMPILE_PCL_TOOLS=OFF + override-pull: | + craftctl default + git -C "$CRAFT_PART_SRC" submodule update --init libspinscale + prime: + - -usr/lib/x86_64-linux-gnu/liburing-ffi.so* diff --git a/distro/ubuntuCore/ubuntu-core-plan.md b/distro/ubuntuCore/ubuntu-core-plan.md new file mode 100644 index 0000000..0375084 --- /dev/null +++ b/distro/ubuntuCore/ubuntu-core-plan.md @@ -0,0 +1,276 @@ +# Salmanoff on Ubuntu Core — Plan (derived from Yocto work) + +## Purpose of this document + +This plan is **not** a translation of an original Yocto design doc. It reflects **what we actually built and validated** in the Yocto path, reframed as goals for Ubuntu Core. Use it as primary context when scaffolding snaps, gadget/model, and dev VM workflow. + +When done, snap definitions and support files land in the SMO repo under: + +``` +smo/distro/ubuntuCore/ +``` + +(parallel to the existing `smo/distro/yocto/meta-salmanoff/`). + +--- + +## What we proved in Yocto (facts, not aspirations) + +We shipped a **minimal headless image** that runs the Salmanoff (SMO) cognitive robotics runtime on **QEMU x86-64**, bridged onto a lab LAN, talking to a **Livox LiDAR** on a fixed IP. + +### Lab network (canonical — carry forward unchanged) + +| Host | IP | Role | +|---|---|---| +| Laptop / gateway | `10.42.0.1` | Gateway + DNS | +| RPi5 (production target) | `10.42.0.2` | Future production node | +| **Dev guest (QEMU / UC VM)** | **`10.42.0.16`** | SMO dev runtime | +| Livox Avia | `10.42.0.139` | LiDAR sensor | + +### Runtime configuration we validated + +- **Body file**: `yocto-qemu-x86-headless.dapss` — Livox-only, headless (no win0/camera), `smo-ip=10.42.0.16`, includes `avia0.dapss`. +- **SMO build profile** (CMake): + - `RelWithDebInfo` + - `ENABLE_LIB_lcameraDev=ON`, `ENABLE_STIMBUFFAPI_lcameraBuff=ON` (libcamera + OpenCL YUV path compiled in; no camera passthrough needed in QEMU) + - `ENABLE_LIB_xcbXorg=OFF`, `COMPILE_PCL_TOOLS=OFF`, `ENABLE_TESTS=OFF`, `ENABLE_LCAMERADEV_TOOLS=OFF` + - Git submodules required (`libspinscale`, etc.) +- **Runtime command shape** (approximate): + ```bash + salmanoff -d /usr/share/salmanoff/devices/bodies/body_yocto_qemu_x86_headless.daps -p /usr/lib -v + ``` + (exact installed `.daps` filename follows DAPSS build output naming) + +### Platform services / packages we pulled into the image + +| Need | What we used in Yocto | +|---|---| +| SMO runtime + libs | `salmanoff` recipe (`libspinscale`, `liblcameraDev`, `liblcameraBuff`, `liblivoxGen1`, …) | +| Boost | Pinned **1.86** (shared `boost-system`; must stay **< 1.89**) | +| liburing | Runtime dep | +| libcamera + v4l-utils | Present on image; camera optional at runtime | +| OpenCL | Mesa Rusticl via `opencl` distro feature; **needs `RUSTICL_ENABLE=llvmpipe`** or platform shows 0 devices | +| OpenCL verify | `clinfo` (also discovered **256M RAM OOMs** OpenCL probe — use **≥ 2G** for dev VM) | +| Network | Static `eth0`: `10.42.0.16/24`, gw `10.42.0.1` | +| Dev access | SSH (openssh on Yocto image) | +| Kernel probe | `rseqsliceprobe` + kernel config append for rseq slice extension | +| QEMU dev | Host bridge `br-smo`, `runqemu` with `bridge=br-smo`, `nographic`, `-m 2048` | + +### Repo / packaging decisions already made in SMO + +- Yocto layer vendored at `smo/distro/yocto/meta-salmanoff/` (committed on `clast`). +- SMO source changes on `clast`: headless body, flex/bison repo-relative `#line` paths, etc. +- Fetch uses private Gitea (`hayodea/salmanoff`, branch `clast`, submodules via `libspinscale` on separate SSH host alias). + +--- + +## Conceptual goals (portable to Ubuntu Core) + +These are the **intent** items — independent of BitBake: + +1. **Repeatable dev environment** — headless guest on `10.42.0.16`, reachable from laptop, Livox at `10.42.0.139`. +2. **SMO as first-class payload** — not “install deps manually”; runtime + `.daps` bodies + plugin `.so`s packaged and startable. +3. **Same CMake feature profile** as Yocto (lcamera + OpenCL enabled; xcb/PCL tools off). +4. **OpenCL that actually works headless** — Rusticl/llvmpipe with env var set before SMO starts. +5. **libcamera stack present** — libraries available even when no camera device is passed through (QEMU/UC dev). +6. **Boost version constraint honored** — do not silently pick Boost ≥ 1.89. +7. **Production path to RPi5** — same snap(s), different body/IP (`10.42.0.2`), gadget/kernel for Pi hardware. +8. **Distro metadata lives in SMO** — `distro/ubuntuCore/` committed back into SMO when stable (mirrors Yocto layout). + +--- + +## Yocto → Ubuntu Core mapping + +| Yocto (what we did) | Ubuntu Core (target) | +|---|---| +| `meta-salmanoff/` layer | `distro/ubuntuCore/` — snaps, gadget, model, helper scripts | +| `salmanoff-image.bb` | **Model assertion** + `core` + gadget snap + app snaps on seeded image | +| `salmanoff.bb` recipe | **`salmanoff` snap** (`snapcraft.yaml`: cmake build, submodules, organize libs + daps) | +| `IMAGE_INSTALL` | Snap `stage-packages` / parts / content snaps (`mesa`, libcamera deps) | +| `salmanoff-rusticl-env` recipe | Snap `environment:` block or wrapper script: `RUSTICL_ENABLE=llvmpipe` | +| `init-ifupdown` static IP | **Netplan** via gadget default config, `network-setup` snap, or `system-connections` on UC | +| `runqemu-salmanoff-bridge` | **Dev VM script**: UC image in QEMU/LXD/multipass on `br-smo`, static `10.42.0.16`, 2G RAM | +| `DISTRO_FEATURES += opencl` | Ensure Mesa/OpenCL ICD available to snap (layout + plugs or bundled libs) | +| `DEPENDS` / `RDEPENDS` | Snapcraft `build-packages`, `stage-packages`, `slots`/`plugs`, `layout:` for `/usr/lib` ICD paths | +| `gitsm://` + `SRCREV` | Snapcraft `source` + `source-submodules` or `override-pull` git submodule update | +| `yocto-qemu-x86-headless.dapss` | Installed by salmanoff snap; UC-specific body can be `ubuntu-core-x86-headless.dapss` later | +| `rseqsliceprobe` | Validate UC kernel/base; custom kernel snap only if probe fails on stock UC kernel | +| SSH on image | UC: serial console, `ssh-keys` in model, or dedicated snap — **don’t assume openssh like Yocto** | +| Boost 1.86 pin | Pin in snapcraft (`stage-packages` version) or build Boost part from source | +| `RelWithDebInfo` + debug maps | Snapcraft `override-build` cmake flags; consider `debug` snap or stripped separate artifact | + +--- + +## Proposed Ubuntu Core architecture + +``` +Host (10.42.0.1) + └── bridge br-smo (10.42.0.0/24) + └── UC dev VM / RPi5 (10.42.0.16 or .2) + ├── snap: core (or core24) + ├── snap: gadget- (boot, partitions, default netplan) + ├── snap: salmanoff (daemon — main payload) + └── optional: mesa-support / libcamera-support content snaps if not bundled +``` + +### `salmanoff` snap (primary deliverable) + +**Type**: likely `daemon` (simple) or `daemon` (notify) once startup semantics are known. + +**Build** (mirror Yocto recipe intent): + +- Source: SMO repo `clast` (submodules!) +- CMake options: same as Yocto `EXTRA_OECMAKE` block +- Install: `salmanoff` binary, `lib*.so*`, `/usr/share/salmanoff/devices/**` +- Apps: main daemon + optional `salmanoff.probe` for debugging + +**Runtime environment**: + +```yaml +environment: + RUSTICL_ENABLE: llvmpipe +``` + +**Interfaces to evaluate early** (exact set TBD by confinement testing): + +- `network` / `network-bind` — Livox UDP/TCP to `10.42.0.139` +- `hardware-observe` — may be needed for camera discovery on Pi +- `camera` — for libcamera on production Pi +- `opengl` — if Mesa GL stack needed beyond OpenCL ICD +- `raw-usb` / `serial-port` — only if Livox path requires it on some platforms +- `home` — usually **avoid**; use `$SNAP_DATA` for state + +**Layouts** (likely needed): + +- OpenCL ICD loader expects `/etc/OpenCL/vendors` or known `LD_LIBRARY_PATH` +- May need `layout:` bind mounts for Mesa `libRusticlOpenCL.so` / gallium stack + +### Platform snaps + +| Platform | Gadget | Notes | +|---|---|---| +| **QEMU x86 dev** | `pc` or custom gadget | Static IP `10.42.0.16`, 2G RAM, bridged NIC | +| **RPi5 production** | `pi` gadget (22+ arm64) | Static IP `10.42.0.2`, libcamera, Livox | + +### Bodies / config + +- **Dev (UC VM)**: start from Yocto body or add `ubuntu-core-x86-headless.dapss` with same Livox-only intent and `smo-ip=10.42.0.16`. +- **Production (RPi5)**: reuse/adapt existing `rpi5-persys-headless.dapss` pattern with `smo-ip=10.42.0.2`. + +Body files stay in SMO `devices/bodies/`; snap selects default via config or snap config hook. + +--- + +## Phased plan (recommended order) + +### Phase 0 — Repo scaffold + +- [ ] New repo (or `distro/ubuntuCore/` in SMO) with `snapcraft.yaml` skeleton +- [ ] Document lab IP table and bridge prerequisites (port `runqemu-salmanoff-bridge` concepts) +- [ ] Link to SMO `clast` branch + submodule SSH host requirements + +### Phase 1 — Build snap on host (no hardware) + +- [ ] `snapcraft` / `craft` builds SMO with same CMake profile as Yocto +- [ ] Confirm submodule pull (`libspinscale`) in clean CI/local environment +- [ ] Resolve Boost < 1.89 (stage-package pin or source part) +- [ ] Package `.daps` + shared libs; verify `salmanoff -v` / dry-run in snap run environment + +### Phase 2 — OpenCL + libcamera in confinement + +- [ ] Replicate Yocto OpenCL stack inside snap (bundled vs system Mesa) +- [ ] Confirm `RUSTICL_ENABLE=llvmpipe` → `clinfo` shows ≥1 device **inside snap** +- [ ] Confirm `liblcameraDev` / `liblcameraBuff` load; no crash without camera device +- [ ] Document RAM requirement (≥ 2G for dev VM) + +### Phase 3 — UC dev VM on lab LAN + +- [ ] Boot Ubuntu Core x86 VM bridged to `br-smo` +- [ ] Static IP **10.42.0.16** (netplan/gadget) +- [ ] Install seeded `salmanoff` snap (devmode first, then strict) +- [ ] Ping gateway; reach Livox at `10.42.0.139` +- [ ] Run headless body; validate Livox traffic / SMO logs + +### Phase 4 — Production path (RPi5) + +- [ ] Model assertion for Pi5 + arm64 core +- [ ] Gadget with `10.42.0.2` netplan +- [ ] libcamera plug + Pi body file +- [ ] OTA refresh via snap channels + +### Phase 5 — Commit back to SMO + +- [ ] Move stable `distro/ubuntuCore/` into SMO repo +- [ ] Commit on `clast` (or dedicated branch); do **not** block on unfinished WIP snaps + +--- + +## Known constraints & pitfalls (from Yocto — expect repeats) + +1. **OpenCL**: platform visible but **0 devices** until `RUSTICL_ENABLE=llvmpipe`. +2. **Memory**: 256M insufficient for Rusticl/`clinfo`; use **2G** for dev VM. +3. **Boost**: must stay **< 1.89** (shared `boost-system` linkage). +4. **Submodules**: `libspinscale` is mandatory; fetch must be recursive; separate SSH host alias on Gitea. +5. **lcamera without hardware**: compile and ship libs; no QEMU camera passthrough required for basic bring-up. +6. **PCL / xcb**: keep off unless explicitly needed (MPI/cmake pain in Yocto). +7. **Private git**: snapcraft build needs SSH keys / credentials for Gitea fetch (or vendored source tarball part). +8. **Strict confinement**: biggest unknown vs Yocto — plan devmode → strict iteration; interfaces will take multiple passes. +9. **rseq kernel feature**: validate on UC base; custom kernel snap is expensive — only if probe fails. + +--- + +## Success criteria (match Yocto milestone) + +Minimum “we’re at parity” for UC dev: + +- [ ] UC VM at `10.42.0.16` on `10.42.0.0/24` via host bridge +- [ ] `salmanoff` snap installed and daemon running (or manual `snap run` equivalent) +- [ ] Headless Livox body loaded; SMO stable with Livox on `10.42.0.139` +- [ ] OpenCL initialized (llvmpipe); lcamera libs present +- [ ] Snap definitions committed under `smo/distro/ubuntuCore/` + +Stretch (production): + +- [ ] Same snap on RPi5 at `10.42.0.2` with Pi-appropriate body + +--- + +## Explicit non-goals (for now) + +- Re-implement Yocto layer / BitBake in UC repo +- Camera passthrough in QEMU/UC x86 dev +- PCL tools / xcb window stack +- lcameraDev probe tools (need test support libs — off in Yocto too) +- Full OTA/signing production pipeline (Phase 4+) + +--- + +## Reference: Yocto artifacts in SMO repo + +When unsure, read the working Yocto implementation: + +``` +smo/distro/yocto/meta-salmanoff/ + conf/layer.conf + conf/salmanoff-local.inc # opencl distro feature + recipes-salmanoff/salmanoff/salmanoff.bb + recipes-core/images/salmanoff-image.bb + recipes-core/init-ifupdown/... # 10.42.0.16 static IP + recipes-support/salmanoff-rusticl-env/ + scripts/runqemu-salmanoff-bridge + +smo/devices/bodies/yocto-qemu-x86-headless.dapss +``` + +SMO branch: **`clast`** on `hayodea/salmanoff` (Gitea). + +--- + +## Instructions for the LLM in the Ubuntu Core repo + +1. **Treat Yocto work as validated reference**, not something to re-derive from scratch. +2. **Preserve lab IP plan and CMake profile** unless user says otherwise. +3. **Start with snapcraft build on host**, then UC VM, then strict confinement — same order we used (build → package QA → runtime). +4. **Plan for `distro/ubuntuCore/` in SMO** as the final home of snaps/gadget/model/scripts. +5. **Ask before** committing snaps into SMO; user will commit when done. +6. **Do not** assume UC has openssh or classic Ubuntu package management — everything is snaps + interfaces.