#!/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())