diff --git a/src/mpm/commands/server.py b/src/mpm/commands/server.py index 8794ce9..4fd27a1 100644 --- a/src/mpm/commands/server.py +++ b/src/mpm/commands/server.py @@ -17,6 +17,62 @@ def add(ctx, name, path): ctx.obj['config'].save() click.echo("Added server {} in {}".format(name, path)) +@server.command(help="Save a server's current plugin list to mpm.yaml") +@click.pass_context +@click.argument('servers', type=ServerParamType(), nargs=-1) +def lock(ctx, servers): + config = ctx.obj['config'] + if len(servers) == 0: + servers = config.servers() + validRepos = [] + for repo in ctx.obj['config'].repositories(): + if not repo.exists(): + click.echo("Missing repository: '{}' not found at {}".format(repo.name, repo.path)) + else: + validRepos.append(repo) + for server in servers: + click.echo('{} ({}):'.format(server.name, server.path)) + savedPlugins = [] + changedPlugins = [] + for state in server.pluginStates(validRepos): + if isinstance(state, Installed): + savedPlugins.append(state) + elif isinstance(state, Outdated): + changedPlugins.append(state) + elif isinstance(state, MissingVersions): + changedPlugins.append(state) + rows = [] + for state in savedPlugins: + newSpec = PluginSpec(state.plugin.name, + state.currentVersion.version) + if newSpec == state.plugin: + continue + rows.append([ + state.plugin.name, + click.style('{} -> {}'.format(state.plugin.versionSpec, + state.currentVersion.version), fg='green'), + state.currentVersion.version + ]) + server.set_plugin_version(state.plugin.name, state.currentVersion.version) + for state in changedPlugins: + if state.currentVersion is not None: + rows.append([ + click.style(state.plugin.name, fg='yellow'), + click.style('{} -> {}'.format(state.plugin.versionSpec, + state.currentVersion.version), + fg='yellow'), + state.currentVersion.version + ]) + server.set_plugin_version(state.plugin.name, state.currentVersion.version) + if len(rows) > 0: + click.echo(columnar(rows, ['Plugin', 'Wanted', 'Installed'], + no_borders=True)) + config.update_server(server.name, server.config) + click.echo("Apply changes? [y/N]", nl=False) + answer = click.getchar().lower() + if answer == "y": + config.save() + @server.command('list', help='List configured servers') @click.pass_context @click.argument('servers', type=ServerParamType(), nargs=-1) @@ -62,9 +118,9 @@ def list_(ctx, servers): curStr = click.style(str(state.currentVersion.version), fg='yellow') rows.append([ state.plugin.name, - click.style(str(state.wantedVersion), fg='green'), + click.style(str(state.plugin.versionSpec), fg='green'), curStr, - click.style(str(state.plugin.version), fg='green') + click.style(str(state.wantedVersion), fg='green') ]) for state in sorted(missing): rows.append([ @@ -161,9 +217,9 @@ def sync(ctx, servers): curStr = click.style(str(state.currentVersion.version), fg='yellow') rows.append([ state.plugin.name, - click.style(str(state.wantedVersion), fg='green'), + click.style(str(state.plugin.versionSpec), fg='green'), curStr, - click.style(str(state.plugin.version), fg='green') + click.style(str(state.wantedVersion.version), fg='green') ]) click.echo("Missing plugins:") diff --git a/src/mpm/model.py b/src/mpm/model.py index b5d1602..7ae5c16 100644 --- a/src/mpm/model.py +++ b/src/mpm/model.py @@ -7,20 +7,28 @@ version_pattern = re.compile('^(?P.+)-(?P(?:\.?\d+)+).+jar$') @total_ordering class Plugin: - def __init__(self, path): + def __init__(self, path, forceName=None, forceVersion=None): 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)) + if forceVersion is None: + pluginMatches = version_pattern.match(pluginName) - self.name = pluginMatches['name'] + if pluginMatches is None: + raise ValueError("Cannot derive plugin name from '{}'".format(path)) - try: - self.version = Version.coerce(pluginMatches['version']) - except ValueError: - raise ValueError("Cannot derive semver from '{}'".format(path)) + if forceName is None: + self.name = pluginMatches['name'] + else: + self.name = forceName + + try: + self.version = Version.coerce(pluginMatches['version']) + except ValueError: + raise ValueError("Cannot derive semver from '{}'".format(path)) + else: + self.version = forceVersion + self.name = forceName def normalizedFilename(self): return "{}-{}.jar".format(self.name, self.version) @@ -98,19 +106,10 @@ class Outdated(PluginState): def __str__(self): return 'Outdated({})'.format(self.plugin) -class SymlinkConflict(PluginState): - pass - class MissingVersions(PluginState): - pass - -class Available(PluginState): - def __init__(self, repoPlugin, wantedVersion): - super().__init__(repoPlugin) - self.wantedVersion = wantedVersion - - def __str__(self): - return 'Available({})'.format(self.plugin) + def __init__(self, plugin, currentVersion): + super().__init__(plugin) + self.currentVersion = currentVersion class Installed(PluginState): def __init__(self, plugin, currentVersion): diff --git a/src/mpm/server.py b/src/mpm/server.py index d5f712d..7b08ce2 100644 --- a/src/mpm/server.py +++ b/src/mpm/server.py @@ -2,6 +2,7 @@ from mpm.model import * import shutil import pathlib import logging +import hashlib import os import semantic_version @@ -26,13 +27,18 @@ class Server: raise KeyError("Cannot add plugin multiple times.") self.config['plugins'].append({'name': pluginSpec.name, 'version': str(pluginSpec.versionSpec)}) + def set_plugin_version(self, pluginName, versionSpec): + for plugin in self.config['plugins']: + if plugin['name'] == pluginName: + plugin['version'] = str(versionSpec) + def pluginStates(self, repos): managedPluginFilenames = [] for plugin in self.plugins(): logger.debug("Checking %r", plugin) compatibleVersions = [] - currentVersion = self.currentVersionForPlugin(plugin.name) + currentVersion = self.currentVersionForPlugin(plugin.name, repos) logger.debug('Current version: %s', currentVersion) if currentVersion is not None: if plugin.versionSpec.match(currentVersion.version): @@ -48,7 +54,7 @@ class Server: compatibleVersions.append(repoPlugin) if len(compatibleVersions) == 0: logger.debug('No compatible versions found for %s', plugin) - yield MissingVersions(plugin) + yield MissingVersions(plugin, currentVersion) else: preferredVersion = list(reversed(sorted(compatibleVersions)))[0] logger.debug('Wanted %r, found %r', plugin, preferredVersion) @@ -57,7 +63,7 @@ class Server: yield Installed(plugin, currentVersion) else: logger.debug("Update available: %r -> %r", plugin, preferredVersion) - yield Outdated(preferredVersion, currentVersion, plugin.versionSpec) + yield Outdated(plugin, currentVersion, preferredVersion) otherPlugins = os.listdir(self.pluginPath) for pluginFile in otherPlugins: @@ -65,19 +71,39 @@ class Server: logger.debug("Unmanaged file: %s", pluginFile) yield UnmanagedFile(pluginFile) - def currentVersionForPlugin(self, pluginName): + def currentVersionForPlugin(self, pluginName, repos=[]): plugins = os.listdir(self.pluginPath) for pluginJar in plugins: if pluginJar.startswith(pluginName) and pluginJar.endswith(".jar"): - try: - return Plugin(os.path.join(self.pluginPath, pluginJar)) - except ValueError: - continue + if pluginJar == pluginName + ".jar": + logger.debug("Running checksum search to find version of %s", pluginJar) + pluginPath = os.path.join(self.pluginPath, pluginJar) + with open(pluginPath, 'rb') as fd: + installedHash = hashlib.file_digest(fd, 'sha256') + pluginName = pluginJar.split('.jar')[0] + defaultVersion = semantic_version.Version("0.0.0") + for repo in repos: + for plugin in repo.plugins(): + if plugin.name == pluginName: + with open(plugin.path, 'rb') as fd: + foundHash = hashlib.file_digest(fd, 'sha256') + if foundHash.hexdigest() == installedHash.hexdigest(): + return Plugin(pluginPath, + forceName=pluginName, + forceVersion = plugin.version) + return Plugin(pluginPath, + forceName=pluginName, + forceVersion=defaultVersion) + else: + try: + return Plugin(os.path.join(self.pluginPath, pluginJar)) + except ValueError: + continue - def syncToVersion(self, plugin): + def syncToVersion(self, plugin, repos=[]): dest = os.path.join(self.pluginPath, plugin.normalizedFilename()) logger.debug("Syncing %s to %s", dest, plugin) - current = self.currentVersionForPlugin(plugin.name) + current = self.currentVersionForPlugin(plugin.name, repos) if current is not None: logger.debug("Removing current version %r", current) current.unlink()