This is an experimental copy for testing Poikilos' issue mirroring system. Note that Gitea's migration tool can import issues, but the "Issues" checkbox is disabled when "This repository will be a mirror" is enabled (it is for this repo).
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.
 
 
 
 
 
 

651 lines
25 KiB

#!/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"
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 main():
prefix = "[main] "
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(project_meta))
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=VARIANT):
"""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 (string): 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).
Returns:
dict: "destination" is where it was installed if at all. See
"warning" in case there was something incorrect about the
install.
"""
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)
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 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).
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())