#!/usr/bin/env python3 import sys import os import logging import argparse import enum import subprocess import shutil import pathlib logging.basicConfig(format='%(message)s', level=logging.DEBUG) logger = logging.getLogger(__name__) DEVKIT_TOOL_FOLDER = os.path.expanduser('~/devkit-game') # NOTE: only relevant to Frame + OS client with extra arguments # sideloaded client on Frame supports full command line edit instead STEAM_EXTRA_ARGS_FILE = os.path.expanduser('~/.config/systemd/user/steam.service.d/extra_args.conf') class SteamStatus(enum.Enum): # Supported values from steamos-get-status OS = 2 OS_DEV = 3 SIDELOADED = 4 STATUS_STRINGS = [ ( SteamStatus.OS, 'SteamStatus.OS' ), ( SteamStatus.OS_DEV, 'SteamStatus.OS_DEV' ), ( SteamStatus.SIDELOADED, 'SteamStatus.SIDELOADED' ), ] # gamescope-session passes the execution to this script if it exists rather than start steam itself DEVKIT_STEAM_TRAMPOLINE = os.path.expanduser('~/devkit-game/devkit-steam') # this script executes the sideloaded Steam client (part of the steamos-devkit-service package) SIDE_LOADED_STEAM_CLIENT = '/usr/share/steamos-devkit/bin/devkit-standalone.py' def write_trampoline(text): with open(DEVKIT_STEAM_TRAMPOLINE, 'w') as devkit_steam: devkit_steam.write(text) devkit_steam.flush() os.chmod(DEVKIT_STEAM_TRAMPOLINE, 0o770) # trying really hard to avoid leaving a zero sized trampoline if the deck is about to hang on the session restart coming next subprocess.run(['/usr/bin/sync', DEVKIT_TOOL_FOLDER]) def get_os_info(): os_info = {} try: for k, v in [ s.split('=') for s in open('/etc/os-release').read().split('\n') if len(s) > 0 ]: os_info[k] = v.strip('"') except Exception as e: logger.error(e) logger.error('Failed to parse OS release file') return os_info if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--verbose', required=False, action='store_true') parser.add_argument('--client', action='store', required=True, choices=[ v[1] for v in STATUS_STRINGS ]) parser.add_argument('--args', action='store', required=False, help='steam client command line arguments') parser.add_argument('--gameid', required=True, action='store') parser.add_argument('--gdbserver', action='store_true', required=False) conf = parser.parse_args() if conf.verbose: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) target = [ v for v in STATUS_STRINGS if v[1] == conf.client ][0][0] logging.info(f'Set steam client on device to {target}') if os.path.exists(DEVKIT_STEAM_TRAMPOLINE): os.unlink(DEVKIT_STEAM_TRAMPOLINE) os_info = get_os_info() assert os_info is not None is_deckard = os_info.get('VARIANT_ID', None) == 'vr' if target == SteamStatus.OS: if is_deckard: # conf.args is the extra arguments for the normal Steam 'OS client', update it now if conf.args is None or conf.args == '': logger.info('Clearning extra arguments for normal Steam client') if os.path.exists(STEAM_EXTRA_ARGS_FILE): os.unlink(STEAM_EXTRA_ARGS_FILE) subprocess.run(['systemctl', '--user', 'daemon-reload'], check=True) else: logger.info(f'Setting extra arguments for normal Steam client: {conf.args}') os.makedirs(os.path.dirname(STEAM_EXTRA_ARGS_FILE), exist_ok=True) with open(STEAM_EXTRA_ARGS_FILE, 'wt') as extra_args_file: escaped_args = conf.args.replace('"', '\\"') extra_args_file.write(f'[Service]\nEnvironment="STEAM_EXTRA_ARGS={escaped_args}"') subprocess.run(['systemctl', '--user', 'daemon-reload'], check=True) # When disabling a sideloaded client, also delete the ~/.steam symlinks: # They will be re-created by the OS client when starting, # this prevents SteamVR trying to use the sideloaded binaries that are still there for the steam API. # (this may happen because SteamVR starts before Steam starts and has a chance to set those symlinks correctly) for path in pathlib.Path(os.path.expanduser('~/.steam')).glob('*'): if path.is_symlink(): try: lnk = path.resolve() if 'devkit-game' in str(lnk): path.unlink() print(f'Deleted: {path} -> {lnk}') except Exception as e: print(f'Error processing {path}: {e}') logger.info('Devkit Steam client override is disabled - default OS client execution will resume.') sys.exit(0) os.makedirs(os.path.dirname(DEVKIT_STEAM_TRAMPOLINE), exist_ok=True) # OS client steam_client = '$HOME/.local/share/Steam/steam.sh' if target == SteamStatus.SIDELOADED: steam_client = '$HOME/devkit-game/steam/steam.sh' if is_deckard: # RUNSTEAM.sh checks for SIDELOADED_STEAMROOT="${HOME}/devkit-game/steam" # this is consistent with sideload on Steam Deck, but we use a different name 'steamdeckard' # will be addressed when reworking the sideload and debug strategy, for now just drop in a symlink steam_symlink = os.path.expanduser('~/devkit-game/steam') if os.path.lexists(steam_symlink): if os.path.islink(steam_symlink): os.unlink(steam_symlink) else: # this happens if an upload in Steam Deck mode was attempted against a Steam Frame for instance # was an easy mistake to make before recent changes logger.warning('warning: ~/devkit-game/steam exists but is not a symlink. Removing anyway.') shutil.rmtree(steam_symlink) os.symlink( os.path.expanduser('~/devkit-game/steamdeckard'), steam_symlink, ) args = '"$@"' if conf.args is not None: args = conf.args gdbserver = '' if conf.gdbserver: logger.info('Configuring for remote debugging via gdbserver') gdbserver = 'export DEBUGGER="gdbserver 0.0.0.0:2345"' write_trampoline('''#!/bin/bash # Generated by steamos-set-steam-client, do not edit! # configuration tag (do not delete): {} {} mkdir -p $HOME/.steam/steam/logs exec {} {} '''.format( conf.client, gdbserver, steam_client, args ))