diff --git a/.github/scripts/check_verbs.py b/.github/scripts/check_verbs.py new file mode 100644 index 00000000..9c9a6113 --- /dev/null +++ b/.github/scripts/check_verbs.py @@ -0,0 +1,99 @@ +"""This provides a check, if all used verbs are valid. It also warns if local verbs are unused.""" + +import os +import re +import subprocess + +from glob import iglob +from pathlib import Path +from tempfile import mkdtemp + +# 'gui' is a virtual verb for opening the Winetricks GUI +# 'vd=1280x720' is a setting for the virtual desktop and valid +whitelist_verbs = { 'gui', 'vd=1280x720' } + +def extract_verbs_from_glob(path_glob: iglob) -> set[str]: + """Simply strip the extension from all found files.""" + return { + file.stem + for file in path_glob + } + + +def find_verbs(root: Path) -> set[str]: + """Find all used verbs in gamefixes""" + verbs: set[str] = set() + game_fixes = root.glob('gamefixes-*/*.py') + + for fix in game_fixes: + f = fix.read_text() + r = re.finditer(r"util\.protontricks\s*\(\s*('|\")(?P.*)\1\s*\)", f, re.MULTILINE) + for match in r: + verbs.add(match.group('verb')) + + return verbs + + +def find_valid_verbs(root: Path) -> set[str]: + """Winetricks will create temporary files with metadata, these include all valid verbs.""" + # Check if winetricks is present and executable + wt_path = root.joinpath('winetricks') + if not wt_path.is_file() or not os.access(wt_path, os.X_OK): + raise FileNotFoundError('Winetricks can not be found or is not executable') + + # Provide a valid path to create the metadata to winetricks + tmp_dir = Path(mkdtemp()) + if not tmp_dir.is_dir() or not os.access(tmp_dir, os.W_OK): + raise PermissionError(f'Can not write into temporary folder "{tmp_dir}".') + + # Setup environment variables + env = os.environ.copy() + env['TMPDIR'] = tmp_dir + env['WINETRICKS_LATEST_VERSION_CHECK'] = 'disabled' + + # Execute winetricks, suppress output + print('Executing winetricks, using tmp path "{tmp_dir}" - this may take a moment.') + subprocess.run([wt_path, '--no-clean', 'list-all'], env=env, stdout=subprocess.DEVNULL) + + # Get all verbs + vars_glob = tmp_dir.glob('**/*.vars') + return extract_verbs_from_glob(vars_glob) + + +def main() -> None: + """Validate winetricks and protontricks verbs.""" + # Top-level project directory that is expected to contain gamefix directories + project = Path(__file__).parent.parent.parent + print(project) + + # Find all verbs that we use + verbs = find_verbs(project) + + # Find verbs that are in winetricks + valid_verbs = find_valid_verbs(project) + + # Additionally, we need to include our own verbs. + valid_verbs_local = extract_verbs_from_glob(project.glob('verbs/*.verb')) + + print(f'Local verbs: {len(valid_verbs_local)}') + print(f'Winetricks verbs: {len(valid_verbs)}') + print(f'Unique verbs used: {len(verbs)}') + + # Check for unused local verbs + unused_local_verbs = valid_verbs_local.difference(verbs) + if len(unused_local_verbs) > 0: + print(f'WARNING: The following local verbs are unused: {unused_local_verbs}') + + # Compare the results + #FIXME: Implement a more robust mechanism for setting verbs + valid_verbs_all = valid_verbs.union(valid_verbs_local).union(whitelist_verbs) + invalid_verbs = verbs.difference(valid_verbs_all) + + if len(invalid_verbs) > 0: + raise ValueError(f'The following verbs are invalid: {invalid_verbs}') + else: + print('All verbs are valid!') + + +if __name__ == '__main__': + main() diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 963bbf98..238d0782 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,7 @@ jobs: - name: Validate gamefix modules run: | python3 .github/scripts/check_gamefixes.py + python3 .github/scripts/check_verbs.py - name: Test with unittest run: | python3 protonfixes_test.py