diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 0c8e8cc66..6ee9bcc72 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -34,7 +34,7 @@ jobs: shellcheck tests/*.sh - name: Setup venv run: | - uv venv --python 3.11 + uv venv --python 3.10 - name: Test steamrt install run: | source .venv/bin/activate @@ -62,6 +62,7 @@ jobs: rm -rf "$HOME/.local/share/umu" "$HOME/.local/share/Steam/compatibilitytools.d" - name: Test configuration file run: | + uv venv --python 3.11 source .venv/bin/activate sh tests/test_config.sh rm -rf "$HOME/.local/share/umu" "$HOME/Games/umu" "$HOME/.local/share/Steam/compatibilitytools.d" "$HOME/.cache/umu" diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index c95fbc984..c34ef4597 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -13,7 +13,7 @@ jobs: build: strategy: matrix: - version: ["3.11"] + version: ["3.10"] runs-on: ubuntu-latest @@ -28,10 +28,10 @@ jobs: python3 -m pip install --upgrade pip uv mypy - name: Setup venv run: | - uv venv --python 3.11 + uv venv --python 3.10 source .venv/bin/activate uv pip install -r requirements.in - name: Check types with mypy run: | source .venv/bin/activate - mypy --python-version 3.11 . + mypy . diff --git a/.github/workflows/umu-python.yml b/.github/workflows/umu-python.yml index a283d464c..5f313e253 100644 --- a/.github/workflows/umu-python.yml +++ b/.github/workflows/umu-python.yml @@ -15,7 +15,7 @@ jobs: matrix: # tomllib requires Python 3.11 # Ubuntu latest (Jammy) provides Python 3.10 - version: ["3.11", "3.12", "3.13"] + version: ["3.10", "3.11", "3.12", "3.13"] runs-on: ubuntu-latest diff --git a/umu/umu_consts.py b/umu/umu_consts.py index 82b44ee1a..44d7c7ebf 100644 --- a/umu/umu_consts.py +++ b/umu/umu_consts.py @@ -3,6 +3,32 @@ from pathlib import Path +# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +# 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 +# Python Software Foundation; +# Source: https://raw.githubusercontent.com/python/cpython/refs/heads/3.11/Lib/http/__init__.py +# License: https://raw.githubusercontent.com/python/cpython/refs/heads/3.11/LICENSE +class HTTPMethod(Enum): + """HTTP methods and descriptions. + + Methods from the following RFCs are all observed: + + * RFF 9110: HTTP Semantics, obsoletes 7231, which obsoleted 2616 + * RFC 5789: PATCH Method for HTTP + + """ + + CONNECT = "CONNECT" + DELETE = "DELETE" + GET = "GET" + HEAD = "HEAD" + OPTIONS = "OPTIONS" + PATCH = "PATCH" + POST = "POST" + PUT = "PUT" + TRACE = "TRACE" + + class GamescopeAtom(Enum): """Represent Gamescope-specific X11 atom names.""" diff --git a/umu/umu_proton.py b/umu/umu_proton.py index 87cdce916..bfe7d6199 100644 --- a/umu/umu_proton.py +++ b/umu/umu_proton.py @@ -1,7 +1,7 @@ import os from concurrent.futures import Future, ThreadPoolExecutor -from hashlib import file_digest, sha512 -from http import HTTPMethod, HTTPStatus +from hashlib import sha512 +from http import HTTPStatus from pathlib import Path from re import split as resplit from shutil import move, rmtree @@ -14,9 +14,14 @@ from urllib3.poolmanager import PoolManager from urllib3.response import BaseHTTPResponse -from umu.umu_consts import STEAM_COMPAT, UMU_CACHE, UMU_LOCAL +from umu.umu_consts import STEAM_COMPAT, UMU_CACHE, UMU_LOCAL, HTTPMethod from umu.umu_log import log -from umu.umu_util import extract_tarfile, run_zenity, write_file_chunks +from umu.umu_util import ( + extract_tarfile, + file_digest, + run_zenity, + write_file_chunks, +) SessionPools = tuple[ThreadPoolExecutor, PoolManager] @@ -95,7 +100,9 @@ def _fetch_releases( if os.environ.get("PROTONPATH") == "GE-Proton": repo = "/repos/GloriousEggroll/proton-ge-custom/releases/latest" - resp = http_pool.request(HTTPMethod.GET, f"{url}{repo}", headers=headers) + resp = http_pool.request( + HTTPMethod.GET.value, f"{url}{repo}", headers=headers + ) if resp.status != HTTPStatus.OK: return () @@ -159,7 +166,7 @@ def _fetch_proton( # See https://github.com/astral-sh/ruff/issues/7918 log.info("Downloading %s...", proton_hash) - resp = http_pool.request(HTTPMethod.GET, proton_hash_url) + resp = http_pool.request(HTTPMethod.GET.value, proton_hash_url) if resp.status != HTTPStatus.OK: err: str = ( f"Unable to download {proton_hash}\n" @@ -208,7 +215,10 @@ def _fetch_proton( log.info("Downloading %s...", tarball) resp = http_pool.request( - HTTPMethod.GET, tar_url, preload_content=False, headers=headers + HTTPMethod.GET.value, + tar_url, + preload_content=False, + headers=headers, ) # Bail out for unexpected status codes diff --git a/umu/umu_runtime.py b/umu/umu_runtime.py index 3ab3ab3c9..27e4297af 100644 --- a/umu/umu_runtime.py +++ b/umu/umu_runtime.py @@ -1,14 +1,14 @@ import os from collections.abc import Callable from concurrent.futures import Future, ThreadPoolExecutor -from hashlib import file_digest, sha256 +from hashlib import sha256 try: from importlib.resources.abc import Traversable except ModuleNotFoundError: from importlib.abc import Traversable -from http import HTTPMethod, HTTPStatus +from http import HTTPStatus from pathlib import Path from secrets import token_urlsafe from shutil import move, rmtree @@ -21,10 +21,11 @@ from urllib3.poolmanager import PoolManager from urllib3.response import BaseHTTPResponse -from umu.umu_consts import UMU_CACHE, UMU_LOCAL +from umu.umu_consts import UMU_CACHE, UMU_LOCAL, HTTPMethod from umu.umu_log import log from umu.umu_util import ( extract_tarfile, + file_digest, has_umu_setup, run_zenity, write_file_chunks, @@ -139,7 +140,7 @@ def _install_umu( # Get the digest for the runtime archive resp = http_pool.request( - HTTPMethod.GET, f"{host}{endpoint}/SHA256SUMS{token}" + HTTPMethod.GET.value, f"{host}{endpoint}/SHA256SUMS{token}" ) if resp.status != HTTPStatus.OK: err: str = ( @@ -155,7 +156,7 @@ def _install_umu( # Get BUILD_ID.txt. We'll use the value to identify the file when cached. # This will guarantee we'll be picking up the correct file when resuming resp = http_pool.request( - HTTPMethod.GET, f"{host}{endpoint}/BUILD_ID.txt{token}" + HTTPMethod.GET.value, f"{host}{endpoint}/BUILD_ID.txt{token}" ) if resp.status != HTTPStatus.OK: err: str = ( @@ -185,7 +186,7 @@ def _install_umu( log.info("Downloading %s (latest), please wait...", variant) resp = http_pool.request( - HTTPMethod.GET, + HTTPMethod.GET.value, f"{host}{endpoint}/{archive}{token}", preload_content=False, headers=headers, @@ -401,7 +402,7 @@ def _update_umu( f"{host}{endpoint}/SteamLinuxRuntime_{codename}.VERSIONS.txt{token}" ) log.debug("Sending request to '%s' for 'VERSIONS.txt'...", url) - resp = http_pool.request(HTTPMethod.GET, url) + resp = http_pool.request(HTTPMethod.GET.value, url) if resp.status != HTTPStatus.OK: log.error( "%s returned the status: %s", resp.getheader("Host"), resp.status @@ -539,7 +540,7 @@ def _restore_umu_platformid( # Make the request to the VERSIONS.txt endpoint. It's fine to hit the # cache for this endpoint, as it differs to the latest-beta endpoint - resp = http_pool.request(HTTPMethod.GET, f"{host}{url}{versions}") + resp = http_pool.request(HTTPMethod.GET.value, f"{host}{url}{versions}") if resp.status != HTTPStatus.OK: log.error( "%s returned the status: %s", diff --git a/umu/umu_util.py b/umu/umu_util.py index 0bfcc1236..dceeb7304 100644 --- a/umu/umu_util.py +++ b/umu/umu_util.py @@ -3,6 +3,7 @@ from contextlib import contextmanager from ctypes.util import find_library from functools import lru_cache +from hashlib import new as hashnew from io import BufferedIOBase from pathlib import Path from re import Pattern @@ -254,3 +255,51 @@ def has_umu_setup(path: Path = UMU_LOCAL) -> bool: return path.exists() and any( file for file in path.glob("*") if not file.name.endswith("lock") ) + + +# Copyright (C) 2005-2010 Gregory P. Smith (greg@krypto.org) +# Licensed to PSF under a Contributor Agreement. +# Source: https://raw.githubusercontent.com/python/cpython/refs/heads/3.11/Lib/hashlib.py +# License: https://raw.githubusercontent.com/python/cpython/refs/heads/3.11/LICENSE +def file_digest(fileobj, digest, /, *, _bufsize=2**18): # noqa: ANN001 + """Hash the contents of a file-like object. Returns a digest object. + + *fileobj* must be a file-like object opened for reading in binary mode. + It accepts file objects from open(), io.BytesIO(), and SocketIO objects. + The function may bypass Python's I/O and use the file descriptor *fileno* + directly. + + *digest* must either be a hash algorithm name as a *str*, a hash + constructor, or a callable that returns a hash object. + """ + # On Linux we could use AF_ALG sockets and sendfile() to archive zero-copy + # hashing with hardware acceleration. + digestobj = hashnew(digest) if isinstance(digest, str) else digest() + + if hasattr(fileobj, "getbuffer"): + # io.BytesIO object, use zero-copy buffer + digestobj.update(fileobj.getbuffer()) + return digestobj + + # Only binary files implement readinto(). + if not ( + hasattr(fileobj, "readinto") + and hasattr(fileobj, "readable") + and fileobj.readable() + ): + err = ( + f"'{fileobj!r}' is not a file-like object in binary reading mode." + ) + raise ValueError(err) + + # binary file, socket.SocketIO object + # Note: socket I/O uses different syscalls than file I/O. + buf = bytearray(_bufsize) # Reusable buffer to reduce allocations. + view = memoryview(buf) + while True: + size = fileobj.readinto(buf) + if size == 0: + break # EOF + digestobj.update(view[:size]) + + return digestobj