Skip to content

Commit

Permalink
Implement resolve_all on Conda dependency resolver.
Browse files Browse the repository at this point in the history
So this sort of flips the status quo on its face. Everything related to caching, copying, linking, and building environments on the fly should now only apply if both of the following conditions are met (1) there is more than one requirement tag in a tool and (2) not all of them are resolvable exactly by the Conda resolver. For recipes that don't meet these two criteria - the normal case I would suspect going forward - Galaxy will just look for a hashed environment for these requirements built for all the requirements at once whenever the requirements are installed.

Such environments should be much less buggy for several reasons.

- galaxyproject#3299 is solved - in other words Conda is deferred to and if packages have potential conflicts - Conda can choose the right combination of build specifiers to resolve things correctly.
- Environments are built on a per-job basis - this means problems related to linking and copying aren't really an issue and complexity related to caching can be safely ignored.

My guess is we should re-write all the Conda docs to make the other use case seem like a corner case - because hopefully it is now.
  • Loading branch information
jmchilton committed Jan 6, 2017
1 parent 58a4e92 commit 9b28013
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 3 deletions.
21 changes: 18 additions & 3 deletions lib/galaxy/tools/deps/conda_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand All @@ -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):
Expand Down
110 changes: 110 additions & 0 deletions lib/galaxy/tools/deps/resolvers/conda.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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'
Expand Down

0 comments on commit 9b28013

Please sign in to comment.