You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							780 lines
						
					
					
						
							30 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							780 lines
						
					
					
						
							30 KiB
						
					
					
				
								#!/usr/bin/env python
							 | 
						|
								"""
							 | 
						|
								Usage:
							 | 
						|
								install-lmk <project name> [--from <built dir>] [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).
							 | 
						|
								
							 | 
						|
								If the current directory is not ~/minetest-rsync, the suffix "local"
							 | 
						|
								will be used for installed directories and shortcuts instead of rsync to
							 | 
						|
								indicate you are using a downloaded copy (not using a copy obtained via
							 | 
						|
								rsync access to the build server).
							 | 
						|
								
							 | 
						|
								Options:
							 | 
						|
								--from <built dir>    Install from this directory. Defaults to
							 | 
						|
								                      "/opt/minebest/mtkit/minetest" (from home not opt
							 | 
						|
								                      if using Windows).
							 | 
						|
								--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 copy
							 | 
						|
								import json
							 | 
						|
								import os
							 | 
						|
								import platform
							 | 
						|
								import shutil
							 | 
						|
								import stat
							 | 
						|
								import sys
							 | 
						|
								import tempfile
							 | 
						|
								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("/opt", "minebest-rsync", "mtkit", "minetest")
							 | 
						|
								if platform.system() == "Windows":
							 | 
						|
								    INSTALL_SRC = os.path.join(HOME, "minebest-rsync", "mtkit", "minetest")
							 | 
						|
								# ^ Changed later if detected in current dir (in use_if_source).
							 | 
						|
								DETECT_KIT_SUBDIRS = ["minetest", "mtsrc"]  # Use via detect_source
							 | 
						|
								# ^ First entry of DETECT_KIT_SUBDIRS has to be the new INSTALL_SRC!
							 | 
						|
								
							 | 
						|
								VARIANT = "rsync"  # ^ Changed to "local" if not in default INSTALL_SRC
							 | 
						|
								# - "local" copy of linux-minetest-kit.zip is for end users
							 | 
						|
								# - "rsync" copy from /opt/minebest/ is for maintainers
							 | 
						|
								#   (linux-minetest-kit.zip is built by /assemble/util/buildskipwin.sh
							 | 
						|
								#   and then must be manually extracted to /opt/minebest/mtkit)
							 | 
						|
								
							 | 
						|
								MINETEST_KEYWORDS = ("sandbox;world;mining;crafting;blocks;nodes;multiplayer;"
							 | 
						|
								                     "roleplaying;")
							 | 
						|
								
							 | 
						|
								project_metas = {
							 | 
						|
								    'classic': {  # 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 build)",
							 | 
						|
								        'name': "Trolltest (minetest.org)",
							 | 
						|
								        'shortcut_exe_relpaths': [
							 | 
						|
								            os.path.join("bin", "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",
							 | 
						|
								    },
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								arg_project_name = {
							 | 
						|
								    # 'final': "classic",
							 | 
						|
								    'classic': "classic",
							 | 
						|
								    'trolltest': "trolltest",
							 | 
						|
								    # 'troll': "trolltest",
							 | 
						|
								    'finetest': "finetest",
							 | 
						|
								    # 'fine': "finetest",
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								for _name, _meta in project_metas.items():
							 | 
						|
								    _meta['project_name'] = _name
							 | 
						|
								
							 | 
						|
								
							 | 
						|
								def write0(*args):
							 | 
						|
								    sys.stderr.write(*args)
							 | 
						|
								    sys.stderr.flush()
							 | 
						|
								
							 | 
						|
								
							 | 
						|
								def echo0(*args):
							 | 
						|
								    print(*args, file=sys.stderr)
							 | 
						|
								
							 | 
						|
								
							 | 
						|
								def usage():
							 | 
						|
								    echo0(__doc__)
							 | 
						|
								
							 | 
						|
								
							 | 
						|
								
							 | 
						|
								def detect_source(path):
							 | 
						|
								    """Get a built minetest directory inside of path if present.
							 | 
						|
								    It must contain all DETECT_KIT_SUBDIRS for the subdirectory to be
							 | 
						|
								    detected.
							 | 
						|
								
							 | 
						|
								    Returns:
							 | 
						|
								        str: minetest subdirectory. If path does not have
							 | 
						|
								            all DETECT_KIT_SUBDIRS, result is None.
							 | 
						|
								    """
							 | 
						|
								    for sub in DETECT_KIT_SUBDIRS:
							 | 
						|
								        sub_path = os.path.join(path, sub)
							 | 
						|
								        if not os.path.isdir(sub_path):
							 | 
						|
								            return None
							 | 
						|
								    return os.path.join(path, DETECT_KIT_SUBDIRS[0])
							 | 
						|
								
							 | 
						|
								
							 | 
						|
								def use_if_source(path):
							 | 
						|
								    """Use the path as INSTALL_SRC if it contains a minetest install.
							 | 
						|
								    See detect_source for details. A message is shown regarding the
							 | 
						|
								    status.
							 | 
						|
								
							 | 
						|
								    Affects globals:
							 | 
						|
								    - INSTALL_SRC
							 | 
						|
								    - VARIANT
							 | 
						|
								
							 | 
						|
								    Returns:
							 | 
						|
								        bool: True if is a source (even if INSTALL_SRC is already the
							 | 
						|
								            same).
							 | 
						|
								    """
							 | 
						|
								    global INSTALL_SRC
							 | 
						|
								    global VARIANT
							 | 
						|
								    detected_src = detect_source(path)
							 | 
						|
								    if detected_src:
							 | 
						|
								        if detected_src != INSTALL_SRC:
							 | 
						|
								            echo0('Switching from "{}" to local copy:'
							 | 
						|
								                  '\n  "{}"'
							 | 
						|
								                  ''.format(INSTALL_SRC, detected_src))
							 | 
						|
								            INSTALL_SRC = detected_src
							 | 
						|
								            VARIANT = "local"
							 | 
						|
								        else:
							 | 
						|
								            echo0('Using standard source location (same as current dir):'
							 | 
						|
								                  '\n  "{}"'
							 | 
						|
								                  ''.format(INSTALL_SRC))
							 | 
						|
								        return True
							 | 
						|
								    else:
							 | 
						|
								        echo0('Using standard source location'
							 | 
						|
								              ' (since current dir does not have both "mtsrc and "minetest"):'
							 | 
						|
								              '\n  "{}"'
							 | 
						|
								              ''.format(INSTALL_SRC))
							 | 
						|
								    return False
							 | 
						|
								
							 | 
						|
								
							 | 
						|
								def main():
							 | 
						|
								    prefix = "[main] "
							 | 
						|
								    use_if_source(os.getcwd())
							 | 
						|
								    required_bin_suffixes = None
							 | 
						|
								    why_meta = "detected"
							 | 
						|
								    project_meta = detect_project_meta(INSTALL_SRC)
							 | 
						|
								    if project_meta is None:
							 | 
						|
								        why_meta = "undetected"
							 | 
						|
								    key_arg = None
							 | 
						|
								    install_from = None
							 | 
						|
								    project_name = None
							 | 
						|
								    if len(sys.argv) < 2:
							 | 
						|
								        usage()
							 | 
						|
								        if project_meta is None:
							 | 
						|
								            echo0("Error: You must specify one of the names above"
							 | 
						|
								                  " unless well-known executable files can be detected"
							 | 
						|
								                  " to determine what project is being installed.")
							 | 
						|
								            return 1
							 | 
						|
								        else:
							 | 
						|
								            echo0("using detected project: {}".format(
							 | 
						|
								                json.dumps(project_meta, indent=2),
							 | 
						|
								            ))
							 | 
						|
								            # NOTE: ^ shows name_and_variant_fmt with literal "{}" still
							 | 
						|
								            #   (unavoidable without messing with it), so see
							 | 
						|
								            #   "Name={}" further down for that output (Only possible
							 | 
						|
								            #   after `variant` is set).
							 | 
						|
								    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 len(sys.argv) > 1:
							 | 
						|
								        name_arg = sys.argv[1]
							 | 
						|
								        project_name = arg_project_name.get(name_arg)
							 | 
						|
								        if project_name is None:
							 | 
						|
								            raise ValueError(
							 | 
						|
								                "Got %s but expected one from %s"
							 | 
						|
								                % (
							 | 
						|
								                    pformat(name_arg),
							 | 
						|
								                    pformat(list(arg_project_name.keys()))
							 | 
						|
								                )
							 | 
						|
								            )
							 | 
						|
								        if project_meta is not None:
							 | 
						|
								            echo0(prefix+"reverting detected meta due to %s argument."
							 | 
						|
								                  % pformat(name_arg))
							 | 
						|
								            project_meta = None
							 | 
						|
								            why_meta = "cleared by %s argument" % name_arg
							 | 
						|
								    elif project_meta is not None:
							 | 
						|
								        project_name = project_meta.get('project_name')
							 | 
						|
								        # ^ May differ from name. For example, project name for
							 | 
						|
								        #   Final Minetest is "classic".
							 | 
						|
								        echo0(prefix+"detected %s" % project_name)
							 | 
						|
								
							 | 
						|
								    if install_from is None:
							 | 
						|
								        install_from = INSTALL_SRC
							 | 
						|
								
							 | 
						|
								    if project_meta is None:
							 | 
						|
								        if project_name is None:
							 | 
						|
								            raise ValueError(
							 | 
						|
								                "You must either specify one of %"
							 | 
						|
								                " or the source must be a well-known project that can be"
							 | 
						|
								                " detected." % pformat(list(project_metas.keys()))
							 | 
						|
								            )
							 | 
						|
								        project_meta = project_metas[project_name]
							 | 
						|
								        project_meta['required_relpaths'] = []
							 | 
						|
								        if required_bin_suffixes is None:
							 | 
						|
								            required_bin_suffixes = [""]  # only check for * not *server
							 | 
						|
								            # when no options were specified.
							 | 
						|
								            echo0("Warning: No --client or --server option was set, and"
							 | 
						|
								                  " source was %s so only client binary will be verified"
							 | 
						|
								                  " to exist."
							 | 
						|
								                  % why_meta)
							 | 
						|
								        for relpath in project_meta['shortcut_exe_relpaths']:
							 | 
						|
								            for suffix in required_bin_suffixes:
							 | 
						|
								                # for each file such as suffix "" for minetest and
							 | 
						|
								                #   suffix "server" for minetestserver, add to required
							 | 
						|
								                #   files if specified (Instead of if exists, which
							 | 
						|
								                #   only is behavior on detect, though in both cases
							 | 
						|
								                #   they are verified to exist before install, later).
							 | 
						|
								                try_relpath = relpath + suffix
							 | 
						|
								                project_meta['required_relpaths'].append(try_relpath)
							 | 
						|
								        echo0("Generated relpaths: %s" % pformat(project_meta['required_relpaths']))
							 | 
						|
								    else:
							 | 
						|
								        if project_meta.get('required_relpaths') is None:
							 | 
						|
								            raise NotImplementedError(
							 | 
						|
								                "Project %s was detected but required_relpaths was not set."
							 | 
						|
								                % pformat(project_meta.get('project_name'))
							 | 
						|
								            )
							 | 
						|
								        if len(project_meta['required_relpaths']) == 0:
							 | 
						|
								            raise FileNotFoundError(
							 | 
						|
								                "None of the well-known executables for %s could be found: %s"
							 | 
						|
								                % (
							 | 
						|
								                    project_name,
							 | 
						|
								                    pformat(project_meta.get('shortcut_exe_relpaths'))
							 | 
						|
								                )
							 | 
						|
								            )
							 | 
						|
								
							 | 
						|
								    results = install_minetest(
							 | 
						|
								        install_from,
							 | 
						|
								        project_meta,
							 | 
						|
								    )
							 | 
						|
								    error = results.get('error')
							 | 
						|
								    if error is not None:
							 | 
						|
								        echo0("Error: %s" % error)
							 | 
						|
								    return 0
							 | 
						|
								
							 | 
						|
								
							 | 
						|
								def install_minetest(src, project_meta, dst=None, variant_dirname=None,
							 | 
						|
								                     variant=None):
							 | 
						|
								    """Install Minetest
							 | 
						|
								
							 | 
						|
								    Args:
							 | 
						|
								        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.
							 | 
						|
								            - 'required_files' (list): Paths relative to
							 | 
						|
								              src that are required (for ensuring src is intact).
							 | 
						|
								            - There are more required keys for shortcut
							 | 
						|
								              generation (See install_shortcut).
							 | 
						|
								        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).
							 | 
						|
								        variant (str): Append this to the dirname. It also
							 | 
						|
								            affects the shortcut--see "variant" under install_shortcut.
							 | 
						|
								            On desktops environments following the XDG standard,
							 | 
						|
								            also appended to the icon filename so the variant's can
							 | 
						|
								            co-exist with other variants (such as deb and AppImage and
							 | 
						|
								            so on). Defaults to VARIANT (which is set automatically to
							 | 
						|
								            "rsync" or "local" elsewhere).
							 | 
						|
								
							 | 
						|
								    Returns:
							 | 
						|
								        dict: "destination" is where it was installed if at all. See
							 | 
						|
								            "warning" in case there was something incorrect about the
							 | 
						|
								            install.
							 | 
						|
								    """
							 | 
						|
								    if variant is None:
							 | 
						|
								        variant = VARIANT
							 | 
						|
								    project_name = project_meta.get('name')
							 | 
						|
								    project_msg = project_name
							 | 
						|
								    if project_msg is None:
							 | 
						|
								        project_msg = pformat(project_meta)
							 | 
						|
								    del project_name
							 | 
						|
								
							 | 
						|
								    src_files = project_meta.get('required_relpaths')
							 | 
						|
								    if src_files is None:
							 | 
						|
								        usage()
							 | 
						|
								        error = ("There are no specified source files for %s"
							 | 
						|
								                 " so whether it is intact can't be checked."
							 | 
						|
								                 "" % pformat(project_msg))
							 | 
						|
								        raise NotImplementedError(error)
							 | 
						|
								
							 | 
						|
								    missing_files = []
							 | 
						|
								    for src_file in src_files:
							 | 
						|
								        if not os.path.isfile(os.path.join(src, src_file)):
							 | 
						|
								            missing_files.append(src_file)
							 | 
						|
								
							 | 
						|
								    if len(missing_files) > 0:
							 | 
						|
								        error = ("Error: The following files are required to be compiled"
							 | 
						|
								                 " for {} before install but are missing: {}"
							 | 
						|
								                 "".format(project_msg, missing_files))
							 | 
						|
								        return {
							 | 
						|
								            'error': error,
							 | 
						|
								        }
							 | 
						|
								
							 | 
						|
								    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 %s to %s...'
							 | 
						|
								               % (pformat(project_msg), pformat(dst)))
							 | 
						|
								        shutil.copytree(src, dst)
							 | 
						|
								        version_path = project_meta.get('version_path')
							 | 
						|
								        if version_path and os.path.isfile(version_path):
							 | 
						|
								            version_name = os.path.basename(version_path)
							 | 
						|
								            shutil.copy(
							 | 
						|
								                version_path,
							 | 
						|
								                os.path.join(dst, version_name),
							 | 
						|
								            )
							 | 
						|
								        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 generate_caption(project_meta, variant):
							 | 
						|
								    """Generate the icon caption.
							 | 
						|
								
							 | 
						|
								    Args:
							 | 
						|
								        project_meta (dict): The dict containing 'name' and
							 | 
						|
								            'name_and_variant_fmt' where 'name' is like
							 | 
						|
								            "Trolltest (minetest.org)", and 'name_and_variant_fmt' is
							 | 
						|
								            like 'Trolltest ({}) (minetest.org build)'.
							 | 
						|
								    """
							 | 
						|
								    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
							 | 
						|
								    return Name
							 | 
						|
								
							 | 
						|
								
							 | 
						|
								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 = generate_caption(project_meta, variant)
							 | 
						|
								    echo0("Name={}".format(Name))
							 | 
						|
								    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 = "{}.{}.desktop".format(
							 | 
						|
								            project_meta['shortcut_name_noext'],
							 | 
						|
								            variant,
							 | 
						|
								        )
							 | 
						|
								        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 detect_project_meta(mt_share_path):
							 | 
						|
								    """Detect the project info from a source *or* destination.
							 | 
						|
								
							 | 
						|
								    Only first entry will be used & get "server" added.
							 | 
						|
								
							 | 
						|
								    Args:
							 | 
						|
								        mt_share_path (string): The path containing
							 | 
						|
								            project_meta['shortcut_exe_relpaths']
							 | 
						|
								            filename(s).
							 | 
						|
								
							 | 
						|
								    Returns:
							 | 
						|
								        A copy of the matching project_meta (from project_metas) with
							 | 
						|
								        an added entry 'required_relpaths'.
							 | 
						|
								        - *detecting errors*: If the list length is 0 or the key is not
							 | 
						|
								          present, no required files were found and the install source
							 | 
						|
								          is not understandable (There is no known binary such as for
							 | 
						|
								          different code to make a shortcut).
							 | 
						|
								        - If relpath+"server" exists in the case of the first entry in
							 | 
						|
								          'shortcut_exe_relpaths', that server binary will be added to
							 | 
						|
								          the 'required_relpaths' list whether or not the filename
							 | 
						|
								          without "server" in the name exists.
							 | 
						|
								    """
							 | 
						|
								    prefix = "[detect_project_meta] "
							 | 
						|
								    matches = []
							 | 
						|
								    for mode, meta in project_metas.items():
							 | 
						|
								        new_meta = copy.deepcopy(meta)
							 | 
						|
								        if 'required_relpaths' not in new_meta:
							 | 
						|
								            new_meta['required_relpaths'] = \
							 | 
						|
								                meta['shortcut_exe_relpaths'].copy()
							 | 
						|
								        for sub in meta.get('shortcut_exe_relpaths'):
							 | 
						|
								            sub_path = os.path.join(mt_share_path, sub)
							 | 
						|
								            try_extra_rel = sub+"server"
							 | 
						|
								            try_extra_exe = os.path.join(mt_share_path, try_extra_rel)
							 | 
						|
								            found_any = False
							 | 
						|
								            if os.path.isfile(try_extra_exe):
							 | 
						|
								                found_any = True
							 | 
						|
								                if try_extra_rel not in new_meta['required_relpaths']:
							 | 
						|
								                    new_meta['required_relpaths'].append(try_extra_exe)
							 | 
						|
								                    # For example, bin/minetestserver is required
							 | 
						|
								                    #   if in --server install mode (and this
							 | 
						|
								                    #   function detects that mode)
							 | 
						|
								                    #   but there is no shortcut to it in the GUI.
							 | 
						|
								            else:
							 | 
						|
								                echo0(prefix+"There is no %s" % try_extra_exe)
							 | 
						|
								            if os.path.isfile(sub_path):
							 | 
						|
								                found_any = True
							 | 
						|
								            else:
							 | 
						|
								                echo0(prefix+"There is no %s" % sub_path)
							 | 
						|
								                new_meta['required_relpaths'].remove(sub)
							 | 
						|
								                # For example, remove "minetest" if not present (but
							 | 
						|
								                #   install can still proceed if "minetestserver" was
							 | 
						|
								                #   added to the required list).
							 | 
						|
								            mt_share_path = os.path.realpath(mt_share_path)
							 | 
						|
								            version_paths = [
							 | 
						|
								                os.path.join(mt_share_path, "release.txt"),
							 | 
						|
								                os.path.join(os.path.dirname(mt_share_path), "release.txt"),
							 | 
						|
								            ]
							 | 
						|
								            for version_path in version_paths:
							 | 
						|
								                if os.path.isfile(version_path):
							 | 
						|
								                    new_meta['version_path'] = version_path
							 | 
						|
								                    version = None
							 | 
						|
								                    with open(version_path, 'r') as stream:
							 | 
						|
								                        for rawL in stream:
							 | 
						|
								                            if version is None:
							 | 
						|
								                                version = rawL.strip()
							 | 
						|
								                        # ^ Use `for` to avoid Exception on empty file.
							 | 
						|
								                    if version is None:
							 | 
						|
								                        echo0('Warning: "{}" is empty!'.format(version_path))
							 | 
						|
								                        continue
							 | 
						|
								                    elif not version:
							 | 
						|
								                        echo0('Warning: "{}" had a blank line not version'
							 | 
						|
								                              ''.format(version_path))
							 | 
						|
								                        version = None
							 | 
						|
								                        continue
							 | 
						|
								                    if version:
							 | 
						|
								                        new_meta['version'] = version
							 | 
						|
								                        break
							 | 
						|
								            if found_any:
							 | 
						|
								                matches.append(new_meta)
							 | 
						|
								                break  # only first entry will be used & get "server" added
							 | 
						|
								    if len(matches) == 1:
							 | 
						|
								        echo0(prefix+"found source files: %s" % pformat(matches[0]['required_relpaths']))
							 | 
						|
								        return matches[0]
							 | 
						|
								    return None
							 | 
						|
								
							 | 
						|
								
							 | 
						|
								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())
							 | 
						|
								
							 |