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 <cursoragent@cursor.com>
This commit is contained in:
2026-06-25 23:01:52 -04:00
parent 44d12eeb9e
commit 038d59f972
17 changed files with 1295 additions and 0 deletions
+30
View File
@@ -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
@@ -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
+6
View File
@@ -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
@@ -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
@@ -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"
}
]
}
+133
View File
@@ -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-<timestamp>.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"
+78
View File
@@ -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
+96
View File
@@ -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-<timestamp>.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"
+12
View File
@@ -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"
+68
View File
@@ -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."
+105
View File
@@ -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
+120
View File
@@ -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"
+106
View File
@@ -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" <<EOF
{
"type": "system-user",
"authority-id": "${ACCOUNT_ID}",
"brand-id": "${ACCOUNT_ID}",
"series": ["16"],
"models": ["${MODEL_NAME}"],
"name": "Salmanoff Dev",
"username": "${SYSTEM_USER_NAME}",
"email": "${SYSTEM_USER_EMAIL}",
"ssh-keys": ["${SSH_PUB}"],
"since": "2026-06-21T00:00:00+00:00",
"until": "2064-06-21T00:00:00+00:00"
}
EOF
echo "Signing system-user chain → $SYSTEM_USER_OUT"
snap sign -k "$SIGN_KEY_NAME" "$SYSTEM_USER_JSON" --chain > "$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})"
+74
View File
@@ -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[@]}"
+20
View File
@@ -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}" "$@"
@@ -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*
+276
View File
@@ -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 — **dont 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-<platform> (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 “were 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.