diff --git a/lib/galaxy/tools/deps/conda_util.py b/lib/galaxy/tools/deps/conda_util.py index 496828f99afa..6632bd0fdd6c 100644 --- a/lib/galaxy/tools/deps/conda_util.py +++ b/lib/galaxy/tools/deps/conda_util.py @@ -364,6 +364,17 @@ def install_conda(conda_context=None): os.remove(script_path) +def install_conda_targets(conda_targets, env_name, conda_context=None): + conda_context = _ensure_conda_context(conda_context) + conda_context.ensure_channels_configured() + create_args = [ + "--name", env_name, # enviornment for package + ] + for conda_target in conda_targets: + create_args.append(conda_target.package_specifier) + return conda_context.exec_create(create_args) + + def install_conda_target(conda_target, conda_context=None): """ Install specified target into a its own environment. """ @@ -376,10 +387,14 @@ def install_conda_target(conda_target, conda_context=None): return conda_context.exec_create(create_args) -def cleanup_failed_install(conda_target, conda_context=None): +def cleanup_failed_install_of_environment(env, conda_context=None): conda_context = _ensure_conda_context(conda_context) - if conda_context.has_env(conda_target.install_environment): - conda_context.exec_remove([conda_target.install_environment]) + if conda_context.has_env(env): + conda_context.exec_remove([env]) + + +def cleanup_failed_install(conda_target, conda_context=None): + cleanup_failed_install_of_environment(conda_target.install_environment, conda_context=conda_context) def best_search_result(conda_target, conda_context=None, channels_override=None): diff --git a/lib/galaxy/tools/deps/resolvers/conda.py b/lib/galaxy/tools/deps/resolvers/conda.py index f384d1028e48..dde75896d4d4 100644 --- a/lib/galaxy/tools/deps/resolvers/conda.py +++ b/lib/galaxy/tools/deps/resolvers/conda.py @@ -11,10 +11,13 @@ from ..conda_util import ( build_isolated_environment, cleanup_failed_install, + cleanup_failed_install_of_environment, CondaContext, CondaTarget, + hash_conda_packages, install_conda, install_conda_target, + install_conda_targets, installed_conda_targets, is_conda_target_installed, USE_PATH_EXEC_DEFAULT, @@ -102,6 +105,71 @@ def get_option(name): def clean(self, **kwds): return self.conda_context.exec_clean() + def install_all(self, conda_targets): + env = self.hash_targets_if_needed(conda_targets) + return_code = install_conda_targets(conda_targets, env, conda_context=self.conda_context) + if return_code != 0: + is_installed = False + else: + # Recheck if installed + is_installed = self.conda_context.has_env(env) + + if not is_installed: + log.debug("Removing failed conda install of {}".format(str(conda_targets))) + cleanup_failed_install_of_environment(env, conda_context=self.conda_context) + + return is_installed + + def resolve_all(self, requirements, **kwds): + if len(requirements) == 0: + return False + + if not os.path.isdir(self.conda_context.conda_prefix): + return False + + for requirement in requirements: + if requirement.type != "package": + return False + + conda_targets = [] + for requirement in requirements: + version = requirement.version + if self.versionless: + version = None + + conda_targets.append(CondaTarget(requirement.name, version=version)) + + preserve_python_environment = kwds.get("preserve_python_environment", False) + + env = self.merged_environment_name(conda_targets) + dependencies = [] + + is_installed = self.conda_context.has_env(env) + if not is_installed and self.auto_install: + is_installed = self.install_all(conda_targets) + + if is_installed: + for requirement in requirements: + dependency = MergedCondaDependency( + self.conda_context, + self.conda_context.env_path(env), + exact=self.versionless or requirement.version is None, + name=requirement.name, + version=requirement.version, + preserve_python_environment=preserve_python_environment, + ) + dependencies.append(dependency) + + return dependencies + + def merged_environment_name(self, conda_targets): + if len(conda_targets) > 1: + # For continuity with mulled containers this is kind of nice. + return "mulled-v1-%s" % hash_conda_packages(conda_targets) + else: + assert len(conda_targets) == 1 + return conda_targets[0].install_environment + 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. @@ -193,6 +261,48 @@ def prefix(self): return self.conda_context.conda_prefix +class MergedCondaDependency(Dependency): + dict_collection_visible_keys = Dependency.dict_collection_visible_keys + ['environment_path', 'name', 'version'] + dependency_type = 'conda' + + def __init__(self, conda_context, environment_path, exact, name=None, version=None, preserve_python_environment=False): + self.activate = conda_context.activate + self.conda_context = conda_context + self.environment_path = environment_path + self._exact = exact + self._name = name + self._version = version + self.cache_path = None + self._preserve_python_environment = preserve_python_environment + + @property + def exact(self): + return self._exact + + @property + def name(self): + return self._name + + @property + def version(self): + return self._version + + def shell_commands(self, requirement): + if self._preserve_python_environment: + # On explicit testing the only such requirement I am aware of is samtools - and it seems to work + # fine with just appending the PATH as done below. Other tools may require additional + # variables in the future. + return """export PATH=$PATH:'%s/bin' """ % ( + self.environment_path, + ) + else: + return """[ "$CONDA_DEFAULT_ENV" = "%s" ] || . %s '%s' > conda_activate.log 2>&1 """ % ( + self.environment_path, + self.activate, + self.environment_path + ) + + class CondaDependency(Dependency): dict_collection_visible_keys = Dependency.dict_collection_visible_keys + ['environment_path', 'name', 'version'] dependency_type = 'conda'