Mercurial > repos > shellac > sam_consensus_v3
diff env/lib/python3.9/site-packages/galaxy/tool_util/deps/resolvers/conda.py @ 0:4f3585e2f14b draft default tip
"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author | shellac |
---|---|
date | Mon, 22 Mar 2021 18:12:50 +0000 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/lib/python3.9/site-packages/galaxy/tool_util/deps/resolvers/conda.py Mon Mar 22 18:12:50 2021 +0000 @@ -0,0 +1,496 @@ +""" +This is still an experimental module and there will almost certainly be backward +incompatible changes coming. +""" + +import logging +import os +import re + +import galaxy.tool_util.deps.installable +import galaxy.tool_util.deps.requirements +from . import ( + Dependency, + DependencyException, + DependencyResolver, + ListableDependencyResolver, + MappableDependencyResolver, + MultipleDependencyResolver, + NullDependency, + SpecificationPatternDependencyResolver, +) +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, +) + + +DEFAULT_BASE_PATH_DIRECTORY = "_conda" +DEFAULT_CONDARC_OVERRIDE = "_condarc" +# Conda channel order from highest to lowest, following the one used in +# https://github.com/bioconda/bioconda-recipes/blob/master/config.yml +DEFAULT_ENSURE_CHANNELS = "conda-forge,bioconda,defaults" +CONDA_SOURCE_CMD = """[ "$(basename "$CONDA_DEFAULT_ENV")" = "$(basename '{environment_path}')" ] || {{ +MAX_TRIES=3 +COUNT=0 +while [ $COUNT -lt $MAX_TRIES ]; do + . '{activate_path}' '{environment_path}' > conda_activate.log 2>&1 + if [ $? -eq 0 ];then + break + else + let COUNT=COUNT+1 + if [ $COUNT -eq $MAX_TRIES ];then + echo "Failed to activate conda environment! Error was:" + cat conda_activate.log + exit 1 + fi + sleep 10s + fi +done +}} """ + + +log = logging.getLogger(__name__) + + +class CondaDependencyResolver(DependencyResolver, MultipleDependencyResolver, ListableDependencyResolver, SpecificationPatternDependencyResolver, MappableDependencyResolver): + dict_collection_visible_keys = DependencyResolver.dict_collection_visible_keys + ['prefix', 'versionless', 'ensure_channels', 'auto_install', 'auto_init', 'use_local'] + resolver_type = "conda" + config_options = { + 'prefix': None, + 'exec': None, + 'debug': None, + 'ensure_channels': DEFAULT_ENSURE_CHANNELS, + 'auto_install': False, + 'auto_init': True, + 'copy_dependencies': False, + 'use_local': False, + } + _specification_pattern = re.compile(r"https\:\/\/anaconda.org\/\w+\/\w+") + + def __init__(self, dependency_manager, **kwds): + read_only = _string_as_bool(kwds.get('read_only', 'false')) + self.read_only = read_only + self._setup_mapping(dependency_manager, **kwds) + self.versionless = _string_as_bool(kwds.get('versionless', 'false')) + self.dependency_manager = dependency_manager + + def get_option(name): + return dependency_manager.get_resolver_option(self, name, explicit_resolver_options=kwds) + + # Conda context options (these define the environment) + conda_prefix = get_option("prefix") + if conda_prefix is None: + conda_prefix = os.path.join( + dependency_manager.default_base_path, DEFAULT_BASE_PATH_DIRECTORY + ) + conda_prefix = os.path.abspath(conda_prefix) + + self.conda_prefix_parent = os.path.dirname(conda_prefix) + + condarc_override = get_option("condarc_override") + if condarc_override is None: + condarc_override = os.path.join( + dependency_manager.default_base_path, DEFAULT_CONDARC_OVERRIDE + ) + + copy_dependencies = _string_as_bool(get_option("copy_dependencies")) + use_local = _string_as_bool(get_option("use_local")) + conda_exec = get_option("exec") + debug = _string_as_bool(get_option("debug")) + ensure_channels = get_option("ensure_channels") + use_path_exec = get_option("use_path_exec") + if use_path_exec is None: + use_path_exec = USE_PATH_EXEC_DEFAULT + else: + use_path_exec = _string_as_bool(use_path_exec) + 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, + condarc_override=condarc_override, + use_path_exec=use_path_exec, + copy_dependencies=copy_dependencies, + use_local=use_local, + ) + self.use_local = use_local + self.ensure_channels = ensure_channels + + # Conda operations options (these define how resolution will occur) + auto_install = _string_as_bool(get_option("auto_install")) + self.auto_init = _string_as_bool(get_option("auto_init")) + self.conda_context = conda_context + self.disabled = not galaxy.tool_util.deps.installable.ensure_installed(conda_context, install_conda, self.auto_init) + if self.auto_init and not self.disabled: + self.conda_context.ensure_conda_build_installed_if_needed() + self.auto_install = auto_install + self.copy_dependencies = copy_dependencies + + def clean(self, **kwds): + return self.conda_context.exec_clean() + + def uninstall(self, requirements): + """Uninstall requirements installed by install_all or multiple install statements.""" + all_resolved = [r for r in self.resolve_all(requirements) if r.dependency_type] + if not all_resolved: + all_resolved = [self.resolve(requirement) for requirement in requirements] + all_resolved = [r for r in all_resolved if r.dependency_type] + if not all_resolved: + return None + environments = {os.path.basename(dependency.environment_path) for dependency in all_resolved} + return self.uninstall_environments(environments) + + def uninstall_environments(self, environments): + environments = [env if not env.startswith(self.conda_context.envs_path) else os.path.basename(env) for env in environments] + return_codes = [self.conda_context.exec_remove([env]) for env in environments] + final_return_code = 0 + for env, return_code in zip(environments, return_codes): + if return_code == 0: + log.debug("Conda environment '%s' successfully removed." % env) + else: + log.debug("Conda environment '%s' could not be removed." % env) + final_return_code = return_code + return final_return_code + + def install_all(self, conda_targets): + if self.read_only: + return False + + env = self.merged_environment_name(conda_targets) + return_code = install_conda_targets(conda_targets, conda_context=self.conda_context, env_name=env) + 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): + """ + Some combinations of tool requirements need to be resolved all at once, so that Conda can select a compatible + combination of dependencies. This method returns a list of MergedCondaDependency instances (one for each requirement) + if all requirements have been successfully resolved, or an empty list if any of the requirements could not be resolved. + + Parameters specific to this resolver are: + + preserve_python_environment: Boolean, controls whether the python environment should be maintained during job creation for tools + that rely on galaxy being importable. + + install: Controls if `requirements` should be installed. If `install` is True and the requirements are not installed + an attempt is made to install the requirements. If `install` is None requirements will only be installed if + `conda_auto_install` has been activated and the requirements are not yet installed. If `install` is + False will not install requirements. + """ + if len(requirements) == 0: + return [] + + if not os.path.isdir(self.conda_context.conda_prefix): + return [] + + for requirement in requirements: + if requirement.type != "package": + return [] + + ToolRequirements = galaxy.tool_util.deps.requirements.ToolRequirements + expanded_requirements = ToolRequirements([self._expand_requirement(r) for r in requirements]) + if self.versionless: + conda_targets = [CondaTarget(r.name, version=None) for r in expanded_requirements] + else: + conda_targets = [CondaTarget(r.name, version=r.version) for r in expanded_requirements] + + 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) + install = kwds.get('install', None) + if install is None: + # Default behavior, install dependencies if conda_auto_install is active. + install = not is_installed and self.auto_install + elif install: + # Install has been set to True, install if not yet installed. + install = not is_installed + if 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=not self.versionless or requirement.version is None, + name=requirement.name, + version=requirement.version, + preserve_python_environment=preserve_python_environment, + dependency_resolver=self, + ) + 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, requirement, **kwds): + requirement = self._expand_requirement(requirement) + name, version, type = requirement.name, requirement.version, requirement.type + + # 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 NullDependency(version=version, name=name) + + if type != "package": + return NullDependency(version=version, name=name) + + exact = not self.versionless or version is None + if self.versionless: + version = None + + conda_target = CondaTarget(name, version=version) + is_installed = is_conda_target_installed( + conda_target, conda_context=self.conda_context + ) + + preserve_python_environment = kwds.get("preserve_python_environment", False) + + job_directory = kwds.get("job_directory", None) + install = kwds.get('install', None) + if install is None: + install = not is_installed and self.auto_install + elif install: + install = not is_installed + if install: + is_installed = self.install_dependency(name=name, version=version, type=type) + + if not is_installed: + return NullDependency(version=version, name=name) + + # Have installed conda_target and job_directory to send it to. + # If dependency is for metadata generation, store environment in conda-metadata-env + if kwds.get("metadata", False): + conda_env = "conda-metadata-env" + else: + conda_env = "conda-env" + + if job_directory: + conda_environment = os.path.join(job_directory, conda_env) + else: + conda_environment = self.conda_context.env_path(conda_target.install_environment) + + return CondaDependency( + self.conda_context, + conda_environment, + exact, + name, + version, + preserve_python_environment=preserve_python_environment, + ) + + def _expand_requirement(self, requirement): + return self._expand_specs(self._expand_mappings(requirement)) + + def unused_dependency_paths(self, toolbox_requirements_status): + """ + Identify all local environments that are not needed to build requirements_status. + + We try to resolve the requirements, and we note every environment_path that has been taken. + """ + used_paths = set() + for dependencies in toolbox_requirements_status.values(): + for dependency in dependencies: + if dependency.get('dependency_type') == 'conda': + path = os.path.basename(dependency['environment_path']) + used_paths.add(path) + dir_contents = set(os.listdir(self.conda_context.envs_path) if os.path.exists(self.conda_context.envs_path) else []) + unused_paths = dir_contents.difference(used_paths) # New set with paths in dir_contents but not in used_paths + unused_paths = [os.path.join(self.conda_context.envs_path, p) for p in unused_paths] + return unused_paths + + def list_dependencies(self): + for install_target in installed_conda_targets(self.conda_context): + name = install_target.package + version = install_target.version + yield self._to_requirement(name, version) + + def _install_dependency(self, name, version, type, **kwds): + "Returns True on (seemingly) successfull installation" + # should be checked before called + assert not self.read_only + + if type != "package": + log.warning("Cannot install dependencies of type '%s'" % type) + return False + + 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 is_installed: + return is_installed + + return_code = install_conda_target(conda_target, conda_context=self.conda_context) + if return_code != 0: + is_installed = False + else: + # Recheck if installed + is_installed = is_conda_target_installed( + conda_target, conda_context=self.conda_context + ) + if not is_installed: + log.debug(f"Removing failed conda install of {name}, version '{version}'") + cleanup_failed_install(conda_target, conda_context=self.conda_context) + + return is_installed + + @property + 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_resolver'] + dependency_type = 'conda' + + def __init__(self, conda_context, environment_path, exact, name=None, version=None, preserve_python_environment=False, dependency_resolver=None): + 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 + self.dependency_resolver = dependency_resolver + + @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): + 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:'{}/bin' """.format( + self.environment_path, + ) + else: + return CONDA_SOURCE_CMD.format( + activate_path=self.activate, + environment_path=self.environment_path + ) + + +class CondaDependency(Dependency): + dict_collection_visible_keys = Dependency.dict_collection_visible_keys + ['environment_path', 'name', 'version', 'dependency_resolver'] + dependency_type = 'conda' + cacheable = True + + def __init__(self, conda_context, environment_path, exact, name=None, version=None, preserve_python_environment=False, dependency_resolver=None): + 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 + self.dependency_resolver = dependency_resolver + + @property + def exact(self): + return self._exact + + @property + def name(self): + return self._name + + @property + def version(self): + return self._version + + def build_cache(self, cache_path): + self.set_cache_path(cache_path) + self.build_environment() + + def set_cache_path(self, cache_path): + self.cache_path = cache_path + self.environment_path = cache_path + + def build_environment(self): + env_path, exit_code = build_isolated_environment( + CondaTarget(self.name, self.version), + conda_context=self.conda_context, + path=self.environment_path, + copy=self.conda_context.copy_dependencies, + ) + if exit_code: + if len(os.path.abspath(self.environment_path)) > 79: + # TODO: remove this once conda_build version 2 is released and packages have been rebuilt. + raise DependencyException("Conda dependency failed to build job environment. " + "This is most likely a limitation in conda. " + "You can try to shorten the path to the job_working_directory.") + raise DependencyException("Conda dependency seemingly installed but failed to build job environment.") + + def shell_commands(self): + if not self.cache_path: + # Build an isolated environment if not using a cached dependency manager + self.build_environment() + 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:'{}/bin' """.format( + self.environment_path, + ) + else: + return CONDA_SOURCE_CMD.format( + activate_path=self.activate, + environment_path=self.environment_path + ) + + +def _string_as_bool(value): + return str(value).lower() == "true" + + +__all__ = ('CondaDependencyResolver', 'DEFAULT_ENSURE_CHANNELS')