First cut at version compatibility restrictions

This code looks for a '.lutris_compat.json' file at the root of a
component's version's directory.

This contains, in JSON, the minimum Lutris version required. If Lutris
finds this to be too late, it will try earlier versions.

The code is messy and this could download a lot of versions, since
it tries them one at a time. But it's a start.
This commit is contained in:
Daniel Johnson 2023-05-22 17:29:52 -04:00 committed by Mathieu Comandon
parent 567c9a99b5
commit 14ae02536e
5 changed files with 74 additions and 29 deletions

View file

@ -16,7 +16,7 @@ from lutris.util.display import DISPLAY_MANAGER, get_default_dpi
from lutris.util.graphics import vkquery
from lutris.util.log import logger
from lutris.util.steam.config import get_steam_dir
from lutris.util.strings import parse_version, split_arguments
from lutris.util.strings import split_arguments
from lutris.util.wine.d3d_extras import D3DExtrasManager
from lutris.util.wine.dgvoodoo2 import dgvoodoo2Manager
from lutris.util.wine.dxvk import REQUIRED_VULKAN_API_VERSION, DXVKManager
@ -28,7 +28,7 @@ from lutris.util.wine.wine import (
POL_PATH, WINE_DIR, WINE_PATHS, detect_arch, display_vulkan_error, esync_display_limit_warning,
esync_display_version_warning, fsync_display_support_warning, fsync_display_version_warning, get_default_version,
get_overrides_env, get_proton_paths, get_real_executable, get_wine_version, get_wine_versions, is_esync_limit_set,
is_fsync_supported, is_gstreamer_build, is_version_esync, is_version_fsync
is_fsync_supported, is_gstreamer_build, is_version_esync, is_version_fsync, parse_wine_version
)
DEFAULT_WINE_PREFIX = "~/.wine"
@ -729,9 +729,9 @@ class wine(Runner):
wine_versions = get_wine_versions()
if min_version:
min_version_list, _, _ = parse_version(min_version)
min_version_list, _, _ = parse_wine_version(min_version)
for wine_version in wine_versions:
version_list, _, _ = parse_version(wine_version)
version_list, _, _ = parse_wine_version(wine_version)
if version_list > min_version_list:
return True
logger.warning("Wine %s or higher not found", min_version)

View file

@ -69,8 +69,6 @@ def parse_version(version):
Returns:
tuple: (version number as list, prefix, suffix)
"""
version = version.replace("Proton7-", "Proton-7.")
version = version.replace("Proton8-", "Proton-8.")
version_match = re.search(r"(\d[\d\.]+\d)", version)
if not version_match:
return [], "", ""
@ -80,19 +78,6 @@ def parse_version(version):
return [int(p) for p in version_number.split(".")], suffix, prefix
def version_sort(versions, reverse=False):
def version_key(version):
version_list, prefix, suffix = parse_version(version)
# Normalize the length of sub-versions
sort_key = version_list + [0] * (10 - len(version_list))
sort_key.append(prefix)
sort_key.append(suffix)
return sort_key
return sorted(versions, key=version_key, reverse=reverse)
def unpack_dependencies(string):
"""Parse a string to allow for complex dependencies
Works in a similar fashion as Debian dependencies, separate dependencies

View file

@ -4,10 +4,12 @@ import os
import shutil
from gettext import gettext as _
from lutris import settings
from lutris.util import system
from lutris.util.extract import extract_archive
from lutris.util.http import download_file
from lutris.util.log import logger
from lutris.util.strings import parse_version
from lutris.util.wine.prefix import WinePrefixManager
@ -50,10 +52,35 @@ class DLLManager:
recommended_versions = [v for v in versions if self.is_recommended_version(v)]
return recommended_versions[0] if recommended_versions else versions[0]
def get_recommended_versions(self):
"""Returns a list of version numbers that are recommended, based on the versions JSON file;
merely having a directory does not count, but we do return only recommended versions. This
means that a version can be recommended until it is downloaded, and then if it has a
'.lutris_compat.json' file it may cease to be recommended. The DLL download code retries
with an earlier version if this happens.
This list is in the usual highest-version-first order, and we try the downloads in that order.
"""
versions = self.load_versions()
return [v for v in versions if self.is_recommended_version(v)]
def is_recommended_version(self, version):
"""True if the version given should be usable as the default; false if it
should not be the default, but may be selected by the user. If only
non-recommended versions exist, we'll still default to one of them, however."""
non-recommended versions exist, we'll still default to one of them, however.
By default, we check for a '.lutris_compat.json' file; if this Lutris is
too old, we'll reject the version."""
path = os.path.join(self.base_dir, version, ".lutris_compat.json")
if os.path.isfile(path):
with open(path, "r", encoding='utf-8') as json_file:
js = json.load(json_file)
if "min_lutris_version" in js:
min_lutris_version = parse_version(js["min_lutris_version"])
current_lutris_version = parse_version(settings.VERSION)
if current_lutris_version < min_lutris_version:
return False
return True
@property
@ -271,8 +298,19 @@ class DLLManager:
def upgrade(self):
self.fetch_versions()
if not self.is_available():
if self.version:
logger.info("Downloading %s %s...", self.component, self.version)
versions = self.get_recommended_versions()
while versions:
# Try to download the latest recommended version.
version = versions[0]
logger.info("Downloading %s %s...", self.component, version)
self.download()
else:
logger.warning("Unable to download %s because version information was not available.", self.component)
# If the version is still recommended, we're done,
# if not we'll try again with the next one.
new_versions = self.get_recommended_versions()
if version in new_versions:
return
versions = new_versions
logger.warning("Unable to download %s because version information was not available.", self.component)

View file

@ -10,7 +10,7 @@ from lutris.gui.dialogs import DontShowAgainDialog, ErrorDialog
from lutris.runners.steam import steam
from lutris.util import linux, system
from lutris.util.log import logger
from lutris.util.strings import version_sort
from lutris.util.strings import parse_version
from lutris.util.wine import fsync
WINE_DIR = os.path.join(settings.RUNNER_DIR, "wine")
@ -236,6 +236,27 @@ def get_wine_version_exe(version):
return os.path.join(WINE_DIR, "{}/bin/wine".format(version))
def parse_wine_version(version):
"""This is a specialized parse_version() that adjusts some odd
Wine versions for correct parsing."""
version = version.replace("Proton7-", "Proton-7.")
version = version.replace("Proton8-", "Proton-8.")
return parse_version(version)
def version_sort(versions, reverse=False):
def version_key(version):
version_list, prefix, suffix = parse_wine_version(version)
# Normalize the length of sub-versions
sort_key = version_list + [0] * (10 - len(version_list))
sort_key.append(prefix)
sort_key.append(suffix)
return sort_key
return sorted(versions, key=version_key, reverse=reverse)
def is_version_installed(version):
return os.path.isfile(get_wine_version_exe(version))

View file

@ -2,6 +2,7 @@ import os
from collections import OrderedDict
from unittest import TestCase
from lutris.util.wine import wine
from lutris.util import fileio, strings, system
from lutris.util.steam import vdfutils
@ -96,11 +97,11 @@ class TestStringUtils(TestCase):
class TestVersionSort(TestCase):
def test_parse_version(self):
self.assertEqual(strings.parse_version("3.6-staging"), ([3, 6], '-staging', ''))
self.assertEqual(wine.parse_wine_version("3.6-staging"), ([3, 6], '-staging', ''))
def test_versions_are_correctly_sorted(self):
versions = ['1.8', '1.7.4', '1.9.1', '1.9.10', '1.9.4']
versions = strings.version_sort(versions)
versions = wine.version_sort(versions)
self.assertEqual(versions[0], '1.7.4')
self.assertEqual(versions[1], '1.8')
self.assertEqual(versions[2], '1.9.1')
@ -114,7 +115,7 @@ class TestVersionSort(TestCase):
'1.9.10-staging', '1.9.10',
'1.9.4', 'staging-1.9.4'
]
versions = strings.version_sort(versions)
versions = wine.version_sort(versions)
self.assertEqual(versions[0], '1.7.4')
self.assertEqual(versions[1], '1.8')
self.assertEqual(versions[2], '1.8-staging')
@ -126,7 +127,7 @@ class TestVersionSort(TestCase):
def test_versions_can_be_reversed(self):
versions = ['1.9', '1.6', '1.7', '1.8']
versions = strings.version_sort(versions, reverse=True)
versions = wine.version_sort(versions, reverse=True)
self.assertEqual(versions[0], '1.9')
self.assertEqual(versions[3], '1.6')