move src to src/, make packagable

This commit is contained in:
2022-09-03 14:57:05 +02:00
parent 99f4220a67
commit 55c5bc489c
9 changed files with 111 additions and 19 deletions

0
src/mpm/__init__.py Normal file
View File

267
src/mpm/main.py Executable file
View File

@@ -0,0 +1,267 @@
#!/bin/env python
import argparse
import pathlib
import sys
import yaml
import os
from mpm.repo import Repo
from mpm.server import Server
from mpm.model import *
try:
from yaml import CLoader as Loader, CDumper as Dumper
except ImportError:
from yaml import Loader, Dumper
DEFAULT_CONFIG = """
repositories: {}
servers: {}
"""
class Config():
def __init__(self, path):
if path is None:
path = os.path.expanduser('~/mpm.yaml')
self.path = path
with open(path, 'r') as fd:
self.yaml = yaml.load(fd, Loader=Loader)
self.config = yaml.load(DEFAULT_CONFIG, Loader=Loader)
if isinstance(self.yaml, dict):
self.config.update(self.yaml)
def repository(self, name):
return Repo(name, self.config['repositories'][name])
def repositories(self):
return [Repo(name, c) for (name, c) in self.config['repositories'].items()]
def update_repository(self, name, config):
self.config['repositories'][name] = config
def add_repository(self, name, path):
if name in self.config['repositories']:
raise ValueError('Repository already exists')
self.update_repository(name, {
'path': path
})
def servers(self):
return [Server(name, c) for (name, c) in self.config['servers'].items()]
def server(self, name):
return Server(name, self.config['servers'][name])
def update_server(self, server, config):
self.config['servers'][server] = config
def add_server(self, name, path):
if name in self.config['servers']:
raise ValueError("Server already exists")
self.update_server(name, {
'path': path,
'plugins': [],
'inherit': []
})
def save(self):
stream = open(self.path, 'w')
yaml.dump(self.config, stream, Dumper=Dumper)
stream.close()
def do_repo_add(args, config):
if not os.path.exists(args.path):
os.makedirs(args.path)
config.add_repository(args.name, args.path)
config.save()
print("Added repository {}".format(args.path))
def do_repo_list(args, config):
for repo in config.repositories():
print("{} ({})".format(repo.name, repo.path))
for plugin in sorted(repo.plugins()):
print('\t', plugin.name, '\t', plugin.version)
for badFile in sorted(repo.badFiles()):
print('\tWARNING: Unknown file', badFile)
def do_repo_import(args, config):
repo = config.repository(args.name)
plugins = []
for path in args.path:
try:
plugins.append(Plugin(path))
except:
print("Bad plugin filename {}".format(path))
if len(plugins) == 0:
print("No plugins found.")
print('Found the following plugins:')
for plugin in plugins:
print("\t{} {}".format(plugin.name, plugin.version))
print("Import plugins into {}? [y/N]".format(repo.name))
answer = input().lower()
if answer == "y":
for plugin in plugins:
repo.importPlugin(plugin)
print("Imported!")
else:
print("Cancelled.")
def do_server_add(args, config):
config.add_server(args.name, args.path)
config.save()
print("Added server {} in {}".format(args.name, args.path))
def do_server_list(args, config):
for server in config.servers():
print('{} ({}):'.format(server.name, server.path))
outdatedLinks = []
missing = []
installed = []
unmanaged = []
conflicts = []
for state in sorted(server.pluginStates(config.repositories())):
if isinstance(state, OutdatedSymlink):
outdatedLinks.append(state)
elif isinstance(state, Installed):
installed.append(state)
elif isinstance(state, MissingVersions):
missing.append(state)
elif isinstance(state, UnmanagedFile):
unmanaged.append(state)
elif isinstance(state, SymlinkConflict):
conflicts.append(state)
print("Installed plugins:")
for state in sorted(installed):
print("\t{} {}: {}".format(state.plugin.name, state.plugin.versionSpec, state.currentVersion))
print("Oudated symlinks:")
for state in sorted(outdatedLinks):
print("\t{} {}: Current: {} Wanted: {}".format(state.plugin.name, state.plugin.versionSpec, state.currentVersion, state.wantedVersion))
print("Missing plugins:")
for state in sorted(missing):
print("\t{}: {}".format(state.plugin.name, state.plugin.versionSpec))
print("Unmanaged files:")
for state in sorted(unmanaged):
print("\t{}".format(state.filename))
print("Symlink Conflicts:")
for state in sorted(conflicts):
print("\t{}.jar".format(state.plugin.name))
def do_server_add_plugin(args, config):
server = config.server(args.server)
plugins = []
for pluginSpec in args.plugin:
if os.path.exists(pluginSpec):
plugin = Plugin(pluginSpec)
pluginSpec = PluginSpec(plugin.name, str(plugin.version))
else:
allVersions = []
for repo in config.repositories():
allVersions += repo.versionsForPlugin(pluginSpec)
pluginSpec = PluginSpec(pluginSpec, list(reversed(sorted(allVersions)))[0])
plugins.append(pluginSpec)
print("Added {} to {}".format(pluginSpec, server.name))
for pluginSpec in plugins:
print("\t{} {}".format(pluginSpec.name, pluginSpec.versionSpec))
print("Add these plugins to server {}? [y/N]".format(server.name))
answer = input().lower()
if answer == "y":
for pluginSpec in plugins:
server.add_plugin(pluginSpec)
config.update_server(server.name, server.config)
config.save()
print("Added!")
else:
print("Cancelled.")
def do_server_sync(args, config):
for server in config.servers():
print('{} ({}):'.format(server.name, server.path))
outdatedLinks = []
available = []
for state in sorted(server.pluginStates(config.repositories())):
if isinstance(state, OutdatedSymlink):
outdatedLinks.append(state)
elif isinstance(state, Available):
available.append(state)
print("Plugins to update:")
for state in sorted(outdatedLinks):
print("\t{} {}: Current: {} Wanted: {}".format(state.plugin.name, state.plugin.versionSpec, state.currentVersion, state.wantedVersion))
print("New plugins to install:")
for state in sorted(available):
print("\t{}: {}".format(state.plugin.name, state.plugin.version))
if len(outdatedLinks) > 0 or len(available) > 0:
print("Apply changes? [y/N]")
answer = input().lower()
if answer == "y":
for state in available:
server.installVersion(state.plugin)
server.updateSymlinkForPlugin(state.plugin, state.plugin.version)
print("Installed {} {}".format(state.plugin.name, state.plugin.version))
for state in outdatedLinks:
server.updateSymlinkForPlugin(state.plugin, state.wantedVersion)
print("Updated {} to {}".format(state.plugin.name, state.wantedVersion))
else:
print("Not applying changes.")
else:
print("No changes to apply.")
def main():
parser = argparse.ArgumentParser(description='Paper Plugin Sync')
parser.add_argument('--config', dest='config_path', type=Config)
subparsers = parser.add_subparsers()
repos = subparsers.add_parser('repo')
repo_sub = repos.add_subparsers()
repo_add = repo_sub.add_parser('add')
repo_add.add_argument('name', help='Name of the repository')
repo_add.add_argument('path', help='Where to add a repository or create a new one')
repo_add.set_defaults(func=do_repo_add)
repo_list = repo_sub.add_parser('list')
repo_list.set_defaults(func=do_repo_list)
repo_import = repo_sub.add_parser('import')
repo_import.add_argument('name', help='Name of the repository')
repo_import.add_argument('path', nargs="+", help='Path of the plugin to import')
repo_import.set_defaults(func=do_repo_import)
servers = subparsers.add_parser('server')
server_sub = servers.add_subparsers()
server_add = server_sub.add_parser('add')
server_add.add_argument('name', help='Name for the server')
server_add.add_argument('path', help='Path to your server\'s root directory')
server_add.set_defaults(func=do_server_add)
server_list = server_sub.add_parser('list')
server_list.set_defaults(func=do_server_list)
server_add_plugin = server_sub.add_parser('add-plugin')
server_add_plugin.add_argument('server', help='Name of server to modify')
server_add_plugin.add_argument('plugin', nargs='+', help='Plugin file or spec to install')
server_add_plugin.set_defaults(func=do_server_add_plugin)
server_sync = server_sub.add_parser('sync')
server_sync.set_defaults(func=do_server_sync)
args = parser.parse_args()
config = Config(args.config_path)
if 'func' not in args:
parser.print_usage()
else:
args.func(args, config)
if __name__ == "__main__":
main()

97
src/mpm/model.py Normal file
View File

@@ -0,0 +1,97 @@
from semantic_version import Spec, Version
from functools import total_ordering
import os
import re
version_pattern = re.compile('^(?P<name>.+)-(?P<version>(?:\.?\d+)+).+jar$')
@total_ordering
class Plugin:
def __init__(self, path):
self.path = path
pluginName = os.path.basename(path)
pluginMatches = version_pattern.match(pluginName)
if pluginMatches is None:
raise ValueError("Cannot derive plugin name from '{}'".format(path))
self.name = pluginMatches['name']
try:
self.version = Version.coerce(pluginMatches['version'])
except ValueError:
raise ValueError("Cannot derive semver from '{}'".format(path))
def __eq__(self, other):
return self.name == other.name and self.version == other.version
def __ne__(self, other):
return self.name != other.name or self.version != other.version
def __lt__(self, other):
if self.name == other.name:
return self.version < other.version
return self.name < other.name
@total_ordering
class PluginSpec:
def __init__(self, name, versionSpec):
self.name = name
try:
self.versionSpec = Spec(str(versionSpec))
except ValueError:
raise ValueError("Invalid version spec for plugin {}: {}".format(name, versionSpec))
def __str__(self):
return "{} {}".format(self.name, self.versionSpec)
def __eq__(self, other):
return self.name == other.name and self.versionSpec == other.versionSpec
def __ne__(self, other):
return self.name != other.name or self.versionSpec != other.versionSpec
def __lt__(self, other):
return self.name < other.name
@total_ordering
class PluginState:
def __init__(self, plugin):
self.plugin = plugin
def __eq__(self, other):
return self.plugin == other.plugin
def __ne__(self, other):
return self.plugin != other.plugin
def __lt__(self, other):
return self.plugin.name < other.plugin.name
class UnmanagedFile(PluginState):
def __init__(self, filename):
self.filename = filename
def __lt__(self, other):
return self.filename < other.filename
class OutdatedSymlink(PluginState):
def __init__(self, plugin, currentVersion, wantedVersion):
super().__init__(plugin)
self.currentVersion = currentVersion
self.wantedVersion = wantedVersion
class SymlinkConflict(PluginState):
pass
class MissingVersions(PluginState):
pass
class Available(PluginState):
def __init__(self, repoPlugin):
super().__init__(repoPlugin)
class Installed(PluginState):
def __init__(self, plugin, currentVersion):
super().__init__(plugin)
self.currentVersion = currentVersion

73
src/mpm/plugin-sync.py Executable file
View File

@@ -0,0 +1,73 @@
#!/bin/env python
import yaml
import os
import sys
from model import *
try:
from yaml import CLoader as Loader, CDumper as Dumper
except ImportError:
from yaml import Loader, Dumper
conf = yaml.load(open('plugins.yml', 'r'), Loader=Loader)
for (serverName,server) in conf['servers'].items():
changeset = []
if len(sys.argv) > 1 and serverName not in sys.argv[1:]:
continue
if 'pluginPath' not in server:
continue
if not os.path.exists(server['pluginPath']):
print("Missing plugin path for {}: {}".format(serverName, server['pluginPath']))
else:
print("=== Updating server {}".format(serverName))
pluginSpecs = {}
for inherited in server.get('inherit', ()):
for inheritedPlugin in conf['servers'][inherited]['plugins']:
pluginSpecs[inheritedPlugin['name']] = PluginSpec(inheritedPlugin['name'], str(inheritedPlugin.get('version', '*')))
for pluginConf in server.get('plugins', ()):
pluginSpecs[pluginConf['name']] = PluginSpec(pluginConf['name'], str(pluginConf.get('version', '*')))
repo = Repo(server['pluginPath'], pluginSpecs.values())
outdatedLinks = []
missing = []
installed = []
unmanaged = []
conflicts = []
for state in repo.pluginStates():
if isinstance(state, OutdatedSymlink):
outdatedLinks.append(state)
elif isinstance(state, Installed):
installed.append(state)
elif isinstance(state, MissingVersions):
missing.append(state)
elif isinstance(state, UnmanagedFile):
unmanaged.append(state)
elif isinstance(state, SymlinkConflict):
conflicts.append(state)
print("Installed plugins:")
for state in sorted(installed):
print("\t{} {}: {}".format(state.plugin.name, state.plugin.versionSpec, state.currentVersion))
print("Oudated symlinks:")
for state in sorted(outdatedLinks):
print("\t{} {}: Current: {} Wanted: {}".format(state.plugin.name, state.plugin.versionSpec, state.currentVersion, state.wantedVersion))
print("Missing plugins:")
for state in sorted(missing):
print("\t{}: {}".format(state.plugin.name, state.plugin.versionSpec))
print("Unmanaged files:")
for state in sorted(unmanaged):
print("\t{}".format(state.filename))
print("Symlink Conflicts:")
for state in sorted(conflicts):
print("\t{}.jar".format(state.plugin.name))
if len(outdatedLinks) > 0:
print("Apply changes? [y/N]")
answer = input().lower()
if answer == "y":
for state in outdatedLinks:
repo.updateSymlinkForPlugin(state.plugin, state.wantedVersion)
else:
print("Not applying changes.")

110
src/mpm/repo.py Normal file
View File

@@ -0,0 +1,110 @@
import os
from mpm.model import *
import shutil
class Repo:
def __init__(self, name, config):
self.name = name
self.config = config
self.path = config['path']
def importPlugin(self, plugin):
dest = "{}/{}-{}.jar".format(self.path, plugin.name, plugin.version)
shutil.copyfile(plugin.path, dest)
def plugins(self):
plugins = os.listdir(self.path)
for pluginFile in plugins:
fullPath = os.path.join(self.path, pluginFile)
if os.path.isfile(fullPath):
try:
yield Plugin(fullPath)
except ValueError:
continue
def versionsForPlugin(self, name):
for plugin in self.plugins():
if plugin.name == name:
yield plugin.version
def badFiles(self):
plugins = os.listdir(self.path)
for pluginFile in plugins:
fullPath = os.path.join(self.path, pluginFile)
if os.path.isfile(fullPath):
try:
Plugin(fullPath)
except ValueError:
yield fullPath
class Server:
def __init__(self, path, pluginSet):
self.path = path
self.pluginSet = pluginSet
def updateSymlinkForPlugin(self, plugin, version):
pluginFilename = os.path.join(self.path, 'plugins/versions/{}-{}.jar'.format(plugin.name, version))
pluginSymlink = os.path.join(self.path, plugin.name + '.jar')
linkDst = os.path.relpath(pluginFilename, self.path)
if os.path.lexists(pluginSymlink):
os.unlink(pluginSymlink)
os.symlink(linkDst, pluginSymlink)
def pluginStates(self):
managedPluginFilenames = []
for plugin in self.pluginSet:
compatibleVersions = []
pluginLinkName = '{}.jar'.format(plugin.name)
managedPluginFilenames.append(pluginLinkName)
if os.path.exists(os.path.join(self.path, pluginLinkName)) and not os.path.islink(os.path.join(self.path, pluginLinkName)):
yield SymlinkConflict(plugin)
continue
for installedVersion in self.versionsForPlugin(plugin.name):
if installedVersion in plugin.versionSpec:
compatibleVersions.append(installedVersion)
if len(compatibleVersions) == 0:
yield MissingVersions(plugin)
else:
preferredVersion = list(reversed(sorted(compatibleVersions)))[0]
currentVersion = self.currentVersionForPlugin(plugin.name)
if currentVersion == preferredVersion:
yield Installed(plugin, currentVersion)
else:
yield OutdatedSymlink(plugin, currentVersion, preferredVersion)
otherPlugins = os.listdir(self.path)
for pluginFile in otherPlugins:
if os.path.isfile(os.path.join(self.path, pluginFile)) and pluginFile not in managedPluginFilenames:
yield UnmanagedFile(pluginFile)
def currentVersionForPlugin(self, pluginName):
pluginSymlink = os.path.join(self.path, pluginName + '.jar')
if not os.path.lexists(pluginSymlink):
return None
suffix = '.jar'
pluginJar = os.path.basename(os.readlink(pluginSymlink))
jarVersion = pluginJar[len(pluginName)+1:len(pluginJar)-len(suffix)]
try:
pluginSemver = Version.coerce(jarVersion)
except ValueError:
pluginSemver = jarVersion
return pluginSemver
def versionsForPlugin(self, pluginName):
plugins = os.listdir(os.path.join(self.path, 'plugins', 'versions'))
for pluginJar in plugins:
if pluginJar.startswith(pluginName):
prefix = pluginName + '-'
suffix = '.jar'
jarVersion = pluginJar[len(prefix):len(pluginJar)-len(suffix)]
try:
pluginSemver = Version.coerce(jarVersion)
except ValueError:
pluginSemver = jarVersion
yield pluginSemver

96
src/mpm/server.py Normal file
View File

@@ -0,0 +1,96 @@
from mpm.model import *
import shutil
class Server:
def __init__(self, name, config):
self.name = name
self.config = config
self.path = config['path']
self.pluginPath = self.path+'/plugins'
def plugins(self):
return [PluginSpec(p['name'], p['version']) for p in self.config['plugins']]
def add_plugin(self, pluginSpec):
for plugin in self.config['plugins']:
if plugin['name'] == pluginSpec.name:
raise KeyError("Cannot add plugin multiple times.")
self.config['plugins'].append({'name': pluginSpec.name, 'version': str(pluginSpec.versionSpec)})
def pluginStates(self, repos):
managedPluginFilenames = []
for plugin in self.plugins():
compatibleVersions = []
pluginLinkName = '{}.jar'.format(plugin.name)
managedPluginFilenames.append(pluginLinkName)
if os.path.exists(os.path.join(self.pluginPath, pluginLinkName)) and not os.path.islink(os.path.join(self.pluginPath, pluginLinkName)):
yield SymlinkConflict(plugin)
continue
for installedVersion in self.versionsForPlugin(plugin.name, repos):
if installedVersion in plugin.versionSpec:
compatibleVersions.append(installedVersion)
if len(compatibleVersions) == 0:
for repo in repos:
for repoPlugin in repo.plugins():
if repoPlugin.name == plugin.name and repoPlugin.version in plugin.versionSpec:
compatibleVersions.append(repoPlugin)
if len(compatibleVersions) == 0:
yield MissingVersions(plugin)
else:
preferredVersion = list(reversed(sorted(compatibleVersions)))[0]
yield Available(preferredVersion)
else:
preferredVersion = list(reversed(sorted(compatibleVersions)))[0]
currentVersion = self.currentVersionForPlugin(plugin.name)
if currentVersion == preferredVersion:
yield Installed(plugin, currentVersion)
else:
yield OutdatedSymlink(plugin, currentVersion, preferredVersion)
otherPlugins = os.listdir(self.pluginPath)
for pluginFile in otherPlugins:
if os.path.isfile(os.path.join(self.pluginPath, pluginFile)) and pluginFile not in managedPluginFilenames:
yield UnmanagedFile(pluginFile)
def currentVersionForPlugin(self, pluginName):
pluginSymlink = os.path.join(self.pluginPath, pluginName + '.jar')
if not os.path.lexists(pluginSymlink):
return None
suffix = '.jar'
pluginJar = os.path.basename(os.readlink(pluginSymlink))
jarVersion = pluginJar[len(pluginName)+1:len(pluginJar)-len(suffix)]
try:
pluginSemver = Version.coerce(jarVersion)
except ValueError:
pluginSemver = jarVersion
return pluginSemver
def versionsForPlugin(self, pluginName, repos):
plugins = os.listdir(os.path.join(self.pluginPath, 'versions'))
for pluginJar in plugins:
if pluginJar.startswith(pluginName):
prefix = pluginName + '-'
suffix = '.jar'
jarVersion = pluginJar[len(prefix):len(pluginJar)-len(suffix)]
try:
pluginSemver = Version.coerce(jarVersion)
except ValueError:
pluginSemver = jarVersion
yield pluginSemver
def updateSymlinkForPlugin(self, plugin, version):
pluginFilename = os.path.join(self.pluginPath, 'versions/{}-{}.jar'.format(plugin.name, version))
pluginSymlink = os.path.join(self.pluginPath, plugin.name + '.jar')
linkDst = os.path.relpath(pluginFilename, self.pluginPath)
if os.path.lexists(pluginSymlink):
os.unlink(pluginSymlink)
os.symlink(linkDst, pluginSymlink)
def installVersion(self, plugin):
dest = os.path.join(self.pluginPath, 'versions/{}-{}.jar'.format(plugin.name, plugin.version))
shutil.copyfile(plugin.path, dest)