Files
saphid--frame-control/frame/devkit-utils/steam-client-create-shortcut
T
saphidandClaude Opus 5.5 6c4d387ef3 Sideload Linux and Windows builds as Steam Devkit Games
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>
2026-09-26 20:38:14 +10:00

93 lines
3.7 KiB
Python

#!/usr/bin/env python3
import os
import logging
import argparse
import json
import platform
import tempfile
from urllib.parse import quote_plus as urllib_quote_plus
import devkit_utils
logging.basicConfig(format='%(message)s', level=logging.DEBUG)
logger = logging.getLogger()
DEVKIT_TOOL_FOLDER = os.path.expanduser('~/devkit-game')
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--verbose', required=False, action='store_true')
parser.add_argument('--parms', required=True, action='store')
conf = parser.parse_args()
if conf.verbose:
logger.setLevel(logging.DEBUG)
else:
logger.setLevel(logging.INFO)
parms = json.loads(conf.parms)
gameid = parms['gameid']
directory = parms['directory']
assert os.path.isdir(directory)
force_appid = parms['force_appid']
steam_appid_path = os.path.join(directory, 'steam_appid.txt')
if force_appid:
with open(steam_appid_path, 'w') as f:
f.write(force_appid + '\n')
logger.info(f'Wrote {steam_appid_path} with AppID {force_appid}')
elif os.path.exists(steam_appid_path):
# don't overwrite an existing steam_appid.txt file from the content tree
# NOTE: if the user sets an AppID through the tool, then delete it, we may leave it in place ..
# (that's ok for now, do a clean upload if you want to get rid of it)
logger.info(f'{steam_appid_path} already exists, leaving it in place')
# Lepton (Android runtime) titles: write UECommandLine.txt next to the .apk
is_lepton = parms['settings']['compat_tool'] == 'lepton'
uecommandline = parms['lepton_args'] if is_lepton else ''
uecommandline_path = os.path.join(directory, 'UECommandLine.txt')
if uecommandline:
with open(uecommandline_path, 'w') as f:
f.write(uecommandline + '\n')
logger.info(f'Wrote {uecommandline_path}')
elif os.path.exists(uecommandline_path):
# don't overwrite an existing UECommandLine.txt file from the content tree
# NOTE: if the user sets cmdline args through the tool, then clears them, we may leave it in place ..
# (that's ok for now, do a clean upload if you want to get rid of it)
logger.info(f'{uecommandline_path} already exists, leaving it in place')
logger.info(f'Updating command line and runtime settings for {gameid} on {platform.node()}')
devkit_utils.save_argv(gameid, parms['argv'])
devkit_utils.save_env(gameid, parms['env'])
devkit_utils.save_settings(gameid, parms)
ret = {}
try:
devkit_utils.validate_steam_client()
except devkit_utils.SteamClientNotRunningException as e:
skipping = 'The Steam client is not running. Registration did not complete.'
logger.warning(skipping)
ret['error'] = skipping
else:
with tempfile.TemporaryDirectory(prefix='create-shortcut') as tempdir:
logger.info(f'Registering Devkit Game {gameid} with Steam Client')
response = os.path.join(tempdir, 'registered')
cmd = 'create-shortcut?response={}&gameid={}'.format(
urllib_quote_plus(response),
gameid,
)
devkit_utils.execute_steam_client_command(cmd)
try:
with devkit_utils.wait_on_file_response(response) as success_response:
logger.debug(success_response)
ret['success'] = success_response
except devkit_utils.SteamResponse_Timeout:
ret['error'] = 'timeout - Steam client did not respond to registration request'
except devkit_utils.SteamResponse_Error as e:
ret['error'] = e.error_response
# response gets written out to stdout
print(json.dumps(ret))