From 0f863e284cde17aeca1fbea3ac94e097a7e60865 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Tue, 15 Dec 2015 18:43:23 +0000 Subject: [PATCH] Implement conda support. This PR adds conda support to Planemo and via shared libraries to Galaxy in the form of new planemo commands providing a Galaxy tool-centric interface to conda for development and a Galaxy dependency resolver. Once the corresponding Galaxy PR is merged, the following command:: planemo t --conda_dependency_resolution --conda_auto_install --conda_auto_init bwa_and_samtools.xml Will test the supplied tool with only conda dependency resolution available. On startup, if Galaxy cannot find conda it will install it and for every dependency encountered it will attempt to find and install it on-demand. Planemo Conda Commands: - conda_init: Install a conda runtime. - conda_install: Takes in a tool or set of tools and installs their requirements using conda in such a way that they can be recovered and reused by dependency resolution. - conda_env: Build isolated environment for a tool or set of tools. Conda CLI Options: Common options for conda commands in planemo. --conda_prefix: Location of conda runtime (defaults to ~/miniconda2). --conda_exec: Location of conda executble (defaults to """ +# TODO: fill in properties to match CLI args. +CONDA_DEPENDENCY_RESOLUTION_CONF = """ + + + +""" + BREW_DEPENDENCY_RESOLUTION_CONF = """ @@ -84,6 +91,7 @@ STOCK_DEPENDENCY_RESOLUTION_STRATEGIES = { "brew_dependency_resolution": BREW_DEPENDENCY_RESOLUTION_CONF, "shed_dependency_resolution": SHED_DEPENDENCY_RESOLUTION_CONF, + "conda_dependency_resolution": CONDA_DEPENDENCY_RESOLUTION_CONF, } EMPTY_TOOL_CONF_TEMPLATE = """""" @@ -633,6 +641,7 @@ def _handle_dependency_resolution(config_directory, kwds): "brew_dependency_resolution", "dependency_resolvers_config_file", "shed_dependency_resolution", + "conda_dependency_resolution", ] selected_strategies = 0 @@ -644,13 +653,36 @@ def _handle_dependency_resolution(config_directory, kwds): message = "At most one option from [%s] may be specified" raise click.UsageError(message % resolutions_strategies) + dependency_attribute_kwds = { + 'conda_prefix': None, + 'conda_exec': None, + 'conda_debug': False, + 'conda_copy_dependencies': False, + 'conda_auto_init': False, + 'conda_auto_install': False, + 'conda_ensure_channels': '', + } + + attributes = [] + for key, default_value in dependency_attribute_kwds.iteritems(): + value = kwds.get(key, default_value) + if value != default_value: + # Strip leading prefix (conda_) off attributes + attribute_key = "_".join(key.split("_")[1:]) + attributes.append('%s="%s"' % (attribute_key, value)) + + attribute_str = " ".join(attributes) + for key in STOCK_DEPENDENCY_RESOLUTION_STRATEGIES: if kwds.get(key): resolvers_conf = os.path.join( config_directory, "resolvers_conf.xml" ) - conf_contents = STOCK_DEPENDENCY_RESOLUTION_STRATEGIES[key] + template_str = STOCK_DEPENDENCY_RESOLUTION_STRATEGIES[key] + conf_contents = Template(template_str).safe_substitute({ + 'attributes': attribute_str + }) open(resolvers_conf, "w").write(conf_contents) kwds["dependency_resolvers_config_file"] = resolvers_conf diff --git a/planemo/io.py b/planemo/io.py index 886c71256..1904a02e2 100644 --- a/planemo/io.py +++ b/planemo/io.py @@ -101,6 +101,16 @@ def temp_directory(prefix="planemo_tmp_"): shutil.rmtree(temp_dir) +def ps1_for_path(path, base="PS1"): + """ Used by environment commands to build a PS1 shell + variables for tool or directory of tools. + """ + file_name = os.path.basename(path) + base_name = os.path.splitext(file_name)[0] + ps1 = "(%s)${%s}" % (base_name, base) + return ps1 + + def kill_pid_file(pid_file): if not os.path.exists(pid_file): return diff --git a/planemo/options.py b/planemo/options.py index c189c6827..c16943b0e 100644 --- a/planemo/options.py +++ b/planemo/options.py @@ -126,6 +126,14 @@ def brew_dependency_resolution(): ) +def conda_dependency_resolution(): + return click.option( + "--conda_dependency_resolution", + is_flag=True, + help="Configure Galaxy to use only conda for dependency resolution.", + ) + + def shed_dependency_resolution(): return click.option( "--shed_dependency_resolution", @@ -180,6 +188,14 @@ def no_cache_galaxy_option(): ) +def skip_install_option(): + return click.option( + "--skip_install", + is_flag=True, + help="Skip installation - only source requirements already available." + ) + + def brew_option(): return click.option( "--brew", @@ -188,6 +204,67 @@ def brew_option(): ) +def conda_prefix_option(): + return click.option( + "--conda_prefix", + type=click.Path(file_okay=False, dir_okay=True), + help="Conda prefix to use for conda dependency commands." + ) + + +def conda_exec_option(): + return click.option( + "--conda_exec", + type=click.Path(exists=True, file_okay=True, dir_okay=False), + help="Location of conda executable." + ) + + +def conda_debug_option(): + return click.option( + "--conda_debug", + is_flag=True, + help="Enable more verbose conda logging." + ) + + +def conda_ensure_channels_option(): + return click.option( + "--conda_ensure_channels", + type=str, + help=("Ensure conda is configured with specified comma separated " + "list of channels."), + default="r,bioconda" + ) + + +def conda_copy_dependencies_option(): + return click.option( + "--conda_copy_dependencies", + is_flag=True, + help=("Conda dependency resolution for Galaxy will copy dependencies " + "instead of attempting to link them.") + ) + + +def conda_auto_install_option(): + return click.option( + "--conda_auto_install", + is_flag=True, + help=("Conda dependency resolution for Galaxy will auto install " + "will attempt to install requested but missing packages.") + ) + + +def conda_auto_init_option(): + return click.option( + "--conda_auto_init", + is_flag=True, + help=("Conda dependency resolution for Galaxy will auto install " + "conda itself using miniconda if not availabe on conda_prefix.") + ) + + def required_tool_arg(): """ Decorate click method as requiring the path to a single tool. """ @@ -507,6 +584,15 @@ def shed_target_options(): ) +def conda_target_options(): + return _compose( + conda_prefix_option(), + conda_exec_option(), + conda_debug_option(), + conda_ensure_channels_option(), + ) + + def galaxy_run_options(): return _compose( galaxy_target_options(), @@ -523,6 +609,11 @@ def galaxy_config_options(): tool_dependency_dir_option(), brew_dependency_resolution(), shed_dependency_resolution(), + conda_target_options(), + conda_dependency_resolution(), + conda_copy_dependencies_option(), + conda_auto_install_option(), + conda_auto_init_option(), ) diff --git a/planemo_ext/galaxy/tools/deps/__init__.py b/planemo_ext/galaxy/tools/deps/__init__.py index 2bf6f6eac..05217de07 100644 --- a/planemo_ext/galaxy/tools/deps/__init__.py +++ b/planemo_ext/galaxy/tools/deps/__init__.py @@ -10,8 +10,21 @@ from .resolvers import INDETERMINATE_DEPENDENCY from .resolvers.galaxy_packages import GalaxyPackageDependencyResolver from .resolvers.tool_shed_packages import ToolShedPackageDependencyResolver +from .resolvers.conda import CondaDependencyResolver from galaxy.util import plugin_config +# TODO: Load these from the plugins. Would require a two step initialization of +# DependencyManager - where the plugins are loaded first and then the config +# is parsed and sent through. +EXTRA_CONFIG_KWDS = { + 'conda_prefix': None, + 'conda_exec': None, + 'conda_debug': None, + 'conda_channels': 'r,bioconda', + 'conda_auto_install': False, + 'conda_auto_init': False, +} + def build_dependency_manager( config ): if getattr( config, "use_tool_dependencies", False ): @@ -19,6 +32,8 @@ def build_dependency_manager( config ): 'default_base_path': config.tool_dependency_dir, 'conf_file': config.dependency_resolvers_config_file, } + for key, default_value in EXTRA_CONFIG_KWDS.iteritems(): + dependency_manager_kwds[key] = getattr(config, key, default_value) dependency_manager = DependencyManager( **dependency_manager_kwds ) else: dependency_manager = NullDependencyManager() @@ -49,7 +64,7 @@ class DependencyManager( object ): and should each contain a file 'env.sh' which can be sourced to make the dependency available in the current shell environment. """ - def __init__( self, default_base_path, conf_file=None ): + def __init__( self, default_base_path, conf_file=None, **extra_config ): """ Create a new dependency manager looking for packages under the paths listed in `base_paths`. The default base path is app.config.tool_dependency_dir. @@ -58,6 +73,7 @@ def __init__( self, default_base_path, conf_file=None ): log.warn( "Path '%s' does not exist, ignoring", default_base_path ) if not os.path.isdir( default_base_path ): log.warn( "Path '%s' is not directory, ignoring", default_base_path ) + self.extra_config = extra_config self.default_base_path = os.path.abspath( default_base_path ) self.resolver_classes = self.__resolvers_dict() self.dependency_resolvers = self.__build_dependency_resolvers( conf_file ) @@ -106,6 +122,8 @@ def __default_dependency_resolvers( self ): ToolShedPackageDependencyResolver(self), GalaxyPackageDependencyResolver(self), GalaxyPackageDependencyResolver(self, versionless=True), + CondaDependencyResolver(self), + CondaDependencyResolver(self, versionless=True), ] def __parse_resolver_conf_xml(self, plugin_source): diff --git a/planemo_ext/galaxy/tools/deps/conda_util.py b/planemo_ext/galaxy/tools/deps/conda_util.py new file mode 100644 index 000000000..942e7f82c --- /dev/null +++ b/planemo_ext/galaxy/tools/deps/conda_util.py @@ -0,0 +1,305 @@ +import functools +import hashlib +import os.path +import re +import shutil +from sys import platform as _platform +import tempfile + +import six +import yaml + +from ..deps import commands + +# Not sure there are security concerns, lets just fail fast if we are going +# break shell commands we are building. +SHELL_UNSAFE_PATTERN = re.compile(r"[\s\"']") + +IS_OS_X = _platform == "darwin" + +# BSD 3-clause +CONDA_LICENSE = "http://docs.continuum.io/anaconda/eula" + + +def conda_link(): + if IS_OS_X: + url = "https://repo.continuum.io/miniconda/Miniconda-latest-MacOSX-x86_64.sh" + else: + url = "https://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh" + return url + + +def find_conda_prefix(conda_prefix=None): + """ If supplied conda_prefix is not set, default to the default location + for Miniconda installs. + """ + if conda_prefix is None: + return os.path.join(os.path.expanduser("~"), "miniconda2") + return conda_prefix + + +class CondaContext(object): + + def __init__(self, conda_prefix=None, conda_exec=None, + shell_exec=None, debug=False, ensure_channels=''): + if conda_prefix is None: + conda_prefix = find_conda_prefix(conda_prefix) + self.conda_prefix = conda_prefix + if conda_exec is None: + conda_exec = self._bin("conda") + self.conda_exec = conda_exec + self.debug = debug + self.shell_exec = shell_exec or commands.shell + if ensure_channels: + if not isinstance(ensure_channels, list): + ensure_channels = [c for c in ensure_channels.split(",") if c] + + changed = False + conda_conf = self.load_condarc() + if "channels" not in conda_conf: + conda_conf["channels"] = [] + channels = conda_conf["channels"] + for channel in ensure_channels: + if channel not in channels: + changed = True + channels.append(channel) + + if changed: + self.save_condarc(conda_conf) + + def load_condarc(self): + condarc = self.condarc + if os.path.exists(condarc): + with open(condarc, "r") as f: + return yaml.safe_load(f) + else: + return {"channels": ["defaults"]} + + def save_condarc(self, conf): + condarc = self.condarc + with open(condarc, "w") as f: + return yaml.safe_dump(conf, f) + + @property + def condarc(self): + return os.path.join(os.path.expanduser("~"), ".condarc") + + def command(self, operation, args): + if isinstance(args, list): + args = " ".join(args) + conda_prefix = self.conda_exec + if self.debug: + conda_prefix = "%s --debug" + return "%s %s %s" % (conda_prefix, operation, args) + + def exec_command(self, operation, args): + command = self.command(operation, args) + self.shell_exec(command) + + def exec_create(self, args): + create_base_args = [ + "-y" + ] + create_base_args.extend(args) + return self.exec_command("create", create_base_args) + + def exec_install(self, args): + install_base_args = [ + "-y" + ] + install_base_args.extend(args) + return self.exec_command("install", install_base_args) + + def export_list(self, name, path): + return self.exec_command("list", [ + "--name", name, + "--export", ">", path + ]) + + def env_path(self, env_name): + return os.path.join(self.conda_prefix, "envs", env_name) + + def has_env(self, env_name): + env_path = self.env_path(env_name) + return os.path.isdir(env_path) + + @property + def deactivate(self): + return self._bin("deactivate") + + @property + def activate(self): + return self._bin("activate") + + def _bin(self, name): + return os.path.join(self.conda_prefix, "bin", name) + + +@six.python_2_unicode_compatible +class CondaTarget(object): + + def __init__(self, package, version=None, channel=None): + if SHELL_UNSAFE_PATTERN.search(package) is not None: + raise ValueError("Invalid package [%s] encountered." % package) + self.package = package + if version and SHELL_UNSAFE_PATTERN.search(version) is not None: + raise ValueError("Invalid version [%s] encountered." % version) + self.version = version + if channel and SHELL_UNSAFE_PATTERN.search(channel) is not None: + raise ValueError("Invalid version [%s] encountered." % channel) + self.channel = channel + + def __str__(self): + attributes = "package=%s" % self.package + if self.version is not None: + attributes = "%s,version=%s" % (self.package, self.version) + else: + attributes = "%s,unversioned" % self.package + + if self.channel: + attributes = "%s,channel=%s" % self.channel + + return "CondaTarget[%s]" % attributes + + @property + def package_specifier(self): + """ Return a package specifier as consumed by conda install/create. + """ + if self.version: + return "%s=%s" % (self.package, self.version) + else: + return self.package + + @property + def install_environment(self): + """ The dependency resolution and installation frameworks will + expect each target to be installed it its own environment with + a fixed and predictable name given package and version. + """ + if self.version: + return "__package__%s@__version__%s" % (self.package, self.version) + else: + return "__package__%s@__unversion__" % (self.package) + + +def hash_conda_packages(conda_packages, conda_target=None): + """ Produce a unique hash on supplied packages. + TODO: Ideally we would do this in such a way that preserved environments. + """ + h = hashlib.new('sha256') + for conda_package in conda_packages: + h.update(conda_package.install_environment) + return h.hexdigest() + + +def install_conda(conda_context=None): + conda_context = _ensure_conda_context(conda_context) + download_cmd = " ".join(commands.download_command(conda_link(), quote_url=True)) + download_cmd = "%s > /tmp/conda.bash" % download_cmd + install_cmd = "bash /tmp/conda.bash -b -p '%s'" % conda_context.conda_prefix + full_command = "%s; %s" % (download_cmd, install_cmd) + return conda_context.shell_exec(full_command) + + +def install_conda_target(conda_target, conda_context=None): + """ Install specified target into a its own environment. + """ + conda_context = _ensure_conda_context(conda_context) + create_args = [ + "--name", conda_target.install_environment, # enviornment for package + conda_target.package_specifier, + ] + conda_context.exec_create(create_args) + + +def is_conda_target_installed(conda_target, conda_context=None): + conda_context = _ensure_conda_context(conda_context) + return conda_context.has_env(conda_target.install_environment) + + +def filter_installed_targets(conda_targets, conda_context=None): + conda_context = _ensure_conda_context(conda_context) + installed = functools.partial(is_conda_target_installed, + conda_context=conda_context) + return filter(installed, conda_targets) + + +def build_isolated_environment( + conda_packages, + path=None, + copy=False, + conda_context=None, +): + """ Build a new environment (or reuse an existing one from hashes) + for specified conda packages. + """ + if not isinstance(conda_packages, list): + conda_packages = [conda_packages] + + # Lots we could do in here, hashing, checking revisions, etc... + conda_context = _ensure_conda_context(conda_context) + try: + hash = hash_conda_packages(conda_packages) + tempdir = tempfile.mkdtemp(prefix="jobdeps", suffix=hash) + tempdir_name = os.path.basename(tempdir) + + export_paths = [] + for conda_package in conda_packages: + name = conda_package.install_environment + export_path = os.path.join(tempdir, name) + conda_context.export_list( + name, + export_path + ) + export_paths.append(export_path) + create_args = ["--unknown", "--offline"] + if path is None: + create_args.extend(["--name", tempdir_name]) + else: + create_args.extend(["--prefix", path]) + + if copy: + create_args.append("--copy") + for export_path in export_paths: + create_args.extend([ + "--file", export_path, ">", "/dev/null" + ]) + + if path is not None and os.path.exists(path): + exit_code = conda_context.exec_install(create_args) + else: + exit_code = conda_context.exec_create(create_args) + + return (path or tempdir_name, exit_code) + finally: + shutil.rmtree(tempdir) + + +def requirement_to_conda_targets(requirement, conda_context=None): + conda_target = None + if requirement.type == "package": + conda_target = CondaTarget(requirement.name, + version=requirement.version) + return conda_target + + +def requirements_to_conda_targets(requirements, conda_context=None): + r_to_ct = functools.partial(requirement_to_conda_targets, + conda_context=conda_context) + conda_targets = map(r_to_ct, requirements) + return [c for c in conda_targets if c is not None] + + +def _ensure_conda_context(conda_context): + if conda_context is None: + conda_context = CondaContext() + return conda_context + + +__all__ = [ + 'CondaContext', + 'CondaTarget', + 'install_conda', + 'install_conda_target', + 'requirements_to_conda_targets', +] diff --git a/planemo_ext/galaxy/tools/deps/resolvers/__init__.py b/planemo_ext/galaxy/tools/deps/resolvers/__init__.py index 765e9d4da..2f5e1e6da 100644 --- a/planemo_ext/galaxy/tools/deps/resolvers/__init__.py +++ b/planemo_ext/galaxy/tools/deps/resolvers/__init__.py @@ -15,6 +15,18 @@ def resolve( self, name, version, type, **kwds ): version for instance if the request version is 'default'.) """ + def _get_config_option(self, key, dependency_resolver, default=None, prefix=None, **kwds): + """ Look in resolver-specific settings for option and then fallback to + global settings. + """ + global_key = "%s_%s" % (prefix, key) + if key in kwds: + return kwds.get(key) + elif global_key in dependency_resolver.extra_config: + return dependency_resolver.extra_config.get(global_key) + else: + return default + class Dependency( object ): __metaclass__ = ABCMeta diff --git a/planemo_ext/galaxy/tools/deps/resolvers/conda.py b/planemo_ext/galaxy/tools/deps/resolvers/conda.py new file mode 100644 index 000000000..2df46e51b --- /dev/null +++ b/planemo_ext/galaxy/tools/deps/resolvers/conda.py @@ -0,0 +1,128 @@ +""" +This is still an experimental module and there will almost certainly be backward +incompatible changes coming. +""" + + +import os + +from ..resolvers import DependencyResolver, INDETERMINATE_DEPENDENCY +from ..conda_util import ( + CondaContext, + CondaTarget, + install_conda, + is_conda_target_installed, + install_conda_target, + build_isolated_environment, +) + +DEFAULT_ENSURE_CHANNELS = "r,bioconda" + +import logging +log = logging.getLogger(__name__) + + +class CondaDependencyResolver(DependencyResolver): + resolver_type = "conda" + + def __init__(self, dependency_manager, **kwds): + self.versionless = _string_as_bool(kwds.get('versionless', 'false')) + + def get_option(name): + return self._get_config_option(name, dependency_manager, prefix="conda", **kwds) + + # Conda context options (these define the environment) + conda_prefix = get_option("prefix") + conda_exec = get_option("exec") + debug = _string_as_bool(get_option("debug")) + ensure_channels = get_option("ensure_channels") + if ensure_channels is None: + ensure_channels = DEFAULT_ENSURE_CHANNELS + + conda_context = CondaContext( + conda_prefix=conda_prefix, + conda_exec=conda_exec, + debug=debug, + ensure_channels=ensure_channels, + ) + + # Conda operations options (these define how resolution will occur) + auto_init = _string_as_bool(get_option("auto_init")) + auto_install = _string_as_bool(get_option("auto_install")) + copy_dependencies = _string_as_bool(get_option("copy_dependencies")) + + if auto_init and not os.path.exists(conda_context.conda_prefix): + install_conda(conda_context) + + self.conda_context = conda_context + self.auto_install = auto_install + self.copy_dependencies = copy_dependencies + + def resolve(self, name, version, type, **kwds): + # Check for conda just not being there, this way we can enable + # conda by default and just do nothing in not configured. + if not os.path.isdir(self.conda_context.conda_prefix): + return INDETERMINATE_DEPENDENCY + + if type != "package": + return INDETERMINATE_DEPENDENCY + + job_directory = kwds.get("job_directory", None) + if job_directory is None: + log.warn("Conda dependency resolver not sent job directory.") + return INDETERMINATE_DEPENDENCY + + if self.versionless: + version = None + + conda_target = CondaTarget(name, version=version) + is_installed = is_conda_target_installed( + conda_target, conda_context=self.conda_context + ) + if not is_installed and self.auto_install: + install_conda_target(conda_target) + + # Recheck if installed + is_installed = is_conda_target_installed( + conda_target, conda_context=self.conda_context + ) + + if not is_installed: + return INDETERMINATE_DEPENDENCY + + # Have installed conda_target and job_directory to send it too. + conda_environment = os.path.join(job_directory, "conda-env") + env_path, exit_code = build_isolated_environment( + conda_target, + path=conda_environment, + copy=self.copy_dependencies, + conda_context=self.conda_context, + ) + if not exit_code: + return CondaDepenency( + self.conda_context.activate, + conda_environment + ) + else: + raise Exception("Conda dependency seemingly installed but failed to build job environment.") + + +class CondaDepenency(): + + def __init__(self, activate, environment_path): + self.activate = activate + self.environment_path = environment_path + + def shell_commands(self, requirement): + return """[ "$CONDA_DEFAULT_ENV" = "%s" ] || source %s '%s'""" % ( + self.environment_path, + self.activate, + self.environment_path + ) + + +def _string_as_bool( value ): + return str( value ).lower() == "true" + + +__all__ = ['CondaDependencyResolver'] diff --git a/project_templates/conda_testing/bwa.xml b/project_templates/conda_testing/bwa.xml new file mode 100644 index 000000000..e5dcd1a69 --- /dev/null +++ b/project_templates/conda_testing/bwa.xml @@ -0,0 +1,28 @@ + + + bwa + + + + + $output_1 2>&1 + ]]> + + + + + + + + + + + + + + + + diff --git a/project_templates/conda_testing/bwa_and_samtools.xml b/project_templates/conda_testing/bwa_and_samtools.xml new file mode 100644 index 000000000..f7bb752ff --- /dev/null +++ b/project_templates/conda_testing/bwa_and_samtools.xml @@ -0,0 +1,36 @@ + + + bwa + samtools + + + + + $output_1 2>&1 ; + samtools > $output_2 2>&1 + ]]> + + + + + + + + + + + + + + + + + + + + + + diff --git a/update_galaxy_utils.sh b/update_galaxy_utils.sh new file mode 100755 index 000000000..ed52af637 --- /dev/null +++ b/update_galaxy_utils.sh @@ -0,0 +1,81 @@ +#!/bin/bash + +usage() { +cat << EOF +Usage: ${0##*/} [-i] /path/to/galaxy... +Sync Planemo shared modules to those same modules in Galaxy directory (or vice versa if -i). + +EOF +} + +if [ $# -lt 1 ]; then + usage + exit 1 +fi + +invert=0 +OPTIND=1 +while getopts ":i" opt; do + case "$opt" in + h) + usage + exit 0 + ;; + i) + invert=1 + ;; + '?') + usage >&2 + exit 1 + ;; + esac +done +shift "$((OPTIND-1))" # Shift off the options and optional --. + +PLANEMO_DIRECTORY="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +GALAXY_DIRECTORY=$1 +GALAXY_LIB_DIR=$GALAXY_DIRECTORY/lib/galaxy + +if [ "$invert" -ne "1" ]; +then + + rm -rf $GALAXY_LIB_DIR/objectstore + cp -r $PLANEMO_DIRECTORY/planemo_ext/galaxy/objectstore $GALAXY_LIB_DIR + + rm -rf $GALAXY_LIB_DIR/tools/deps + cp -r $PLANEMO_DIRECTORY/planemo_ext/galaxy/tools/deps $GALAXY_LIB_DIR/tools + + rm -rf $GALAXY_LIB_DIR/jobs/metrics + cp -r $PLANEMO_DIRECTORY/planemo_ext/galaxy/jobs/metrics $GALAXY_LIB_DIR/jobs + + rm -rf $GALAXY_LIB_DIR/tools/linters + cp -r $PLANEMO_DIRECTORY/planemo_ext/galaxy/tools/linters $GALAXY_LIB_DIR/tools + + cp $PLANEMO_DIRECTORY/planemo_ext/galaxy/tools/lint.py $GALAXY_LIB_DIR/tools/lint.py + cp $PLANEMO_DIRECTORY/planemo_ext/galaxy/tools/loader.py $GALAXY_LIB_DIR/tools/loader.py + cp $PLANEMO_DIRECTORY/planemo_ext/galaxy/tools/loader_directory.py $GALAXY_LIB_DIR/tools/loader_directory.py + + cp $PLANEMO_DIRECTORY/planemo_ext/galaxy/util/plugin_config.py $GALAXY_LIB_DIR/util + cp $PLANEMO_DIRECTORY/planemo_ext/galaxy/util/xml_macros.py $GALAXY_LIB_DIR/util + +else + + rm -rf $PLANEMO_DIRECTORY/planemo_ext/galaxy/objectstore + cp -r $GALAXY_LIB_DIR/objectstore $PLANEMO_DIRECTORY/planemo_ext/galaxy + + rm -rf $PLANEMO_DIRECTORY/planemo_ext/galaxy/tools/deps + cp -r $GALAXY_LIB_DIR/tools/deps $PLANEMO_DIRECTORY/planemo_ext/galaxy/tools + + rm -rf $PLANEMO_DIRECTORY/planemo_ext/galaxy/jobs/metrics + cp -r $GALAXY_LIB_DIR/jobs/metrics $PLANEMO_DIRECTORY/planemo_ext/galaxy/jobs + + rm -rf $PLANEMO_DIRECTORY/planemo_ext/galaxy/tools/linters + cp -r $GALAXY_LIB_DIR/tools/linters $PLANEMO_DIRECTORY/planemo_ext/galaxy/tools + + cp $GALAXY_LIB_DIR/tools/lint.py $PLANEMO_DIRECTORY/planemo_ext/galaxy/tools + cp $GALAXY_LIB_DIR/tools/loader.py $PLANEMO_DIRECTORY/planemo_ext/galaxy/tools + cp $GALAXY_LIB_DIR/tools/loader_directory.py $PLANEMO_DIRECTORY/planemo_ext/galaxy/tools + cp $GALAXY_LIB_DIR/util/plugin_config.py $PLANEMO_DIRECTORY/planemo_ext/galaxy/util/ + cp $GALAXY_LIB_DIR/util/xml_macros.py $PLANEMO_DIRECTORY/planemo_ext/galaxy/util/ + +fi