This is an automated email from the git hooks/post-receive script. smcv pushed a commit to branch master in repository game-data-packager.
commit da495b4a5d15d1b4ccfaa996d27b4192e04545f6 Author: Simon McVittie <[email protected]> Date: Fri Oct 14 19:44:40 2016 +0100 Launcher: split into GUI and non-GUI parts The non-GUI part is *almost* a single self-contained file that can be used for servers, although it does import the version number. --- Makefile | 6 +- runtime/gdp-launcher.py | 304 ++++++++++++++++++++++++ runtime/{launcher.py => gdp_launcher_base.py} | 319 +++----------------------- runtime/gdp_launcher_version.py | 12 + 4 files changed, 351 insertions(+), 290 deletions(-) diff --git a/Makefile b/Makefile index eb4707d..5ffdfd2 100644 --- a/Makefile +++ b/Makefile @@ -430,9 +430,9 @@ install: install -m0644 out/vfs.zip $(DESTDIR)$(pkgdatadir)/ install -d $(DESTDIR)$(runtimedir)/ - sed -e '/^#__insert_version_here__/ r out/installed-version.py' \ - < runtime/launcher.py > $(DESTDIR)$(runtimedir)/gdp-launcher - chmod 0755 $(DESTDIR)$(runtimedir)/gdp-launcher + install runtime/gdp_launcher_base.py $(DESTDIR)$(runtimedir)/ + install -m0644 out/installed-version.py $(DESTDIR)$(runtimedir)/gdp_launcher_version.py + install runtime/gdp-launcher.py $(DESTDIR)$(runtimedir)/gdp-launcher install runtime/openurl.py $(DESTDIR)$(runtimedir)/gdp-openurl install -m0644 $(launcher_desktops) $(DESTDIR)$(runtimedir)/ install -m0644 runtime/confirm-binary-only.txt $(DESTDIR)$(runtimedir)/ diff --git a/runtime/gdp-launcher.py b/runtime/gdp-launcher.py new file mode 100755 index 0000000..007a966 --- /dev/null +++ b/runtime/gdp-launcher.py @@ -0,0 +1,304 @@ +#!/usr/bin/python3 +# encoding=utf-8 + +# game-data-packager Gtk launcher stub. See doc/launcher.mdwn for design + +# Copyright © 2015-2016 Simon McVittie <[email protected]> +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# +# You can find the GPL license text on a Debian system under +# /usr/share/common-licenses/GPL-2. + +import fnmatch +import logging +import os +import traceback + +import gi + +gi.require_version('Gtk', '3.0') + +from gi.repository import (GLib, GObject) +from gi.repository import Gtk + +from gdp_launcher_base import ( + Launcher, + RUNTIME_BUILT, + ) + +logger = logging.getLogger('game-data-packager.launcher') + +class IniEditor: + def __init__(self, edits): + self.lines = [] + self.edits = edits + self.__section = None + self.__section_lines = [] + self.__sections = set() + + def load(self, reader): + # Simple INI parser. Not using ConfigParser because Unreal + # uses duplicate keys within sections, and we want to preserve + # comments, blank lines etc. + self.__section = None + self.__section_lines = [] + self.__sections = set() + + for line in reader: + line = line.rstrip('\r\n') + + if line.startswith('[') and line.endswith(']'): + self.__end_section() + self.__section = line[1:-1] + + self.__section_lines.append(line) + + self.__end_section() + + for edit in self.edits: + if edit['section'] not in self.__sections: + self.__section = edit['section'] + self.__section_lines = ['[%s]' % edit['section']] + self.__end_section() + + def __end_section(self): + self.__sections.add(self.__section) + + for edit in self.edits: + if edit['section'] != self.__section: + continue + + logger.debug('editing %s', self.__section) + extra_lines = [] + + for k, v in sorted(edit.get('replace_key', {}).items()): + logger.debug('replacing %s with %s', k, v) + self.__section_lines = [l for l in self.__section_lines + if not l.startswith(k + '=')] + extra_lines.append('%s=%s' % (k, v)) + + for pattern in sorted(edit.get('delete_matched', [])): + logger.debug('deleting lines matching %s', pattern) + self.__section_lines = [l for l in self.__section_lines + if not fnmatch.fnmatchcase(l, pattern)] + + for pattern in edit.get('comment_out_matched', []): + logger.debug('commenting out lines matching %s', pattern) + for i in range(len(self.__section_lines)): + if fnmatch.fnmatchcase(self.__section_lines[i], pattern): + self.__section_lines[i] = ';' + self.__section_lines[i] + self.__section_lines.insert(i, '; ' + + edit['comment_out_reason']) + + for append in edit.get('append_unique', []): + logger.debug('appending unique line %s', append) + for line in self.__section_lines: + if line == append: + break + else: + extra_lines.append(append) + + i = len(self.__section_lines) - 1 + + while i >= 0: + if self.__section_lines[i]: + # _s_l[i] is the last non-empty line, insert after it + self.__section_lines[i + 1:i + 1] = extra_lines + break + i -= 1 + else: + # no non-empty lines, insert after the section-opening heading + self.__section_lines[1:1] = extra_lines + + self.lines.extend(self.__section_lines) + self.__section_lines = [] + self.__section = None + + def save(self, writer): + for line in self.lines: + print(line, file=writer) + +class FullLauncher(Launcher): + def set_id(self): + self.keyfile = GLib.KeyFile() + desktop = os.path.join(RUNTIME_BUILT, self.id + '.desktop') + if os.path.exists(desktop): + self.keyfile.load_from_file(desktop, GLib.KeyFileFlags.NONE) + else: + self.keyfile.load_from_data_dirs( + 'applications/%s.desktop' % self.id, + GLib.KeyFileFlags.NONE) + + self.name = self.keyfile.get_string(GLib.KEY_FILE_DESKTOP_GROUP, + GLib.KEY_FILE_DESKTOP_KEY_NAME) + logger.debug('Name: %s', self.name) + GLib.set_application_name(self.name) + + self.icon_name = self.keyfile.get_string(GLib.KEY_FILE_DESKTOP_GROUP, + GLib.KEY_FILE_DESKTOP_KEY_ICON) + logger.debug('Icon: %s', self.icon_name) + + self.expansion_name = self.args.expansion + + try: + override_id = self.keyfile.get_string(GLib.KEY_FILE_DESKTOP_GROUP, + 'X-GameDataPackager-ExpansionFor') + except GLib.Error: + pass + else: + if self.expansion_name is None: + self.expansion_name = self.id + + if self.expansion_name.startswith(override_id + '-'): + self.expansion_name = self.expansion_name[len(override_id) + 1:] + + self.id = override_id + + def exec_game(self, _unused=None): + # Edit before copying, so that we can detect whether this is + # the first run or not + for ini, details in self.data.get('edit_unreal_ini', {}).items(): + assert self.dot_directory is not None + target = os.path.join(self.dot_directory, ini) + encoding = details.get('encoding', 'windows-1252') + + if os.path.exists(target): + first_time = False + try: + reader = open(target, encoding='utf-16') + reader.readline() + except: + reader = open(target, encoding=encoding) + else: + reader.seek(0) + else: + first_time = True + + if os.path.lexists(target): + logger.info('Removing dangling symlink %s', target) + os.remove(target) + + for base in self.base_directories: + source = os.path.join(base, ini) + + if os.path.exists(source): + try: + reader = open(source, encoding='utf-16') + reader.readline() + except: + reader = open(source, encoding=encoding) + else: + reader.seek(0) + break + else: + raise AssertionError('Required file %s not found', ini) + + if first_time: + edits = details.get('once', []) + details.get('always', []) + else: + edits = details.get('always', []) + + logger.debug('%s', edits) + editor = IniEditor(edits) + + with reader: + editor.load(reader) + + d = os.path.dirname(target) + + if d: + logger.info('Creating directory: %s', d) + os.makedirs(d, exist_ok=True) + + with open(target, 'w', encoding=encoding, + newline=details.get('newline', '\n')) as writer: + editor.save(writer) + + super(FullLauncher, self).exec_game() + + def write_confirm_binary_only_stamp(self): + open(self.warning_stamp, 'a').close() + + def __init__(self): + super(FullLauncher, self).__init__() + + self.window = Gtk.Window() + self.window.set_default_size(600, 300) + self.window.connect('delete-event', Gtk.main_quit) + self.window.set_title(self.name) + self.window.set_icon_name(self.icon_name) + + self.grid = Gtk.Grid(row_spacing=6, column_spacing=6, + margin_top=12, margin_bottom=12, margin_start=12, margin_end=12) + self.window.add(self.grid) + + image = Gtk.Image.new_from_icon_name(self.icon_name, + Gtk.IconSize.DIALOG) + image.set_valign(Gtk.Align.START) + self.grid.attach(image, 0, 0, 1, 1) + + self.text_view = Gtk.TextView(editable=False, cursor_visible=False, + hexpand=True, vexpand=True, wrap_mode=Gtk.WrapMode.WORD, + top_margin=6, left_margin=6, right_margin=6, bottom_margin=6) + self.grid.attach(self.text_view, 1, 0, 1, 1) + + subgrid = Gtk.Grid(column_spacing=6, column_homogeneous=True, + halign=Gtk.Align.END) + + cancel_button = Gtk.Button.new_with_label('Cancel') + cancel_button.connect('clicked', Gtk.main_quit) + subgrid.attach(cancel_button, 0, 0, 1, 1) + + self.check_box = Gtk.CheckButton.new_with_label("I'll be careful") + self.check_box.set_hexpand(True) + self.grid.attach(self.check_box, 0, 1, 2, 1) + + self.ok_button = Gtk.Button.new_with_label('Run') + self.ok_button.set_sensitive(False) + subgrid.attach(self.ok_button, 1, 0, 1, 1) + + self.grid.attach(subgrid, 0, 2, 2, 1) + + self.window.show_all() + + def run_error(self, message): + self.show_error(message) + Gtk.main() + + def show_error(self, message): + self.text_view.get_buffer().set_text(message) + self.ok_button.set_sensitive(False) + self.window.show_all() + self.check_box.hide() + + def run_confirm_binary_only(self): + self.text_view.get_buffer().set_text( + self.load_text('confirm-binary-only.txt', + 'Binary-only game, we cannot fix bugs or security ' + 'vulnerabilities!')) + self.check_box.bind_property('active', self.ok_button, 'sensitive', + GObject.BindingFlags.SYNC_CREATE) + self.ok_button.connect('clicked', + lambda _: self.__confirm_binary_only_cb()) + self.window.show_all() + Gtk.main() + + def __confirm_binary_only_cb(self): + try: + self.write_confirm_binary_only_stamp() + self.exec_game() + except: + self.show_error(traceback.format_exc()) + +if __name__ == '__main__': + logging.basicConfig() + FullLauncher().main() + diff --git a/runtime/launcher.py b/runtime/gdp_launcher_base.py similarity index 61% rename from runtime/launcher.py rename to runtime/gdp_launcher_base.py index dcf9f48..8acd640 100755 --- a/runtime/launcher.py +++ b/runtime/gdp_launcher_base.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # encoding=utf-8 -# game-data-packager Gtk launcher stub. See doc/launcher.mdwn for design +# game-data-packager command-line launcher stub # Copyright © 2015-2016 Simon McVittie <[email protected]> # @@ -18,9 +18,8 @@ # /usr/share/common-licenses/GPL-2. import argparse -import glob -import fnmatch import json +import glob import logging import os import shlex @@ -29,13 +28,7 @@ import string import sys import traceback -import gi -from gi.repository import (GLib, GObject) - -# edited automatically, be careful -GAME_PACKAGE_VERSION = '(uninstalled)' -GAME_PACKAGE_RELEASE = '' -#__insert_version_here__ +from gdp_launcher_version import GAME_PACKAGE_VERSION if 'GDP_UNINSTALLED' in os.environ: RUNTIME_BUILT = './out' @@ -54,7 +47,7 @@ os.environ.setdefault('XDG_CONFIG_DIRS', '/etc/xdg') os.environ.setdefault('XDG_DATA_HOME', os.path.expanduser('~/.local/share')) os.environ.setdefault('XDG_DATA_DIRS', '/usr/local/share:/usr/share') -logger = logging.getLogger('game-data-packager.launcher') +logger = logging.getLogger('game-data-packager.launcher.base') if os.environ.get('GDP_DEBUG'): logger.setLevel(logging.DEBUG) @@ -84,96 +77,6 @@ def expand(path, **kwargs): return os.path.expanduser(string.Template(path).substitute(os.environ, **kwargs)) -class IniEditor: - def __init__(self, edits): - self.lines = [] - self.edits = edits - self.__section = None - self.__section_lines = [] - self.__sections = set() - - def load(self, reader): - # Simple INI parser. Not using ConfigParser because Unreal - # uses duplicate keys within sections, and we want to preserve - # comments, blank lines etc. - self.__section = None - self.__section_lines = [] - self.__sections = set() - - for line in reader: - line = line.rstrip('\r\n') - - if line.startswith('[') and line.endswith(']'): - self.__end_section() - self.__section = line[1:-1] - - self.__section_lines.append(line) - - self.__end_section() - - for edit in self.edits: - if edit['section'] not in self.__sections: - self.__section = edit['section'] - self.__section_lines = ['[%s]' % edit['section']] - self.__end_section() - - def __end_section(self): - self.__sections.add(self.__section) - - for edit in self.edits: - if edit['section'] != self.__section: - continue - - logger.debug('editing %s', self.__section) - extra_lines = [] - - for k, v in sorted(edit.get('replace_key', {}).items()): - logger.debug('replacing %s with %s', k, v) - self.__section_lines = [l for l in self.__section_lines - if not l.startswith(k + '=')] - extra_lines.append('%s=%s' % (k, v)) - - for pattern in sorted(edit.get('delete_matched', [])): - logger.debug('deleting lines matching %s', pattern) - self.__section_lines = [l for l in self.__section_lines - if not fnmatch.fnmatchcase(l, pattern)] - - for pattern in edit.get('comment_out_matched', []): - logger.debug('commenting out lines matching %s', pattern) - for i in range(len(self.__section_lines)): - if fnmatch.fnmatchcase(self.__section_lines[i], pattern): - self.__section_lines[i] = ';' + self.__section_lines[i] - self.__section_lines.insert(i, '; ' + - edit['comment_out_reason']) - - for append in edit.get('append_unique', []): - logger.debug('appending unique line %s', append) - for line in self.__section_lines: - if line == append: - break - else: - extra_lines.append(append) - - i = len(self.__section_lines) - 1 - - while i >= 0: - if self.__section_lines[i]: - # _s_l[i] is the last non-empty line, insert after it - self.__section_lines[i + 1:i + 1] = extra_lines - break - i -= 1 - else: - # no non-empty lines, insert after the section-opening heading - self.__section_lines[1:1] = extra_lines - - self.lines.extend(self.__section_lines) - self.__section_lines = [] - self.__section = None - - def save(self, writer): - for line in self.lines: - print(line, file=writer) - class Launcher: def __init__(self, argv=None): name = os.path.basename(sys.argv[0]) @@ -200,6 +103,9 @@ class Launcher: help='run engine under a debugger') parser.add_argument('--quiet', '-q', default=False, action='store_true', help='silence console logging') + parser.add_argument('--allow-binary-only', default=False, + action='store_true', + help='Allow running binary-only games') parser.add_argument('--version', action='version', version='game-data-packager launcher ' + GAME_PACKAGE_VERSION) parser.add_argument('arguments', nargs=argparse.REMAINDER, @@ -212,39 +118,10 @@ class Launcher: sys.exit(2) self.id = self.args.id - self.keyfile = GLib.KeyFile() - desktop = os.path.join(RUNTIME_BUILT, self.id + '.desktop') - if os.path.exists(desktop): - self.keyfile.load_from_file(desktop, GLib.KeyFileFlags.NONE) - else: - self.keyfile.load_from_data_dirs( - 'applications/%s.desktop' % self.id, - GLib.KeyFileFlags.NONE) - - self.name = self.keyfile.get_string(GLib.KEY_FILE_DESKTOP_GROUP, - GLib.KEY_FILE_DESKTOP_KEY_NAME) - logger.debug('Name: %s', self.name) - GLib.set_application_name(self.name) - - self.icon_name = self.keyfile.get_string(GLib.KEY_FILE_DESKTOP_GROUP, - GLib.KEY_FILE_DESKTOP_KEY_ICON) - logger.debug('Icon: %s', self.icon_name) - - self.expansion_name = self.args.expansion - - try: - override_id = self.keyfile.get_string(GLib.KEY_FILE_DESKTOP_GROUP, - 'X-GameDataPackager-ExpansionFor') - except GLib.Error: - pass - else: - if self.expansion_name is None: - self.expansion_name = self.id - - if self.expansion_name.startswith(override_id + '-'): - self.expansion_name = self.expansion_name[len(override_id) + 1:] + self.name = self.id + self.expansion_name = None - self.id = override_id + self.set_id() self.data = json.load(open('%s/launch-%s.json' % (RUNTIME_BUILT, self.id), encoding='utf-8')) @@ -364,6 +241,9 @@ class Launcher: logger.debug('Arguments: %r', self.argv) + def set_id(self): + pass + def check_required_files(self, base_directories, required_files, warn=True): for f in required_files: @@ -381,6 +261,9 @@ class Launcher: else: return True + def run_error(self, message): + logger.error(message) + def main(self): if self.engines: for e in self.engines: @@ -389,7 +272,7 @@ class Launcher: self.engine = e break else: - Gui.run_error(self, + self.run_error( '\n'.join( [self.load_text('missing-engine.txt', 'Game engine missing, tried:')] + @@ -400,97 +283,37 @@ class Launcher: os.makedirs(self.dot_directory, exist_ok=True) if not self.have_all_data: - Gui.run_error(self, + self.run_error( self.load_text('missing-data.txt', 'Data files missing')) sys.exit(72) # EX_OSFILE - elif self.binary_only and not os.path.exists(self.warning_stamp): + if (self.binary_only and not os.path.exists(self.warning_stamp) and + not self.args.allow_binary_only): self.exit_status = 77 # EX_NOPERM - Gui.run_confirm_binary_only(self, self._confirm_binary_only_cb) + self.run_confirm_binary_only() sys.exit(self.exit_status) + raise AssertionError('not reached') + try: + self.exec_game() + except: + self.run_error(traceback.format_exc()) + sys.exit(self.exit_status) else: - try: - self.exec_game() - except: - Gui.run_error(self, traceback.format_exc()) - sys.exit(self.exit_status) - else: - raise AssertionError('exec_game should never return') + raise AssertionError('exec_game should never return') def flush(self): for f in (sys.stdout, sys.stderr): f.flush() - def _confirm_binary_only_cb(self, gui): - try: - open(self.warning_stamp, 'a').close() - self.exec_game() - except: - gui.show_error(traceback.format_exc()) + def run_confirm_binary_only(self): + # don't do anything, we have no GUI + self.run_error('Not running binary-only game without ' + '--allow-binary-only') def exec_game(self, _unused=None): self.exit_status = 69 # EX_UNAVAILABLE - # Edit before copying, so that we can detect whether this is - # the first run or not - for ini, details in self.data.get('edit_unreal_ini', {}).items(): - assert self.dot_directory is not None - target = os.path.join(self.dot_directory, ini) - encoding = details.get('encoding', 'windows-1252') - - if os.path.exists(target): - first_time = False - try: - reader = open(target, encoding='utf-16') - reader.readline() - except: - reader = open(target, encoding=encoding) - else: - reader.seek(0) - else: - first_time = True - - if os.path.lexists(target): - logger.info('Removing dangling symlink %s', target) - os.remove(target) - - for base in self.base_directories: - source = os.path.join(base, ini) - - if os.path.exists(source): - try: - reader = open(source, encoding='utf-16') - reader.readline() - except: - reader = open(source, encoding=encoding) - else: - reader.seek(0) - break - else: - raise AssertionError('Required file %s not found', ini) - - if first_time: - edits = details.get('once', []) + details.get('always', []) - else: - edits = details.get('always', []) - - logger.debug('%s', edits) - editor = IniEditor(edits) - - with reader: - editor.load(reader) - - d = os.path.dirname(target) - - if d: - logger.info('Creating directory: %s', d) - os.makedirs(d, exist_ok=True) - - with open(target, 'w', encoding=encoding, - newline=details.get('newline', '\n')) as writer: - editor.save(writer) - # Copy before linking, so that the copies will suppress symlink # creation for pattern in self.data.get('copy_into_dot_directory', ()): @@ -632,84 +455,6 @@ class Launcher: else: return placeholder -class Gui: - def __init__(self, launcher): - gi.require_version('Gtk', '3.0') - from gi.repository import Gtk - self.Gtk = Gtk - - self.window = Gtk.Window() - self.window.set_default_size(600, 300) - self.window.connect('delete-event', Gtk.main_quit) - self.window.set_title(launcher.name) - self.window.set_icon_name(launcher.icon_name) - - self.grid = Gtk.Grid(row_spacing=6, column_spacing=6, - margin_top=12, margin_bottom=12, margin_start=12, margin_end=12) - self.window.add(self.grid) - - image = Gtk.Image.new_from_icon_name(launcher.icon_name, - Gtk.IconSize.DIALOG) - image.set_valign(Gtk.Align.START) - self.grid.attach(image, 0, 0, 1, 1) - - self.text_view = Gtk.TextView(editable=False, cursor_visible=False, - hexpand=True, vexpand=True, wrap_mode=Gtk.WrapMode.WORD, - top_margin=6, left_margin=6, right_margin=6, bottom_margin=6) - self.grid.attach(self.text_view, 1, 0, 1, 1) - - subgrid = Gtk.Grid(column_spacing=6, column_homogeneous=True, - halign=Gtk.Align.END) - - cancel_button = Gtk.Button.new_with_label('Cancel') - cancel_button.connect('clicked', Gtk.main_quit) - subgrid.attach(cancel_button, 0, 0, 1, 1) - - self.check_box = Gtk.CheckButton.new_with_label("I'll be careful") - self.check_box.set_hexpand(True) - self.grid.attach(self.check_box, 0, 1, 2, 1) - - self.ok_button = Gtk.Button.new_with_label('Run') - self.ok_button.set_sensitive(False) - subgrid.attach(self.ok_button, 1, 0, 1, 1) - - self.grid.attach(subgrid, 0, 2, 2, 1) - - self.window.show_all() - - @classmethod - def run_error(cls, launcher, message): - try: - gui = cls(launcher) - except: - logger.error('Unable to show error in GUI:\n%s', message) - else: - gui.show_error(message) - gui.Gtk.main() - - def show_error(self, message): - self.text_view.get_buffer().set_text(message) - self.ok_button.set_sensitive(False) - self.window.show_all() - self.check_box.hide() - - @classmethod - def run_confirm_binary_only(cls, launcher, callback): - try: - gui = cls(launcher) - except: - logger.error('Unable to do binary-only confirmation in GUI') - else: - gui.text_view.get_buffer().set_text( - launcher.load_text('confirm-binary-only.txt', - 'Binary-only game, we cannot fix bugs or security ' - 'vulnerabilities!')) - gui.check_box.bind_property('active', gui.ok_button, 'sensitive', - GObject.BindingFlags.SYNC_CREATE) - gui.ok_button.connect('clicked', lambda _: callback(gui)) - gui.window.show_all() - gui.Gtk.main() - if __name__ == '__main__': logging.basicConfig() Launcher().main() diff --git a/runtime/gdp_launcher_version.py b/runtime/gdp_launcher_version.py new file mode 100644 index 0000000..6f81398 --- /dev/null +++ b/runtime/gdp_launcher_version.py @@ -0,0 +1,12 @@ +# encoding=utf-8 + +# This version of this file is only used when run uninstalled. It is replaced +# with a generated version during installation. +from game_data_packager.version import ( + GAME_PACKAGE_RELEASE, + GAME_PACKAGE_VERSION, + ) + +# reassure pyflakes +GAME_PACKAGE_RELEASE +GAME_PACKAGE_VERSION -- Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-games/game-data-packager.git _______________________________________________ Pkg-games-commits mailing list [email protected] http://lists.alioth.debian.org/cgi-bin/mailman/listinfo/pkg-games-commits

