mirror of
https://github.com/lutris/lutris
synced 2024-11-05 18:10:49 +00:00
Scripting interpreter rewrite (WIP)
This commit is contained in:
parent
5788984c09
commit
26f59c9bc7
4 changed files with 144 additions and 79 deletions
21
docs/installers.rst
Normal file
21
docs/installers.rst
Normal file
|
@ -0,0 +1,21 @@
|
|||
==================
|
||||
Writing installers
|
||||
==================
|
||||
|
||||
|
||||
Moving files
|
||||
------------
|
||||
|
||||
Move files by using the ``move`` command. ``move`` requires two parameters:
|
||||
``src`` and ``dst``.
|
||||
|
||||
The ``src`` parameter can either be a ``file id`` or a relative location. If the
|
||||
parameter value is not found in the list of ``file ids``, then it must be
|
||||
prefixed by either ``$CACHE`` or ``$GAMEDIR`` to move a file or directory from
|
||||
the download cache or the game installation dir, respectively.
|
||||
|
||||
The ``dst`` parameter should be prefixed by either ``$GAMEDIR`` or ``$HOME``
|
||||
to move files to path relative to the game dir or the current user's home
|
||||
directory.
|
||||
|
||||
The ``move`` command cannot overwrite files.
|
|
@ -29,13 +29,12 @@ from gi.repository import Gtk
|
|||
from os.path import join, exists
|
||||
|
||||
from lutris import pga
|
||||
from lutris.util import log
|
||||
from lutris.util import http
|
||||
from lutris.util.log import logger
|
||||
from lutris.util import http
|
||||
from lutris.util.strings import slugify
|
||||
from lutris.util.files import calculate_md5
|
||||
from lutris.game import LutrisGame
|
||||
from lutris.config import LutrisConfig
|
||||
#from lutris.config import LutrisConfig
|
||||
from lutris.gui.dialogs import ErrorDialog, FileDialog
|
||||
from lutris.gui.widgets import DownloadProgressBox, FileChooserEntry
|
||||
from lutris.shortcuts import create_launcher
|
||||
|
@ -71,7 +70,7 @@ def untar(filename, dest=None, method='gzip'):
|
|||
else:
|
||||
compression_flag = ''
|
||||
cmd = "tar x%sf %s" % (compression_flag, filename)
|
||||
log.logger.debug(cmd)
|
||||
logger.debug(cmd)
|
||||
subprocess.Popen(cmd, shell=True)
|
||||
os.chdir(cwd)
|
||||
|
||||
|
@ -87,6 +86,88 @@ def reporthook(piece, received_bytes, total_size):
|
|||
print("%d %%" % ((piece * received_bytes) * 100 / total_size))
|
||||
|
||||
|
||||
class ScriptingError(Exception):
|
||||
def __init__(self, message, faulty_data=None):
|
||||
self.message = message
|
||||
self.faulty_data = faulty_data
|
||||
logger.error(self.message + repr(self.faulty_data))
|
||||
|
||||
def __str__(self):
|
||||
return self.message + "\n" + repr(self.faulty_data)
|
||||
|
||||
|
||||
class ScriptInterpreter(object):
|
||||
game_name = None
|
||||
errors = []
|
||||
|
||||
def __init__(self, script):
|
||||
self.script = yaml.safe_load(script)
|
||||
|
||||
def is_valid(self):
|
||||
required_fields = ('runner', 'name', 'installer')
|
||||
for field in required_fields:
|
||||
if not self.script.get(field):
|
||||
self.errors.append("Missing field '%s'" % field)
|
||||
print self.errors
|
||||
return not bool(self.errors)
|
||||
|
||||
#def parse_config(self):
|
||||
# """ Reads the installer file. """
|
||||
# self.game = self.rules['name']
|
||||
# self.game_slug = os.path.basename(self.installer_path)[:-4]
|
||||
|
||||
# self.actions = self.rules['installer']
|
||||
# self.lutris_config = LutrisConfig(runner=self.game_info['runner'])
|
||||
# return True
|
||||
def substitute(self, path_ref, path_type):
|
||||
if not path_ref.startswith("$%s" % path_type):
|
||||
return
|
||||
if path_type == "GAMEDIR":
|
||||
if not self.gamedir:
|
||||
raise ValueError("No gamedir set")
|
||||
else:
|
||||
return path_ref.replace("$GAMEDIR", self.gamedir)
|
||||
if path_type == "CACHE":
|
||||
return path_ref.replace("$CACHE", settings.DATA_DIR)
|
||||
if path_type == "HOME":
|
||||
return path_ref.replace("$HOME", os.path.expanduser("~"))
|
||||
|
||||
def _get_move_paths(self, params):
|
||||
for required_param in ('dst', 'src'):
|
||||
if required_param not in params:
|
||||
raise ScriptingError(
|
||||
"The '%s' parameter is required for 'move'"
|
||||
% required_param, params
|
||||
)
|
||||
src_ref = params['src']
|
||||
src = (self.files.get(src_ref)
|
||||
or self.substitute(src_ref, 'CACHE')
|
||||
or self.substitute(src_ref, 'GAMEDIR'))
|
||||
if not src:
|
||||
raise ScriptingError("Wrong value for 'src' param", src_ref)
|
||||
dst_ref = params['dst']
|
||||
dst = (self.substitute(dst_ref, 'GAMEDIR')
|
||||
or self.substitute(dst_ref, 'HOME'))
|
||||
if not dst:
|
||||
raise ScriptingError("Wrong value for 'dst' param", dst_ref)
|
||||
return (src, dst)
|
||||
|
||||
def move(self, params):
|
||||
src, dst = self._get_move_paths(params)
|
||||
if not os.path.exists(src):
|
||||
self.errors.append("I can't move %s, it does not exist" % src)
|
||||
return False
|
||||
target = os.path.join(dst, os.path.basename(src))
|
||||
if os.path.exists(target):
|
||||
self.errors.append("Destination %s already exists" % target)
|
||||
try:
|
||||
shutil.move(src, target)
|
||||
except shutil.Error:
|
||||
self.errors.append("Can't move %s to destination %s" % (src, dst))
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# pylint: disable=R0904
|
||||
class Installer(Gtk.Dialog):
|
||||
game_dir = None
|
||||
|
@ -211,6 +292,11 @@ class Installer(Gtk.Dialog):
|
|||
return False
|
||||
|
||||
# Parse installer file
|
||||
script_data = file(self.installer_path, 'r').read()
|
||||
self.interpreter = ScriptInterpreter(script_data)
|
||||
if not self.interpreter.is_valid():
|
||||
raise ScriptingError("Installation script contains errors",
|
||||
self.interpreter.errors)
|
||||
success = self.parse_config()
|
||||
games_dir = self.lutris_config.get_path()
|
||||
|
||||
|
@ -222,7 +308,7 @@ class Installer(Gtk.Dialog):
|
|||
return True
|
||||
|
||||
self.game_dir = join(games_dir, self.game_slug)
|
||||
log.logger.debug("Setting default path to : %s", self.game_dir)
|
||||
logger.debug("Setting default path to : %s", self.game_dir)
|
||||
if not os.path.exists(self.game_dir):
|
||||
os.mkdir(self.game_dir)
|
||||
return success
|
||||
|
@ -235,7 +321,7 @@ class Installer(Gtk.Dialog):
|
|||
|
||||
dest_dir = join(settings.CACHE_DIR, "installer/%s" % self.game_slug)
|
||||
if not exists(dest_dir):
|
||||
log.logger.debug('Creating destination directory %s' % dest_dir)
|
||||
logger.debug('Creating destination directory %s' % dest_dir)
|
||||
os.mkdir(dest_dir)
|
||||
self.location_entry.destroy()
|
||||
self.install_button.set_sensitive(False)
|
||||
|
@ -279,22 +365,18 @@ class Installer(Gtk.Dialog):
|
|||
url = game_file[file_id]
|
||||
filename = None
|
||||
|
||||
log.logger.debug("Fetching [%s]: %s" % (file_id, url))
|
||||
logger.debug("Fetching [%s]: %s" % (file_id, url))
|
||||
pga_url = pga.check_for_file(self.game_slug, file_id)
|
||||
if pga_url:
|
||||
url = pga_url
|
||||
|
||||
# wat?
|
||||
#if self.download_progress is not None:
|
||||
# self.download_progress.destroy()
|
||||
|
||||
self.status_label.set_text('Fetching %s' % url)
|
||||
dest_dir = join(settings.CACHE_DIR, "installer/%s" % self.game_slug)
|
||||
if not filename:
|
||||
filename = os.path.basename(url)
|
||||
dest_file = os.path.join(dest_dir, filename)
|
||||
if os.path.exists(dest_file):
|
||||
log.logger.debug("Destination file exists")
|
||||
logger.debug("Destination file exists")
|
||||
os.remove(dest_file)
|
||||
if url == "N/A":
|
||||
if not filename:
|
||||
|
@ -312,6 +394,9 @@ class Installer(Gtk.Dialog):
|
|||
dest_dir, os.path.basename(filename)
|
||||
))
|
||||
elif url.startswith("http"):
|
||||
if self.download_progress:
|
||||
# Remove existing progress bar
|
||||
self.download_progress.destroy()
|
||||
self.download_progress = DownloadProgressBox(
|
||||
{'url': url, 'dest': dest_file}, cancelable=True
|
||||
)
|
||||
|
@ -324,8 +409,6 @@ class Installer(Gtk.Dialog):
|
|||
"""Actual game installation"""
|
||||
logger.debug("Running installation")
|
||||
|
||||
if self.download_progress is not None:
|
||||
self.download_progress.destroy()
|
||||
if not os.path.exists(self.game_dir):
|
||||
os.makedirs(self.game_dir)
|
||||
os.chdir(self.game_dir)
|
||||
|
@ -348,8 +431,11 @@ class Installer(Gtk.Dialog):
|
|||
'check_md5': self._check_md5,
|
||||
'delete': self._delete,
|
||||
}
|
||||
if not hasattr(ScriptInterpreter, action_name):
|
||||
raise ScriptingError("The command %s does not exists"
|
||||
% action_name)
|
||||
if action_name not in mappings.keys():
|
||||
log.logger.error("Action " + action_name + " not supported !")
|
||||
logger.error("Action " + action_name + " not supported !")
|
||||
continue
|
||||
mappings[action_name](action_data)
|
||||
if self.errors:
|
||||
|
@ -389,28 +475,6 @@ class Installer(Gtk.Dialog):
|
|||
play_button.connect('clicked', self.launch_game)
|
||||
self.action_buttons.add(play_button)
|
||||
|
||||
def parse_config(self):
|
||||
""" Reads the installer file. """
|
||||
installer_contents = file(self.installer_path, 'r').read()
|
||||
self.rules = yaml.safe_load(installer_contents)
|
||||
logger.debug("Installer content:\n %s" % installer_contents)
|
||||
|
||||
mandatory_fields = ['runner', 'name']
|
||||
optional_fields = ['exe', 'exe64', 'iso', 'rom', 'disk']
|
||||
for field in mandatory_fields:
|
||||
self.game_info[field] = self.rules[field]
|
||||
for field in optional_fields:
|
||||
if field in self.rules:
|
||||
self.game_info[field] = self.rules[field]
|
||||
|
||||
# FIXME : weird redefinition of self.game
|
||||
self.game = self.rules['name']
|
||||
self.game_slug = os.path.basename(self.installer_path)[:-4]
|
||||
|
||||
self.actions = self.rules['installer']
|
||||
self.lutris_config = LutrisConfig(runner=self.game_info['runner'])
|
||||
return True
|
||||
|
||||
def write_config(self):
|
||||
"""Write the game configuration as a Lutris launcher."""
|
||||
config_filename = join(settings.CONFIG_DIR,
|
||||
|
@ -476,7 +540,7 @@ class Installer(Gtk.Dialog):
|
|||
logger.error("%s does not exists" % filename)
|
||||
return False
|
||||
msg = "Extracting %s" % filename
|
||||
log.logger.debug(msg)
|
||||
logger.debug(msg)
|
||||
self.status_label.set_text(msg)
|
||||
_, extension = os.path.splitext(filename)
|
||||
if extension == ".zip":
|
||||
|
@ -486,48 +550,9 @@ class Installer(Gtk.Dialog):
|
|||
elif filename.endswith('.tar.bz2'):
|
||||
untar(filename, None, 'bzip2')
|
||||
else:
|
||||
log.logger.error("unrecognised file extension %s" % extension)
|
||||
logger.error("unrecognised file extension %s" % extension)
|
||||
return False
|
||||
|
||||
def _move(self, data):
|
||||
""" Moves a file. """
|
||||
file_id = data['src']
|
||||
src = self.gamefiles.get(file_id)
|
||||
if not src:
|
||||
msg = "Can't find %s in %s" % (file_id, self.gamefiles)
|
||||
logger.error(msg)
|
||||
self.errors.append(msg)
|
||||
return False
|
||||
|
||||
if data['dst'] == 'gamedir':
|
||||
dst = self.game_dir
|
||||
else:
|
||||
dst = data['dst'].replace('homedir', os.path.expanduser('~'))
|
||||
if not os.path.exists(dst):
|
||||
dst = '/tmp'
|
||||
|
||||
self.status_label.set_text("Moving %s" % src)
|
||||
logger.debug("[%s] Moving to %s" % (src, dst))
|
||||
if not os.path.exists(src):
|
||||
msg = "I can't move %s, it does not exist" % src
|
||||
logger.error(msg)
|
||||
self.errors.append(msg)
|
||||
return False
|
||||
dest_filename = os.path.join(dst, os.path.basename(src))
|
||||
if os.path.exists(dest_filename):
|
||||
os.remove(dest_filename)
|
||||
try:
|
||||
shutil.move(src, dst)
|
||||
except shutil.Error:
|
||||
msg = "Couln't move %s to destination %s" % (
|
||||
src, dst
|
||||
)
|
||||
logger.error(msg)
|
||||
self.errors.append(msg)
|
||||
return False
|
||||
self.gamefiles[file_id] = dest_filename
|
||||
return True
|
||||
|
||||
def _delete(self, data):
|
||||
print "Script has requested to delete %s" % data
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ from lutris.util.log import logger
|
|||
|
||||
class RessourceOpener(urllib.FancyURLopener):
|
||||
def http_error_default(self, url, fp, errcode, errmsg, headers):
|
||||
if errcode == 404:
|
||||
if errcode in (404, 500):
|
||||
raise IOError(errmsg, errcode, url)
|
||||
|
||||
|
||||
|
|
19
tests/test_installer.py
Normal file
19
tests/test_installer.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
from unittest import TestCase
|
||||
from lutris.installer import ScriptInterpreter, ScriptingError
|
||||
|
||||
|
||||
class TestScriptInterpreter(TestCase):
|
||||
def test_script_with_correct_values_is_valid(self):
|
||||
script_data = """
|
||||
runner: foo
|
||||
installer: bar
|
||||
name: baz
|
||||
"""
|
||||
interpreter = ScriptInterpreter(script_data)
|
||||
self.assertFalse(interpreter.errors)
|
||||
self.assertTrue(interpreter.is_valid())
|
||||
|
||||
def test_move_requires_src_and_dst(self):
|
||||
interpreter = ScriptInterpreter("")
|
||||
with self.assertRaises(ScriptingError):
|
||||
interpreter._get_move_paths({})
|
Loading…
Reference in a new issue