diff --git a/easybuild/easyblocks/generic/juliapackage.py b/easybuild/easyblocks/generic/juliapackage.py new file mode 100644 index 00000000000..d68fb249ff7 --- /dev/null +++ b/easybuild/easyblocks/generic/juliapackage.py @@ -0,0 +1,166 @@ +## +# Copyright 2022-2022 Vrije Universiteit Brussel +# +# This file is part of EasyBuild, +# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en), +# with support of Ghent University (http://ugent.be/hpc), +# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be), +# Flemish Research Foundation (FWO) (http://www.fwo.be/en) +# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en). +# +# https://github.com/easybuilders/easybuild +# +# EasyBuild is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation v2. +# +# EasyBuild is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with EasyBuild. If not, see . +## +""" +EasyBuild support for Julia Packages, implemented as an easyblock + +@author: Alex Domingo (Vrije Universiteit Brussel) +""" +import os + +from distutils.version import LooseVersion + +import easybuild.tools.environment as env +from easybuild.framework.easyconfig import CUSTOM +from easybuild.framework.extensioneasyblock import ExtensionEasyBlock +from easybuild.tools.build_log import EasyBuildError +from easybuild.tools.modules import get_software_root, get_software_version +from easybuild.tools.filetools import copy_dir +from easybuild.tools.run import run_cmd + +EXTS_FILTER_JULIA_PACKAGES = ("julia -e 'using %(ext_name)s'", "") + + +class JuliaPackage(ExtensionEasyBlock): + """Builds and installs Julia Packages.""" + + @staticmethod + def extra_options(extra_vars=None): + """Extra easyconfig parameters specific to JuliaPackage.""" + extra_vars = ExtensionEasyBlock.extra_options(extra_vars=extra_vars) + extra_vars.update({ + 'download_pkg_deps': [ + False, "Let Julia download and bundle all needed dependencies for this installation", CUSTOM + ], + }) + return extra_vars + + def set_pkg_offline(self): + """Enable offline mode of Julia Pkg""" + if get_software_root('Julia') is None: + raise EasyBuildError("Julia not included as dependency!") + + if not self.cfg['download_pkg_deps']: + julia_version = get_software_version('Julia') + if LooseVersion(julia_version) >= LooseVersion('1.5'): + # Enable offline mode of Julia Pkg + # https://pkgdocs.julialang.org/v1/api/#Pkg.offline + env.setvar('JULIA_PKG_OFFLINE', 'true') + else: + errmsg = ( + "Cannot set offline mode in Julia v%s (needs Julia >= 1.5). " + "Enable easyconfig option 'download_pkg_deps' to allow installation " + "with any extra downloaded dependencies." + ) + raise EasyBuildError(errmsg, julia_version) + + def prepare_step(self, *args, **kwargs): + """Prepare for installing Julia package.""" + super(JuliaPackage, self).prepare_step(*args, **kwargs) + self.set_pkg_offline() + + def configure_step(self): + """No separate configuration for JuliaPackage.""" + pass + + def build_step(self): + """No separate build procedure for JuliaPackage.""" + pass + + def test_step(self): + """No separate (standard) test procedure for JuliaPackage.""" + pass + + def install_step(self): + """Install Julia package with Pkg""" + + # prepend installation directory to Julia DEPOT_PATH + # see https://docs.julialang.org/en/v1/manual/environment-variables/#JULIA_DEPOT_PATH + depot_path = os.getenv('JULIA_DEPOT_PATH') + if depot_path is not None: + depot_path = ':'.join([self.installdir, depot_path]) + env.setvar('JULIA_DEPOT_PATH', depot_path) + + # command sequence for Julia.Pkg + julia_pkg_cmd = ['using Pkg'] + if os.path.isdir(os.path.join(self.start_dir, '.git')): + # sources from git repos can be installed as any remote package + self.log.debug('Installing Julia package in normal mode (Pkg.add)') + + julia_pkg_cmd.extend([ + # install package from local path preserving existing dependencies + 'Pkg.add(url="%s"; preserve=Pkg.PRESERVE_ALL)' % self.start_dir, + ]) + else: + # plain sources have to be installed in develop mode + # copy sources to install directory and install + self.log.debug('Installing Julia package in develop mode (Pkg.develop)') + + install_pkg_path = os.path.join(self.installdir, 'packages', self.name) + copy_dir(self.start_dir, install_pkg_path) + + julia_pkg_cmd.extend([ + 'Pkg.develop(PackageSpec(path="%s"))' % install_pkg_path, + 'Pkg.build("%s")' % self.name, + ]) + + julia_pkg_cmd = ';'.join(julia_pkg_cmd) + cmd = ' '.join([ + self.cfg['preinstallopts'], + "julia -e '%s'" % julia_pkg_cmd, + self.cfg['installopts'], + ]) + (out, _) = run_cmd(cmd, log_all=True, simple=False) + + return out + + def run(self): + """Install Julia package as an extension.""" + + if not self.src: + errmsg = "No source found for Julia package %s, required for installation. (src: %s)" + raise EasyBuildError(errmsg, self.name, self.src) + ExtensionEasyBlock.run(self, unpack_src=True) + + self.set_pkg_offline() + self.install_step() + + def sanity_check_step(self, *args, **kwargs): + """Custom sanity check for JuliaPackage""" + + pkg_dir = os.path.join('packages', self.name) + + custom_paths = { + 'files': [], + 'dirs': [pkg_dir], + } + kwargs.update({'custom_paths': custom_paths}) + + return ExtensionEasyBlock.sanity_check_step(self, EXTS_FILTER_JULIA_PACKAGES, *args, **kwargs) + + def make_module_extra(self): + """Prepend installation directory to JULIA_DEPOT_PATH in module file.""" + txt = super(JuliaPackage, self).make_module_extra() + txt += self.module_generator.prepend_paths('JULIA_DEPOT_PATH', ['']) + return txt diff --git a/test/easyblocks/module.py b/test/easyblocks/module.py index 034dc65961a..6d8bc1c7298 100644 --- a/test/easyblocks/module.py +++ b/test/easyblocks/module.py @@ -42,6 +42,7 @@ from easybuild.base import fancylogger from easybuild.base.testing import TestCase from easybuild.easyblocks.generic.gopackage import GoPackage +from easybuild.easyblocks.generic.juliapackage import JuliaPackage from easybuild.easyblocks.generic.intelbase import IntelBase from easybuild.easyblocks.generic.pythonbundle import PythonBundle from easybuild.easyblocks.gcc import EB_GCC @@ -286,6 +287,11 @@ def template_module_only_test(self, easyblock, name, version='1.3.2', extra_txt= os.environ['EBROOTGO'] = '/fake/install/prefix/Go/1.14' os.environ['EBVERSIONGO'] = '1.14' + elif app_class == JuliaPackage: + # $EBROOTJULIA must be set for JuliaPackage easyblock + os.environ['EBROOTJULIA'] = '/fake/install/prefix/Julia/1.6.7' + os.environ['EBVERSIONJULIA'] = '1.6.7' + elif app_class == EB_OpenFOAM: # proper toolchain must be used for OpenFOAM(-Extend), to determine value to set for $WM_COMPILER write_file(os.path.join(tmpdir, 'GCC', '4.9.3-2.25'), '\n'.join([