mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-28 00:00:33 +01:00
Device findings (SteamOS 0.4.3, SteamVR 2.18.2, containerized ft-powerd connected to the running vrserver, verified in vrserver.txt): - OpenVR's path registry (~/.config/openvr) must be visible at both the container HOME and the absolute /home/steamos paths it references; the referenced dirs (Steam config/logs, the ft_pointer driver dir) are mounted at the same paths. - SteamVR's IPC control file lives in /tmp: a private /tmp namespace makes VR_Init fail with Init_Internal (124). - Rootless podman maps container root to the desktop user, so a plain run writes host files as steamos and needs no --user; forcing --user 1000 breaks the subuid mapping instead. - HOME must be set explicitly: podman derives it from the workdir, which is /src/frametop. - ft-powerd binds the abstract socket @ft_powerd: second instances refuse cleanly (single-instance by interface, not by accident).
203 lines
9.1 KiB
Bash
Executable File
203 lines
9.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# ft — build and run Frametop out of its OCI image (pack/Containerfile).
|
|
#
|
|
# The image is the product: one container that holds the frozen toolchain, the
|
|
# native binaries, and the locked Python environment. Everything Frametop runs
|
|
# goes through this wrapper, so the integration points (mounts, IPC, devices)
|
|
# live in exactly one place.
|
|
#
|
|
# The wrapper works from two homes:
|
|
# Repo mode the script sits in a checkout (pack/Containerfile beside it):
|
|
# development commands build from the sources, and runs mount
|
|
# the repo at /src/frametop
|
|
# Installed the script sits in ~/.local/bin (put there by install.sh):
|
|
# programs run from the image's own copy, no repo needed
|
|
#
|
|
# Image reference, in order: $FT_IMAGE, then ~/.config/frametop/image — which
|
|
# install.sh writes and `ft update` keeps digest-pinned. A moving :latest tag
|
|
# is refused for what gets deployed (the pinned image and the update source):
|
|
# what runs on a headset must be reproducible and rollback-able. Local
|
|
# development is free to use any tag, :latest included.
|
|
#
|
|
# Usage:
|
|
# ft <prog> [args] [both] run a program from /opt/frametop
|
|
# ft update [both] pull the published image, pin its digest
|
|
# ft clean [both] remove frametop's leftover containers and
|
|
# unused frametop images (see below)
|
|
# Development commands live behind "ft dev" (they need a repo checkout):
|
|
# ft dev build [repo] build the image from the repo
|
|
# ft dev shell [repo] interactive shell in the image
|
|
# ft dev test [repo] run the Python test suites inside the image
|
|
#
|
|
# About `ft clean` and the shared podman store: on SteamOS, frametop's
|
|
# containers share one rootless podman store with Valve's (named
|
|
# lepton-<context>). `ft clean` touches ONLY objects that are provably
|
|
# frametop's: containers named frametop-*, and images whose reference
|
|
# contains a /frametop or frametop: component. Everything else — every
|
|
# lepton-* container, every dangling image, every cache layer — is left
|
|
# alone. Never run `podman rm -a`, `podman rmi -a`, or `podman system prune`
|
|
# on a Frame: those are store-wide and would delete Valve's containers.
|
|
set -euo pipefail
|
|
|
|
root=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
[ -f "$root/pack/Containerfile" ] && repo_mode=1 || repo_mode=0
|
|
config_dir=${XDG_CONFIG_HOME:-$HOME/.config}/frametop
|
|
ref_file=$config_dir/image # installed: the pinned image reference
|
|
published_file=$config_dir/published # installed: where updates come from
|
|
|
|
reject_latest() { # reject_latest <what> <ref>
|
|
case $2 in
|
|
*:latest|*:latest@*|latest)
|
|
echo "ft: $2 uses the moving :latest tag — refused for $1." >&2
|
|
echo " Pin a version tag or a digest (repo@sha256:...) instead," >&2
|
|
echo " e.g. in $published_file or $ref_file." >&2
|
|
exit 2 ;;
|
|
esac
|
|
}
|
|
|
|
# The reference updates pull from: $FT_IMAGE_PUBLISHED, then the config file.
|
|
# It must be configured and versioned — there is no silent :latest default.
|
|
default_published() {
|
|
local ref
|
|
if [ -n "${FT_IMAGE_PUBLISHED:-}" ]; then ref=$FT_IMAGE_PUBLISHED
|
|
elif [ -r "$published_file" ]; then ref=$(<"$published_file")
|
|
else
|
|
echo "ft update: no published reference configured." >&2
|
|
echo " Write a versioned reference (never :latest) to:" >&2
|
|
echo " $published_file" >&2
|
|
echo " e.g. ghcr.io/0x1f6/frametop:v0.2.1 — install.sh does this." >&2
|
|
exit 2
|
|
fi
|
|
reject_latest "the update source" "$ref"
|
|
echo "$ref"
|
|
}
|
|
|
|
# The reference programs run: $FT_IMAGE, then the pinned file. Installed mode
|
|
# refuses to run without a pin — a headset must never end up on :latest by
|
|
# accident, and must always know exactly what image it is running.
|
|
default_image() {
|
|
if [ "$repo_mode" = 1 ]; then echo "frametop:local"; return; fi
|
|
local ref
|
|
if [ -r "$ref_file" ] && [ -n "$(<"$ref_file")" ]; then ref=$(<"$ref_file")
|
|
elif [ -n "${FT_IMAGE:-}" ]; then echo "$FT_IMAGE"; return
|
|
else
|
|
echo "ft: no image pinned. Nothing has been installed yet." >&2
|
|
echo " install.sh writes the pinned reference to:" >&2
|
|
echo " $ref_file" >&2
|
|
exit 2
|
|
fi
|
|
reject_latest "the pinned image" "$ref"
|
|
echo "$ref"
|
|
}
|
|
image=${FT_IMAGE:-}
|
|
published=${FT_IMAGE_PUBLISHED:-}
|
|
|
|
engine=$(command -v podman || command -v docker || true)
|
|
[ -n "$engine" ] || { echo "ft: need podman or docker on PATH" >&2; exit 1; }
|
|
|
|
# Repo mode mounts the checkout; installed mode uses the image's own copy.
|
|
mounts=()
|
|
[ "$repo_mode" = 1 ] && mounts+=(-v "$root":/src/frametop)
|
|
|
|
# The Frame's additions (shared memory, sockets, the real SteamVR) are gated
|
|
# behind FT_FRAME=1 until they are validated on the device.
|
|
if [ "${FT_FRAME:-0}" = 1 ]; then
|
|
mounts+=(
|
|
--env HOME=/home/steamos # OpenVR's registry is looked up under this HOME
|
|
--ipc=host # /dev/shm for the hands/gaze shared-memory files
|
|
-v /run/user/"${FT_UID:-1000}":/run/user/"${FT_UID:-1000}" # Wayland + frametop sockets
|
|
-v /opt/steamvr:/opt/steamvr:ro # the real SteamVR runtime (openvr_api, vrpathreg)
|
|
-v /tmp:/tmp # SteamVR's IPC control file lives in /tmp
|
|
# (verified on device: without it VR_Init fails 124)
|
|
# OpenVR's path registry. Rootless podman maps the container's root to
|
|
# the desktop user, so files written into these mounts land as steamos
|
|
# on the host — no root-owned files, and the registry's absolute
|
|
# /home/steamos/... references stay valid.
|
|
-v "${HOME}/.config/openvr:/root/.config/openvr:ro"
|
|
-v "${HOME}/.config/openvr:/home/steamos/.config/openvr"
|
|
-v "${HOME}/.local/share/Steam/config:/home/steamos/.local/share/Steam/config"
|
|
-v "${HOME}/.local/share/Steam/logs:/home/steamos/.local/share/Steam/logs"
|
|
-v "${HOME}/.local/share/frametop/ft_pointer:/home/steamos/.local/share/frametop/ft_pointer:ro"
|
|
)
|
|
fi
|
|
|
|
run_in_image() {
|
|
[ -n "$image" ] || image=$(default_image)
|
|
# A stable name (frametop-<program>) so systemd, podman ps and podman logs
|
|
# read like the system reads; a leftover from a crashed run gets replaced.
|
|
local prog=${1%% *}
|
|
local name
|
|
name=frametop-$(printf '%s' "$prog" | tr -c 'a-zA-Z0-9._-' '-')
|
|
"$engine" rm -f "$name" >/dev/null 2>&1 || true
|
|
exec "$engine" run --rm --name "$name" ${mounts[@]+"${mounts[@]}"} \
|
|
--workdir /src/frametop ${FT_FRAME:+--network=host} "$image" "$@"
|
|
}
|
|
|
|
need_repo() {
|
|
[ "$repo_mode" = 1 ] || { echo "ft $1: only in a repo checkout (this copy runs installed)" >&2; exit 2; }
|
|
}
|
|
|
|
update() {
|
|
[ -n "$published" ] || published=$(default_published)
|
|
reject_latest "the update source" "$published"
|
|
"$engine" pull "$published"
|
|
# Pin what was pulled, by digest, so every later run and any bug report
|
|
# names exactly this image — and rollback is editing one file.
|
|
local digest
|
|
if digest=$("$engine" inspect --format '{{index .RepoDigests 0}}' "$published" 2>/dev/null) && [ -n "$digest" ]; then
|
|
mkdir -p "$config_dir"
|
|
printf '%s\n' "$digest" > "$ref_file"
|
|
echo "ft update: pinned $digest"
|
|
else
|
|
echo "ft update: could not read a digest for $published — keeping $image." >&2
|
|
fi
|
|
}
|
|
|
|
clean() {
|
|
# Frametop's leftovers, and nothing else. See the header: the podman store
|
|
# is shared with Valve's lepton-* containers, so this only deletes objects
|
|
# whose name proves they are ours.
|
|
local found=0 obj
|
|
while IFS= read -r obj; do
|
|
[ -n "$obj" ] || continue
|
|
found=1
|
|
echo "removing container $obj"
|
|
"$engine" rm -f "$obj" >/dev/null
|
|
done < <("$engine" ps -a --format '{{.Names}}' 2>/dev/null | grep '^frametop-' || true)
|
|
# Images: only references that name frametop, and only ones no container
|
|
# uses (podman rmi refuses those anyway — the guard is belt and braces).
|
|
while IFS= read -r obj; do
|
|
[ -n "$obj" ] || continue
|
|
case $obj in *frametop*) ;; *) continue ;; esac
|
|
found=1
|
|
echo "removing image $obj"
|
|
"$engine" rmi "$obj" >/dev/null 2>&1 \
|
|
|| echo " (in use or already gone — left in place)" >&2
|
|
done < <("$engine" images --format '{{.Repository}}:{{.Tag}} {{.ID}}' 2>/dev/null \
|
|
| grep -E '(^|[ /])frametop(:| |$)' | awk '{print $1}' || true)
|
|
[ "$found" = 1 ] || echo "nothing of frametop's to clean"
|
|
[ "$found" = 1 ] || exit 0
|
|
}
|
|
|
|
case "${1:-help}" in
|
|
dev)
|
|
shift
|
|
image=${FT_IMAGE:-frametop:local} # dev always works against the local build
|
|
case "${1:-help}" in
|
|
build) need_repo build
|
|
exec "$engine" build -f "$root/pack/Containerfile" -t "$image" "$root" ;;
|
|
shell) need_repo shell
|
|
exec "$engine" run --rm -it ${mounts[@]+"${mounts[@]}"} --workdir /src/frametop "$image" bash ;;
|
|
test) need_repo test; run_in_image just test ;;
|
|
help|-h|--help|"") sed -n '2,40p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' ;;
|
|
*) echo "ft dev: unknown command: $1 (build, shell, test)" >&2; exit 2 ;;
|
|
esac ;;
|
|
update) update ;;
|
|
clean) clean ;;
|
|
help|-h|--help)
|
|
sed -n '2,40p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
|
[ -z "${1:-}" ] || exit 0 ;;
|
|
*) [ -z "${1:-}" ] && { echo "ft: no command given" >&2; exit 2; }
|
|
run_in_image "$@" ;;
|
|
esac
|