diff --git a/README.md b/README.md index 692abf6c..fada45e8 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ Valid options for each software package are the keyword arguments for the class | | use_installer | If true, use FSL's Python installer. Only valid on CentOS images. | | **MINC** | version* | 1.9.15 | | **Miniconda** | env_name* | Name of this conda environment. | +| | yaml_file | Environment specification file. Can be path on host or URL. | | | conda_install | Packages to install with conda. e.g., `conda_install="python=3.6 numpy traits"` | | | pip_install | Packages to install with pip. | | | conda_opts | Command-line options to pass to [`conda create`](https://conda.io/docs/commands/conda-create.html). e.g., `conda_opts="-c vida-nyu"` | diff --git a/neurodocker/interfaces/miniconda.py b/neurodocker/interfaces/miniconda.py index c858d156..d655f846 100644 --- a/neurodocker/interfaces/miniconda.py +++ b/neurodocker/interfaces/miniconda.py @@ -8,7 +8,7 @@ import logging import posixpath -from neurodocker.utils import _indent_pkgs, check_url, indent +from neurodocker.utils import _indent_pkgs, check_url, indent, is_url logger = logging.getLogger(__name__) @@ -23,6 +23,8 @@ class Miniconda(object): Name to give this environment. pkg_manager : {'apt', 'yum'} Linux package manager. + yaml_file : path-like or url-like + Conda environment specification file. conda_install : str or list or tuple Packages to install using `conda`, including Python. Follow the syntax for `conda install`. For example, the input ['numpy=1.12', 'scipy'] is @@ -55,10 +57,12 @@ class Miniconda(object): INSTALLED = False INSTALL_PATH = "/opt/conda" - def __init__(self, env_name, pkg_manager, conda_install=None, - pip_install=None, conda_opts=None, pip_opts=None, - add_to_path=False, miniconda_verion='latest', check_urls=True): + def __init__(self, env_name, pkg_manager, yaml_file=None, + conda_install=None, pip_install=None, conda_opts=None, + pip_opts=None, add_to_path=False, miniconda_verion='latest', + check_urls=True): self.env_name = env_name + self.yaml_file = yaml_file self.pkg_manager = pkg_manager self.conda_install = conda_install self.pip_install = pip_install @@ -68,8 +72,15 @@ def __init__(self, env_name, pkg_manager, conda_install=None, self.miniconda_verion = miniconda_verion self.check_urls = check_urls + self._check_args() self.cmd = self._create_cmd() + def _check_args(self): + if self.yaml_file and (self.conda_install is not None + or self.pip_install is not None): + raise ValueError("Packages cannot be installed while creating an" + " environment from a yaml file.") + def _create_cmd(self): cmds = [] comment = ("#------------------" @@ -80,13 +91,16 @@ def _create_cmd(self): cmds.append(self.install_miniconda()) cmds.append('') - create = not (self.env_name in Miniconda.created_envs) + create = self.env_name not in Miniconda.created_envs _comment_base = "Create" if create else "Update" comment = ("#-------------------------" "\n# {} conda environment" "\n#-------------------------").format(_comment_base) cmds.append(comment) - cmds.append(self.conda_and_pip_install(create=create)) + if self.yaml_file is not None: + cmds.append(self.create_from_yaml()) + else: + cmds.append(self.conda_and_pip_install(create=create)) return "\n".join(cmds) @@ -120,6 +134,34 @@ def install_miniconda(self): return "\n".join((env_cmd, cmd)) + def create_from_yaml(self): + """Return Dockerfile instructions to create conda environment from + a YAML file. + """ + tmp_yml = "/tmp/environment.yml" + cmd = ("conda env create -q --name {n} --file {tmp}" + "\n&& rm -f {tmp}") + + if is_url(self.yaml_file): + get_file = "curl -sSL {f} > {tmp}" + cmd = get_file + "\n&& " + cmd + if self.check_urls: + check_url(self.yaml_file) + cmd = indent("RUN", cmd) + else: + get_file = 'COPY ["{f}", "{tmp}"]' + cmd = indent("RUN", cmd) + cmd = "\n".join((get_file, cmd)) + + cmd = cmd.format(n=self.env_name, f=self.yaml_file, tmp=tmp_yml) + + if self.add_to_path: + bin_path = posixpath.join(Miniconda.INSTALL_PATH, 'envs', + self.env_name, 'bin') + env_cmd = "ENV PATH={}:$PATH".format(bin_path) + return "\n".join((cmd, env_cmd)) + return cmd + def conda_and_pip_install(self, create=True): """Return Dockerfile instructions to create conda environment with desired version of Python and desired conda and pip packages. diff --git a/neurodocker/utils.py b/neurodocker/utils.py index d61828b6..aa957d89 100644 --- a/neurodocker/utils.py +++ b/neurodocker/utils.py @@ -112,11 +112,21 @@ def _namespace_to_specs(namespace): specs = {'pkg_manager': namespace.pkg_manager, 'check_urls': namespace.check_urls, - 'instructions': instructions,} + 'instructions': instructions, } return specs +def is_url(string): + try: + from urllib.parse import urlparse # Python 3 + except ImportError: + from urlparse import urlparse # Python 2 + + result = urlparse(string) + return (result.scheme and result.netloc) + + def check_url(url, timeout=5, **kwargs): """Return true if `url` is returns a status code < 400. Otherwise, raise an error. `kwargs` are arguments for `requests.head()`.