mirror of
https://github.com/Cogential/frametanium.git
synced 2026-10-17 16:00:28 +02:00
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:
1 parent
6c9c5f4ce7
commit
6c27fd8fc2
7 files changed
+178
-40
No files matched your search
@@ -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."
|
||||
@@ -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
@@ -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."
|
||||
}
|
||||
]
|
||||
@@ -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
@@ -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
@@ -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 = ""
|
||||
|
||||
Executable
+12
@@ -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"
|
||||
Reference in new issue
Block a user