-
-
Notifications
You must be signed in to change notification settings - Fork 41
/
ctmod_steamtinkerlaunch.py
445 lines (374 loc) · 22.5 KB
/
ctmod_steamtinkerlaunch.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
# pupgui2 compatibility tools module
# SteamTinkerLaunch
# Copyright (C) 2021 DavidoTek, partially based on AUNaseef's protonup
import datetime, locale, os, requests, shutil, subprocess, tarfile
from PySide6.QtCore import QObject, QCoreApplication, Signal, Property
from PySide6.QtWidgets import QMessageBox
from pupgui2 import constants
from pupgui2.datastructures import MsgBoxType, MsgBoxResult
from pupgui2.steamutil import get_fish_user_paths, remove_steamtinkerlaunch, get_external_steamtinkerlaunch_intall
from pupgui2.util import host_which, config_advanced_mode
from pupgui2.util import ghapi_rlcheck
from pupgui2.util import build_headers_with_authorization
CT_NAME = 'SteamTinkerLaunch'
CT_LAUNCHERS = ['steam', 'native-only']
CT_DESCRIPTION = {'en': QCoreApplication.instance().translate('ctmod_steamtinkerlaunch', '''
Linux wrapper tool for use with the Steam client which allows for easy graphical configuration of game tools for Proton and native Linux games.
<br/><br/>
On <b>Steam Deck</b>, relevant dependencies will be installed for you. If you are not on Steam Deck, <b>ensure you have the following dependencies installed</b>:
<ul>
<li>awk (or gawk)</li>
<li>bash</li>
<li>git</li>
<li>pgrep</li>
<li>unzip</li>
<li>wget</li>
<li>xdotool</li>
<li>xprop</li>
<li>xrandr</li>
<li>xwininfo</li>
<li>xxd</li>
<li>Yad >= <b>v7.2</b></li>
</ul>
More information is available on the SteamTinkerLaunch Installation wiki page.
<br/><br/>
SteamTinkerLaunch has a number of <b>Optional Dependencies</b> which have to be installed separately for extra functionality. Please see the Optional Dependencies section
of the SteamTinkerLaunch Installation guide on its GitHub page.''')}
class CtInstaller(QObject):
BUFFER_SIZE = 4096
CT_URL = 'https://api.github.com/repos/sonic2kk/steamtinkerlaunch/releases'
CT_BRANCHES_URL = 'https://api.github.com/repos/sonic2kk/steamtinkerlaunch/branches'
CT_GH_URL = 'https://github.com/sonic2kk/steamtinkerlaunch'
CT_INFO_URL = CT_GH_URL + '/releases/tag/'
p_download_progress_percent = 0
download_progress_percent = Signal(float)
message_box_message = Signal((str, str, QMessageBox.Icon))
question_box_message = Signal((str, str, str, MsgBoxType, QMessageBox.Icon))
def __init__(self, main_window = None, allow_git=False):
super(CtInstaller, self).__init__()
self.p_download_canceled = False
self.remove_existing_installation = False
self.main_window = main_window
self.rs = requests.Session()
rs_headers = build_headers_with_authorization({}, main_window.web_access_tokens, 'github')
self.rs.headers.update(rs_headers)
self.allow_git = allow_git
proc_prefix = ['flatpak-spawn', '--host'] if os.path.exists('/.flatpak-info') else []
self.distinfo = subprocess.run(
proc_prefix + ['cat', '/etc/lsb-release', '/etc/os-release'],
universal_newlines=True,
stdout=subprocess.PIPE
).stdout.strip().lower()
def get_download_canceled(self):
return self.p_download_canceled
def set_download_canceled(self, val):
self.p_download_canceled = val
download_canceled = Property(bool, get_download_canceled, set_download_canceled)
def __set_download_progress_percent(self, value : int):
if self.p_download_progress_percent == value:
return
self.p_download_progress_percent = value
self.download_progress_percent.emit(value)
def __download(self, url, destination):
"""
Download files from url to destination
Return Type: bool
"""
try:
# Sometimes we don't get Content-Length in the header
# Add a loop to retry until we get Content-Length
retries = 10
for _ in range(retries):
file = self.rs.get(url, stream=True)
if 'Content-Length' in file.headers:
break
else:
print('Could not download SteamTinkerLaunch. Please try again.')
return False
except OSError:
return False
self.__set_download_progress_percent(1) # 1 download started
f_size = int(file.headers.get('content-length'))
c_count = int(f_size / self.BUFFER_SIZE)
c_current = 1
destination = os.path.expanduser(destination)
os.makedirs(os.path.dirname(destination), exist_ok=True)
with open(destination, 'wb') as dest:
for chunk in file.iter_content(chunk_size=self.BUFFER_SIZE):
if self.download_canceled:
self.download_canceled = False
self.__set_download_progress_percent(-2) # -2 download canceled
return False
if chunk:
dest.write(chunk)
dest.flush()
self.__set_download_progress_percent(int(min(c_current / c_count * 98.0, 98.0))) # 1-98, 100 after extract
c_current += 1
self.__set_download_progress_percent(99) # 99 download complete
return True
def __fetch_github_data(self, tag):
"""
Fetch GitHub release information
Return Type: dict
Content(s):
'version', 'download'
"""
if self.allow_git:
return {'version': tag, 'download': f'https://github.com/sonic2kk/steamtinkerlaunch/archive/{tag}.tar.gz'}
url = self.CT_URL + (f'/tags/{tag}' if tag else '/latest')
data = self.rs.get(url).json()
if 'tag_name' not in data:
return None
return {'version': data['tag_name'], 'download': data['tarball_url'] if 'tarball_url' in data else None}
def __stl_config_change_language(self, stl_cfg_path: str, lang_file: str) -> bool:
"""
Change the language in the SteamTinkerLaunch global.conf configuration
Example: __stl_config_change_language('~/.config/steamtinkerlaunch', 'german.txt')
Return Type: bool
"""
stl_global_conf = os.path.join(os.path.expanduser(stl_cfg_path), 'global.conf')
new_lang = lang_file.replace('.txt', '')
if not os.path.isfile(stl_global_conf):
return False
with open(stl_global_conf, 'r+') as f:
c = f.readlines()
f.seek(0)
for line in c:
if line.startswith('STLLANG'):
f.write(f'STLLANG="{new_lang}"\n')
else:
f.write(line)
return True
def is_system_compatible(self):
"""
Are the system requirements met?
Return Type: bool
"""
# Possibly excuse some of these if not on Steam Deck and ignore if Flatpak
proc_prefix = ['flatpak-spawn', '--host'] if os.path.exists('/.flatpak-info') else []
yad_exe = host_which('yad')
if yad_exe:
try:
yad_vers = subprocess.run(proc_prefix + ['yad', '--version'], universal_newlines=True, stdout=subprocess.PIPE).stdout.strip().split(' ')[0].split('.')
yad_ver = float(f'{yad_vers[0]}.{yad_vers[1]}')
except Exception as e:
print('STL is_system_compatible Could not parse yad version:', e)
yad_ver = 0.0
# Don't check dependencies on Steam Deck, STL will manage dependencies itself in that case
deps_met = {}
if "steamos" not in self.distinfo:
deps_met = {
'awk-gawk': host_which('awk') or host_which('gawk'),
'git': host_which('git'),
'pgrep': host_which('pgrep'),
'unzip': host_which('unzip'),
'wget': host_which('wget'),
'xdotool': host_which('xdotool'),
'xprop': host_which('xprop'),
'xrandr': host_which('xrandr'),
'xxd': host_which('xxd'),
'xwininfo': host_which('xwininfo'),
'yad >= 7.2': yad_exe and yad_ver >= 7.2
}
if all(deps_met.values()):
return True
msg = QCoreApplication.instance().translate('ctmod_steamtinkerlaunch', 'You have several unmet dependencies for SteamTinkerLaunch.\n\n')
msg += '\n'.join([f'{dep_name}: {"found" if is_dep_met else "missing"}' for (dep_name, is_dep_met) in deps_met.items()])
msg += QCoreApplication.instance().translate('ctmod_steamtinkerlaunch', '\n\nInstallation will be cancelled.')
self.message_box_message.emit(QCoreApplication.instance().translate('ctmod_steamtinkerlaunch', 'Missing dependencies!'), msg, QMessageBox.Warning)
return False # Installation would fail without dependencies.
def fetch_releases(self, count=100):
"""
List available releases or available branches for SteamTinkerLaunch-git
Return Type: str[]
"""
main_branch = 'master'
j = ghapi_rlcheck(self.rs.get(f'{self.CT_URL}?per_page={str(count)}').json())
if 'message' in j:
return []
branches = [branch['name'] for branch in self.rs.get(self.CT_BRANCHES_URL).json()] if self.allow_git else [release['tag_name'] for release in j]
if self.allow_git and main_branch in branches:
branches.insert(0, branches.pop(branches.index(main_branch))) # Force main branch to top of list
return branches
def get_tool(self, version, install_dir, temp_dir):
"""
Download and install the compatibility tool
Return Type: bool
"""
has_existing_install = False
# If there's an existing STL installation that isn't installed by ProtonUp-Qt, ask the user if they still want to install
has_external_install = get_external_steamtinkerlaunch_intall(os.path.join(install_dir, 'SteamTinkerLaunch'))
if has_external_install:
print('Non-ProtonUp-Qt installation of SteamTinkerLaunch detected. Asking the user what they want to do...')
self.question_box_message.emit(
QCoreApplication.instance().translate('ctmod_steamtinkerlaunch', 'Existing SteamTinkerLaunch Installation'),
QCoreApplication.instance().translate('ctmod_steamtinkerlaunch', 'It looks like you have an existing SteamTinkerLaunch installation at \'{EXTERNAL_INSTALL_PATH}\' that was not installed by ProtonUp-Qt.\n\nReinstalling SteamTinkerLaunch with ProtonUp-Qt will move your installation folder to \'{STL_INSTALL_PATH}\'.\n\nYou may also choose to remove your existing installation, if ProtonUp-Qt has write access to this folder. Do you want to continue installing SteamTinkerLaunch? (This will not affect any existing SteamTinkerLaunch configuration.)').format(EXTERNAL_INSTALL_PATH=has_external_install, STL_INSTALL_PATH=constants.STEAM_STL_INSTALL_PATH),
QCoreApplication.instance().translate('ctmod_steamtinkerlaunch' ,'Remove existing SteamTinkerLaunch installation'),
MsgBoxType.OK_CANCEL_CB,
QMessageBox.Warning
)
remove_existing_installation_result = self.main_window.get_msgcb_answer()
if remove_existing_installation_result.button_clicked == MsgBoxResult.BUTTON_OK:
# Remove the Non-ProtonUp-Qt SteamTinkerLaunch if the user checked the box (disabled by default)
print('User opted to continue installing SteamTinkerLaunch.')
if remove_existing_installation_result.is_checked:
# This will show a warning dialog if it can't be removed, but uninstallation will continue
# The user was previously asked if they wanted to stop installation, so there is no need to pause installation and ask again
print('User opted to remove the existing SteamTinkerLaunch installation as well - Attempting to do so')
remove_steamtinkerlaunch(compat_folder=os.path.join(install_dir, 'SteamTinkerLaunch'), remove_config=False, ctmod_object=self)
# Nothing more to do here, just continue with the rest of the installation as normal
else:
# Don't remove anything
print('User opted to not continue installing SteamTinkerLaunch. Aborting...')
return False
print('Downloading SteamTinkerLaunch...')
data = self.__fetch_github_data(version)
if not data or 'download' not in data:
return False
destination = temp_dir
destination += data['download'].split('/')[-1]
destination = destination
if not self.__download(url=data['download'], destination=destination):
return False
with tarfile.open(destination, "r:gz") as tar:
print('Extracting SteamTinkerLaunch...')
if os.path.exists(constants.STEAM_STL_INSTALL_PATH) and len(os.listdir(constants.STEAM_STL_INSTALL_PATH)) > 0:
has_existing_install = True # This will also be True for users who installed normally on Steam Deck, but not sure how to differentiate between PUPQT and manual Steam Deck installs
remove_steamtinkerlaunch(remove_config=False, ctmod_object=self)
if not os.path.exists(constants.STEAM_STL_INSTALL_PATH):
os.mkdir(constants.STEAM_STL_INSTALL_PATH)
os.chdir(constants.STEAM_STL_INSTALL_PATH)
tar.extractall(constants.STEAM_STL_INSTALL_PATH)
tarname = tar.getnames()[0]
# Location of SteamTinkerLaunch script to add to path later
old_stl_path = os.path.join(constants.STEAM_STL_INSTALL_PATH, tarname)
stl_path = os.path.join(constants.STEAM_STL_INSTALL_PATH, 'prefix')
# Rename folder ~/stl/<tarname> to ~/stl/prefix
os.rename(old_stl_path, stl_path)
os.chdir(stl_path)
# ProtonUp-Qt Flatpak: Run STL on host system
stl_proc_prefix = ['flatpak-spawn', '--host'] if os.path.exists('/.flatpak-info') else []
# If on Steam Deck, run script for initial Steam Deck config
# On Steam Deck, STL is installed to "/home/deck/stl/prefix"
self.__set_download_progress_percent(99.5) # 99.5 installing tool
print('Setting up SteamTinkerLaunch...')
if "steamos" in self.distinfo:
subprocess.run(['chmod', '+x', 'steamtinkerlaunch'])
subprocess.run(stl_proc_prefix + ['./steamtinkerlaunch'])
# Change location of STL script to add to path as this is different on Steam Deck
stl_path = os.path.join(constants.STEAM_STL_INSTALL_PATH, 'prefix')
# Change to STL prefix dir on Steam Deck so that the compatibility tool is symlinked correctly
os.chdir(stl_path)
else:
# Get STL language and default to 'en_US' if the language is not available
# This step should not be necessary on Steam Deck
syslang = locale.getdefaultlocale()[0] or 'en_US'
stl_langs = {
'de_DE': 'german.txt',
'en_GB': 'englishUK.txt',
'en_US': 'english.txt',
'fr_FR': 'french.txt',
'il_IL': 'italian.txt',
'nl_NL': 'dutch.txt',
'pl_PL': 'polish.txt',
'ru_RU': 'russian.txt',
'zh_CN': 'chinese.txt',
}
stl_lang = stl_langs[syslang] if syslang in stl_langs else stl_langs['en_US']
stl_lang_path = os.path.join(constants.STEAM_STL_CONFIG_PATH, 'lang')
# Generate config file structure and copy relevant lang file
os.makedirs(stl_lang_path, exist_ok=True)
if not os.path.isfile(os.path.join(stl_lang_path, 'english.txt')):
shutil.copyfile('lang/english.txt', os.path.join(stl_lang_path, 'english.txt'))
if not os.path.isfile(os.path.join(stl_lang_path, stl_lang)):
shutil.copyfile(f'lang/{stl_lang}', os.path.join(stl_lang_path, stl_lang))
subprocess.run(stl_proc_prefix + ['./steamtinkerlaunch', f'lang={stl_lang.replace(".txt", "")}'])
self.__stl_config_change_language(constants.STEAM_STL_CONFIG_PATH, stl_lang)
# Add SteamTinkerLaunch to all available shell paths (native Linux)
# Dialog warning - Only warn on new installs or overwritten manual installs
# For background see this issue: https://github.com/DavidoTek/ProtonUp-Qt/issues/127
should_show_shellmod_dialog = has_external_install or not has_existing_install
should_add_path = True
# Checkbox is only shown to users who have ProtonUp-Qt Advanced mode enalbed
if should_show_shellmod_dialog:
shellmod_msgbox_type = MsgBoxType.OK_CANCEL_CB_CHECKED if config_advanced_mode() == 'enabled' else MsgBoxType.OK_CANCEL
self.question_box_message.emit(
QCoreApplication.instance().translate('ctmod_steamtinkerlaunch', 'Add SteamTinkerLaunch to PATH'),
QCoreApplication.instance().translate('ctmod_steamtinkerlaunch', 'By default, ProtonUp-Qt will add SteamTinkerLaunch to all available Shell paths. This makes it easier to use with native Linux games. It also enables SteamTinkerLaunch commands from anywhere in the command line.\n\nSome users may not want this functionality. Do you want to continue installing SteamTinkerLaunch?'),
QCoreApplication.instance().translate('ctmod_steamtinkerlaunch', 'Allow PATH modification'),
shellmod_msgbox_type,
QMessageBox.Warning
)
shellmod_msgbox_result = self.main_window.get_msgcb_answer()
if shellmod_msgbox_result.button_clicked == MsgBoxResult.BUTTON_CANCEL:
# Cancel installation after shell modification warning
print('User asked to cancel installation. Not installing SteamTinkerLaunch...')
should_add_path = False # Shouldn't matter since installation will end here, but setting for completeness
remove_steamtinkerlaunch(remove_config=False, ctmod_object=self) # shouldn't need compat_folder arg - (compat_folder=os.path.join(install_dir, 'SteamTinkerLaunch'))
self.__set_download_progress_percent(-2)
return
elif not shellmod_msgbox_result.is_checked and shellmod_msgbox_result.button_clicked == MsgBoxResult.BUTTON_OK:
# Continue installation but skip adding to PATH
print('User asked not to add SteamTinkerLaunch to shell paths, skipping...')
should_add_path = False
else:
should_add_path = True # Probably won't get here anyway, but set to True to be sure
if should_add_path:
# Add to shell PATH
print('Adding SteamTinkerLaunch to shell paths...')
pup_stl_path_date = f'# Added by ProtonUp-Qt on {datetime.datetime.now().strftime("%d-%m-%Y %H:%M:%S")}'
pup_stl_path_line = f'if [ -d "{stl_path}" ]; then export PATH="$PATH:{stl_path}"; fi'
present_shell_files = [
os.path.join(constants.HOME_DIR, f) for f in os.listdir(constants.HOME_DIR) if os.path.isfile(os.path.join(constants.HOME_DIR, f)) and f in constants.STEAM_STL_SHELL_FILES
]
if os.path.exists(constants.STEAM_STL_FISH_VARIABLES):
present_shell_files.append(constants.STEAM_STL_FISH_VARIABLES)
for shell_file in present_shell_files:
with open(shell_file, 'r+') as mfile:
stl_already_in_path = constants.STEAM_STL_INSTALL_PATH in [line for line in mfile.readlines()]
if not stl_already_in_path:
# Add Fish user path, preserving any existing paths
if 'fish' in mfile.name:
mfile.seek(0)
curr_fish_user_paths = get_fish_user_paths(mfile)
curr_fish_user_paths.insert(0, stl_path)
updated_fish_user_paths = '\\x1e'.join(curr_fish_user_paths)
pup_stl_path_line = f'SETUVAR fish_user_paths:{updated_fish_user_paths}'
mfile.seek(0)
new_fish_contents = ''.join([line for line in mfile.readlines() if 'fish_user_paths:' not in line])
mfile.seek(0)
mfile.write(new_fish_contents)
mfile.write(f'\n{pup_stl_path_date}\n{pup_stl_path_line}\n')
# Install Compatibility Tool (Proton games)
print('Adding SteamTinkerLaunch as a compatibility tool...')
subprocess.run(stl_proc_prefix + ['./steamtinkerlaunch', 'compat', 'add'])
os.chdir(constants.HOME_DIR)
protondir = os.path.join(install_dir, 'SteamTinkerLaunch')
# We can't use the version arg to this method because we need to list the PROGVERS stored by the SteamTinkerLaunch script
if os.path.exists(protondir):
# Get PROGVERS from STL script
stl_filename = 'steamtinkerlaunch'
stl_ver = ''
with open(os.path.join(protondir, stl_filename)) as stl_script:
for i, line in enumerate(stl_script):
if 'PROGVERS' in line:
stl_ver = line.split('=')[1].replace('"', '') # E.g. turn `PROGVERS="v12.0"` into `v12.0`
print(f'Storing SteamTinkerLaunch version from STL script file as {stl_ver}')
# Write version to file
with open(os.path.join(protondir, 'VERSION.txt'), 'w') as f:
f.write(stl_ver)
f.write('\n')
break
if i > 19:
print("Couldn't find SteamTinkerLaunch version in script file, quitting...")
break
self.__set_download_progress_percent(100)
print('Successfully installed SteamTinkerLaunch!')
return True
def get_info_url(self, version):
"""
Return link with GitHub release page.
If SteamTinkerLaunch-git, returns the project homepage.
Return Type: str
"""
return self.CT_GH_URL if self.allow_git else self.CT_INFO_URL + version