poikilos
1 year ago
1 changed files with 468 additions and 0 deletions
@ -0,0 +1,468 @@ |
|||||
|
#!/usr/bin/env python |
||||
|
""" |
||||
|
Usage: |
||||
|
deploy-minetest-kit <project name> [--from <built dir>] [options] |
||||
|
|
||||
|
The available project names are: |
||||
|
classic (or final) to install ~/minetest-rsync, |
||||
|
finetest (or fine) to install ~/finetest-rsync |
||||
|
(for the game that ripped off Multicraft.org's name), or |
||||
|
trolltest (or troll) to install ~/trolltest-rsync (based on MT5). |
||||
|
|
||||
|
Options: |
||||
|
--from <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 |
||||
|
|
||||
|
if platform.system() == "Windows": |
||||
|
HOME = os.environ['USERPROFILE'] |
||||
|
SHORTCUTS_DIR = os.path.join(HOME, "Desktop") |
||||
|
elif platform.system() == "Darwin": |
||||
|
HOME = os.environ['HOME'] |
||||
|
SHORTCUTS_DIR = os.path.join(HOME, "Desktop") |
||||
|
else: |
||||
|
HOME = os.environ['HOME'] |
||||
|
SHORTCUTS_DIR = os.path.join(HOME, ".local", "share", "applications") |
||||
|
|
||||
|
if sys.version_info.major < 3: |
||||
|
FileNotFoundError = IOError |
||||
|
ModuleNotFoundError = ImportError |
||||
|
|
||||
|
|
||||
|
INSTALL_SRC = os.path.join(HOME, "linux-minetest-kit-rsync", "mtkit", "minetest") |
||||
|
VERSION = "rsync" |
||||
|
|
||||
|
mode_of_path = { |
||||
|
os.path.join("bin", "finetest"): { |
||||
|
'name': "finetest", |
||||
|
'appender': "", |
||||
|
'executable': "finetest", |
||||
|
}, |
||||
|
os.path.join("bin", "finetestserver"): { |
||||
|
'name': "finetest", |
||||
|
'appender': "server", |
||||
|
'executable': "finetestserver", |
||||
|
}, |
||||
|
# indeterminate if bin/minetest: same exe for classic & trolltest |
||||
|
} |
||||
|
|
||||
|
KEYWORDS = ("sandbox;world;mining;crafting;blocks;nodes;multiplayer;" |
||||
|
"roleplaying;minetest;") |
||||
|
|
||||
|
project_metas = { |
||||
|
'classic': { |
||||
|
'name_fmt': "Final Minetest ({})", |
||||
|
'GenericName': "Final Minetest", |
||||
|
'exe_relpath': os.path.join("bin", "minetest"), |
||||
|
'platform_icon_relpath': { |
||||
|
'Linux': os.path.join("misc", "minetest.svg"), |
||||
|
'Darwin': os.path.join("misc", "minetest-icon.icns"), |
||||
|
'Windows': os.path.join("misc", "minetest-icon-24x24.png"), |
||||
|
}, |
||||
|
'shortcut_relpath': os.path.join("misc", "net.minetest.minetest.desktop"), |
||||
|
'shortcut_name_noext': "org.minetest.minetest", |
||||
|
}, |
||||
|
'finetest': { |
||||
|
'name_fmt': "Finetest ({})", |
||||
|
'GenericName': "Finetest", |
||||
|
'exe_relpath': os.path.join("bin", "finetest"), |
||||
|
'platform_icon_relpath': { |
||||
|
'Linux': os.path.join("misc", "multicraft-xorg-icon-128.png"), |
||||
|
'Darwin': os.path.join("misc", "minetest-icon.icns"), |
||||
|
'Windows': os.path.join("misc", "multicraft-xorg-icon-128.png"), |
||||
|
}, |
||||
|
'shortcut_relpath': os.path.join("misc", "net.minetest.minetest.desktop"), |
||||
|
'shortcut_name_noext': "org.minetest.finetest", |
||||
|
}, |
||||
|
'trolltest': { |
||||
|
'name_fmt': "Trolltest (minetest.org {})", |
||||
|
'GenericName': "Trolltest", |
||||
|
'platform_icon_relpath': { |
||||
|
'Linux': os.path.join("misc", "minetest.svg"), |
||||
|
'Darwin': os.path.join("misc", "minetest-icon.icns"), |
||||
|
'Windows': os.path.join("misc", "minetest-icon-24x24.png"), |
||||
|
}, |
||||
|
'shortcut_relpath': os.path.join("misc", "net.minetest.minetest.desktop"), |
||||
|
'shortcut_name_noext': "org.minetest.trolltest", |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
|
||||
|
def write0(*args): |
||||
|
sys.stderr.write(*args) |
||||
|
sys.stderr.flush() |
||||
|
|
||||
|
def echo0(*args): |
||||
|
print(*args, file=sys.stderr) |
||||
|
|
||||
|
|
||||
|
def usage(): |
||||
|
echo0(__doc__) |
||||
|
|
||||
|
|
||||
|
def main(): |
||||
|
appenders = None |
||||
|
# project_info = detect_project_info(INSTALL_SRC) |
||||
|
key_arg = None |
||||
|
install_from = None |
||||
|
if len(sys.argv) < 2: |
||||
|
usage() |
||||
|
# if project_info is None: |
||||
|
echo0("Error: You must specify one of the names above.") |
||||
|
return 1 |
||||
|
# TODO: maybe detect it: |
||||
|
# else: |
||||
|
# echo0("using detected project: {}".format(project_info)) |
||||
|
elif len(sys.argv) == 2: |
||||
|
pass # 1st arg (arg [1]) is always handled further down |
||||
|
else: |
||||
|
for argi in range(2, len(sys.argv)): |
||||
|
arg = sys.argv[argi] |
||||
|
if key_arg is not None: |
||||
|
if arg.startswith("--"): |
||||
|
usage() |
||||
|
echo0("Error: {} must be followed by a value but got {}." |
||||
|
"".format(key_arg, arg)) |
||||
|
return 1 |
||||
|
if key_arg == "--from": |
||||
|
install_from = arg |
||||
|
else: |
||||
|
usage() |
||||
|
echo0("Error: unknown argument {}".format(key_arg)) |
||||
|
return 1 |
||||
|
elif arg == "--server": |
||||
|
if appenders is None: |
||||
|
appenders = ["server"] |
||||
|
else: |
||||
|
appenders.append("server") |
||||
|
elif arg == "--client": |
||||
|
if appenders is None: |
||||
|
appenders = [""] |
||||
|
else: |
||||
|
appenders.append("") |
||||
|
elif arg == "--from": |
||||
|
key_arg = arg |
||||
|
else: |
||||
|
usage() |
||||
|
echo0('Error: The 2nd argument must be "server" or left out') |
||||
|
return 1 |
||||
|
if key_arg is not None: |
||||
|
usage() |
||||
|
echo0("Error: {} must be followed by a value." |
||||
|
"".format(key_arg)) |
||||
|
return 1 |
||||
|
if appenders is None: |
||||
|
appenders = [""] # Always require at least the plain exe name. |
||||
|
if install_from is None: |
||||
|
install_from = INSTALL_SRC |
||||
|
project_name = sys.argv[1] |
||||
|
results = install_minetest( |
||||
|
project_name, |
||||
|
install_from, |
||||
|
appenders=appenders, |
||||
|
) |
||||
|
return 0 |
||||
|
|
||||
|
|
||||
|
def install_minetest(project_name, src, dst=None, versioned_dirname=None, |
||||
|
appenders=[""]): |
||||
|
"""Install Minetest & generate a shortcut based on the destination. |
||||
|
|
||||
|
The metadata written to shortcut must be the installed metadata! |
||||
|
- except the shortcut path itself: |
||||
|
- shortcut_src generated below based on OS given relative path |
||||
|
- shortcut_dst generated based on OS and shortcut_name_noext |
||||
|
|
||||
|
Args: |
||||
|
project_name (string): Must be classic, finetest, or trolltest. |
||||
|
src (string): The location of the minetest install source to |
||||
|
copy. |
||||
|
dst (Optional[string]): Install here. If None, it will become |
||||
|
the default. Defaults to versioned_dirname under C:\games on |
||||
|
Windows, otherwise under HOME. |
||||
|
versioned_dirname (Optional[string]): Set the install directory |
||||
|
name (ignored if dst is set). If None, it will become the |
||||
|
default. Defaults to project_name + "-" + VERSION (such as |
||||
|
minetest-rsync). |
||||
|
appenders (list): Install client if [""], or server if |
||||
|
["server"]. Include both to require that both were built. |
||||
|
|
||||
|
Returns: |
||||
|
dict: "destination" is where it was installed if at all. See |
||||
|
"warning" in case there was something incorrect about the |
||||
|
install. |
||||
|
""" |
||||
|
arg = project_name |
||||
|
project_name = arg_to_mode(arg) |
||||
|
if project_name is None: |
||||
|
usage() |
||||
|
echo0("{} is not a valid project name.".format(arg)) |
||||
|
return 1 |
||||
|
src_files = expected_src_files(src, project_name, appenders) |
||||
|
if src_files is None: |
||||
|
usage() |
||||
|
raise NotImplementedError( |
||||
|
"There are no source files for {}" |
||||
|
"".format(project_name) |
||||
|
) |
||||
|
return 1 |
||||
|
|
||||
|
missing_files = [] |
||||
|
for src_file in src_files: |
||||
|
if not os.path.isfile(src_file): |
||||
|
missing_files.append(src_file) |
||||
|
|
||||
|
if len(missing_files) > 0: |
||||
|
echo0("Error: The following files are required to be compiled" |
||||
|
" {} before install but are not present: {}" |
||||
|
"".format(project_name, missing_files)) |
||||
|
return 1 |
||||
|
|
||||
|
versioned_dirname = project_name + "-" + VERSION |
||||
|
if dst is None: |
||||
|
if platform.system() == "Windows": |
||||
|
GAMES = "C:\\games" |
||||
|
if not os.path.isdir(GAMES): |
||||
|
os.mkdir(GAMES) |
||||
|
dst = os.path.join(GAMES, versioned_dirname) |
||||
|
else: |
||||
|
dst = os.path.join(HOME, versioned_dirname) |
||||
|
warning = None |
||||
|
if not os.path.isdir(dst): |
||||
|
write0('Installing {} to "{}"...'.format(project_name, dst)) |
||||
|
shutil.copytree(src, dst) |
||||
|
echo0("Done") |
||||
|
result_path = dst |
||||
|
else: |
||||
|
# Leave result_path as None |
||||
|
warning = 'Skipping installed "{}".'.format(dst) |
||||
|
echo0('WARNING: {}'.format(warning)) |
||||
|
|
||||
|
|
||||
|
project_meta = project_metas[project_name] |
||||
|
|
||||
|
Name = project_meta['name_fmt'].format(VERSION) |
||||
|
GenericName = project_meta['GenericName'] |
||||
|
Exec = os.path.join(dst, project_meta['exe_relpath']) |
||||
|
icon_relpath = project_meta['platform_icon_relpath'].get(platform.system()) |
||||
|
if icon_relpath is None: |
||||
|
raise NotImplementedError( |
||||
|
"There is no platform icon for {}.".format(platform.system()) |
||||
|
) |
||||
|
Icon = os.path.join(dst, icon_relpath) |
||||
|
shortcut_src = os.path.join(src, project_meta['shortcut_relpath']) |
||||
|
changes = { |
||||
|
"Name": Name, |
||||
|
"GenericName": GenericName, |
||||
|
"Exec": Exec, |
||||
|
"Icon": Icon, |
||||
|
"Keywords": KEYWORDS, |
||||
|
} |
||||
|
|
||||
|
if platform.system() == "Linux": |
||||
|
shortcut_src = os.path.join(dst, project_meta['shortcut_relpath']) |
||||
|
shortcut_name = project_meta['shortcut_name_noext'] + ".desktop" |
||||
|
shortcut_dst = os.path.join(SHORTCUTS_DIR, shortcut_name) |
||||
|
if not os.path.isdir(SHORTCUTS_DIR): |
||||
|
os.makedirs(SHORTCUTS_DIR) # default mode is 511 |
||||
|
write0('Installing icon to "{}"...'.format(shortcut_dst)) |
||||
|
rewrite_conf( |
||||
|
shortcut_src, |
||||
|
shortcut_dst, |
||||
|
changes=changes, |
||||
|
) |
||||
|
echo0("OK") |
||||
|
elif platform.system() == "Darwin": |
||||
|
shortcut_name = Name + ".command" |
||||
|
shortcut_dst = os.path.join(SHORTCUTS_DIR, shortcut_name) |
||||
|
with open(shortcut_dst) as stream: |
||||
|
stream.write('"%s"\n' % Exec) |
||||
|
# ^ Run the game & close Command Prompt immediately. |
||||
|
# ^ First arg is Command Prompt title, so leave it blank. |
||||
|
st = os.stat(shortcut_dst) |
||||
|
os.chmod(shortcut_dst, st.st_mode | stat.S_IXUSR) |
||||
|
# ^ same as stat.S_IEXEC: "Unix V7 synonym for S_IXUSR." |
||||
|
elif platform.system() == "Windows": |
||||
|
shortcut_name = Name + ".bat" |
||||
|
shortcut_dst = os.path.join(SHORTCUTS_DIR, shortcut_name) |
||||
|
with open(shortcut_dst) as stream: |
||||
|
stream.write('start "" "%s"\n' % Exec) |
||||
|
# ^ Run the game & close Command Prompt immediately. |
||||
|
# ^ First arg is Command Prompt title, so leave it blank. |
||||
|
else: |
||||
|
echo0("Icon install isn't implemented for {}." |
||||
|
"".format(platform.system())) |
||||
|
return { |
||||
|
"warning": warning, # may be None |
||||
|
"destination": dst, |
||||
|
} |
||||
|
|
||||
|
|
||||
|
# def get_missing_subs(mt_share_path, subs): |
||||
|
# """Get a list of any missing files for a source *or* destination. |
||||
|
# """ |
||||
|
|
||||
|
|
||||
|
def arg_to_mode(arg): |
||||
|
if arg == "final": |
||||
|
return "classic" |
||||
|
elif arg == "classic": |
||||
|
return "classic" |
||||
|
elif arg == "trolltest": |
||||
|
return "trolltest" |
||||
|
elif arg == "troll": |
||||
|
return "trolltest" |
||||
|
elif arg == "finetest": |
||||
|
return "finetest" |
||||
|
elif arg == "fine": |
||||
|
return "finetest" |
||||
|
return None |
||||
|
|
||||
|
|
||||
|
def detect_project_info(mt_share_path): |
||||
|
"""Detect the project name from a source *or* destination. |
||||
|
""" |
||||
|
for sub, project_info in mode_of_path.items(): |
||||
|
sub_path = os.path.join(mt_share_path, sub) |
||||
|
if os.path.isfile(sub_path): |
||||
|
return project_info |
||||
|
return None |
||||
|
|
||||
|
|
||||
|
def expected_src_files(src, project_name, appenders=[""]): |
||||
|
"""Get full paths of required files. |
||||
|
|
||||
|
Args: |
||||
|
appenders (list): Usually either [""] or ["server"], but if |
||||
|
the list contains both, both will be required |
||||
|
|
||||
|
Returns: |
||||
|
list: Files that are required for install to continue. |
||||
|
""" |
||||
|
if ((appenders is None) or (len(appenders) < 1) |
||||
|
or (not isinstance(appenders, list))): |
||||
|
appenders = [""] |
||||
|
echo0('Warning: reverting to appenders=[""] since {}' |
||||
|
''.format(appenders)) |
||||
|
|
||||
|
src_files = None |
||||
|
|
||||
|
for appender in appenders: |
||||
|
src_file = None |
||||
|
if project_name == "classic": |
||||
|
src_file = os.path.join(src, "bin", "minetest"+appender) |
||||
|
elif project_name == "trolltest": |
||||
|
src_file = os.path.join(src, "bin", "minetest"+appender) |
||||
|
elif project_name == "finetest": |
||||
|
src_file = os.path.join(src, "bin", "finetest"+appender) |
||||
|
else: |
||||
|
break |
||||
|
if src_file is not None: |
||||
|
if src_files is None: |
||||
|
src_files = [] |
||||
|
src_files.append(src_file) |
||||
|
# else caller must say project_name is not valid. |
||||
|
return src_files |
||||
|
|
||||
|
|
||||
|
def rewrite_conf(src, dst, changes={}): |
||||
|
"""Install a conf such as an XDG desktop shortcut with changes. |
||||
|
|
||||
|
Args: |
||||
|
src (string): The conf file to read. |
||||
|
dst (string): The conf file to write or overwrite. |
||||
|
changes (dict): A set of values to change by name. For any value |
||||
|
that is None, the line will be removed! |
||||
|
""" |
||||
|
# This function is redefined further down in the case of Python 2. |
||||
|
fd, path = tempfile.mkstemp() |
||||
|
try: |
||||
|
with os.fdopen(fd, 'wb') as tmp: |
||||
|
# ^ ensure still exists when moving |
||||
|
write0("Generating temporary icon %s..." % path) |
||||
|
# NOTE: tmp.name is just some number (int)! |
||||
|
with open(src, "rb") as stream: |
||||
|
for rawL in stream: |
||||
|
signI = rawL.find(b'=') |
||||
|
# commentI = rawl.find(b'#') |
||||
|
if rawL.strip().startswith(b"#"): |
||||
|
tmp.write(rawL) |
||||
|
continue |
||||
|
if rawL.strip().startswith(b"["): |
||||
|
tmp.write(rawL) |
||||
|
continue |
||||
|
if signI < 0: |
||||
|
tmp.write(rawL) |
||||
|
continue |
||||
|
key_bytes = rawL[:signI].strip() |
||||
|
key = key_bytes.decode("utf-8") |
||||
|
value = changes.get(key) |
||||
|
if key not in changes: |
||||
|
# The value wasn't changed so write it as-is |
||||
|
# echo0("%s not in %s" % (key, changes)) |
||||
|
tmp.write(rawL) |
||||
|
continue |
||||
|
if value is None: |
||||
|
echo0("%s was excluded from the icon" % key) |
||||
|
continue |
||||
|
line = "%s=%s\n" % (key, value) |
||||
|
tmp.write(line.encode("utf-8")) |
||||
|
shutil.copy(path, dst) |
||||
|
finally: |
||||
|
write0("removing tmp file...") |
||||
|
os.remove(path) |
||||
|
|
||||
|
|
||||
|
if sys.version_info.major < 3: |
||||
|
# Python 2 (strings are bytes) |
||||
|
def rewrite_conf(src, dst, changes={}): |
||||
|
"""Install a conf such as an XDG desktop shortcut with changes. |
||||
|
""" |
||||
|
fd, path = tempfile.mkstemp() |
||||
|
try: |
||||
|
with os.fdopen(fd, 'wb') as tmp: |
||||
|
write0("Generating temporary icon %s..." % path) |
||||
|
with open(src, "rb") as stream: |
||||
|
for rawL in stream: |
||||
|
signI = rawL.find('=') |
||||
|
# commentI = rawl.find('#') |
||||
|
if rawL.strip().startswith("#"): |
||||
|
tmp.write(rawL) |
||||
|
continue |
||||
|
if rawL.strip().startswith("["): |
||||
|
tmp.write(rawL) |
||||
|
continue |
||||
|
if signI < 0: |
||||
|
tmp.write(rawL) |
||||
|
continue |
||||
|
key_bytes = rawL[:signI].strip() |
||||
|
key = key_bytes |
||||
|
value = changes.get(key) |
||||
|
if key not in changes: |
||||
|
# The value wasn't changed so write it as-is |
||||
|
tmp.write(rawL) |
||||
|
continue |
||||
|
if value is None: |
||||
|
echo0("%s was excluded from the icon" % key) |
||||
|
continue |
||||
|
line = "%s=%s\n" % (key, value) |
||||
|
tmp.write(line) |
||||
|
shutil.copy(path, dst) |
||||
|
finally: |
||||
|
write0("removing tmp file...") |
||||
|
os.remove(path) |
||||
|
|
||||
|
if __name__ == "__main__": |
||||
|
sys.exit(main()) |
Loading…
Reference in new issue