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.graphics import vkquery
from lutris.util.log import logger from lutris.util.log import logger
from lutris.util.steam.config import get_steam_dir 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.d3d_extras import D3DExtrasManager
from lutris.util.wine.dgvoodoo2 import dgvoodoo2Manager from lutris.util.wine.dgvoodoo2 import dgvoodoo2Manager
from lutris.util.wine.dxvk import REQUIRED_VULKAN_API_VERSION, DXVKManager 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, 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, 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, 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" DEFAULT_WINE_PREFIX = "~/.wine"
@ -729,9 +729,9 @@ class wine(Runner):
wine_versions = get_wine_versions() wine_versions = get_wine_versions()
if min_version: if min_version:
min_version_list, _, _ = parse_version(min_version) min_version_list, _, _ = parse_wine_version(min_version)
for wine_version in wine_versions: 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: if version_list > min_version_list:
return True return True
logger.warning("Wine %s or higher not found", min_version) logger.warning("Wine %s or higher not found", min_version)

View file

@ -69,8 +69,6 @@ def parse_version(version):
Returns: Returns:
tuple: (version number as list, prefix, suffix) 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) version_match = re.search(r"(\d[\d\.]+\d)", version)
if not version_match: if not version_match:
return [], "", "" return [], "", ""
@ -80,19 +78,6 @@ def parse_version(version):
return [int(p) for p in version_number.split(".")], suffix, prefix 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): def unpack_dependencies(string):
"""Parse a string to allow for complex dependencies """Parse a string to allow for complex dependencies
Works in a similar fashion as Debian dependencies, separate dependencies Works in a similar fashion as Debian dependencies, separate dependencies

View file

@ -4,10 +4,12 @@ import os
import shutil import shutil
from gettext import gettext as _ from gettext import gettext as _
from lutris import settings
from lutris.util import system from lutris.util import system
from lutris.util.extract import extract_archive from lutris.util.extract import extract_archive
from lutris.util.http import download_file from lutris.util.http import download_file
from lutris.util.log import logger from lutris.util.log import logger
from lutris.util.strings import parse_version
from lutris.util.wine.prefix import WinePrefixManager 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)] recommended_versions = [v for v in versions if self.is_recommended_version(v)]
return recommended_versions[0] if recommended_versions else versions[0] 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): def is_recommended_version(self, version):
"""True if the version given should be usable as the default; false if it """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 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 return True
@property @property
@ -271,8 +298,19 @@ class DLLManager:
def upgrade(self): def upgrade(self):
self.fetch_versions() self.fetch_versions()
if not self.is_available(): if not self.is_available():
if self.version: versions = self.get_recommended_versions()
logger.info("Downloading %s %s...", self.component, self.version)
while versions:
# Try to download the latest recommended version.
version = versions[0]
logger.info("Downloading %s %s...", self.component, version)
self.download() 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.runners.steam import steam
from lutris.util import linux, system from lutris.util import linux, system
from lutris.util.log import logger 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 from lutris.util.wine import fsync
WINE_DIR = os.path.join(settings.RUNNER_DIR, "wine") 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)) 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): def is_version_installed(version):
return os.path.isfile(get_wine_version_exe(version)) return os.path.isfile(get_wine_version_exe(version))

View file

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