#!/usr/bin/env python
"""
Usage:
deploy-minetest-kit <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).
Options:
--from <built dir> 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())