From 791f73d3cbdc0da516c6e4f4c8c22b1875b356d2 Mon Sep 17 00:00:00 2001 From: poikilos <7557867+poikilos@users.noreply.github.com> Date: Tue, 1 Aug 2023 01:11:25 -0400 Subject: [PATCH] Add a script to install a built linux-minetest-kit binary and a shortcut to it. --- utilities/install-minetest-kit | 468 +++++++++++++++++++++++++++++++++ 1 file changed, 468 insertions(+) create mode 100755 utilities/install-minetest-kit diff --git a/utilities/install-minetest-kit b/utilities/install-minetest-kit new file mode 100755 index 0000000..8b56b7e --- /dev/null +++ b/utilities/install-minetest-kit @@ -0,0 +1,468 @@ +#!/usr/bin/env python +""" +Usage: +deploy-minetest-kit [--from ] [options] + +The available project names are: +classic (or final) to install ~/minetest-rsync, +finetest (or fine) to install ~/finetest-rsync + (for the game that ripped off Multicraft.org's name), or +trolltest (or troll) to install ~/trolltest-rsync (based on MT5). + +Options: +--from Install from this directory. Defaults to + "$HOME/linux-minetest-kit-rsync/mtkit/minetest". +--server Require a binary ending with "server" to be + present in built dir. Defaults to False. +--client Require a binary ending with "server" to be + present in built dir. Defaults to True, but False + if --server is used without --client. +""" +from __future__ import print_function +import sys +import platform +import os +import shutil +import tempfile +import stat + +if platform.system() == "Windows": + HOME = os.environ['USERPROFILE'] + SHORTCUTS_DIR = os.path.join(HOME, "Desktop") +elif platform.system() == "Darwin": + HOME = os.environ['HOME'] + SHORTCUTS_DIR = os.path.join(HOME, "Desktop") +else: + HOME = os.environ['HOME'] + SHORTCUTS_DIR = os.path.join(HOME, ".local", "share", "applications") + +if sys.version_info.major < 3: + FileNotFoundError = IOError + ModuleNotFoundError = ImportError + + +INSTALL_SRC = os.path.join(HOME, "linux-minetest-kit-rsync", "mtkit", "minetest") +VERSION = "rsync" + +mode_of_path = { + os.path.join("bin", "finetest"): { + 'name': "finetest", + 'appender': "", + 'executable': "finetest", + }, + os.path.join("bin", "finetestserver"): { + 'name': "finetest", + 'appender': "server", + 'executable': "finetestserver", + }, + # indeterminate if bin/minetest: same exe for classic & trolltest +} + +KEYWORDS = ("sandbox;world;mining;crafting;blocks;nodes;multiplayer;" + "roleplaying;minetest;") + +project_metas = { + 'classic': { + 'name_fmt': "Final Minetest ({})", + 'GenericName': "Final Minetest", + 'exe_relpath': os.path.join("bin", "minetest"), + 'platform_icon_relpath': { + 'Linux': os.path.join("misc", "minetest.svg"), + 'Darwin': os.path.join("misc", "minetest-icon.icns"), + 'Windows': os.path.join("misc", "minetest-icon-24x24.png"), + }, + 'shortcut_relpath': os.path.join("misc", "net.minetest.minetest.desktop"), + 'shortcut_name_noext': "org.minetest.minetest", + }, + 'finetest': { + 'name_fmt': "Finetest ({})", + 'GenericName': "Finetest", + 'exe_relpath': os.path.join("bin", "finetest"), + 'platform_icon_relpath': { + 'Linux': os.path.join("misc", "multicraft-xorg-icon-128.png"), + 'Darwin': os.path.join("misc", "minetest-icon.icns"), + 'Windows': os.path.join("misc", "multicraft-xorg-icon-128.png"), + }, + 'shortcut_relpath': os.path.join("misc", "net.minetest.minetest.desktop"), + 'shortcut_name_noext': "org.minetest.finetest", + }, + 'trolltest': { + 'name_fmt': "Trolltest (minetest.org {})", + 'GenericName': "Trolltest", + 'platform_icon_relpath': { + 'Linux': os.path.join("misc", "minetest.svg"), + 'Darwin': os.path.join("misc", "minetest-icon.icns"), + 'Windows': os.path.join("misc", "minetest-icon-24x24.png"), + }, + 'shortcut_relpath': os.path.join("misc", "net.minetest.minetest.desktop"), + 'shortcut_name_noext': "org.minetest.trolltest", + }, +} + + +def write0(*args): + sys.stderr.write(*args) + sys.stderr.flush() + +def echo0(*args): + print(*args, file=sys.stderr) + + +def usage(): + echo0(__doc__) + + +def main(): + appenders = None + # project_info = detect_project_info(INSTALL_SRC) + key_arg = None + install_from = None + if len(sys.argv) < 2: + usage() + # if project_info is None: + echo0("Error: You must specify one of the names above.") + return 1 + # TODO: maybe detect it: + # else: + # echo0("using detected project: {}".format(project_info)) + elif len(sys.argv) == 2: + pass # 1st arg (arg [1]) is always handled further down + else: + for argi in range(2, len(sys.argv)): + arg = sys.argv[argi] + if key_arg is not None: + if arg.startswith("--"): + usage() + echo0("Error: {} must be followed by a value but got {}." + "".format(key_arg, arg)) + return 1 + if key_arg == "--from": + install_from = arg + else: + usage() + echo0("Error: unknown argument {}".format(key_arg)) + return 1 + elif arg == "--server": + if appenders is None: + appenders = ["server"] + else: + appenders.append("server") + elif arg == "--client": + if appenders is None: + appenders = [""] + else: + appenders.append("") + elif arg == "--from": + key_arg = arg + else: + usage() + echo0('Error: The 2nd argument must be "server" or left out') + return 1 + if key_arg is not None: + usage() + echo0("Error: {} must be followed by a value." + "".format(key_arg)) + return 1 + if appenders is None: + appenders = [""] # Always require at least the plain exe name. + if install_from is None: + install_from = INSTALL_SRC + project_name = sys.argv[1] + results = install_minetest( + project_name, + install_from, + appenders=appenders, + ) + return 0 + + +def install_minetest(project_name, src, dst=None, versioned_dirname=None, + appenders=[""]): + """Install Minetest & generate a shortcut based on the destination. + + The metadata written to shortcut must be the installed metadata! + - except the shortcut path itself: + - shortcut_src generated below based on OS given relative path + - shortcut_dst generated based on OS and shortcut_name_noext + + Args: + project_name (string): Must be classic, finetest, or trolltest. + src (string): The location of the minetest install source to + copy. + dst (Optional[string]): Install here. If None, it will become + the default. Defaults to versioned_dirname under C:\games on + Windows, otherwise under HOME. + versioned_dirname (Optional[string]): Set the install directory + name (ignored if dst is set). If None, it will become the + default. Defaults to project_name + "-" + VERSION (such as + minetest-rsync). + appenders (list): Install client if [""], or server if + ["server"]. Include both to require that both were built. + + Returns: + dict: "destination" is where it was installed if at all. See + "warning" in case there was something incorrect about the + install. + """ + arg = project_name + project_name = arg_to_mode(arg) + if project_name is None: + usage() + echo0("{} is not a valid project name.".format(arg)) + return 1 + src_files = expected_src_files(src, project_name, appenders) + if src_files is None: + usage() + raise NotImplementedError( + "There are no source files for {}" + "".format(project_name) + ) + return 1 + + missing_files = [] + for src_file in src_files: + if not os.path.isfile(src_file): + missing_files.append(src_file) + + if len(missing_files) > 0: + echo0("Error: The following files are required to be compiled" + " {} before install but are not present: {}" + "".format(project_name, missing_files)) + return 1 + + versioned_dirname = project_name + "-" + VERSION + if dst is None: + if platform.system() == "Windows": + GAMES = "C:\\games" + if not os.path.isdir(GAMES): + os.mkdir(GAMES) + dst = os.path.join(GAMES, versioned_dirname) + else: + dst = os.path.join(HOME, versioned_dirname) + warning = None + if not os.path.isdir(dst): + write0('Installing {} to "{}"...'.format(project_name, dst)) + shutil.copytree(src, dst) + echo0("Done") + result_path = dst + else: + # Leave result_path as None + warning = 'Skipping installed "{}".'.format(dst) + echo0('WARNING: {}'.format(warning)) + + + project_meta = project_metas[project_name] + + Name = project_meta['name_fmt'].format(VERSION) + GenericName = project_meta['GenericName'] + Exec = os.path.join(dst, project_meta['exe_relpath']) + icon_relpath = project_meta['platform_icon_relpath'].get(platform.system()) + if icon_relpath is None: + raise NotImplementedError( + "There is no platform icon for {}.".format(platform.system()) + ) + Icon = os.path.join(dst, icon_relpath) + shortcut_src = os.path.join(src, project_meta['shortcut_relpath']) + changes = { + "Name": Name, + "GenericName": GenericName, + "Exec": Exec, + "Icon": Icon, + "Keywords": KEYWORDS, + } + + if platform.system() == "Linux": + shortcut_src = os.path.join(dst, project_meta['shortcut_relpath']) + shortcut_name = project_meta['shortcut_name_noext'] + ".desktop" + shortcut_dst = os.path.join(SHORTCUTS_DIR, shortcut_name) + if not os.path.isdir(SHORTCUTS_DIR): + os.makedirs(SHORTCUTS_DIR) # default mode is 511 + write0('Installing icon to "{}"...'.format(shortcut_dst)) + rewrite_conf( + shortcut_src, + shortcut_dst, + changes=changes, + ) + echo0("OK") + elif platform.system() == "Darwin": + shortcut_name = Name + ".command" + shortcut_dst = os.path.join(SHORTCUTS_DIR, shortcut_name) + with open(shortcut_dst) as stream: + stream.write('"%s"\n' % Exec) + # ^ Run the game & close Command Prompt immediately. + # ^ First arg is Command Prompt title, so leave it blank. + st = os.stat(shortcut_dst) + os.chmod(shortcut_dst, st.st_mode | stat.S_IXUSR) + # ^ same as stat.S_IEXEC: "Unix V7 synonym for S_IXUSR." + elif platform.system() == "Windows": + shortcut_name = Name + ".bat" + shortcut_dst = os.path.join(SHORTCUTS_DIR, shortcut_name) + with open(shortcut_dst) as stream: + stream.write('start "" "%s"\n' % Exec) + # ^ Run the game & close Command Prompt immediately. + # ^ First arg is Command Prompt title, so leave it blank. + else: + echo0("Icon install isn't implemented for {}." + "".format(platform.system())) + return { + "warning": warning, # may be None + "destination": dst, + } + + +# def get_missing_subs(mt_share_path, subs): +# """Get a list of any missing files for a source *or* destination. +# """ + + +def arg_to_mode(arg): + if arg == "final": + return "classic" + elif arg == "classic": + return "classic" + elif arg == "trolltest": + return "trolltest" + elif arg == "troll": + return "trolltest" + elif arg == "finetest": + return "finetest" + elif arg == "fine": + return "finetest" + return None + + +def detect_project_info(mt_share_path): + """Detect the project name from a source *or* destination. + """ + for sub, project_info in mode_of_path.items(): + sub_path = os.path.join(mt_share_path, sub) + if os.path.isfile(sub_path): + return project_info + return None + + +def expected_src_files(src, project_name, appenders=[""]): + """Get full paths of required files. + + Args: + appenders (list): Usually either [""] or ["server"], but if + the list contains both, both will be required + + Returns: + list: Files that are required for install to continue. + """ + if ((appenders is None) or (len(appenders) < 1) + or (not isinstance(appenders, list))): + appenders = [""] + echo0('Warning: reverting to appenders=[""] since {}' + ''.format(appenders)) + + src_files = None + + for appender in appenders: + src_file = None + if project_name == "classic": + src_file = os.path.join(src, "bin", "minetest"+appender) + elif project_name == "trolltest": + src_file = os.path.join(src, "bin", "minetest"+appender) + elif project_name == "finetest": + src_file = os.path.join(src, "bin", "finetest"+appender) + else: + break + if src_file is not None: + if src_files is None: + src_files = [] + src_files.append(src_file) + # else caller must say project_name is not valid. + return src_files + + +def rewrite_conf(src, dst, changes={}): + """Install a conf such as an XDG desktop shortcut with changes. + + Args: + src (string): The conf file to read. + dst (string): The conf file to write or overwrite. + changes (dict): A set of values to change by name. For any value + that is None, the line will be removed! + """ + # This function is redefined further down in the case of Python 2. + fd, path = tempfile.mkstemp() + try: + with os.fdopen(fd, 'wb') as tmp: + # ^ ensure still exists when moving + write0("Generating temporary icon %s..." % path) + # NOTE: tmp.name is just some number (int)! + with open(src, "rb") as stream: + for rawL in stream: + signI = rawL.find(b'=') + # commentI = rawl.find(b'#') + if rawL.strip().startswith(b"#"): + tmp.write(rawL) + continue + if rawL.strip().startswith(b"["): + tmp.write(rawL) + continue + if signI < 0: + tmp.write(rawL) + continue + key_bytes = rawL[:signI].strip() + key = key_bytes.decode("utf-8") + value = changes.get(key) + if key not in changes: + # The value wasn't changed so write it as-is + # echo0("%s not in %s" % (key, changes)) + tmp.write(rawL) + continue + if value is None: + echo0("%s was excluded from the icon" % key) + continue + line = "%s=%s\n" % (key, value) + tmp.write(line.encode("utf-8")) + shutil.copy(path, dst) + finally: + write0("removing tmp file...") + os.remove(path) + + +if sys.version_info.major < 3: + # Python 2 (strings are bytes) + def rewrite_conf(src, dst, changes={}): + """Install a conf such as an XDG desktop shortcut with changes. + """ + fd, path = tempfile.mkstemp() + try: + with os.fdopen(fd, 'wb') as tmp: + write0("Generating temporary icon %s..." % path) + with open(src, "rb") as stream: + for rawL in stream: + signI = rawL.find('=') + # commentI = rawl.find('#') + if rawL.strip().startswith("#"): + tmp.write(rawL) + continue + if rawL.strip().startswith("["): + tmp.write(rawL) + continue + if signI < 0: + tmp.write(rawL) + continue + key_bytes = rawL[:signI].strip() + key = key_bytes + value = changes.get(key) + if key not in changes: + # The value wasn't changed so write it as-is + tmp.write(rawL) + continue + if value is None: + echo0("%s was excluded from the icon" % key) + continue + line = "%s=%s\n" % (key, value) + tmp.write(line) + shutil.copy(path, dst) + finally: + write0("removing tmp file...") + os.remove(path) + +if __name__ == "__main__": + sys.exit(main())