Skip to content

Commit 69fa871

Browse files
authored
DXVK Nightly: Simplify by Inheriting from DXVK Ctmod (#509)
* DXVK: Refactor to use __get_data method Same idea to what we do in GE-Proton ctmod, allows us to override the data and extract path in child classes * DXVK: Refactor extraction to `self.__extract` method * DXVK: Type hinting * DXVK Nightly: Simplify by Inheriting DXVK * DXVK Nightly: Fix fetching Windows MSVC Builds * DXVK Nightly: Refactor to use commit hash again instead of workflow ID This allows us to display the commit hash in the releases dropdown which helps with readibility, and it also allows us to preserve the functionality of the `get_info_url` method which relies on using the commit hash. * DXVK Nightly: Only include workflow runs from `master` * DXVK Nightly: Refactor extraction The logic specifically for extracting DXVK Nightly (ZIP) now lives inside of the DXVK Nightly ctmod. The main DXVK Ctmod now only knows how to extract itself.
1 parent 87d7e9d commit 69fa871

File tree

2 files changed

+138
-104
lines changed

2 files changed

+138
-104
lines changed

pupgui2/resources/ctmods/ctmod_z0dxvk.py

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,20 @@
2121

2222
class CtInstaller(QObject):
2323

24-
BUFFER_SIZE = 65536
25-
CT_URL = 'https://api.github.com/repos/doitsujin/dxvk/releases'
26-
CT_INFO_URL = 'https://github.com/doitsujin/dxvk/releases/tag/'
24+
BUFFER_SIZE: int = 65536
25+
CT_URL: str = 'https://api.github.com/repos/doitsujin/dxvk/releases'
26+
CT_INFO_URL: str = 'https://github.com/doitsujin/dxvk/releases/tag/'
2727

28-
p_download_progress_percent = 0
29-
download_progress_percent = Signal(int)
30-
message_box_message = Signal((str, str, QMessageBox.Icon))
28+
p_download_progress_percent: int = 0
29+
download_progress_percent: Signal = Signal(int)
30+
message_box_message: Signal = Signal((str, str, QMessageBox.Icon))
3131

3232
def __init__(self, main_window = None):
3333
super(CtInstaller, self).__init__()
34-
self.p_download_canceled = False
35-
self.release_format = 'tar.gz'
34+
self.p_download_canceled: bool = False
35+
self.release_format: str = 'tar.gz'
3636

37-
self.rs = requests.Session()
37+
self.rs: requests.Session = requests.Session()
3838
rs_headers = build_headers_with_authorization({}, main_window.web_access_tokens, 'github')
3939
self.rs.headers.update(rs_headers)
4040

@@ -95,37 +95,66 @@ def is_system_compatible(self):
9595
"""
9696
return True
9797

98-
def fetch_releases(self, count=100, page=1):
98+
def fetch_releases(self, count: int = 100, page: int = 1):
9999
"""
100100
List available releases
101101
Return Type: list[str]
102102
"""
103103
return fetch_project_releases(self.CT_URL, self.rs, count=count, page=page)
104104

105-
def get_tool(self, version, install_dir, temp_dir):
105+
def __get_data(self, version: str, install_dir: str) -> tuple[dict | None, str | None]:
106+
106107
"""
107-
Download and install the compatibility tool
108-
Return Type: bool
108+
Get needed download data and path to extract directory.
109+
Return Type: diple[dict | None, str | None]
109110
"""
110111

111112
data = self.__fetch_data(version)
112113
if not data or 'download' not in data:
114+
return (None, None)
115+
116+
dxvk_dir = self.get_extract_dir(install_dir)
117+
118+
return (data, dxvk_dir)
119+
120+
def __extract(self, archive_path: str, extract_dir: str) -> bool:
121+
122+
"""
123+
Extract the tool archive at the given path.
124+
Return Type: bool
125+
"""
126+
127+
if not archive_path or not extract_dir:
128+
return False
129+
130+
# DXVK and DXVK Async are 'tar.gz'
131+
tar_type = self.release_format.split('.')[-1]
132+
133+
return extract_tar(archive_path, extract_dir, mode=tar_type)
134+
135+
def get_tool(self, version: str, install_dir: str, temp_dir: str) -> bool:
136+
"""
137+
Download and install the compatibility tool
138+
Return Type: bool
139+
"""
140+
141+
data, dxvk_dir = self.__get_data(version, install_dir)
142+
if not data:
113143
return False
114144

115145
# Should be updated to support Heroic, like ctmod_d8vk
116-
dxvk_tar = os.path.join(temp_dir, data['download'].split('/')[-1])
117-
if not self.__download(url=data['download'], destination=dxvk_tar, known_size=data.get('size', 0)):
146+
dxvk_archive: str = os.path.join(temp_dir, data['download'].split('/')[-1])
147+
if not self.__download(url=data['download'], destination=dxvk_archive, known_size=data.get('size', 0)):
118148
return False
119149

120-
dxvk_dir = self.get_extract_dir(install_dir)
121-
if not extract_tar(dxvk_tar, dxvk_dir, mode='gz'):
150+
if not dxvk_dir or not self.__extract(dxvk_archive, dxvk_dir):
122151
return False
123152

124153
self.__set_download_progress_percent(100)
125154

126155
return True
127156

128-
def get_info_url(self, version):
157+
def get_info_url(self, version: str) -> str:
129158
"""
130159
Get link with info about version (eg. GitHub release page)
131160
Return Type: str

pupgui2/resources/ctmods/ctmod_z2dxvknightly.py

Lines changed: 91 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -3,99 +3,115 @@
33
# Copyright (C) 2022 DavidoTek, partially based on AUNaseef's protonup
44

55
import os
6-
import requests
76

8-
from PySide6.QtCore import QObject, QCoreApplication, Signal, Property
7+
from PySide6.QtCore import QCoreApplication
98

10-
from pupgui2.util import ghapi_rlcheck, extract_zip
11-
from pupgui2.util import build_headers_with_authorization
9+
from pupgui2.util import extract_zip, ghapi_rlcheck
10+
11+
from pupgui2.resources.ctmods.ctmod_z0dxvk import CtInstaller as DXVKInstaller
1212

1313

1414
CT_NAME = 'DXVK (nightly)'
1515
CT_LAUNCHERS = ['lutris', 'advmode']
1616
CT_DESCRIPTION = {'en': QCoreApplication.instance().translate('ctmod_z2dxvknightly', '''Nightly version of DXVK (master branch), a Vulkan based implementation of Direct3D 8, 9, 10 and 11 for Linux/Wine.<br/><br/><b>Warning: Nightly version is unstable, use with caution!</b>''')}
1717

1818

19-
class CtInstaller(QObject):
19+
class CtInstaller(DXVKInstaller):
2020

21-
BUFFER_SIZE = 65536
22-
CT_URL = 'https://api.github.com/repos/doitsujin/dxvk/actions/artifacts'
23-
CT_INFO_URL = 'https://github.com/doitsujin/dxvk/commit/'
21+
BUFFER_SIZE: int = 65536
22+
CT_WORKFLOW_URL: str = 'https://api.github.com/repos/doitsujin/dxvk/actions/workflows'
23+
CT_ARTIFACT_URL: str = 'https://api.github.com/repos/doitsujin/dxvk/actions/runs/{}/artifacts'
24+
CT_ALL_ARTIFACTS_URL: str = 'https://api.github.com/repos/doitsujin/dxvk/actions/artifacts'
25+
CT_INFO_URL: str = 'https://github.com/doitsujin/dxvk/commit/'
2426

25-
p_download_progress_percent = 0
26-
download_progress_percent = Signal(int)
27+
DXVK_WORKFLOW_NAME: str = 'artifacts'
2728

2829
def __init__(self, main_window = None):
29-
super(CtInstaller, self).__init__()
30-
self.p_download_canceled = False
3130

32-
self.rs = requests.Session()
33-
rs_headers = build_headers_with_authorization({}, main_window.web_access_tokens, 'github')
34-
self.rs.headers.update(rs_headers)
31+
super().__init__(main_window)
32+
33+
self.release_format: str = 'zip'
3534

36-
def get_download_canceled(self):
37-
return self.p_download_canceled
35+
def __fetch_workflows(self, count: int = 30) -> list[str]:
3836

39-
def set_download_canceled(self, val):
40-
self.p_download_canceled = val
37+
"""
38+
Get all active, successful runs in the DXVK Linux-compatible workflow.
39+
Return Type: list
40+
"""
4141

42-
download_canceled = Property(bool, get_download_canceled, set_download_canceled)
42+
workflow_request_url: str = f'{self.CT_WORKFLOW_URL}?per_page={str(count)}'
43+
workflow_response_json: dict = self.rs.get(workflow_request_url).json()
4344

44-
def __set_download_progress_percent(self, value : int):
45-
if self.p_download_progress_percent == value:
46-
return
47-
self.p_download_progress_percent = value
48-
self.download_progress_percent.emit(value)
45+
tags: list[str] = []
46+
for workflow in workflow_response_json.get('workflows', {}):
47+
if workflow['state'] != "active" or self.DXVK_WORKFLOW_NAME not in workflow['path']:
48+
continue
49+
50+
page = 1
51+
while page != -1 and page <= 5: # fetch more (up to 5 pages) if first releases all failed
52+
at_least_one_failed = False # ensure the reason that len(tags)=0 is that releases failed
53+
54+
workflow_runs_request_url: str = f'{workflow["url"]}/runs?per_page={count}&page={page}'
55+
workflow_runs_response_json: dict = self.rs.get(workflow_runs_request_url).json()
56+
57+
for run in workflow_runs_response_json.get('workflow_runs', {}):
58+
if run['head_branch'] != 'master':
59+
continue
60+
61+
if run['conclusion'] == "failure":
62+
at_least_one_failed = True
63+
64+
continue
65+
66+
# TODO can make this generic so that i.e. this DXVK Ctmod can use commmit SHAs but Proton-tkg can use workflow IDs?
67+
# then this could be a generic function shared between ctmods and could be in a util file, unit tested, etc
68+
commit_hash: str = str(run['head_commit']['id'][:7])
69+
tags.append(commit_hash)
70+
71+
if len(tags) == 0 and at_least_one_failed:
72+
page += 1
73+
74+
continue
75+
76+
page = -1
77+
78+
return tags
79+
80+
def fetch_releases(self, count: int = 30, page: int = 1) -> list[str]:
4981

50-
def __download(self, url, destination, f_size):
51-
# f_size in argumentbecause artifacts don't have Content-Length.
5282
"""
53-
Download files from url to destination
54-
Return Type: bool
83+
List available releases.
84+
Return Type: str[]
5585
"""
56-
try:
57-
file = requests.get(url, stream=True)
58-
except OSError:
59-
return False
6086

61-
self.__set_download_progress_percent(1) # 1 download started
62-
c_count = int(f_size / self.BUFFER_SIZE)
63-
c_current = 1
64-
destination = os.path.expanduser(destination)
65-
os.makedirs(os.path.dirname(destination), exist_ok=True)
66-
with open(destination, 'wb') as dest:
67-
for chunk in file.iter_content(chunk_size=self.BUFFER_SIZE):
68-
if self.download_canceled:
69-
self.download_canceled = False
70-
self.__set_download_progress_percent(-2) # -2 download canceled
71-
return False
72-
if chunk:
73-
dest.write(chunk)
74-
dest.flush()
75-
self.__set_download_progress_percent(int(min(c_current / c_count * 98.0, 98.0))) # 1-98, 100 after extract
76-
c_current += 1
77-
self.__set_download_progress_percent(99) # 99 download complete
78-
return True
87+
return self.__fetch_workflows(count=count)
7988

8089
def __get_artifact_from_commit(self, commit):
90+
8191
"""
8292
Get artifact from commit
8393
Return Type: str
8494
"""
85-
for artifact in self.rs.get(f'{self.CT_URL}?per_page=100').json()["artifacts"]:
86-
if artifact['workflow_run']['head_sha'][:len(commit)] == commit:
95+
96+
for artifact in self.rs.get(f'{self.CT_ALL_ARTIFACTS_URL}?per_page=100').json()["artifacts"]:
97+
# DXVK appends '-msvc-output' to Windows builds
98+
# See: https://github.com/doitsujin/dxvk/blob/20a6fae8a7f60e7719724b229552eba1ae6c3427/.github/workflows/test-build-windows.yml#L80
99+
if artifact['workflow_run']['head_sha'][:len(commit)] == commit and not artifact['name'].endswith('-msvc-output'):
87100
artifact['workflow_run']['head_sha'] = commit
88101
return artifact
102+
89103
return None
90104

91-
def __fetch_github_data(self, tag):
105+
def __fetch_github_data(self, tag: str):
106+
92107
"""
93108
Fetch GitHub release information
94109
Return Type: dict
95110
Content(s):
96-
'version', 'date', 'download', 'size', 'checksum'
111+
'version', 'date', 'download', 'size'
97112
"""
98-
# Tag in this case is the commit hash.
113+
114+
# Tag in this case is the commit hash
99115
data = self.__get_artifact_from_commit(tag)
100116
if not data:
101117
return
@@ -105,50 +121,39 @@ def __fetch_github_data(self, tag):
105121
values['size'] = data['size_in_bytes']
106122
return values
107123

108-
def is_system_compatible(self):
109-
"""
110-
Are the system requirements met?
111-
Return Type: bool
112-
"""
113-
return True
124+
def __get_data(self, version: str, install_dir: str) -> tuple[dict | None, str | None]:
114125

115-
def fetch_releases(self, count=100, page=1):
116126
"""
117-
List available releases
118-
Return Type: str[]
127+
Get needed download data and path to extract directory.
128+
Return Type: diple[dict | None, str | None]
119129
"""
120-
tags = []
121-
for artifact in ghapi_rlcheck(self.rs.get(f'{self.CT_URL}?per_page={count}&page={page}').json()).get("artifacts", {}):
122-
workflow = artifact['workflow_run']
123-
if workflow["head_branch"] != "master" or artifact["expired"]:
124-
continue
125-
tags.append(workflow['head_sha'][:7])
126-
return tags
127130

128-
def get_tool(self, version, install_dir, temp_dir):
131+
data = self.__fetch_github_data(version)
132+
if not data or not 'download' in data:
133+
return (None, None)
134+
135+
# TODO This is hardcoded to Lutris as DXVK Nightly currently doesn't support any other launchers -- Could possibly add support for Heroic in future
136+
dxvk_dir = os.path.join(install_dir, '../../runtime/dxvk', 'dxvk-git-' + data['version'])
137+
138+
return (data, dxvk_dir)
139+
140+
def __extract(self, archive_path: str, extract_dir: str) -> bool:
141+
129142
"""
130-
Download and install the compatibility tool
143+
Extract the tool archive at the given path.
131144
Return Type: bool
132145
"""
133-
data = self.__fetch_github_data(version)
134-
if not data or 'download' not in data:
135-
return False
136146

137-
dxvk_zip = os.path.join(temp_dir, data['download'].split('/')[-1])
138-
if not self.__download(url=data['download'], destination=dxvk_zip, f_size=data['size']):
147+
if not archive_path or not extract_dir:
139148
return False
140149

141-
dxvk_dir = os.path.join(install_dir, '../../runtime/dxvk', 'dxvk-git-' + data['version'])
142-
if not extract_zip(dxvk_zip, dxvk_dir):
143-
return False
150+
return extract_zip(archive_path, extract_dir)
144151

145-
self.__set_download_progress_percent(100)
152+
def get_info_url(self, version: str) -> str:
146153

147-
return True
148-
149-
def get_info_url(self, version):
150154
"""
151155
Get link with info about version (eg. GitHub release page)
152156
Return Type: str
153157
"""
158+
154159
return self.CT_INFO_URL + version

0 commit comments

Comments
 (0)