2024-02-25 21:46:15 +01:00

2021 lines
67 KiB
Python
Executable File

#!/usr/bin/env python3
# Copyright © 2019-2022 Collabora Ltd.
#
# SPDX-License-Identifier: MIT
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import argparse
import contextlib
import logging
import os
import shlex
import subprocess
import sys
try:
import typing
except ImportError:
pass
import gi
gi.require_version('Gtk', '3.0')
# Ignore E402: import not at top of file. gi.require_version() must come first
from gi.repository import GLib # noqa: E402
from gi.repository import Gtk # noqa: E402
logger = logging.getLogger('steam-runtime-launch-options')
assert sys.version_info >= (3, 4), 'Python 3.4+ is required for this script'
# Linux runtime environments we can target with the Steam Runtime.
# Traditionally, all native Linux games have run in a scout environment,
# variously referred to as 'linux' or 'ubuntu12_32' by Steam.
RUNTIMES = [
'scout', # Steam Runtime 1 'scout', based on Ubuntu 12.04
'heavy', # Steam Runtime 1½ 'heavy', based on Debian 8
'soldier', # Steam Runtime 2 'soldier', based on Debian 10
'sniper', # Steam Runtime 3 'sniper', based on Debian 11
'medic', # Steam Runtime 4 'medic', provisionally based on Debian 12
'steamrt5', # Steam Runtime 5, provisionally based on Debian 13
]
# All available compatibility targets, including Windows (via Proton)
# and native Linux (whatever the host system happens to be running, e.g.
# SteamOS, Debian, Arch, Fedora).
COMPAT_TARGETS = RUNTIMES + ['windows', 'host']
def tristate_environment(name):
# type: (str) -> typing.Optional[bool]
value = os.getenv(name)
if value is None or value == '':
return None
if value == '1':
return True
if value == '0':
return False
logger.warning('Unrecognised value %r for $%s', value, name)
return None
def boolean_environment(name, default):
# type: (str, bool) -> bool
value = os.getenv(name)
if value is None:
return default
if value == '1':
return True
if value in ('', '0'):
return False
logger.warning('Unrecognised value %r for $%s', value, name)
return default
def to_shell(argv):
# type: (typing.Iterable[str]) -> str
return ' '.join(map(shlex.quote, argv))
class Component:
def __init__(
self,
path, # type: str
home, # type: str
): # type: (...) -> None
self.home = home
self.path = path
self.argv = [] # type: typing.List[str]
self.description = ''
self.runs_on = ''
class App(Component):
def __init__(
self,
path, # type: str
home, # type: str
argv, # type: typing.List[str]
): # type: (...) -> None
super().__init__(path, home=home)
self.argv = argv
self.description = 'App or game to run'
self.runs_on = 'scout'
class Proton(Component):
def __init__(
self,
path, # type: str
home, # type: str
argv, # type: typing.List[str]
): # type: (...) -> None
super().__init__(path, home=home)
self.argv = argv
if path.startswith(self.home + '/'):
path = '~' + path[len(self.home):]
version = os.path.basename(self.path)
self.description = '{}\n({})'.format(version or '(unknown)', path)
# TODO: Parse this with python3-vdf?
try:
with open(os.path.join(self.path, 'toolmanifest.vdf')) as reader:
content = reader.read().strip()
except Exception:
logger.debug('Failed to get Proton tool manifest', exc_info=True)
content = ''
if '1391110' in content:
self.runs_on = 'soldier'
elif '1628350' in content:
self.runs_on = 'sniper'
else:
self.runs_on = 'scout'
class PressureVessel(Component):
def __init__(
self,
path, # type: str
home, # type: str
): # type: (...) -> None
super().__init__(path, home=home)
if path.startswith(self.home + '/'):
path = '~' + path[len(self.home):]
try:
subproc = subprocess.Popen(
[
os.path.join(
self.path,
'bin',
'pressure-vessel-wrap'
),
'--version-only',
],
stdout=subprocess.PIPE,
)
stdout, _ = subproc.communicate()
version = stdout.decode('utf-8', errors='replace').strip()
except Exception:
logger.debug(
'Failed to run %s/bin/pressure-vessel-wrap --version-only',
self.path,
exc_info=True,
)
version = ''
self.adverb = os.path.join(
self.path, 'bin', 'pressure-vessel-adverb',
)
self.description = '{}\n({})'.format(version or '(unknown)', path)
self.unruntime = os.path.join(
self.path, 'bin', 'pressure-vessel-unruntime',
)
self.version = version
class Runtime(Component):
def __init__(
self,
path, # type: str
home, # type: str
): # type: (...) -> None
super().__init__(path, home=home)
self.provides = ''
class LdlpRuntime(Runtime):
def __init__(
self,
path, # type: str
home, # type: str
): # type: (...) -> None
super().__init__(path, home=home)
try:
with open(os.path.join(path, 'version.txt')) as reader:
version = reader.read().strip()
except Exception:
logger.debug('Failed to get LDLP runtime version', exc_info=True)
version = ''
if version.startswith('steam-runtime-heavy_'):
version = 'heavy ' + version[len('steam-runtime-heavy_'):]
if not self.provides:
self.provides = 'heavy'
elif version.startswith('steam-runtime_'):
version = 'scout ' + version[len('steam-runtime_'):]
if not self.provides:
self.provides = 'scout'
if path.startswith(self.home + '/'):
path = '~' + path[len(self.home):]
self.description = '{}\n({})'.format(
version or '(unknown version)',
path,
)
self.argv = [
os.path.join(self.path, 'scripts', 'switch-runtime.sh'),
'--runtime=' + self.path,
'--',
]
class LaunchWrapper(Component):
def __init__(
self,
path, # type: str
home, # type: str
argv, # type: typing.List[str]
): # type: (...) -> None
super().__init__(path, home=home)
self.argv = argv
class Reaper(Component):
def __init__(
self,
path, # type: str
home, # type: str
argv, # type: typing.List[str]
): # type: (...) -> None
super().__init__(path, home=home)
self.argv = argv
class LayeredRuntime(LdlpRuntime):
def __init__(
self,
path, # type: str
home, # type: str
argv, # type: typing.List[str]
): # type: (...) -> None
super().__init__(path, home=home)
self.argv = argv
if path.startswith(self.home + '/'):
path = '~' + path[len(self.home):]
self.description = path
self.provides = 'scout'
self.runs_on = 'soldier'
class ContainerRuntime(Runtime):
def get_sort_weight(
self,
default='' # type: str
): # type: (...) -> typing.Any
return (0,)
def _runtime_version(self):
# type: (...) -> typing.Any
if self.provides.startswith('steamrt'):
return int(self.provides[len('steamrt'):])
return {
'scout': 1,
'heavy': 1.5,
'soldier': 2,
'sniper': 3,
'medic': 4,
}.get(self.provides, 0)
class ContainerRuntimeDepot(ContainerRuntime):
def __init__(
self,
path, # type: str
home, # type: str
argv, # type: typing.List[str]
): # type: (...) -> None
super().__init__(path, home=home)
self.argv = argv
if path.startswith(self.home + '/'):
path = '~' + path[len(self.home):]
try:
self.description = self.__describe_runtime(path)
except Exception:
logger.debug('Failed to get runtime info', exc_info=True)
self.description = os.path.basename(path)
self.description = '{}\n({})'.format(self.description, path)
self.pressure_vessel = None # type: typing.Optional[PressureVessel]
self.var_path = os.path.join(self.path, 'var')
try:
pv = PressureVessel(
os.path.join(self.path, 'pressure-vessel'),
home=home,
)
except Exception:
logger.debug('Failed to get PV info', exc_info=True)
else:
self.pressure_vessel = pv
def get_sort_weight(self, default):
if default == self.provides:
weight = -10
else:
weight = -1
return (weight, self._runtime_version(), self.path)
def __describe_runtime(
self,
path # type: str
):
# type: (...) -> str
platform = ['', '']
depot_version = ''
with open(os.path.join(self.path, 'VERSIONS.txt')) as reader:
for row in reader:
if row.startswith('#'):
continue
if row.startswith('depot\t'):
depot_version = row.split('\t')[1]
if row.startswith(('soldier\t', 'sniper\t')):
platform = row.split('\t')[:1]
if platform[0]:
self.provides = platform[0]
return platform[0] + ' ' + (depot_version or platform[1])
return '(unknown)'
class DirectoryRuntime(ContainerRuntime):
def __init__(
self,
path, # type: str
home, # type: str
): # type: (...) -> None
super().__init__(path, home=home)
self.description = self.__describe_runtime(path)
def get_sort_weight(self, default):
return (1, self._runtime_version(), self.path)
def __describe_runtime(
self,
path # type: str
):
# type: (...) -> str
description = path
files = os.path.join(self.path, 'files')
metadata = os.path.join(self.path, 'metadata')
if os.path.islink(files):
description = os.path.realpath(files)
if description.startswith(self.home + '/'):
description = '~' + description[len(self.home):]
name = None # type: typing.Optional[str]
pretty_name = None # type: typing.Optional[str]
build_id = None # type: typing.Optional[str]
variant = None # type: typing.Optional[str]
try:
keyfile = GLib.KeyFile.new()
keyfile.load_from_file(
metadata, GLib.KeyFileFlags.NONE)
try:
build_id = keyfile.get_string('Runtime', 'x-flatdeb-build-id')
except GLib.Error:
pass
try:
name = keyfile.get_string('Runtime', 'runtime')
except GLib.Error:
pass
else:
assert name is not None
variant = name.split('.')[-1]
except GLib.Error:
pass
try:
with open(
os.path.join(files, 'lib', 'os-release')
) as reader:
for line in reader:
if line.startswith('PRETTY_NAME='):
pretty_name = line.split('=', 1)[1].strip()
pretty_name = GLib.shell_unquote(pretty_name)
elif line.startswith('BUILD_ID='):
build_id = line.split('=', 1)[1].strip()
build_id = GLib.shell_unquote(build_id)
elif line.startswith('VARIANT='):
variant = line.split('=', 1)[1].strip()
variant = GLib.shell_unquote(variant)
except (GLib.Error, EnvironmentError):
pass
if pretty_name is None:
pretty_name = name
if pretty_name is None:
pretty_name = os.path.basename(path)
if build_id is None:
build_id = ''
else:
build_id = ' build {}'.format(build_id)
if variant is None:
variant = ''
else:
variant = ' {}'.format(variant)
description = '{}{}{}\n({})'.format(
pretty_name,
variant,
build_id,
description,
)
return description
class ArchiveRuntime(ContainerRuntime):
def __init__(
self,
path, # type: str
buildid_file, # type: str
home, # type: str
): # type: (...) -> None
super().__init__(path, home=home)
if path.startswith(self.home + '/'):
path = '~' + path[len(self.home):]
description = os.path.basename(path)
sdk_suffix = ''
if description.startswith('com.valvesoftware.SteamRuntime.'):
description = description[len('com.valvesoftware.SteamRuntime.'):]
if description.startswith('Platform-'):
description = description[len('Platform-'):]
if description.startswith('Sdk-'):
sdk_suffix = '-sdk'
description = description[len('Sdk-'):]
if description.startswith('amd64,i386-'):
description = description[len('amd64,i386-'):]
if description.endswith('.tar.gz'):
description = description[:-len('.tar.gz')]
if description.endswith('-runtime'):
description = description[:-len('-runtime')]
with open(buildid_file) as reader:
build = reader.read().strip()
self.deploy_id = '{}{}_{}'.format(description, sdk_suffix, build)
self.description = '{} build {}\n({})'.format(description, build, path)
def get_sort_weight(self, default):
return (2, self._runtime_version(), self.path)
class Gui:
def __init__(self):
# type: (...) -> None
self.steam_runtime_env = {} # type: typing.Dict[str, str]
self.failed = False
self.home = GLib.get_home_dir()
self.app = App(path='', argv=[], home=self.home)
self.launch_wrapper = None # type: typing.Optional[LaunchWrapper]
self.reaper = None # type: typing.Optional[Reaper]
self.default_container_runtime = (
None
) # type: typing.Optional[ContainerRuntimeDepot]
self.default_pressure_vessel = (
None
) # type: typing.Optional[PressureVessel]
self.default_layered_runtime = (
None
) # type: typing.Optional[LayeredRuntime]
self.default_proton = (
None
) # type: typing.Optional[Proton]
self.container_runtimes = {
} # type: typing.Dict[str, ContainerRuntime]
self.pressure_vessels = {
} # type: typing.Dict[str, PressureVessel]
self.layered_runtimes = {
} # type: typing.Dict[str, LayeredRuntime]
self.ldlp_runtimes = {
} # type: typing.Dict[str, LdlpRuntime]
self.proton_versions = {
} # type: typing.Dict[str, Proton]
self._changing = 0
self._changing_container_runtime = 0
self._container_runtime_changed_id = 0
self._layered_runtime_changed_id = 0
self._pressure_vessel_changed_id = 0
self._ldlp_runtime_changed_id = 0
self._proton_changed_id = 0
self.window = Gtk.Window()
self.window.set_default_size(720, 480)
self.window.connect('delete-event', Gtk.main_quit)
self.window.set_title('Launch options')
self.vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 6)
self.window.add(self.vbox)
row = 0
self.grid = Gtk.Grid(
row_spacing=6,
column_spacing=6,
margin_top=12,
margin_bottom=12,
margin_start=12,
margin_end=12,
)
scrolled_window = Gtk.ScrolledWindow.new(None, None)
scrolled_window.add(self.grid)
if hasattr(scrolled_window.props, 'propagate_natural_width'):
scrolled_window.props.propagate_natural_width = True
scrolled_window.props.propagate_natural_height = True
scrolled_window.set_policy(
Gtk.PolicyType.NEVER,
Gtk.PolicyType.AUTOMATIC,
)
self.vbox.pack_start(scrolled_window, True, True, 0)
label = Gtk.Label.new('')
label.set_markup(
'This is a test UI for developers. '
'<b>'
'Some options are known to break games and Steam features.'
'</b>'
' Use at your own risk!'
)
label.set_line_wrap(True)
self.grid.attach(label, 0, row, 3, 1)
row += 1
label = Gtk.Label.new('Container runtime')
self.grid.attach(label, 0, row, 1, 1)
self.container_runtime_combo = Gtk.ComboBoxText.new()
self.grid.attach(self.container_runtime_combo, 1, row, 2, 1)
row += 1
label = Gtk.Label.new('Variable data path')
self.grid.attach(label, 0, row, 1, 1)
self.var_path_entry = Gtk.Entry.new()
self.var_path_entry.props.editable = False
self.var_path_entry.props.has_frame = False
self.var_path_entry.props.hexpand = True
self.grid.attach(self.var_path_entry, 1, row, 1, 1)
self.var_path_browse = Gtk.Button.new_with_label('Browse...')
self.var_path_browse.connect('clicked', self.var_path_browse_cb)
self.var_path_browse.props.hexpand = False
self.grid.attach(self.var_path_browse, 2, row, 1, 1)
row += 1
label = Gtk.Label.new('pressure-vessel')
self.grid.attach(label, 0, row, 1, 1)
self.pressure_vessel_combo = Gtk.ComboBoxText.new()
self.grid.attach(self.pressure_vessel_combo, 1, row, 2, 1)
row += 1
label = Gtk.Label.new('Layered runtime scripts')
self.grid.attach(label, 0, row, 1, 1)
self.layered_runtime_combo = Gtk.ComboBoxText.new()
self.grid.attach(self.layered_runtime_combo, 1, row, 2, 1)
row += 1
label = Gtk.Label.new('LD_LIBRARY_PATH runtime')
self.grid.attach(label, 0, row, 1, 1)
self.ldlp_runtime_combo = Gtk.ComboBoxText.new()
self.grid.attach(self.ldlp_runtime_combo, 1, row, 2, 1)
row += 1
label = Gtk.Label.new('Proton')
self.grid.attach(label, 0, row, 1, 1)
self.proton_combo = Gtk.ComboBoxText.new()
self.grid.attach(self.proton_combo, 1, row, 2, 1)
row += 1
label = Gtk.Label.new('SDL video driver')
self.grid.attach(label, 0, row, 1, 1)
self.sdl_videodriver_combo = Gtk.ComboBoxText.new()
self.sdl_videodriver_combo.append(None, "Don't override")
self.sdl_videodriver_combo.append('wayland', 'Wayland')
self.sdl_videodriver_combo.append('x11', 'X11')
self.sdl_videodriver_combo.set_active(0)
self.grid.attach(self.sdl_videodriver_combo, 1, row, 2, 1)
row += 1
label = Gtk.Label.new('Graphics stack')
self.grid.attach(label, 0, row, 1, 1)
self.graphics_provider_combo = Gtk.ComboBoxText.new()
self.graphics_provider_combo.append(None, "Don't override")
env = os.getenv('PRESSURE_VESSEL_GRAPHICS_PROVIDER')
if env is not None:
self.graphics_provider_combo.append(
env,
'$PRESSURE_VESSEL_GRAPHICS_PROVIDER ({})'.format(
env or 'empty'
),
)
if env is None or env != '/':
self.graphics_provider_combo.append(
'/', 'Current execution environment',
)
if (
(env is None or env != '/run/host')
and os.path.isdir('/run/host/etc')
and os.path.isdir('/run/host/usr')
):
self.graphics_provider_combo.append(
'/run/host', 'Host system',
)
if env is None or env != '':
self.graphics_provider_combo.append(
'',
"Container's own libraries (probably won't work)",
)
self.graphics_provider_combo.set_active(0)
self.graphics_provider_combo.connect(
'changed', self._something_changed_cb,
)
self.grid.attach(self.graphics_provider_combo, 1, row, 2, 1)
row += 1
label = Gtk.Label.new('Home directory')
self.grid.attach(label, 0, row, 1, 1)
self.share_home_combo = Gtk.ComboBoxText.new()
self.share_home_combo.append(None, "Don't override")
self.share_home_combo.append('1', 'Shared between all games')
self.share_home_combo.append(
'0',
('Separate per game '
'(experimental, breaks Steam features)'),
)
self.share_home_combo.set_active(0)
self.share_home_combo.connect(
'changed', self._something_changed_cb,
)
self.grid.attach(self.share_home_combo, 1, row, 2, 1)
row += 1
label = Gtk.Label.new('Process ID namespace')
self.grid.attach(label, 0, row, 1, 1)
self.share_pid_combo = Gtk.ComboBoxText.new()
self.share_pid_combo.append(None, "Don't override")
self.share_pid_combo.append(
'1',
'Use the same process ID namespace as Steam',
)
self.share_pid_combo.append(
'0',
('Create a new process ID namespace '
'(experimental, breaks Steam features)'),
)
self.share_pid_combo.set_active(0)
self.share_pid_combo.connect(
'changed', self._something_changed_cb,
)
self.grid.attach(self.share_pid_combo, 1, row, 2, 1)
row += 1
label = Gtk.Label.new('Steam Overlay')
self.grid.attach(label, 0, row, 1, 1)
self.remove_game_overlay_combo = Gtk.ComboBoxText.new()
self.remove_game_overlay_combo.append(None, "Don't override")
self.remove_game_overlay_combo.append('0', 'Keep Steam Overlay')
self.remove_game_overlay_combo.append(
'1',
'Remove Steam Overlay (breaks Steam features)',
)
self.remove_game_overlay_combo.set_active(0)
self.remove_game_overlay_combo.connect(
'changed', self._something_changed_cb,
)
self.grid.attach(self.remove_game_overlay_combo, 1, row, 2, 1)
row += 1
label = Gtk.Label.new('Vulkan layers')
self.grid.attach(label, 0, row, 1, 1)
self.vulkan_layers_combo = Gtk.ComboBoxText.new()
self.vulkan_layers_combo.append(None, "Don't override")
self.vulkan_layers_combo.append(
'1',
'Force importing Vulkan layers from host',
)
self.vulkan_layers_combo.append(
'0',
'Disable Vulkan layers from host',
)
self.vulkan_layers_combo.set_active(0)
self.vulkan_layers_combo.connect(
'changed', self._something_changed_cb,
)
self.grid.attach(self.vulkan_layers_combo, 1, row, 2, 1)
row += 1
label = Gtk.Label.new('Command injection')
self.grid.attach(label, 0, row, 1, 1)
self.launcher_service_combo = Gtk.ComboBoxText.new()
self.launcher_service_combo.append(None, "Don't override")
self.launcher_service_combo.append(
'container-runtime', 'SteamLinuxRuntime_{soldier,sniper,...}',
)
self.launcher_service_combo.append(
'proton', 'any Proton version',
)
self.launcher_service_combo.append(
'scout-in-container', 'any layered scout-on-* runtime',
)
self.launcher_service_combo.append(
'', 'None',
)
self.launcher_service_combo.set_active(0)
self.launcher_service_combo.connect(
'changed', self._something_changed_cb,
)
self.grid.attach(self.launcher_service_combo, 1, row, 2, 1)
row += 1
label = Gtk.Label.new('Interactive terminal')
self.grid.attach(label, 0, row, 1, 1)
self.terminal_combo = Gtk.ComboBoxText.new()
self.terminal_combo.append(None, "Don't override")
self.terminal_combo.append('xterm', 'Run in an xterm')
self.terminal_combo.append(
'none',
"Don't run in an interactive terminal",
)
self.terminal_combo.set_active(0)
self.terminal_combo.connect(
'changed', self._something_changed_cb,
)
self.grid.attach(self.terminal_combo, 1, row, 2, 1)
row += 1
label = Gtk.Label.new('Interactive shell')
self.grid.attach(label, 0, row, 1, 1)
self.shell_combo = Gtk.ComboBoxText.new()
self.shell_combo.append(None, "Don't override")
self.shell_combo.append('none', 'No, just run the command')
self.shell_combo.append('after', 'After running the command')
self.shell_combo.append('fail', 'If the command fails')
self.shell_combo.append('instead', 'Instead of running the command')
self.shell_combo.set_active(0)
self.shell_combo.connect(
'changed', self._something_changed_cb,
)
self.grid.attach(self.shell_combo, 1, row, 2, 1)
row += 1
self.debug_check = Gtk.CheckButton.new_with_label(
'Extra debug logging',
)
self.debug_check.set_active(False)
self.grid.attach(self.debug_check, 1, row, 2, 1)
row += 1
label = Gtk.Label.new('Command to run')
self.grid.attach(label, 0, row, 1, 1)
self.command_entry = Gtk.Entry.new()
self.command_entry.props.editable = True
self.command_entry.set_text(to_shell(self.app.argv))
self.command_entry.connect(
'notify::text', self._command_entry_changed_cb,
)
self.grid.attach(self.command_entry, 1, row, 2, 1)
row += 1
label = Gtk.Label.new('Preview of final command')
self.grid.attach(label, 0, row, 1, 1)
self.final_command_view = Gtk.TextView.new()
self.final_command_view.props.editable = False
scrolled_window = Gtk.ScrolledWindow.new(None, None)
scrolled_window.add(self.final_command_view)
if hasattr(scrolled_window.props, 'propagate_natural_width'):
scrolled_window.props.propagate_natural_width = True
scrolled_window.props.propagate_natural_height = True
else:
scrolled_window.props.height_request = 120
self.grid.attach(scrolled_window, 1, row, 2, 1)
row += 1
buttons_grid = Gtk.Grid(
column_spacing=6,
column_homogeneous=True,
halign=Gtk.Align.END,
)
cancel_button = Gtk.Button.new_with_label('Cancel')
cancel_button.connect('clicked', Gtk.main_quit)
buttons_grid.attach(cancel_button, 0, 0, 1, 1)
run_button = Gtk.Button.new_with_label('Run')
run_button.connect('clicked', self.run_cb)
buttons_grid.attach(run_button, 1, 0, 1, 1)
self.vbox.pack_end(buttons_grid, False, False, 0)
self._container_runtime_changed_id = (
self.container_runtime_combo.connect(
'changed',
self._container_runtime_changed,
)
)
self._pressure_vessel_changed_id = (
self.pressure_vessel_combo.connect(
'changed',
self._pressure_vessel_changed,
)
)
self._layered_runtime_changed_id = (
self.layered_runtime_combo.connect(
'changed',
self._layered_runtime_changed,
)
)
self._ldlp_runtime_changed_id = (
self.ldlp_runtime_combo.connect(
'changed',
self._ldlp_runtime_changed,
)
)
self._proton_changed_id = (
self.proton_combo.connect(
'changed',
self._proton_changed,
)
)
def parse_args(
self,
argv # type: typing.List[str]
):
# type: (...) -> None
parser = argparse.ArgumentParser()
parser.add_argument(
'--compatible-with',
choices=COMPAT_TARGETS + ['auto', 'any'],
default='auto',
)
parser.add_argument(
'--steam-runtime-env',
action='append',
default=[],
)
parser.add_argument('--verbose', action='store_true')
parser.add_argument('command', nargs='+')
args = parser.parse_args()
for token in args.steam_runtime_env:
if '=' not in token:
parser.error('--steam-runtime-env requires VAR=VALUE argument')
var, value = token.split('=', 1)
self.steam_runtime_env[var] = value
command_argv = args.command
assert len(command_argv) >= 1
self.app.runs_on = args.compatible_with
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)
if (
len(command_argv) > 2
and command_argv[0].endswith('ubuntu12_32/reaper')
and '--' in command_argv[:-1]
):
reaper_args = [] # type: typing.List[str]
while len(command_argv) > 0:
reaper_args.append(command_argv[0])
command_argv = command_argv[1:]
if reaper_args[-1] == '--':
break
logger.debug('Detected reaper: %s', to_shell(reaper_args))
logger.debug(
'Remaining arguments: %s', to_shell(command_argv),
)
self.reaper = Reaper(
path=command_argv[0],
home=self.home,
argv=reaper_args,
)
if (
len(command_argv) > 2
and command_argv[0].endswith('ubuntu12_32/steam-launch-wrapper')
and '--' in command_argv[:-1]
):
wrapper_args = [] # type: typing.List[str]
while len(command_argv) > 0:
wrapper_args.append(command_argv[0])
command_argv = command_argv[1:]
if wrapper_args[-1] == '--':
break
logger.debug('Detected launch wrapper %s', to_shell(wrapper_args))
logger.debug(
'Remaining arguments: %s', to_shell(command_argv),
)
self.launch_wrapper = LaunchWrapper(
path=command_argv[0],
home=self.home,
argv=wrapper_args,
)
for target in RUNTIMES:
if (
len(command_argv) > 2
and command_argv[0].endswith((
'/SteamLinuxRuntime_%s/run' % target,
'/SteamLinuxRuntime_%s/run-in-%s' % (target, target),
'/SteamLinuxRuntime_%s/_v2-entry-point' % target,
))
and '--' in command_argv[:-1]
):
if args.compatible_with == 'auto':
self.app.runs_on = target
runtime_args = [] # type: typing.List[str]
while len(command_argv) > 0:
runtime_args.append(command_argv[0])
command_argv = command_argv[1:]
if runtime_args[-1] == '--':
break
runtime = ContainerRuntimeDepot(
path=os.path.dirname(runtime_args[0]),
home=self.home,
argv=runtime_args,
)
runtime.provides = target
self.default_container_runtime = runtime
self.default_pressure_vessel = runtime.pressure_vessel
logger.debug('Detected SLR: %s', to_shell(runtime_args))
logger.debug(
'Remaining arguments: %s', to_shell(command_argv),
)
if (
len(command_argv) > 2
and command_argv[0].endswith(
'/scout-on-soldier-entry-point-v2'
)
and '--' in command_argv[:-1]
):
if args.compatible_with == 'auto':
self.app.runs_on = 'scout'
runtime_args = []
while len(command_argv) > 0:
runtime_args.append(command_argv[0])
command_argv = command_argv[1:]
if runtime_args[-1] == '--':
break
self.default_layered_runtime = LayeredRuntime(
path=os.path.dirname(runtime_args[0]),
home=self.home,
argv=runtime_args,
)
logger.debug('Detected layered SLR: %s', to_shell(runtime_args))
logger.debug(
'Remaining arguments: %s', to_shell(command_argv),
)
if (
len(command_argv) > 2
and command_argv[0].endswith('/proton')
and command_argv[1] in ('run', 'waitforexitandrun')
):
if args.compatible_with == 'auto':
self.app.runs_on = 'windows'
runtime_args = command_argv[:2]
command_argv = command_argv[2:]
if command_argv[0] == '--':
runtime_args.append('--')
command_argv = command_argv[1:]
self.default_proton = Proton(
path=os.path.dirname(runtime_args[0]),
home=self.home,
argv=runtime_args,
)
logger.debug('Detected Proton: %s', to_shell(runtime_args))
logger.debug(
'Remaining arguments: %s', to_shell(command_argv),
)
if 'PRESSURE_VESSEL_PREFIX' in os.environ:
self.default_pressure_vessel = PressureVessel(
os.environ['PRESSURE_VESSEL_PREFIX'],
self.home,
)
if self.app.runs_on == 'auto':
self.app.runs_on = 'scout'
logger.debug('Assuming final app runs on: %s', self.app.runs_on)
self.app.argv = command_argv
self.command_entry.set_text(to_shell(command_argv))
self.refresh_runtimes()
def var_path_browse_cb(self, button):
# type: (typing.Any) -> None
dialog = Gtk.FileChooserDialog(
title='Choose variable data directory',
parent=self.window,
action=Gtk.FileChooserAction.SELECT_FOLDER,
buttons=("Open", Gtk.ResponseType.ACCEPT),
)
if dialog.run() == Gtk.ResponseType.ACCEPT:
self.var_path_entry.set_text(dialog.get_filename())
dialog.destroy()
def _search(
self,
source_of_runtimes, # type: str
seen, # type: typing.Set[str]
in_runtime=False # type: bool
):
# type: (...) -> None
if not os.path.isdir(source_of_runtimes):
return
try:
source_of_runtimes = os.path.realpath(source_of_runtimes)
except OSError:
return
if source_of_runtimes in seen:
return
seen.add(source_of_runtimes)
for member in os.listdir(source_of_runtimes):
path = os.path.realpath(
os.path.join(source_of_runtimes, member)
)
if member.startswith('SteamLinuxRuntime') and not in_runtime:
if (
os.path.isdir(path)
and os.path.exists(os.path.join(path, 'run'))
and path not in self.container_runtimes
):
# Note that SteamLinuxRuntime (1070560) also has a
# _v2-entry-point, so we can't use that to detect
# complete container runtimes; but we should prefer
# to use it to run container runtimes, to be more like
# what Steam would do in the absence of this script.
exe = os.path.join(path, '_v2-entry-point')
if not os.path.exists(exe):
exe = os.path.join(path, 'run')
container_runtime = ContainerRuntimeDepot(
path=path,
home=self.home,
argv=[exe, '--'],
)
logger.debug(
'Discovered container runtime depot: %s', path,
)
logger.debug(
'Arguments: %s', to_shell(container_runtime.argv),
)
self.container_runtimes[path] = container_runtime
pv = container_runtime.pressure_vessel
if pv is not None:
logger.debug(
'Discovered pressure-vessel in %s: %s',
path, pv.path,
)
if pv.path not in self.pressure_vessels:
self.pressure_vessels[pv.path] = pv
self._search(path, seen, in_runtime=True)
elif (
os.path.isdir(path)
and os.path.exists(
os.path.join(path, 'scout-on-soldier-entry-point-v2')
)
and path not in self.layered_runtimes
):
layered_runtime = LayeredRuntime(
path=path,
home=self.home,
argv=[
os.path.join(
path,
'scout-on-soldier-entry-point-v2',
),
'--',
],
)
logger.debug(
'Discovered layered runtime depot: %s', path,
)
logger.debug(
'Arguments: %s', to_shell(layered_runtime.argv),
)
self.layered_runtimes[path] = layered_runtime
continue
if member.startswith('Proton ') and not in_runtime:
if (
os.path.isdir(path)
and os.path.exists(os.path.join(path, 'proton'))
and path not in self.proton_versions
):
proton = Proton(
path=path,
home=self.home,
argv=[
os.path.join(path, 'proton'), 'waitforexitandrun',
],
)
logger.debug(
'Discovered Proton: %s', path,
)
logger.debug(
'Arguments: %s', to_shell(proton.argv),
)
self.proton_versions[path] = proton
continue
metadata = os.path.join(path, 'metadata')
files = os.path.join(path, 'files')
if os.path.isdir(files) and os.path.isfile(metadata):
logger.debug(
'Discovered possible runtime directory: %s', path,
)
if path not in self.container_runtimes:
self.container_runtimes[path] = DirectoryRuntime(
path,
home=self.home,
)
continue
if member.endswith(('-runtime.tar.gz', '-sysroot.tar.gz')):
# runtime and sysroot happen to be the same length!
buildid_file = os.path.join(
source_of_runtimes,
member[:-len('-runtime.tar.gz')] + '-buildid.txt',
)
if os.path.exists(buildid_file):
logger.debug(
'Discovered possible archive runtime: %s', path,
)
if path not in self.container_runtimes:
self.container_runtimes[path] = ArchiveRuntime(
path,
buildid_file=buildid_file,
home=self.home,
)
continue
if member in ('steam-runtime', 'steam-runtime-heavy'):
logger.debug(
'Discovered possible LD_LIBRARY_PATH runtime: %s', path,
)
if path not in self.container_runtimes:
self.ldlp_runtimes[path] = LdlpRuntime(
path,
home=self.home,
)
if (
member == 'pressure-vessel'
and os.path.exists(
os.path.join(
path,
'bin',
'pressure-vessel-wrap',
)
)
and path not in self.pressure_vessels
):
logger.debug(
'Discovered additional pressure-vessel version: %s',
path,
)
self.pressure_vessels[path] = PressureVessel(
path,
self.home,
)
@contextlib.contextmanager
def _pause_changes(self):
# type: (...) -> typing.Generator[Gui, None, None]
try:
self._changing += 1
yield self
finally:
self._changing -= 1
if self._changing:
return
if self._changing_container_runtime:
self._changing_container_runtime = False
widgets = [
self.graphics_provider_combo,
self.layered_runtime_combo,
self.remove_game_overlay_combo,
self.share_home_combo,
self.share_pid_combo,
self.vulkan_layers_combo,
]
if (
self.default_pressure_vessel is None
and not self.pressure_vessels
):
widgets.extend([
self.shell_combo,
self.terminal_combo,
])
if self.container_runtime_combo.get_active_id() == '/':
logger.debug('Selected absence of container runtime')
for widget in widgets:
widget.set_sensitive(False)
else:
rt = self.container_runtimes.get(
self.container_runtime_combo.get_active_id()
)
assert rt
logger.debug(
'Selected container runtime: %s %s (%r)',
rt.__class__.__name__, rt.path, rt.argv,
)
selected = self.pressure_vessel_combo.get_active_id()
assert selected is not None
assert selected
assert selected != '/'
if isinstance(rt, ContainerRuntimeDepot):
its_pv = rt.pressure_vessel
if its_pv is not None and selected != its_pv.path:
self.pressure_vessel_combo.set_active_id(its_pv.path)
self.var_path_entry.set_text(rt.var_path)
else:
# keep the previous self.var_path_entry, it's better
# than nothing...
pass
for widget in widgets:
widget.set_sensitive(True)
# If soldier or sniper was selected, try to use a layered
# runtime to provide a scout-compatible ABI.
logger.debug(
'App runs on %s, runtime provides %s',
self.app.runs_on,
rt.provides,
)
if self.app.runs_on == 'scout' and rt.provides != 'scout':
layered_runtime = self.default_layered_runtime
if (
layered_runtime is not None
and layered_runtime.provides == 'scout'
):
logger.debug(
'Using layered runtime %s',
layered_runtime.path,
)
self.layered_runtime_combo.set_active_id(
layered_runtime.path,
)
self.build_argv()
def refresh_runtimes(self):
# type: (...) -> None
with self._pause_changes():
selected_container = self.container_runtime_combo.get_active_id()
self.container_runtime_combo.remove_all()
self.container_runtimes = {}
container_runtime = self.default_container_runtime
if container_runtime is None:
self.container_runtime_combo.append('/', 'None')
else:
path = container_runtime.path
self.container_runtimes[path] = container_runtime
self.container_runtime_combo.append(
path, container_runtime.description,
)
selected_layered = self.layered_runtime_combo.get_active_id()
self.layered_runtime_combo.remove_all()
self.layered_runtimes = {}
layered_runtime = self.default_layered_runtime
if layered_runtime is None:
self.layered_runtime_combo.append('/', 'None')
else:
self.layered_runtimes[layered_runtime.path] = layered_runtime
self.layered_runtime_combo.append(
layered_runtime.path,
layered_runtime.description,
)
selected_ldlp = self.ldlp_runtime_combo.get_active_id()
self.ldlp_runtime_combo.remove_all()
self.ldlp_runtime_combo.append(None, "Don't override")
selected_pv = self.pressure_vessel_combo.get_active_id()
self.pressure_vessel_combo.remove_all()
self.pressure_vessels = {}
pressure_vessel = self.default_pressure_vessel
if pressure_vessel is not None:
self.pressure_vessels[pressure_vessel.path] = pressure_vessel
self.pressure_vessel_combo.append(
pressure_vessel.path,
pressure_vessel.description,
)
selected_proton = self.proton_combo.get_active_id()
self.proton_combo.remove_all()
self.proton_versions = {}
proton = self.default_proton
if proton is None:
self.proton_combo.append('/', 'None')
else:
self.proton_versions[proton.path] = proton
self.proton_combo.append(
proton.path,
proton.description,
)
# Search for SteamLinuxRuntime, etc. in plausible Steam libraries
search_path = [] # type: typing.List[typing.Optional[str]]
seen = set() # type: typing.Set[str]
search_path.append(os.path.expanduser('~/.steam/root/ubuntu12_32'))
search_path.append(os.path.expanduser('~/.steam/root/ubuntu12_64'))
for path in os.getenv('STEAM_COMPAT_LIBRARY_PATHS', '').split(':'):
if path:
search_path.append(
os.path.join(path, 'steamapps', 'common'),
)
search_path.append(
os.path.expanduser('~/.steam/steam/steamapps/common')
)
if 'XDG_DATA_HOME' in os.environ:
search_path.append(
os.path.expanduser('$XDG_DATA_HOME/Steam/steamapps/common')
)
search_path.append(
os.path.expanduser('~/.local/share/Steam/steamapps/common')
)
search_path.append(
os.path.expanduser('~/SteamLibrary/steamapps/common')
)
search_path.append(os.path.expanduser('~/tmp'))
search_path.append('.')
search_path.append(os.getenv('PRESSURE_VESSEL_RUNTIME_BASE'))
for search in search_path:
if search is None:
continue
source_of_runtimes = os.path.join(
os.path.dirname(__file__),
search,
)
if not os.path.isdir(source_of_runtimes):
continue
self._search(source_of_runtimes, seen)
already_had_default = (self.default_layered_runtime is not None)
# Do these first, because they influence what's listed in the
# container runtime chooser
for path, layered_runtime in sorted(self.layered_runtimes.items()):
assert layered_runtime is not None
if layered_runtime != self.default_layered_runtime:
self.layered_runtime_combo.append(
path, layered_runtime.description,
)
if self.default_layered_runtime is None:
self.default_layered_runtime = layered_runtime
if already_had_default:
self.layered_runtime_combo.append('/', 'None')
if self.app.runs_on in RUNTIMES:
list_first = self.app.runs_on
elif self.default_container_runtime is not None:
list_first = self.default_container_runtime.provides
else:
list_first = ''
for path, runtime in sorted(
self.container_runtimes.items(),
key=lambda pair: pair[1].get_sort_weight(list_first),
):
assert runtime is not None
if (
runtime != self.default_container_runtime
and (
self.app.runs_on not in RUNTIMES
or not runtime.provides
or self.app.runs_on == runtime.provides
or (
self.app.runs_on == 'scout'
and self.default_layered_runtime is not None
)
)
):
self.container_runtime_combo.append(
path, runtime.description,
)
for path, pressure_vessel in sorted(self.pressure_vessels.items()):
assert pressure_vessel is not None
if pressure_vessel != self.default_pressure_vessel:
self.pressure_vessel_combo.append(
path,
pressure_vessel.description,
)
for path, ldlp_runtime in sorted(self.ldlp_runtimes.items()):
assert ldlp_runtime is not None
self.ldlp_runtime_combo.append(path, ldlp_runtime.description)
for path, proton in sorted(self.proton_versions.items()):
assert proton is not None
if (
proton != self.default_proton
and self.app.runs_on == 'windows'
):
self.proton_combo.append(path, proton.description)
if self.default_container_runtime is not None:
self.container_runtime_combo.append('/', 'None')
# There is no "none" option for pressure-vessel
self.ldlp_runtime_combo.append('/', 'None')
if (
self.default_proton is not None
and self.app.runs_on != 'windows'
):
self.proton_combo.append('/', 'None')
if (
selected_container is not None
and self.container_runtime_combo.set_active_id(
selected_container,
)
):
pass
else:
self.container_runtime_combo.set_active(0)
if (
selected_pv is not None
and self.pressure_vessel_combo.set_active_id(selected_pv)
):
pass
else:
self.pressure_vessel_combo.set_active(0)
if (
selected_layered is not None
and self.layered_runtime_combo.set_active_id(selected_layered)
):
pass
else:
self.layered_runtime_combo.set_active(0)
if (
selected_ldlp is not None
and self.ldlp_runtime_combo.set_active_id(selected_ldlp)
):
pass
else:
self.ldlp_runtime_combo.set_active(0)
if (
selected_proton is not None
and self.proton_combo.set_active_id(selected_proton)
):
pass
else:
self.proton_combo.set_active(0)
self._container_runtime_changed(self.container_runtime_combo)
def _container_runtime_changed(self, combo):
# type: (typing.Any) -> None
with self._pause_changes():
logger.debug(
'Selected container runtime: %s', combo.get_active_id(),
)
self._changing_container_runtime = True
def _pressure_vessel_changed(self, combo):
# type: (typing.Any) -> None
with self._pause_changes():
logger.debug(
'Selected pressure-vessel: %s', combo.get_active_id(),
)
def _layered_runtime_changed(self, combo):
# type: (typing.Any) -> None
with self._pause_changes():
container = self.container_runtime_combo.get_active_id()
if combo.get_active_id() == '/':
logger.debug('Selected absence of layered runtime')
if container and container != '/':
self.ldlp_runtime_combo.set_sensitive(False)
else:
self.ldlp_runtime_combo.set_sensitive(True)
else:
rt = self.layered_runtimes.get(combo.get_active_id())
assert rt
logger.debug(
'Selected layered runtime: %s %s (%r)',
rt.__class__.__name__, rt.path, rt.argv,
)
if container and container != '/':
self.ldlp_runtime_combo.set_sensitive(True)
else:
self.ldlp_runtime_combo.set_sensitive(False)
def _ldlp_runtime_changed(self, combo):
# type: (typing.Any) -> None
with self._pause_changes():
if combo.get_active_id() == '/':
logger.debug('Selected absence of LD_LIBRARY_PATH runtime')
elif combo.get_active_id() is None:
logger.debug(
'Selected automatic choice of LD_LIBRARY_PATH runtime',
)
else:
rt = self.ldlp_runtimes.get(combo.get_active_id())
assert rt
logger.debug(
'Selected LD_LIBRARY_PATH runtime: %s %s (%r)',
rt.__class__.__name__, rt.path, rt.argv,
)
def _proton_changed(self, combo):
# type: (typing.Any) -> None
with self._pause_changes():
logger.debug(
'Selected Proton: %s', combo.get_active_id(),
)
def _something_changed_cb(self, sender='Something', *args, **kwargs):
# type: (...) -> None
with self._pause_changes():
logger.debug('%s changed', sender)
def _command_entry_changed_cb(self, entry, param_spec):
# type: (typing.Any, typing.Any) -> None
with self._pause_changes():
logger.debug('Command to run changed to: %s', entry.props.text)
argv = shlex.split(entry.props.text)
logger.debug('Command parsed to %r', argv)
self.app.argv = argv
def run_cb(self, _ignored=None):
# type: (typing.Any) -> None
argv, environ = self.build_argv()
try:
os.execvpe(argv[0], argv, environ)
except OSError:
logger.error('Unable to run: %s', to_shell(argv))
Gtk.main_quit()
self.failed = True
raise
def build_argv(self):
# type: (...) -> typing.Tuple[typing.List[str], typing.Dict[str, str]]
lines = [] # type: typing.List[str]
argv = [] # type: typing.List[str]
environ = {} # type: typing.Dict[str, str]
components = [] # type: typing.List[Component]
container = None # type: typing.Optional[Component]
component = None # type: typing.Optional[Component]
has_container_runtime = False
inherit_ldlp_runtime = True
reaper = self.reaper
if reaper is not None:
components.append(reaper)
launch_wrapper = self.launch_wrapper
if launch_wrapper is not None:
components.append(launch_wrapper)
selected = self.container_runtime_combo.get_active_id()
if selected is None or not selected or selected == '/':
container = None
else:
container = self.container_runtimes.get(selected)
if container is not None:
components.append(container)
selected = self.layered_runtime_combo.get_active_id()
if selected is None or not selected or selected == '/':
component = None
else:
component = self.layered_runtimes.get(selected)
if component is not None:
components.append(component)
component = None
selected = self.ldlp_runtime_combo.get_active_id()
if selected is None or not selected:
pass
elif selected == '/':
# TODO: Shouldn't be allowed?
pass
else:
component = self.ldlp_runtimes.get(selected)
if component is not None:
environ['STEAM_RUNTIME_SCOUT'] = component.path
lines.append(to_shell([
'env', 'STEAM_RUNTIME_SCOUT={}'.format(component.path),
]))
else:
selected = self.ldlp_runtime_combo.get_active_id()
component = None
if selected is None or not selected:
pass
elif selected == '/':
inherit_ldlp_runtime = False
else:
component = self.ldlp_runtimes.get(selected)
if component is not None:
components.append(component)
selected = self.proton_combo.get_active_id()
if selected is None or not selected or selected == '/':
component = None
else:
component = self.proton_versions.get(selected)
if component is not None:
components.append(component)
if inherit_ldlp_runtime:
environ.update(self.steam_runtime_env)
for component in components:
assert component is not None
args = component.argv[:]
if isinstance(component, ContainerRuntime):
has_container_runtime = True
selected = self.pressure_vessel_combo.get_active_id()
assert selected is not None
assert selected
assert selected != '/'
pv = self.pressure_vessels[selected]
assert pv is not None
if isinstance(component, ContainerRuntimeDepot):
its_pv = component.pressure_vessel
if (
its_pv is None
or pv.path != its_pv.path
or environ.get('PRESSURE_VESSEL_PREFIX') != pv.path
):
environ['PRESSURE_VESSEL_PREFIX'] = pv.path
else:
args = [
pv.unruntime,
]
if isinstance(component, ArchiveRuntime):
args.append(
'--runtime-archive=' + component.path
)
args.append(
'--runtime-id=' + component.deploy_id
)
elif isinstance(component, DirectoryRuntime):
args.append('--runtime=' + component.path)
else:
raise AssertionError(
'Unhandled ContainerRuntime subclass: %r'
% component
)
args.append('--')
value = self.graphics_provider_combo.get_active_id()
if value is not None:
environ['PRESSURE_VESSEL_GRAPHICS_PROVIDER'] = value
value = self.share_home_combo.get_active_id()
if value is not None:
environ['PRESSURE_VESSEL_SHARE_HOME'] = value
value = self.share_pid_combo.get_active_id()
if value is not None:
environ['PRESSURE_VESSEL_SHARE_PID'] = value
value = self.remove_game_overlay_combo.get_active_id()
if value is not None:
environ['PRESSURE_VESSEL_REMOVE_GAME_OVERLAY'] = value
value = self.vulkan_layers_combo.get_active_id()
if value is not None:
environ['PRESSURE_VESSEL_IMPORT_VULKAN_LAYERS'] = value
value = self.launcher_service_combo.get_active_id()
if value is not None:
environ['STEAM_COMPAT_LAUNCHER_SERVICE'] = value
value = self.terminal_combo.get_active_id()
if value is not None:
environ['PRESSURE_VESSEL_TERMINAL'] = value
value = self.shell_combo.get_active_id()
if value is not None:
environ['PRESSURE_VESSEL_SHELL'] = value
var_path = self.var_path_entry.get_text()
if var_path:
os.makedirs(var_path, mode=0o755, exist_ok=True)
environ['PRESSURE_VESSEL_VARIABLE_DIR'] = var_path
lines.append(to_shell(args))
argv.extend(args)
value = self.sdl_videodriver_combo.get_active_id()
if value is not None:
environ['SDL_VIDEODRIVER'] = value
if self.debug_check.get_active():
environ['STEAM_LINUX_RUNTIME_VERBOSE'] = '1'
environ['G_MESSAGES_DEBUG'] = 'all'
shell = self.shell_combo.get_active_id()
terminal = self.terminal_combo.get_active_id()
# If we're not using a container runtime, we can try to borrow the
# pressure-vessel-adverb from any random copy of pressure-vessel
# to get its "run in an xterm" code
if (
(shell is not None or terminal is not None)
and not has_container_runtime
):
any_pv = self.default_pressure_vessel
if any_pv is None and self.pressure_vessels:
any_pv = next(iter(sorted(self.pressure_vessels.items())))[1]
if any_pv is not None:
adverb = [any_pv.adverb]
if shell is not None:
adverb.append('--shell=' + shell)
if terminal is not None:
adverb.append('--terminal=' + terminal)
adverb.append('--')
argv.extend(adverb)
lines.append(to_shell(adverb))
argv.extend(self.app.argv)
lines.append(to_shell(self.app.argv))
env_lines = [] # type: typing.List[str]
for var, value in sorted(environ.items()):
if value != os.environ.get(var):
env_lines.append('{}={}'.format(var, to_shell([value])))
lines = env_lines + lines
self.final_command_view.get_buffer().set_text(
' \\\n'.join(lines), -1,
)
final_env = {} # type: typing.Dict[str, str]
final_env.update(os.environ)
final_env.update(environ)
# The older pressure-vessel-test-ui would be redundant here,
# so disable it.
if 'PRESSURE_VESSEL_WRAP_GUI' in final_env:
del final_env['PRESSURE_VESSEL_WRAP_GUI']
return argv, final_env
def run(self):
# type: (...) -> None
self.window.show_all()
Gtk.main()
if self.failed:
sys.exit(126)
if __name__ == '__main__':
logging.basicConfig()
logging.getLogger().setLevel(logging.INFO)
if '--check-gui-dependencies' in sys.argv:
sys.exit(0)
try:
gui = Gui()
gui.parse_args(sys.argv[1:])
gui.run()
except KeyboardInterrupt:
sys.exit(130)
except Exception as e:
logger.exception(str(e))
sys.exit(125)
except SystemExit:
# Catch exit(2) from argparse.ArgumentParser.error, because we
# want to reserve exit statuses < 125 for the launched tool
sys.exit(125)