Frametanium updates itself from its releases; no box art in the repo

- Frametanium is in its own list (installer mode): updating downloads the
  release bundle and runs install.sh, which now installs the headset app and
  its tile too and, when the service runs it, restarts the service from a
  timer of its own a few seconds later
- release.yml builds the headset app on an ARM64 runner and publishes
  frametanium-steam-frame-arm64.tar.gz (tools/bundle.sh); deploy.sh uses the
  same bundle and install.sh
- Built-in ports' library art is fetched at install (GitHub card + the
  project's icon) instead of shipping box art
- Built-in Banjo-Kazooie (Lighthouse), installing over the Banjo_Kazooie
  tile; a repo the user added before it became built-in isn't listed twice
- Frametanium can't remove itself

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHDaz86aayqMULHsjByEmT
This commit is contained in:
CogentialandClaude Opus 5.5 committed 2026-10-09 21:05:01 -04:00
1 parent 6c9c5f4ce7
commit 6c27fd8fc2
7 files changed
+178 -40

No files matched your search

+63
View File
@@ -0,0 +1,63 @@
name: Release
# Builds the headset app on an ARM64 runner (Ubuntu 24.04: glibc 2.39, as SteamOS on the Frame) and publishes
# frametanium-steam-frame-arm64.tar.gz, which Frametanium installs to update itself (install.sh inside).
# Push a tag v* to release.
on:
push:
tags: ["v*"]
workflow_dispatch:
permissions:
contents: write
env:
SDL2_VERSION: 2.30.12
jobs:
release:
runs-on: ubuntu-24.04-arm
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Install the toolchain and SDL2's dependencies
run: |
sudo apt-get update -q
sudo apt-get install -y -q clang lld ninja-build cmake \
libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev \
libxkbcommon-dev libwayland-dev wayland-protocols libdecor-0-dev libdrm-dev libgbm-dev libegl-dev libgles-dev \
libpulse-dev libasound2-dev libpipewire-0.3-dev libudev-dev libdbus-1-dev
# A source build loads X11, Wayland, audio... at run time instead of linking them.
- name: Build SDL2
run: |
curl -fsSL "https://github.com/libsdl-org/SDL/releases/download/release-$SDL2_VERSION/SDL2-$SDL2_VERSION.tar.gz" | tar -xz -C "$RUNNER_TEMP"
cmake -S "$RUNNER_TEMP/SDL2-$SDL2_VERSION" -B "$RUNNER_TEMP/sdl2-build" -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER=clang -DCMAKE_INSTALL_PREFIX="$RUNNER_TEMP/sdl2" -DSDL_SHARED=ON -DSDL_STATIC=OFF -DSDL_TEST=OFF
ninja -C "$RUNNER_TEMP/sdl2-build" install
- name: Build the headset app
run: |
mkdir -p dist/app/lib
clang -O2 -Wall -march=armv8.2-a -fuse-ld=lld -I"$RUNNER_TEMP/sdl2/include/SDL2" -D_REENTRANT app/app.c \
-L"$RUNNER_TEMP/sdl2/lib" -lSDL2-2.0 -Wl,-rpath,'$ORIGIN/lib' -o dist/app/frametanium
strip dist/app/frametanium
cp -L "$RUNNER_TEMP/sdl2/lib/libSDL2-2.0.so.0" dist/app/lib/
sed -n '/^cat > "$OUT\/run.sh" <<.RUN.$/,/^RUN$/p' app/build-arm64.sh | sed '1d;$d' > dist/app/run.sh
chmod +x dist/app/run.sh
grep -q 'exec ./frametanium' dist/app/run.sh
- name: Package
run: |
tools/bundle.sh dist/app dist/bundle
tar -C dist/bundle -czf frametanium-steam-frame-arm64.tar.gz .
tar -tzf frametanium-steam-frame-arm64.tar.gz | grep -v '^./art/'
- name: Publish
if: startsWith(github.ref, 'refs/tags/v')
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "$GITHUB_REF_NAME" frametanium-steam-frame-arm64.tar.gz --target "$GITHUB_SHA" \
--title "Frametanium $GITHUB_REF_NAME" --notes "Update from Frametanium itself (its own row in the list), or unpack and run install.sh on the Frame."
+13 -4
View File
@@ -5,7 +5,7 @@ and Frametanium keeps it up to date. Works from the headset or a browser.
- **Add any GitHub repo** by typing `owner/name` (or pasting a link) on the headset's on-screen keyboard, from the
browser page, or by picking a suggestion made for the Frame. The built-in ports (Ship of Harkinian, 2 Ship 2
Harkinian) are there from the start.
Harkinian, Starship, Lighthouse, OpenCE) are there from the start.
- **Discover new Frame apps:** every 12 hours (or on demand) it searches GitHub for Steam Frame projects (the
`steam-frame` topic, "steam frame" in names and descriptions, and the community's
[awesome-steam-frame](https://github.com/kosantosbik/awesome-steam-frame) list) and shows the ones whose latest
@@ -17,7 +17,8 @@ and Frametanium keeps it up to date. Works from the headset or a browser.
AppImages and single programs all work.
- **Two ways to install,** picked from the download unless you choose:
- *Library app:* unpacked into `~/devkit-game/<name>`, given a `run.sh` (its own, or one that starts the ARM64
program it finds), added to the Steam library with art (the repo's GitHub card and the owner's avatar).
program it finds), added to the Steam library with art (the repo's GitHub card, and the project's icon or the owner's avatar),
fetched when it's installed.
- *Its installer:* when the download has `install.sh` (or `contrib/install.sh`, `setup.sh`), it's unpacked into
`~/.local/share/frametanium/apps/<id>` and that script runs, for tools such as SteamVR overlays. Removing the app
runs its uninstaller when it has one.
@@ -35,6 +36,10 @@ and Frametanium keeps it up to date. Works from the headset or a browser.
Updates are checked every 6 hours and on demand.
**Frametanium updates itself:** it's in its own list. Updating it downloads its latest
[release](https://github.com/Cogential/frametanium/releases), reinstalls the service and the headset app, and
restarts the service a few seconds later (reopen the headset app to get the new one).
## Files
- `catalog.json`: the built-in ports.
- `suggestions.json`: apps offered on the Add screen.
@@ -51,14 +56,18 @@ ROMs), or save a token in the web page's Settings: a fine-grained token limited
to the page.
## Install
From a computer that can SSH into the Frame:
On the Frame: download `frametanium-steam-frame-arm64.tar.gz` from the latest release, unpack it and run
`sh install.sh`. It installs the service and adds the Frametanium app to the library.
Or from a computer that can SSH into the Frame:
```sh
app/build-arm64.sh # cross-compiles the headset app into dist/app
./deploy.sh [host] # host defaults to frame.local
```
Service only, on the Frame: copy this folder over and run `sh install.sh`.
Releases: push a tag `v*`; `.github/workflows/release.yml` builds the headset app on an ARM64 runner and publishes
the bundle (`tools/bundle.sh`).
Uninstall: `systemctl --user disable --now frametanium.service` and remove Frametanium from the library.
## Notes
+34 -3
View File
@@ -15,7 +15,8 @@
"data": "~/.local/share/soh",
"rom_hint": "Send your Ocarina of Time ROM with Frame Inbox, or put a .z64 in ~/.local/share/soh.",
"prereleases": true,
"mode": "game"
"mode": "game",
"icon_url": "https://raw.githubusercontent.com/Cogential/Shipwright-frame/steam-frame/soh/macosx/sohIcon.png"
},
{
"id": "2ship",
@@ -45,7 +46,8 @@
"cd \"$D/usr/bin\" && exec ./2s2h.elf \"$@\""
],
"prereleases": true,
"mode": "game"
"mode": "game",
"icon_url": "https://raw.githubusercontent.com/Cogential/2ship2harkinian-frame/claude/focused-brahmagupta-sylmhh/mm/linux/2s2hIcon.png"
},
{
"id": "starship",
@@ -63,7 +65,8 @@
"data": "~/.local/share/starship",
"rom_hint": "Send your Star Fox 64 ROM (US) with Frame Inbox, or put a .z64 in ~/.local/share/starship.",
"prereleases": true,
"mode": "game"
"mode": "game",
"icon_url": "https://raw.githubusercontent.com/Cogential/Starship-frame/steam-frame/logo.png"
},
{
"id": "halo",
@@ -80,6 +83,34 @@
"data": "~/.local/share/halo-ce",
"rom_hint": "Needs your Halo: Combat Evolved Xbox disc image: put the .iso in ~/.local/share/halo-ce (or send it with Frame Inbox) and the first start copies its maps out.",
"prereleases": true,
"mode": "game",
"icon_url": "https://raw.githubusercontent.com/Cogential/OpenCE-frame/steam-frame/port/android/art/android-icon.png"
},
{
"id": "banjo",
"name": "Banjo-Kazooie (Lighthouse)",
"repo": "Cogential/Lighthouse",
"asset": "lighthouse-steam-frame-arm64.zip",
"type": "zip",
"gameid": "Banjo_Kazooie",
"launch": "run.sh",
"keep": [
"*.z64"
],
"data": "~/.local/share/Lighthouse",
"rom_hint": "Send your Banjo-Kazooie ROM (US) with Frame Inbox, or put a .z64 in ~/.local/share/Lighthouse.",
"icon_url": "https://raw.githubusercontent.com/Cogential/Lighthouse/android/logo.png",
"prereleases": true,
"mode": "game"
},
{
"id": "frametanium",
"name": "Frametanium",
"repo": "Cogential/frametanium",
"asset": "frametanium-steam-frame-arm64.tar.gz",
"type": "tar",
"prereleases": false,
"mode": "installer",
"description": "This app. Updating it reinstalls the service and the headset app, then restarts the service."
}
]
+4 -28
View File
@@ -1,6 +1,7 @@
#!/bin/sh
# Installs or updates Frametanium on a Steam Frame from this computer over SSH: the service, and the
# "Frametanium" app in the Steam library with its art.
# "Frametanium" app in the Steam library with its art. (Once it's installed, it can also update itself from
# its GitHub releases.)
#
# Usage: ./deploy.sh [host] host defaults to frame.local
# Env: SSH_KEY (default ~/.ssh/frame_devkit), SSH_USER (default steamos)
@@ -11,37 +12,12 @@ HOST="${1:-frame.local}"
KEY="${SSH_KEY:-$HOME/.ssh/frame_devkit}"
USER_AT="${SSH_USER:-steamos}@$HOST"
SSH="ssh -i $KEY -o ConnectTimeout=10"
GAMEID=Frametanium
[ -x dist/app/frametanium ] || { echo "Build the app first: app/build-arm64.sh"; exit 1; }
$SSH "$USER_AT" true || { echo "Can't reach $HOST over SSH. Is the Frame on and on this network?"; exit 1; }
echo "== Service"
tools/bundle.sh dist/app dist/bundle
$SSH "$USER_AT" 'rm -rf ~/.cache/frametanium-src && mkdir -p ~/.cache/frametanium-src'
rsync -a -e "$SSH" manager.py page.html catalog.json suggestions.json frametanium.service install.sh art "$USER_AT:.cache/frametanium-src/"
rsync -a -e "$SSH" dist/bundle/ "$USER_AT:.cache/frametanium-src/"
$SSH "$USER_AT" 'sh ~/.cache/frametanium-src/install.sh'
echo "== Headset app"
rsync -a --delete -e "$SSH" dist/app/ "$USER_AT:devkit-game/$GAMEID/"
# The app used to be called Port Manager: remove its old folder and library tile.
$SSH "$USER_AT" '[ ! -e ~/devkit-game/Port_Manager ] || python3 ~/devkit-utils/steamos-delete --delete-title Port_Manager'
$SSH "$USER_AT" "python3 ~/devkit-utils/steam-client-create-shortcut --parms '{\"gameid\": \"$GAMEID\", \"directory\": \"/home/steamos/devkit-game/$GAMEID\", \"argv\": [\"./run.sh\"], \"env\": {}, \"settings\": {\"compat_tool\": \"SteamLinuxRuntime_4-arm64\", \"steam_play\": \"0\"}}'" >/dev/null
echo "== Library art"
APPID=""
for i in 1 2 3 4 5 6 7 8 9 10; do
APPID=$($SSH "$USER_AT" "python3 - $GAMEID" < tools/find_appid.py) && [ -n "$APPID" ] && break
sleep 2
done
if [ -n "$APPID" ]; then
for USERDIR in $($SSH "$USER_AT" 'ls -d ~/.local/share/Steam/userdata/[1-9]*/config 2>/dev/null'); do
$SSH "$USER_AT" "mkdir -p $USERDIR/grid"
for pair in portrait.png:p.png wide.png:.png hero.png:_hero.png logo.png:_logo.png icon.png:_icon.png; do
rsync -a -e "$SSH" "art/app/${pair%%:*}" "$USER_AT:$USERDIR/grid/${APPID}${pair##*:}"
done
done
echo "Art added for app id $APPID (shows after Steam restarts)."
else
echo "Couldn't find the shortcut's app id yet; run this again later to add the art."
fi
echo "Done. Open \"Frametanium\" from the library, or http://$HOST:8643 in a browser."
+40
View File
@@ -24,6 +24,46 @@ rm -rf "$dest/art" && cp -r "$here/art" "$dest/art"
cp "$here/frametanium.service" "$HOME/.config/systemd/user/"
systemctl --user daemon-reload
systemctl --user enable frametanium.service >/dev/null 2>&1
# The headset app, when the bundle has it (releases and deploy.sh do): files are replaced rather than overwritten,
# so a running copy keeps going until it's closed.
if [ -x "$here/app/frametanium" ]; then
game="$HOME/devkit-game/Frametanium"
mkdir -p "$game"
rm -rf "$game/frametanium" "$game/run.sh" "$game/lib"
cp -r "$here/app/." "$game/"
chmod +x "$game/frametanium" "$game/run.sh"
# The app used to be called Port Manager: remove its old folder and library tile.
if [ -e "$HOME/devkit-game/Port_Manager" ]; then
python3 "$HOME/devkit-utils/steamos-delete" --delete-title Port_Manager >/dev/null 2>&1 || true
fi
if [ -f "$HOME/devkit-utils/steam-client-create-shortcut" ]; then
python3 "$HOME/devkit-utils/steam-client-create-shortcut" --parms "{\"gameid\": \"Frametanium\", \"directory\": \"$game\", \"argv\": [\"./run.sh\"], \"env\": {}, \"settings\": {\"compat_tool\": \"SteamLinuxRuntime_4-arm64\", \"steam_play\": \"0\"}}" >/dev/null 2>&1 || true
# Library art, without replacing art already there.
appid=""
for i in 1 2 3 4 5 6 7 8 9 10; do
appid=$(python3 "$here/tools/find_appid.py" Frametanium 2>/dev/null) && [ -n "$appid" ] && break
sleep 2
done
if [ -n "$appid" ]; then
for grid in "$HOME"/.local/share/Steam/userdata/[1-9]*/config; do
[ -d "$grid" ] || continue
mkdir -p "$grid/grid"
for pair in portrait.png:p.png wide.png:.png hero.png:_hero.png logo.png:_logo.png icon.png:_icon.png; do
[ -e "$grid/grid/$appid${pair##*:}" ] || cp "$here/art/app/${pair%%:*}" "$grid/grid/$appid${pair##*:}"
done
done
fi
fi
fi
# When Frametanium updates itself it runs this script from the service, and restarting the service now would stop
# this script and the service before they finish: restart it a few seconds later from a unit of its own instead.
if [ "$(systemctl --user show -p MainPID --value frametanium.service 2>/dev/null)" = "$PPID" ]; then
systemd-run --user --on-active=3 --collect systemctl --user restart frametanium.service >/dev/null
echo "Frametanium restarts in a few seconds."
exit 0
fi
systemctl --user restart frametanium.service
sleep 1
ip=$(python3 -c 'import socket;s=socket.socket(2,2);s.connect(("192.0.2.1",9));print(s.getsockname()[0])' 2>/dev/null || hostname)
+12 -5
View File
@@ -85,9 +85,14 @@ def catalog():
apps = {}
for p in load_json(APP_DIR / "catalog.json", []):
apps[p["id"]] = {**p, "builtin": True}
for p in load_json(USER_CATALOG, []):
if "id" in p:
apps[p["id"]] = {**apps.get(p["id"], {}), **p}
users = [p for p in load_json(USER_CATALOG, []) if "id" in p]
# A repo the user added before it became a built-in one stays the user's entry, so it isn't listed twice.
added = {p["repo"].lower(): p["id"] for p in users if p.get("repo")}
for pid, p in list(apps.items()):
if added.get(p["repo"].lower(), pid) != pid:
del apps[pid]
for p in users:
apps[p["id"]] = {**apps.get(p["id"], {}), **p}
return {k: v for k, v in apps.items() if not v.get("hidden") and v.get("repo")}
@@ -593,7 +598,7 @@ def fetch(url, dest, limit=8 * 1024 * 1024):
def art_folder(port):
"""The app's library art: the built-in ports ship theirs, added apps get their GitHub preview card and the
owner's avatar (fetched once)."""
if port.get("builtin"):
if port.get("builtin") and (APP_DIR / "art" / port["id"]).is_dir():
return APP_DIR / "art" / port["id"]
art = DATA_DIR / "art" / port["id"]
if not (art / "wide.png").exists():
@@ -601,7 +606,7 @@ def art_folder(port):
try:
fetch(f"https://opengraph.githubassets.com/1/{owner}/{name}", art / "wide.png")
shutil.copyfile(art / "wide.png", art / "hero.png")
fetch(f"https://github.com/{owner}.png?size=256", art / "icon.png")
fetch(port.get("icon_url") or f"https://github.com/{owner}.png?size=256", art / "icon.png")
except OSError:
pass
return art
@@ -1043,6 +1048,8 @@ def remove(pid, delete_files=True):
outside its folder stay."""
port = catalog()[pid]
try:
if pid == "frametanium":
raise RuntimeError("Frametanium can't remove itself.")
installed = load_json(INSTALLED_FILE, {})
mode = installed.get(pid, {}).get("mode") or app_mode(port)
note = ""
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
# Puts together what install.sh installs: tools/bundle.sh <built headset app folder> <out folder>
# (the app folder is what app/build-arm64.sh makes: frametanium, run.sh, lib/).
set -e
cd "$(dirname "$0")/.."
APPDIR="$1" OUT="$2"
[ -x "$APPDIR/frametanium" ] || { echo "No built headset app in $APPDIR" >&2; exit 1; }
rm -rf "$OUT" && mkdir -p "$OUT/tools"
cp manager.py page.html catalog.json suggestions.json frametanium.service install.sh README.md "$OUT/"
cp tools/find_appid.py "$OUT/tools/"
cp -r art "$OUT/art"
cp -r "$APPDIR" "$OUT/app"