diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 09ea1ef3..ef4d8cca 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -9,6 +9,8 @@ Release date: TBD New features ------------ +- Support for julia `Project.toml`, `JuliaProject.toml` and `Manifest.toml` files in :pr:`595` by + :user:`davidanthoff` API changes ----------- diff --git a/docs/source/config_files.rst b/docs/source/config_files.rst index ea7fc9a8..66ed69fb 100644 --- a/docs/source/config_files.rst +++ b/docs/source/config_files.rst @@ -71,15 +71,30 @@ To install your repository like a Python package, you may include a ``setup.py`` file. repo2docker installs ``setup.py`` files by running ``pip install -e .``. +.. _Project.toml: + +``Project.toml`` - Install a Julia environment +============================================== + +A ``Project.toml`` (or ``JuliaProject.toml``) file can specify both the +version of Julia to be used and a list of Julia packages to be installed. +If a ``Manifest.toml`` is present, it will determine the exact versions +of the Julia packages that are installed. + .. _REQUIRE: -``REQUIRE`` - Install a Julia environment -========================================= +``REQUIRE`` - Install a Julia environment (legacy) +================================================== -This specifies a list of Julia packages. To see an example of a -Julia repository with ``REQUIRE`` and ``environment.yml``, -visit `binder-examples/julia-python `_. +A ``REQUIRE`` file can specify both the version of Julia to be used and +which Julia packages should be used. The use of ``REQUIRE`` is only +recommended for pre 1.0 Julia versions. The recommended way of installing +a Julia environment that uses Julia 1.0 or newer is to use a ``Project.toml`` +file. If both a ``REQUIRE`` and a ``Project.toml`` file are detected, +the ``REQUIRE`` file is ignored. To see an example of a Julia repository +with ``REQUIRE`` and ``environment.yml``, visit +`binder-examples/julia-python `_. .. _install.R: @@ -186,7 +201,7 @@ For these cases, we have a special file, ``runtime.txt``. ``runtime.txt`` is only supported when used with environment specifications that do not already support specifying the runtime - (e.g. when using ``environment.yml`` for conda or ``REQUIRE`` for Julia, + (e.g. when using ``environment.yml`` for conda or ``Project.toml`` for Julia, ``runtime.txt`` will be ignored). To use python-2.7: add ``python-2.7`` in runtime.txt file. diff --git a/docs/source/faq.rst b/docs/source/faq.rst index 4d676415..6ad01baa 100644 --- a/docs/source/faq.rst +++ b/docs/source/faq.rst @@ -42,12 +42,9 @@ environment. Julia ~~~~~ -The following versions of Julia are supported (specified in the -:ref:`REQUIRE ` configuration file): - -- 1.0 (added in 0.7) -- 0.7 (added in 0.7) -- 0.6 (default) +All Julia versions since Julia 0.7.0 are supported via a :ref:`Project.toml ` +file, and this is the recommended way to install Julia environments. +Julia versions 0.6.x and earlier are supported via a :ref:`REQUIRE ` file. R ~ diff --git a/docs/source/howto/languages.rst b/docs/source/howto/languages.rst index 7fa3bafb..bca97abd 100644 --- a/docs/source/howto/languages.rst +++ b/docs/source/howto/languages.rst @@ -72,11 +72,11 @@ Julia ===== To build an environment with Julia, include a configuration file called -``REQUIRE``. Each line of this file should include a package that you wish -to have installed with Julia. For example, the following contents of ``REQURE`` -would install the ``PyPlot`` package with your Julia environment.:: - - PyPlot +``Project.toml``. The format of this file is documented at +`the Julia Pkg.jl documentation `_. +To specify a specific version of Julia to install, put a Julia version in the +``Compat`` section of the ``Project.toml`` file, as described +here: https://julialang.github.io/Pkg.jl/stable/compatibility. Languages not covered here ========================== diff --git a/repo2docker/app.py b/repo2docker/app.py index 3f13871b..7fcd170e 100644 --- a/repo2docker/app.py +++ b/repo2docker/app.py @@ -30,7 +30,7 @@ from traitlets.config import Application from . import __version__ from .buildpacks import ( PythonBuildPack, DockerBuildPack, LegacyBinderDockerBuildPack, - CondaBuildPack, JuliaBuildPack, RBuildPack, NixBuildPack + CondaBuildPack, JuliaProjectTomlBuildPack, JuliaRequireBuildPack, RBuildPack, NixBuildPack ) from . import contentproviders from .utils import ByteSpecification, chdir @@ -85,7 +85,8 @@ class Repo2Docker(Application): [ LegacyBinderDockerBuildPack, DockerBuildPack, - JuliaBuildPack, + JuliaProjectTomlBuildPack, + JuliaRequireBuildPack, NixBuildPack, RBuildPack, CondaBuildPack, diff --git a/repo2docker/buildpacks/__init__.py b/repo2docker/buildpacks/__init__.py index 1900a449..73b47bf3 100644 --- a/repo2docker/buildpacks/__init__.py +++ b/repo2docker/buildpacks/__init__.py @@ -1,7 +1,8 @@ from .base import BuildPack, BaseImage from .python import PythonBuildPack from .conda import CondaBuildPack -from .julia import JuliaBuildPack +from .julia import JuliaProjectTomlBuildPack +from .julia import JuliaRequireBuildPack from .docker import DockerBuildPack from .legacy import LegacyBinderDockerBuildPack from .r import RBuildPack diff --git a/repo2docker/buildpacks/julia/__init__.py b/repo2docker/buildpacks/julia/__init__.py index f60b6b69..13d4dcd7 100644 --- a/repo2docker/buildpacks/julia/__init__.py +++ b/repo2docker/buildpacks/julia/__init__.py @@ -1,170 +1,2 @@ -"""Generates a Dockerfile based on an input matrix for Julia""" -import os -from ..python import PythonBuildPack - - -class JuliaBuildPack(PythonBuildPack): - """ - Julia build pack which uses conda. - """ - - minor_julias = { - '0.6': '0.6.4', - '0.7': '0.7.0', - '1.0': '1.0.3', - '1.1': '1.1.0', - } - major_julias = { - '1': '1.1.0', - } - - @property - def julia_version(self): - require = self.binder_path('REQUIRE') - try: - with open(require) as f: - julia_version_line = f.readline().strip() # First line is optionally a julia version - except FileNotFoundError: - julia_version_line = '' - - if not julia_version_line.startswith('julia '): - # not a Julia version line. - # use the default Julia. - self._julia_version = self.minor_julias['0.6'] - return self._julia_version - - julia_version_info = julia_version_line.split(' ', 1)[1].split('.') - julia_version = '' - if len(julia_version_info) == 1: - julia_version = self.major_julias[julia_version_info[0]] - elif len(julia_version_info) == 2: - # get major.minor - julia_version = self.minor_julias['.'.join(julia_version_info)] - else: - # use supplied julia version - julia_version = '.'.join(julia_version_info) - self._julia_version = julia_version - return self._julia_version - - def get_build_env(self): - """Get additional environment settings for Julia and Jupyter - - Returns: - an ordered list of environment setting tuples - - The tuples contain a string of the environment variable name and - a string of the environment setting: - - `JULIA_PATH`: base path where all Julia Binaries and libraries - will be installed - - `JULIA_HOME`: path where all Julia Binaries will be installed - - `JULIA_PKGDIR`: path where all Julia libraries will be installed - - `JULIA_DEPOT_PATH`: path where Julia libraries are installed. - Similar to JULIA_PKGDIR, used in 1.x. - - `JULIA_VERSION`: default version of julia to be installed - - `JUPYTER`: environment variable required by IJulia to point to - the `jupyter` executable - - For example, a tuple may be `('JULIA_VERSION', '0.6.0')`. - - """ - return super().get_build_env() + [ - ('JULIA_PATH', '${APP_BASE}/julia'), - ('JULIA_HOME', '${JULIA_PATH}/bin'), # julia <= 0.6 - ('JULIA_BINDIR', '${JULIA_HOME}'), # julia >= 0.7 - ('JULIA_PKGDIR', '${JULIA_PATH}/pkg'), - ('JULIA_DEPOT_PATH', '${JULIA_PKGDIR}'), # julia >= 0.7 - ('JULIA_VERSION', self.julia_version), - ('JUPYTER', '${NB_PYTHON_PREFIX}/bin/jupyter') - ] - - def get_path(self): - """Adds path to Julia binaries to user's PATH. - - Returns: - an ordered list of path strings. The path to the Julia - executable is added to the list. - - """ - return super().get_path() + ['${JULIA_HOME}'] - - def get_build_scripts(self): - """ - Return series of build-steps common to "ALL" Julia repositories - - All scripts found here should be independent of contents of a - particular repository. - - This creates a directory with permissions for installing julia packages - (from get_assemble_scripts). - - """ - return super().get_build_scripts() + [ - ( - "root", - r""" - mkdir -p ${JULIA_PATH} && \ - curl -sSL "https://julialang-s3.julialang.org/bin/linux/x64/${JULIA_VERSION%[.-]*}/julia-${JULIA_VERSION}-linux-x86_64.tar.gz" | tar -xz -C ${JULIA_PATH} --strip-components 1 - """ - ), - ( - "root", - r""" - mkdir -p ${JULIA_PKGDIR} && \ - chown ${NB_USER}:${NB_USER} ${JULIA_PKGDIR} - """ - ), - ( - "${NB_USER}", - # HACK: Can't seem to tell IJulia to install in sys-prefix - # FIXME: Find way to get it to install under /srv and not $HOME? - r""" - julia -e 'if (VERSION > v"0.7-") using Pkg; else Pkg.init(); end; Pkg.add("IJulia"); using IJulia;' && \ - mv ${HOME}/.local/share/jupyter/kernels/julia-${JULIA_VERSION%[.-]*} ${NB_PYTHON_PREFIX}/share/jupyter/kernels/julia-${JULIA_VERSION%[.-]*} - """ - ) - ] - - def get_assemble_scripts(self): - """ - Return series of build-steps specific to "this" Julia repository - - Precompile all Julia libraries found in the repository's REQUIRE - file. The parent, CondaBuildPack, will add the build steps for - any needed Python packages found in environment.yml. - - """ - require = self.binder_path('REQUIRE') - return super().get_assemble_scripts() + [( - "${NB_USER}", - # Install and pre-compile all libraries if they've opted into it. - # In v0.6, Pkg.resolve() installs all the packages, but in v0.7+, we - # have to manually Pkg.add() each of them (since the REQUIRES file - # format is deprecated). - # The precompliation is done via `using {libraryname}`. - r""" - julia /tmp/install-repo-dependencies.jl "%(require)s" - """ % {"require": require} - # TODO: For some reason, `rm`ing the file fails with permission denied. - # && rm /tmp/install-repo-dependencies.jl - )] - - def get_build_script_files(self): - files = { - 'julia/install-repo-dependencies.jl': '/tmp/install-repo-dependencies.jl', - } - files.update(super().get_build_script_files()) - return files - - def detect(self): - """ - Check if current repo should be built with the Julia Build pack - - super().detect() is not called in this function - it would return - false unless an `environment.yml` is present and we do not want to - require the presence of a `environment.yml` to use Julia. - - Instead we just check if the path to `REQUIRE` exists - - """ - # TODO(nhdaly): Add support for Project.toml here as well. - return os.path.exists(self.binder_path('REQUIRE')) +from .julia_project import JuliaProjectTomlBuildPack +from .julia_require import JuliaRequireBuildPack diff --git a/repo2docker/buildpacks/julia/julia_project.py b/repo2docker/buildpacks/julia/julia_project.py new file mode 100644 index 00000000..e9bd4fb8 --- /dev/null +++ b/repo2docker/buildpacks/julia/julia_project.py @@ -0,0 +1,139 @@ +"""Generates a Dockerfile based on an input matrix for Julia""" +import os +import toml +from ..python import PythonBuildPack +from .julia_semver import find_semver_match + +class JuliaProjectTomlBuildPack(PythonBuildPack): + """ + Julia build pack which uses conda. + """ + + # ALL EXISTING JULIA VERSIONS + # Note that these must remain ordered, in order for the find_semver_match() + # function to behave correctly. + all_julias = [ + "0.7.0", + "1.0.0", "1.0.1", "1.0.2", "1.0.3", + "1.1.0", + ] + + @property + def julia_version(self): + default_julia_version = self.all_julias[-1] + + if os.path.exists(self.binder_path('JuliaProject.toml')): + project_toml = toml.load(self.binder_path('JuliaProject.toml')) + else: + project_toml = toml.load(self.binder_path('Project.toml')) + + if 'compat' in project_toml: + if 'julia' in project_toml['compat']: + julia_version_str = project_toml['compat']['julia'] + + # For Project.toml files, install the latest julia version that + # satisfies the given semver. + _julia_version = find_semver_match(julia_version_str, self.all_julias) + if _julia_version is not None: + return _julia_version + + return default_julia_version + + def get_build_env(self): + """Get additional environment settings for Julia and Jupyter + + Returns: + an ordered list of environment setting tuples + + The tuples contain a string of the environment variable name and + a string of the environment setting: + - `JULIA_PATH`: base path where all Julia Binaries and libraries + will be installed + - `JULIA_BINDIR`: path where all Julia Binaries will be installed + - `JULIA_DEPOT_PATH`: path where Julia libraries are installed. + - `JULIA_VERSION`: default version of julia to be installed + - `JUPYTER`: environment variable required by IJulia to point to + the `jupyter` executable + + For example, a tuple may be `('JULIA_VERSION', '0.6.0')`. + + """ + return super().get_build_env() + [ + ('JULIA_PATH', '${APP_BASE}/julia'), + ('JULIA_BINDIR', '${JULIA_PATH}/bin'), + ('JULIA_DEPOT_PATH', '${JULIA_PATH}/pkg'), + ('JULIA_VERSION', self.julia_version), + ('JUPYTER', '${NB_PYTHON_PREFIX}/bin/jupyter') + ] + + def get_path(self): + """Adds path to Julia binaries to user's PATH. + + Returns: + an ordered list of path strings. The path to the Julia + executable is added to the list. + + """ + return super().get_path() + ['${JULIA_BINDIR}'] + + def get_build_scripts(self): + """ + Return series of build-steps common to "ALL" Julia repositories + + All scripts found here should be independent of contents of a + particular repository. + + This creates a directory with permissions for installing julia packages + (from get_assemble_scripts). + + """ + return super().get_build_scripts() + [ + ( + "root", + r""" + mkdir -p ${JULIA_PATH} && \ + curl -sSL "https://julialang-s3.julialang.org/bin/linux/x64/${JULIA_VERSION%[.-]*}/julia-${JULIA_VERSION}-linux-x86_64.tar.gz" | tar -xz -C ${JULIA_PATH} --strip-components 1 + """ + ), + ( + "root", + r""" + mkdir -p ${JULIA_DEPOT_PATH} && \ + chown ${NB_USER}:${NB_USER} ${JULIA_DEPOT_PATH} + """ + ), + ] + + def get_assemble_scripts(self): + """ + Return series of build-steps specific to "this" Julia repository + + Instantiate and then precompile all packages in the repos julia + environment. + + The parent, CondaBuildPack, will add the build steps for + any needed Python packages found in environment.yml. + """ + return super().get_assemble_scripts() + [ + ( + "${NB_USER}", + r""" + julia -e "using Pkg; Pkg.add(\"IJulia\"); using IJulia; installkernel(\"Julia\", \"--project=${REPO_DIR}\", env=Dict(\"JUPYTER_DATA_DIR\"=>\"${NB_PYTHON_PREFIX}/share/jupyter\"));" && \ + julia --project=${REPO_DIR} -e 'using Pkg; Pkg.instantiate(); pkg"precompile"' + """ + ) + ] + + def detect(self): + """ + Check if current repo should be built with the Julia Build pack + + super().detect() is not called in this function - it would return + false unless an `environment.yml` is present and we do not want to + require the presence of a `environment.yml` to use Julia. + + Instead we just check if the path to `Project.toml` or + `JuliaProject.toml` exists. + + """ + return os.path.exists(self.binder_path('Project.toml')) or os.path.exists(self.binder_path('JuliaProject.toml')) diff --git a/repo2docker/buildpacks/julia/julia_require.py b/repo2docker/buildpacks/julia/julia_require.py new file mode 100644 index 00000000..589562d2 --- /dev/null +++ b/repo2docker/buildpacks/julia/julia_require.py @@ -0,0 +1,170 @@ +"""Generates a Dockerfile based on an input matrix with REQUIRE for legacy Julia""" +import os +from ..python import PythonBuildPack + + +class JuliaRequireBuildPack(PythonBuildPack): + """ + Julia build pack which uses conda and REQUIRE. + """ + + minor_julias = { + '0.6': '0.6.4', + '0.7': '0.7.0', + '1.0': '1.0.3', + '1.1': '1.1.0', + } + major_julias = { + '1': '1.1.0', + } + + @property + def julia_version(self): + require = self.binder_path('REQUIRE') + try: + with open(require) as f: + julia_version_line = f.readline().strip() # First line is optionally a julia version + except FileNotFoundError: + julia_version_line = '' + + if not julia_version_line.startswith('julia '): + # not a Julia version line. + # use the default Julia. + self._julia_version = self.minor_julias['0.6'] + return self._julia_version + + julia_version_info = julia_version_line.split(' ', 1)[1].split('.') + julia_version = '' + if len(julia_version_info) == 1: + julia_version = self.major_julias[julia_version_info[0]] + elif len(julia_version_info) == 2: + # get major.minor + julia_version = self.minor_julias['.'.join(julia_version_info)] + else: + # use supplied julia version + julia_version = '.'.join(julia_version_info) + self._julia_version = julia_version + return self._julia_version + + def get_build_env(self): + """Get additional environment settings for Julia and Jupyter + + Returns: + an ordered list of environment setting tuples + + The tuples contain a string of the environment variable name and + a string of the environment setting: + - `JULIA_PATH`: base path where all Julia Binaries and libraries + will be installed + - `JULIA_HOME`: path where all Julia Binaries will be installed + - `JULIA_PKGDIR`: path where all Julia libraries will be installed + - `JULIA_DEPOT_PATH`: path where Julia libraries are installed. + Similar to JULIA_PKGDIR, used in 1.x. + - `JULIA_VERSION`: default version of julia to be installed + - `JUPYTER`: environment variable required by IJulia to point to + the `jupyter` executable + + For example, a tuple may be `('JULIA_VERSION', '0.6.0')`. + + """ + return super().get_build_env() + [ + ('JULIA_PATH', '${APP_BASE}/julia'), + ('JULIA_HOME', '${JULIA_PATH}/bin'), # julia <= 0.6 + ('JULIA_BINDIR', '${JULIA_HOME}'), # julia >= 0.7 + ('JULIA_PKGDIR', '${JULIA_PATH}/pkg'), + ('JULIA_DEPOT_PATH', '${JULIA_PKGDIR}'), # julia >= 0.7 + ('JULIA_VERSION', self.julia_version), + ('JUPYTER', '${NB_PYTHON_PREFIX}/bin/jupyter') + ] + + def get_path(self): + """Adds path to Julia binaries to user's PATH. + + Returns: + an ordered list of path strings. The path to the Julia + executable is added to the list. + + """ + return super().get_path() + ['${JULIA_HOME}'] + + def get_build_scripts(self): + """ + Return series of build-steps common to "ALL" Julia repositories + + All scripts found here should be independent of contents of a + particular repository. + + This creates a directory with permissions for installing julia packages + (from get_assemble_scripts). + + """ + return super().get_build_scripts() + [ + ( + "root", + r""" + mkdir -p ${JULIA_PATH} && \ + curl -sSL "https://julialang-s3.julialang.org/bin/linux/x64/${JULIA_VERSION%[.-]*}/julia-${JULIA_VERSION}-linux-x86_64.tar.gz" | tar -xz -C ${JULIA_PATH} --strip-components 1 + """ + ), + ( + "root", + r""" + mkdir -p ${JULIA_PKGDIR} && \ + chown ${NB_USER}:${NB_USER} ${JULIA_PKGDIR} + """ + ), + ( + "${NB_USER}", + # HACK: Can't seem to tell IJulia to install in sys-prefix + # FIXME: Find way to get it to install under /srv and not $HOME? + r""" + julia -e 'if (VERSION > v"0.7-") using Pkg; else Pkg.init(); end; Pkg.add("IJulia"); using IJulia;' && \ + mv ${HOME}/.local/share/jupyter/kernels/julia-${JULIA_VERSION%[.-]*} ${NB_PYTHON_PREFIX}/share/jupyter/kernels/julia-${JULIA_VERSION%[.-]*} + """ + ) + ] + + def get_assemble_scripts(self): + """ + Return series of build-steps specific to "this" Julia repository + + Precompile all Julia libraries found in the repository's REQUIRE + file. The parent, CondaBuildPack, will add the build steps for + any needed Python packages found in environment.yml. + + """ + require = self.binder_path('REQUIRE') + return super().get_assemble_scripts() + [( + "${NB_USER}", + # Install and pre-compile all libraries if they've opted into it. + # In v0.6, Pkg.resolve() installs all the packages, but in v0.7+, we + # have to manually Pkg.add() each of them (since the REQUIRES file + # format is deprecated). + # The precompliation is done via `using {libraryname}`. + r""" + julia /tmp/install-repo-dependencies.jl "%(require)s" + """ % {"require": require} + # TODO: For some reason, `rm`ing the file fails with permission denied. + # && rm /tmp/install-repo-dependencies.jl + )] + + def get_build_script_files(self): + files = { + 'julia/install-repo-dependencies.jl': '/tmp/install-repo-dependencies.jl', + } + files.update(super().get_build_script_files()) + return files + + def detect(self): + """ + Check if current repo should be built with the Julia Legacy Build pack + + super().detect() is not called in this function - it would return + false unless an `environment.yml` is present and we do not want to + require the presence of a `environment.yml` to use Julia. + + Instead we just check if the path to `REQUIRE` exists and that there is + no julia 1.0 style environment + + """ + return os.path.exists(self.binder_path('REQUIRE')) and not(os.path.exists(self.binder_path('Project.toml')) or os.path.exists(self.binder_path('JuliaProject.toml'))) diff --git a/repo2docker/buildpacks/julia/julia_semver.py b/repo2docker/buildpacks/julia/julia_semver.py new file mode 100644 index 00000000..590d59f9 --- /dev/null +++ b/repo2docker/buildpacks/julia/julia_semver.py @@ -0,0 +1,119 @@ +# This file implements the julia-specific logic for handling SemVer (Semantic +# Versioning) strings in .toml files. +# +# It uses the python "semver" package to do most version string comparisons, but +# the places where julia's SemVer handling differs from the semver package have +# been implemented directly. +# +# Here, we use tuples to represent a Version, and functors as "matchers". The matcher +# functors take a version string and return true if it passes its constraints. + +import re +import semver + +# Main algorithm: +# Create an AbstractMatcher instance from the constraint string, and check it +# against each version in the versions_list, returning the first success. +def find_semver_match(constraint, versions_list): + matcher = create_semver_matcher(constraint) + for vstr in reversed(versions_list): + if matcher.match(str_to_version(vstr)): + return vstr + return None + +def str_to_version(vstr): + return tuple([int(n) for n in vstr.split('.')]) + +# --- Matcher interface ------------------------------------------- + +class AbstractMatcher: + def match(self, v): + pass + +class SemverMatcher(AbstractMatcher): + """ Match a version tuple to a given constraint_str using the `semver` package. """ + def __init__(self, constraint_str): + self.constraint_str = constraint_str + def match(self, v): + while len(v) < 3: + v = v+(0,) + v_str = '.'.join(map(str, v)) + return semver.match(v_str, self.constraint_str) + def __repr__(self): + return self.constraint_str + +# --- Custom matcher for julia-specific SemVer handling: --------- + +from enum import Enum +class Exclusivity(Enum): + EXCLUSIVE = 1 + INCLUSIVE = 2 + +class VersionRange(AbstractMatcher): + """ Match a version tuple between lower and upper bounds. """ + def __init__(self, lower, upper, upper_exclusivity): + self.lower = lower + self.upper = upper + self.upper_exclusivity = upper_exclusivity + def match(self, v): + if self.upper_exclusivity == Exclusivity.EXCLUSIVE: + return self.lower <= v < self.upper + else: + return self.lower <= v <= self.upper + def __repr__(self): + return ("["+".".join(map(str, self.lower)) +"-"+ ".".join(map(str, self.upper)) + + (")" if self.upper_exclusivity == Exclusivity.EXCLUSIVE else "]")) + +# Helpers +def major(v): return v[0] +def minor(v): return v[1] if len(v) >= 2 else 0 +def patch(v): return v[2] if len(v) >= 3 else 0 + +# --- main constraint parser function ------------------------------------ + +def create_semver_matcher(constraint_str): + """ + Returns a derived-class instance of AbstractMatcher that matches version + tuples against the provided constraint_str. + """ + constraint_str = constraint_str.strip() + first_digit = re.search(r"\d", constraint_str) + if not first_digit: + # Invalid version string (no numbers in it) + return "" + constraint = str_to_version(constraint_str[first_digit.start():]) + + comparison_symbol = constraint_str[0:first_digit.start()].strip() + + # Default to "^" search if no matching mode specified (up to next major version) + if (first_digit.start() == 0) or (comparison_symbol == "^"): + if major(constraint) == 0: + # Also, julia treats pre-1.0 releases specially, as if the first + # non-zero number is actually a major number: + # https://docs.julialang.org/en/latest/stdlib/Pkg/#Caret-specifiers-1 + # So we need to handle it separately by bumping the first non-zero number. + for i,n in enumerate(constraint): + if n != 0 or i == len(constraint)-1: # (using the last existing number handles situations like "^0.0" or "^0") + upper = constraint[0:i] + (n+1,) + break + return VersionRange(constraint, upper, Exclusivity.EXCLUSIVE) + else: + return VersionRange(constraint, (major(constraint)+1,), Exclusivity.EXCLUSIVE) + + # '~' matching (only allowed to bump the last present number by one) + if (comparison_symbol == "~"): + return VersionRange(constraint, constraint[:-1] +(constraint[-1]+1,), Exclusivity.INCLUSIVE) + + # Use semver package's comparisons for everything else: + + # semver requires three version numbers + if len(constraint) < 3: + while len(constraint) < 3: + constraint = constraint+(0,) + constraint_str = constraint_str[0:first_digit.start()] + ".".join(map(str, constraint)) + + # Convert special comparison strings to format accepted by `semver` library. + constraint_str = constraint_str.replace("≥", ">=").replace("≤", "<=") + constraint_str = re.sub(r"(^|\b)=\b", "==", constraint_str) + + return SemverMatcher(constraint_str) diff --git a/setup.py b/setup.py index 6997f66f..52b2eebf 100644 --- a/setup.py +++ b/setup.py @@ -18,6 +18,8 @@ setup( 'escapism', 'jinja2', 'ruamel.yaml>=0.15', + 'toml', + 'semver', ], python_requires='>=3.4', author='Project Jupyter Contributors', diff --git a/tests/julia/julia_version-1.0.2/Project.toml b/tests/julia/julia_version-1.0.2/Project.toml new file mode 100644 index 00000000..c2e6033b --- /dev/null +++ b/tests/julia/julia_version-1.0.2/Project.toml @@ -0,0 +1,5 @@ +[deps] +IteratorInterfaceExtensions = "82899510-4779-5014-852e-03e436cf321d" + +[compat] +julia = "=1.0.2" diff --git a/tests/julia/julia_version-1.0.2/README.rst b/tests/julia/julia_version-1.0.2/README.rst new file mode 100644 index 00000000..94123768 --- /dev/null +++ b/tests/julia/julia_version-1.0.2/README.rst @@ -0,0 +1,4 @@ +Julia - Project.toml: test version for julia v1.0.2 +--------------------------------------------------- + +Test a Project.toml file for julia. diff --git a/tests/julia/julia_version-1.0.2/verify b/tests/julia/julia_version-1.0.2/verify new file mode 100755 index 00000000..e9168361 --- /dev/null +++ b/tests/julia/julia_version-1.0.2/verify @@ -0,0 +1,20 @@ +#!/usr/bin/env julia +# Verify the version: +if VERSION != v"1.0.2" + exit(1) +end + +using Pkg +pkg"activate ." + +try + # Test that the package was installed. + using IteratorInterfaceExtensions + + # Verify that the environment variables are set correctly for julia 1.0+ + @assert "julia" ∈ readdir(Sys.BINDIR) +catch + exit(1) +end + +exit(0) diff --git a/tests/julia/julia_version-default/Project.toml b/tests/julia/julia_version-default/Project.toml new file mode 100644 index 00000000..ee7dad5c --- /dev/null +++ b/tests/julia/julia_version-default/Project.toml @@ -0,0 +1,2 @@ +[deps] +IteratorInterfaceExtensions = "82899510-4779-5014-852e-03e436cf321d" diff --git a/tests/julia/julia_version-default/README.rst b/tests/julia/julia_version-default/README.rst new file mode 100644 index 00000000..563f676e --- /dev/null +++ b/tests/julia/julia_version-default/README.rst @@ -0,0 +1,4 @@ +Julia - Project.toml: test version for julia v1.1 +------------------------------------------------- + +Test a Project.toml file for julia. diff --git a/tests/julia/julia_version-default/verify b/tests/julia/julia_version-default/verify new file mode 100755 index 00000000..3518e515 --- /dev/null +++ b/tests/julia/julia_version-default/verify @@ -0,0 +1,20 @@ +#!/usr/bin/env julia +# Verify the version: +if VERSION < v"1.1.0" + exit(1) +end + +using Pkg +pkg"activate ." + +try + # Test that the package was installed. + using IteratorInterfaceExtensions + + # Verify that the environment variables are set correctly for julia 1.0+ + @assert "julia" ∈ readdir(Sys.BINDIR) +catch + exit(1) +end + +exit(0) diff --git a/tests/julia/julia_version-0.6.3/README.rst b/tests/julia/julialegacy_version-0.6.3/README.rst similarity index 100% rename from tests/julia/julia_version-0.6.3/README.rst rename to tests/julia/julialegacy_version-0.6.3/README.rst diff --git a/tests/julia/julia_version-0.6.3/REQUIRE b/tests/julia/julialegacy_version-0.6.3/REQUIRE similarity index 100% rename from tests/julia/julia_version-0.6.3/REQUIRE rename to tests/julia/julialegacy_version-0.6.3/REQUIRE diff --git a/tests/julia/julia_version-0.6.3/verify b/tests/julia/julialegacy_version-0.6.3/verify similarity index 100% rename from tests/julia/julia_version-0.6.3/verify rename to tests/julia/julialegacy_version-0.6.3/verify diff --git a/tests/julia/julia_version-1.0/README.rst b/tests/julia/julialegacy_version-1.0/README.rst similarity index 100% rename from tests/julia/julia_version-1.0/README.rst rename to tests/julia/julialegacy_version-1.0/README.rst diff --git a/tests/julia/julia_version-1.0/REQUIRE b/tests/julia/julialegacy_version-1.0/REQUIRE similarity index 100% rename from tests/julia/julia_version-1.0/REQUIRE rename to tests/julia/julialegacy_version-1.0/REQUIRE diff --git a/tests/julia/julia_version-1.0/verify b/tests/julia/julialegacy_version-1.0/verify similarity index 100% rename from tests/julia/julia_version-1.0/verify rename to tests/julia/julialegacy_version-1.0/verify diff --git a/tests/julia/julia_version-1.1/README.rst b/tests/julia/julialegacy_version-1.1/README.rst similarity index 100% rename from tests/julia/julia_version-1.1/README.rst rename to tests/julia/julialegacy_version-1.1/README.rst diff --git a/tests/julia/julia_version-1.1/REQUIRE b/tests/julia/julialegacy_version-1.1/REQUIRE similarity index 100% rename from tests/julia/julia_version-1.1/REQUIRE rename to tests/julia/julialegacy_version-1.1/REQUIRE diff --git a/tests/julia/julia_version-1.1/verify b/tests/julia/julialegacy_version-1.1/verify similarity index 100% rename from tests/julia/julia_version-1.1/verify rename to tests/julia/julialegacy_version-1.1/verify diff --git a/tests/julia/julia_version-1/README.rst b/tests/julia/julialegacy_version-1/README.rst similarity index 100% rename from tests/julia/julia_version-1/README.rst rename to tests/julia/julialegacy_version-1/README.rst diff --git a/tests/julia/julia_version-1/REQUIRE b/tests/julia/julialegacy_version-1/REQUIRE similarity index 100% rename from tests/julia/julia_version-1/REQUIRE rename to tests/julia/julialegacy_version-1/REQUIRE diff --git a/tests/julia/julia_version-1/verify b/tests/julia/julialegacy_version-1/verify similarity index 100% rename from tests/julia/julia_version-1/verify rename to tests/julia/julialegacy_version-1/verify