#!/usr/bin/env python """ Usage: deploy-minetest-kit [--from ] [options] The available project names are: classic (or final or minetest) 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 import copy from pprint import pformat 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") VARIANT = "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 } MINETEST_KEYWORDS = ("sandbox;world;mining;crafting;blocks;nodes;multiplayer;" "roleplaying;") project_metas = { 'minetest': { # minetest is the project name (in mtsrc/newline dir) 'shortcut': { 'GenericName': "Final Minetest", 'Keywords': MINETEST_KEYWORDS, }, 'dirname': "minetest", 'name_and_variant_fmt': "Final Minetest ({})", 'name': "Final Minetest", 'shortcut_exe_relpaths': [ 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': { 'shortcut': { 'GenericName': "Finetest", 'Keywords': MINETEST_KEYWORDS+"minetest;", }, 'dirname': "finetest", 'name_and_variant_fmt': "Finetest ({})", 'name': "Finetest", 'shortcut_exe_relpaths': [ # os.path.join("bin", "multicraft"), 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': { 'shortcut': { 'GenericName': "Trolltest", 'Keywords': MINETEST_KEYWORDS+"minetest;", }, 'dirname': "trolltest", 'name_and_variant_fmt': "Trolltest ({}) (minetest.org)", 'name': "Trolltest (minetest.org)", 'shortcut_exe_relpaths': [ 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.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(): required_bin_suffixes = 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 required_bin_suffixes is None: required_bin_suffixes = ["server"] else: required_bin_suffixes.append("server") elif arg == "--client": if required_bin_suffixes is None: required_bin_suffixes = [""] else: required_bin_suffixes.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 required_bin_suffixes is None: required_bin_suffixes = [""] # 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, required_bin_suffixes=required_bin_suffixes, ) error = results.get('error') if error is not None: echo0("Error: %s" % error) return 0 def install_minetest(project_name, src, dst=None, variant_dirname=None, required_bin_suffixes=[""], variant=VARIANT): """Install Minetest Requires globals: project_meta (dict[string]): The information necessary to install the program. It must have the keys: - 'dirname' (string): The directory under the OS program files. - There are more required keys for shortcut generation (See install_shortcut). Args: project_name (string): Must be minetest, 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 variant_dirname under C:\games on Windows, otherwise under HOME. variant_dirname (Optional[string]): Set the install directory name (ignored if dst is set). If None, it will become the default. Defaults to project_name + "-" + VARIANT (such as minetest-rsync). If VARIANT is blank or None, the variant_dirname will become the same as the dirname (such as minetest). required_bin_suffixes (list): Install client if [""], or server if ["server"]. Include both to require that both are in os.path.join(src, "bin"). variant (string): Append this to the dirname. It also affects the shortcut--see "variant" under install_shortcut. 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_project_name(arg) if project_name is None: usage() return { 'error': "{} is not a valid project name.".format(arg), } src_files = expected_src_files(src, project_name, required_bin_suffixes) if src_files is None: usage() error = "There are no source files for {}".format(project_name) raise NotImplementedError(error) 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: error = ("Error: The following files are required to be compiled" " {} before install but are not present: {}" "".format(project_name, missing_files)) return { 'error': error, } project_meta = project_metas[project_name] dirname = project_meta['dirname'] variant_dirname = dirname if (variant is not None) and (len(variant.strip()) > 0): variant_dirname += "-" + variant else: variant = None 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, variant_dirname) else: dst = os.path.join(HOME, variant_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)) for Exec_relpath in project_meta['shortcut_exe_relpaths']: Exec = os.path.join(dst, Exec_relpath) sc_results = install_shortcut(Exec, dst, project_meta, variant) sc_warning = sc_results.get('warning') if sc_warning is not None: if warning is not None: warning += "; " + sc_warning else: warning = sc_warning return { 'dst': dst, 'warning': warning, } def install_shortcut(Exec, dst, project_meta, variant): """Install a shortcut to any program on any understood platform. - sc_template_path is determined based on dst and shortcut_relpath - sc_installed_path (path) is determined from OS and shortcut_name_noext (and variant if not None). - sc_template_path is read, Exec string is filled based on dst (the selected destination where the program is installed) then the resulting shortcut is saved to sc_installed_path (only after temp file is complete). Args: Exec (string): The executable path where the shortcut should point. dst (string): The directory path where the program is installed. project_meta (dict): All metadata describing the program. For this method, it must have the keys: - 'name': The entire name (except variant) that should be displayed as the shortcut's caption. - 'name_and_variant_fmt': Should either be not present or contain the name and the placeholder "{}" where the variant should go. If not present, " " and variant will be added to the end of Name. - 'shortcut' (dict): contains: - 'GenericName' (Optional[string]): A simplified name for the program. If None, the GenericName line will be removed from the shortcut. This option is only for GNU/Linux systems or other systems using XDG. - 'Keywords' (Optional[string]): If None, Keywords line will be removed from the shortcut. This option is only for GNU/Linux systems or other systems using XDG. - 'shortcut_relpath': The location of an existing shortcut file to use and modify. - 'platform_icon_relpath' (dict[string]): A dict where the key is platform.system() (Must have at least 'Linux', 'Windows', *AND* 'Darwin') and the value is the relative path from dst to the icon image file. variant (string): The special string to put in parenthesis after the name to denote what kind of package or source was used to obtain the program, such as "rsync" if a local custom build, or more commonly "git", "deb", etc. If it is an official binary archive, set this to "release". However, if the package type (such as deb) is native to your distro, set this to None to indicate it is the package supported for your distro. - Name is constructed using project_meta['name_and_variant_fmt'] if present, otherwise Name will be project_meta['name] + " (" + 'variant' + ")". If variant is None, name is project_meta['name']. Raises: FileNotFoundError: If src does not exist. """ warning = None Name = project_meta['name'] if variant is not None: name_and_variant_fmt = project_meta.get('name_and_variant_fmt') if name_and_variant_fmt is not None: Name = name_and_variant_fmt.format(variant) else: Name += " (" + project_meta['variant'] + ")" # raise if None platform_icon_relpath = project_meta.get('platform_icon_relpath') icon_relpath = None if platform_icon_relpath is not None: icon_relpath = 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_meta = copy.deepcopy(project_meta.get('shortcut')) shortcut_meta['Name'] = Name shortcut_meta['Exec'] = Exec shortcut_meta['Icon'] = Icon # ^ rewrite_conf *removes* any lines where value is None if platform.system() == "Linux": sc_template_path = os.path.join(dst, project_meta['shortcut_relpath']) shortcut_name = project_meta['shortcut_name_noext'] + ".desktop" sc_installed_path = 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(sc_installed_path)) rewrite_conf( sc_template_path, sc_installed_path, changes=shortcut_meta, ) echo0("OK") elif platform.system() == "Darwin": shortcut_name = Name + ".command" sc_installed_path = os.path.join(SHORTCUTS_DIR, shortcut_name) with open(sc_installed_path) 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(sc_installed_path) os.chmod(sc_installed_path, 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" sc_installed_path = os.path.join(SHORTCUTS_DIR, shortcut_name) with open(sc_installed_path) 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: warning = ("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_project_name(arg): if arg == "final": return "minetest" elif arg == "classic": return "minetest" elif arg == "minetest": return "minetest" 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, required_bin_suffixes=[""]): """Get full paths of required files. Args: required_bin_suffixes (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 ((required_bin_suffixes is None) or (len(required_bin_suffixes) < 1) or (not isinstance(required_bin_suffixes, list))): required_bin_suffixes = [""] echo0('Warning: reverting to required_bin_suffixes=[""] since {}' ''.format(required_bin_suffixes)) src_files = None for appender in required_bin_suffixes: src_file = None if project_name == "minetest": 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())