Dropping a game's .zip, folder or .exe on Send to Frame now adds it to the headset's Steam library through Valve's SteamOS Devkit title path, with the runtime picked from the program's header: Windows PE -> Proton Experimental (steam_play=1), aarch64 ELF -> SteamLinuxRuntime_4-arm64, x86-64 ELF -> SteamLinuxRuntime_4 (through FEX). Other architectures are refused. - frame/devkit-utils: Valve's devkit-utils vendored unmodified (MIT, steamos-devkit v0.20260925.1), synced to ~/devkit-utils by stamp, bundled in the app and compiled in CI. - ui/frame_titles.py: inspect (safe unzip, ELF/PE classification, launch target ranking), install(path, name=None, exe=None, runtime=None, progress=None), list, launch, remove, plus a CLI. - ui/server.py: /api/titles (inspect/install/discard/launch/remove), /api/titles/job progress, and an upload mode 'title'. - ui/index.html: confirm dialog (name, launch target, runtime), install progress, and a Sideloaded titles list with Launch and Remove. The app's preload passes a dropped folder's path. - tests and docs/sideloading.md. Device-side behaviour is inferred from Valve's source; the headset was offline, so none of it has been checked on a Frame yet. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
8.1 KiB
Sideloading Linux and Windows games
A game you have as files (an itch.io download, your own build, a DRM-free release) can go into the Frame's Steam library without a Steam store page. Frame Control uses the same path as Valve's SteamOS Devkit Client: the title becomes a Steam Devkit Game, with a runtime (Proton or a Steam Linux Runtime) chosen from the program itself.
For Android APKs, see apks.md instead.
Status: nothing here has run on a headset yet. Every device-side step is
inferred from Valve's steamos-devkit source (release v0.20260925.1). The
local steps (reading the zip, picking the program and runtime, building the
request) are covered by tests/test_frame_titles.py.
Using it
Drop a game's .zip, folder or .exe on Send to Frame. (Folders need the
desktop app, which knows where a dropped folder lives; in a plain browser, zip
it.) A dialog shows:
- Name: what Steam shows. Steam uses the title id as the name, so it's
limited to letters, digits,
_and-; the dialog shows the result. - Launches: the program picked to start the game, with the other candidates in the list.
- Runtime: picked from the program, see below. Windows programs can switch between Proton Experimental and Proton (stable).
Install copies it to the Frame and registers it with Steam; progress shows in the bar and the activity log. Sideloaded titles lists what's installed, with Launch and Remove. Copy to ~/Downloads instead keeps the old behaviour for a zip that isn't a game.
From a terminal:
python3 ui/frame_titles.py inspect Game.zip # what would be installed, no headset needed
python3 ui/frame_titles.py install Game.zip [--name N] [--exe REL] [--runtime R]
python3 ui/frame_titles.py list | launch ID | remove ID
Choosing the runtime
The program's header decides, not its file name:
| Program | Runtime (Steam compat tool) | steam_play |
Confidence |
|---|---|---|---|
Windows .exe, x86-64 (PE machine 0x8664) |
proton-experimental |
1 | Inferred: ARM64 Proton runs x86-64 code through FEX |
Windows .exe, 32-bit x86 (0x14c) or ARM64 (0xaa64) |
proton-experimental |
1 | Inferred |
Linux ELF, aarch64 (e_machine 0xB7) |
SteamLinuxRuntime_4-arm64 |
0 | Inferred: native |
Linux ELF, x86-64 (0x3E) |
SteamLinuxRuntime_4 |
0 | Inferred: runs through FEX; the least certain row |
| Shell script | the runtime of the Linux binary beside it, else SteamLinuxRuntime_4-arm64 |
0 | Guess |
| Anything else (32-bit Linux, other CPUs, DLLs, data) | refused with a message |
Proton Experimental is the default rather than stable because the Frame's ARM64 Proton and FEX stack is new and Proton fixes reach Experimental first. If a game misbehaves, reinstall it with Proton (stable).
The aliases and settings are the ones Valve's client sends: RUNTIME_ALIASES
in devkit_client/__init__.py, and gui2._update_game, which sets
steam_play=1, steam_play_debug=0, steam_play_debug_version=2019 for Proton
and steam_play=0 otherwise, plus compat_tool=<alias>. Valve's client only
offers SteamLinuxRuntime_4-arm64 and Lepton when the device reports itself
as Deckard (the Frame).
Picking the program
ui/frame_titles.py reads every file's header: ELF executables (PIE ones are
told from shared libraries by their PT_INTERP segment), PE executables (not
DLLs) and scripts with #!. A zip with a single top-level folder is treated
as that folder. Candidates are ranked by:
- Not a helper: names like
UnityCrashHandler64,CrashReportClient,*setup*,unins*,vc_redist*,dxsetup,*prereq*, and anything under_CommonRedist,Redist,DirectXorEnginego last. - Platform: native ARM64 Linux, then Windows x86-64, then x86-64 Linux, then other Windows builds.
- Name: a program named like the zip or folder (build words such as
-linux-arm64or_v1.2are dropped from the name). - Depth, then size: Unreal's top-level
Game.exebeatsGame/Binaries/Win64/Game-Win64-Shipping.exe.
A top-level shell script beats a Linux binary one folder down (run.sh +
bin/game); a binary next to a script wins. The list in the dialog lets you
pick another.
What happens on the Frame (inferred)
- Tools.
frame/devkit-utils/(Valve's scripts, vendored unmodified, MIT) is copied to~/devkit-utils, where Valve's client puts it, unless the stamp file there already matches. Files are merged, not replaced, so a newer copy from Valve's client keeps its extra files. - Folder.
python3 ~/devkit-utils/steamos-prepare-upload --gameid IDmakes~/devkit-game/IDand prints{user, directory}. - Copy. The files go there with
rsync -a --deleteon macOS and Linux, orscp -rinto a fresh folder that then replaces it on Windows. Thenchmod -R 755, the modes Valve's client gives an upload. - Register.
python3 ~/devkit-utils/steam-client-create-shortcut --parms JSONwith{gameid, directory, argv: [target], env: {}, settings, clear_settings, force_appid: "", lepton_args: ""}. It writesID-argv.json,ID-env.jsonandID-settings.jsonnext to the folder, then sendscreate-shortcutto the running Steam client over~/.steam/steam.pipe(authenticated by~/.steam/steam.token) and waits up to 5 s for Steam's answer file. Itserror, for example "The Steam client is not running", is shown as the install error. The files stay, so installing again with Steam running finishes the job. - Launch is
steam-devkit-rpc run-game gameid=ID. Remove issteamos-delete --delete-title ID, which deletes the folder and has Steam drop shortcuts with no folder. Frame Control then removes theID-*.jsonfiles that Valve's script leaves behind.
Frame Control also writes ~/devkit-game/ID-framecontrol.json (name, source
file, target, runtime, size). Sideloaded titles lists every folder in
~/devkit-game, including titles uploaded with Valve's client.
argv is one string, as in Valve's client (the start command may carry
arguments), so a program path with spaces is sent in double quotes. How Steam
splits that string is not checked.
Safety
- Zips are unpacked on your computer first. Entries with absolute paths,
.., drive letters or:anywhere in the path, or links that point outside the zip (or at a folder they're in) are refused. So are zips over 64 GB unpacked, over 200,000 entries, more than 200× compressed past 1 GB, or bigger than the free space. - No symlink is created while unpacking, so no write can be redirected
through one. A link to a file inside the zip (
libfoo.so.1 → libfoo.so.1.2) becomes a copy of that file, which also works on Windows. Links to folders, loops and dangling links are left out. - A dropped folder that contains symlinks (or Windows junctions) is copied on your computer first,
with the same rule, because
scp -rwould follow a link out of the folder and upload whatever it points at. - Installs run one at a time, and Remove is refused while one runs.
- The title id is limited to
[A-Za-z0-9_-], at most 64 characters. Valve's scripts pass it to a shell (steamos-deleterunsrm -ron it). Valve's reserved sideload names (steam,steamvr, and theirdeckardforms, which would replace the Steam client itself) get-gameadded. - Nothing needs
sudo; everything goes to your home folder on the Frame. - In the app, a dropped folder is read from its local path by the app's own server, which only accepts requests from its own page (see frame-control.md).
To check on a headset
create-shortcutregisters a title and it shows in the library under its id.- An aarch64 build launches in
SteamLinuxRuntime_4-arm64. - An x86-64 Windows
.exelaunches under Proton Experimental through FEX. - An x86-64 Linux build launches in
SteamLinuxRuntime_4through FEX. steam-devkit-rpc run-gamestarts it, andsteamos-deleteremoves the shortcut.- How Steam splits a start command with a quoted path.
- Whether these titles open as flat panels or need anything VR-specific.