|
| 1 | +#!/usr/bin/env python |
| 2 | +# # |
| 3 | +# Copyright 2015-2015 Ghent University |
| 4 | +# |
| 5 | +# This file is part of EasyBuild, |
| 6 | +# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en), |
| 7 | +# with support of Ghent University (http://ugent.be/hpc), |
| 8 | +# the Flemish Supercomputer Centre (VSC) (https://vscentrum.be/nl/en), |
| 9 | +# the Hercules foundation (http://www.herculesstichting.be/in_English) |
| 10 | +# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en). |
| 11 | +# |
| 12 | +# http://github.com/hpcugent/easybuild |
| 13 | +# |
| 14 | +# EasyBuild is free software: you can redistribute it and/or modify |
| 15 | +# it under the terms of the GNU General Public License as published by |
| 16 | +# the Free Software Foundation v2. |
| 17 | +# |
| 18 | +# EasyBuild is distributed in the hope that it will be useful, |
| 19 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 20 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 21 | +# GNU General Public License for more details. |
| 22 | +# |
| 23 | +# You should have received a copy of the GNU General Public License |
| 24 | +# along with EasyBuild. If not, see <http://www.gnu.org/licenses/>. |
| 25 | +# # |
| 26 | +""" |
| 27 | +Support for including additional Python modules, for easyblocks, module naming schemes and toolchains. |
| 28 | +
|
| 29 | +@author: Kenneth Hoste (Ghent University) |
| 30 | +""" |
| 31 | +import os |
| 32 | +import sys |
| 33 | +from vsc.utils import fancylogger |
| 34 | + |
| 35 | +from easybuild.tools.build_log import EasyBuildError |
| 36 | +from easybuild.tools.filetools import expand_glob_paths, symlink |
| 37 | +# these are imported just to we can reload them later |
| 38 | +import easybuild.tools.module_naming_scheme |
| 39 | +import easybuild.toolchains |
| 40 | +import easybuild.toolchains.compiler |
| 41 | +import easybuild.toolchains.fft |
| 42 | +import easybuild.toolchains.linalg |
| 43 | +import easybuild.toolchains.mpi |
| 44 | +# importing easyblocks namespace may fail if easybuild-easyblocks is not available |
| 45 | +# for now, we don't really care |
| 46 | +try: |
| 47 | + import easybuild.easyblocks |
| 48 | + import easybuild.easyblocks.generic |
| 49 | +except ImportError: |
| 50 | + pass |
| 51 | + |
| 52 | + |
| 53 | +_log = fancylogger.getLogger('tools.include', fname=False) |
| 54 | + |
| 55 | + |
| 56 | +# body for __init__.py file in package directory, which takes care of making sure the package can be distributed |
| 57 | +# across multiple directories |
| 58 | +PKG_INIT_BODY = """ |
| 59 | +from pkgutil import extend_path |
| 60 | +
|
| 61 | +# extend path so Python knows this is not the only place to look for modules in this package |
| 62 | +__path__ = extend_path(__path__, __name__) |
| 63 | +""" |
| 64 | + |
| 65 | +# more extensive __init__.py specific to easybuild.easyblocks package; |
| 66 | +# this is required because of the way in which the easyblock Python modules are organised in the easybuild-easyblocks |
| 67 | +# repository, i.e. in first-letter subdirectories |
| 68 | +EASYBLOCKS_PKG_INIT_BODY = """ |
| 69 | +from pkgutil import extend_path |
| 70 | +
|
| 71 | +# extend path so Python finds our easyblocks in the subdirectories where they are located |
| 72 | +subdirs = [chr(l) for l in range(ord('a'), ord('z') + 1)] + ['0'] |
| 73 | +for subdir in subdirs: |
| 74 | + __path__ = extend_path(__path__, '%s.%s' % (__name__, subdir)) |
| 75 | +
|
| 76 | +# extend path so Python knows this is not the only place to look for modules in this package |
| 77 | +__path__ = extend_path(__path__, __name__) |
| 78 | +
|
| 79 | +del subdir, subdirs, l |
| 80 | +""" |
| 81 | + |
| 82 | + |
| 83 | +def create_pkg(path, pkg_init_body=None): |
| 84 | + """Write package __init__.py file at specified path.""" |
| 85 | + init_path = os.path.join(path, '__init__.py') |
| 86 | + try: |
| 87 | + # note: can't use mkdir, since that required build options to be initialised |
| 88 | + if not os.path.exists(path): |
| 89 | + os.makedirs(path) |
| 90 | + |
| 91 | + # put __init__.py files in place, with required pkgutil.extend_path statement |
| 92 | + # note: can't use write_file, since that required build options to be initialised |
| 93 | + with open(init_path, 'w') as handle: |
| 94 | + if pkg_init_body is None: |
| 95 | + handle.write(PKG_INIT_BODY) |
| 96 | + else: |
| 97 | + handle.write(pkg_init_body) |
| 98 | + |
| 99 | + except (IOError, OSError) as err: |
| 100 | + raise EasyBuildError("Failed to create package at %s: %s", path, err) |
| 101 | + |
| 102 | + |
| 103 | +def set_up_eb_package(parent_path, eb_pkg_name, subpkgs=None, pkg_init_body=None): |
| 104 | + """ |
| 105 | + Set up new easybuild subnamespace in specified path. |
| 106 | +
|
| 107 | + @param parent_path: directory to create package in, using 'easybuild' namespace |
| 108 | + @param eb_pkg_name: full package name, must start with 'easybuild' |
| 109 | + @param subpkgs: list of subpackages to create |
| 110 | + @parak pkg_init_body: body of package's __init__.py file (does not apply to subpackages) |
| 111 | + """ |
| 112 | + if not eb_pkg_name.startswith('easybuild'): |
| 113 | + raise EasyBuildError("Specified EasyBuild package name does not start with 'easybuild': %s", eb_pkg_name) |
| 114 | + |
| 115 | + pkgpath = os.path.join(parent_path, eb_pkg_name.replace('.', os.path.sep)) |
| 116 | + |
| 117 | + # handle subpackages first |
| 118 | + if subpkgs: |
| 119 | + for subpkg in subpkgs: |
| 120 | + create_pkg(os.path.join(pkgpath, subpkg)) |
| 121 | + |
| 122 | + # creata package dirs on each level |
| 123 | + while pkgpath != parent_path: |
| 124 | + create_pkg(pkgpath, pkg_init_body=pkg_init_body) |
| 125 | + pkgpath = os.path.dirname(pkgpath) |
| 126 | + |
| 127 | + |
| 128 | +def verify_imports(pymods, pypkg, from_path): |
| 129 | + """Verify that import of specified modules from specified package and expected location works.""" |
| 130 | + for pymod in pymods: |
| 131 | + pymod_spec = '%s.%s' % (pypkg, pymod) |
| 132 | + try: |
| 133 | + pymod = __import__(pymod_spec, fromlist=[pypkg]) |
| 134 | + # different types of exceptions may be thrown, not only ImportErrors |
| 135 | + # e.g. when module being imported contains syntax errors or undefined variables |
| 136 | + except Exception as err: |
| 137 | + raise EasyBuildError("Failed to import easyblock %s from %s: %s", pymod_spec, from_path, err) |
| 138 | + |
| 139 | + if not os.path.samefile(os.path.dirname(pymod.__file__), from_path): |
| 140 | + raise EasyBuildError("Module %s not imported from expected location (%s): %s", |
| 141 | + pymod_spec, from_path, pymod.__file__) |
| 142 | + |
| 143 | + _log.debug("Import of %s from %s verified", pymod_spec, from_path) |
| 144 | + |
| 145 | + |
| 146 | +def include_easyblocks(tmpdir, paths): |
| 147 | + """Include generic and software-specific easyblocks found in specified locations.""" |
| 148 | + easyblocks_path = os.path.join(tmpdir, 'included-easyblocks') |
| 149 | + |
| 150 | + set_up_eb_package(easyblocks_path, 'easybuild.easyblocks', |
| 151 | + subpkgs=['generic'], pkg_init_body=EASYBLOCKS_PKG_INIT_BODY) |
| 152 | + |
| 153 | + easyblocks_dir = os.path.join(easyblocks_path, 'easybuild', 'easyblocks') |
| 154 | + |
| 155 | + allpaths = expand_glob_paths(paths) |
| 156 | + for easyblock_module in allpaths: |
| 157 | + filename = os.path.basename(easyblock_module) |
| 158 | + |
| 159 | + # generic easyblocks are expected to be in a directory named 'generic' |
| 160 | + parent_dir = os.path.basename(os.path.dirname(easyblock_module)) |
| 161 | + if parent_dir == 'generic': |
| 162 | + target_path = os.path.join(easyblocks_dir, 'generic', filename) |
| 163 | + else: |
| 164 | + target_path = os.path.join(easyblocks_dir, filename) |
| 165 | + |
| 166 | + symlink(easyblock_module, target_path) |
| 167 | + |
| 168 | + included_ebs = [x for x in os.listdir(easyblocks_dir) if x not in ['__init__.py', 'generic']] |
| 169 | + included_generic_ebs = [x for x in os.listdir(os.path.join(easyblocks_dir, 'generic')) if x != '__init__.py'] |
| 170 | + _log.debug("Included generic easyblocks: %s", included_generic_ebs) |
| 171 | + _log.debug("Included software-specific easyblocks: %s", included_ebs) |
| 172 | + |
| 173 | + # inject path into Python search path, and reload modules to get it 'registered' in sys.modules |
| 174 | + sys.path.insert(0, easyblocks_path) |
| 175 | + reload(easybuild) |
| 176 | + if 'easybuild.easyblocks' in sys.modules: |
| 177 | + reload(easybuild.easyblocks) |
| 178 | + reload(easybuild.easyblocks.generic) |
| 179 | + |
| 180 | + # sanity check: verify that included easyblocks can be imported (from expected location) |
| 181 | + for subdir, ebs in [('', included_ebs), ('generic', included_generic_ebs)]: |
| 182 | + pkg = '.'.join(['easybuild', 'easyblocks', subdir]).strip('.') |
| 183 | + loc = os.path.join(easyblocks_dir, subdir) |
| 184 | + verify_imports([os.path.splitext(eb)[0] for eb in ebs], pkg, loc) |
| 185 | + |
| 186 | + return easyblocks_path |
| 187 | + |
| 188 | + |
| 189 | +def include_module_naming_schemes(tmpdir, paths): |
| 190 | + """Include module naming schemes at specified locations.""" |
| 191 | + mns_path = os.path.join(tmpdir, 'included-module-naming-schemes') |
| 192 | + |
| 193 | + set_up_eb_package(mns_path, 'easybuild.tools.module_naming_scheme') |
| 194 | + |
| 195 | + mns_dir = os.path.join(mns_path, 'easybuild', 'tools', 'module_naming_scheme') |
| 196 | + |
| 197 | + allpaths = expand_glob_paths(paths) |
| 198 | + for mns_module in allpaths: |
| 199 | + filename = os.path.basename(mns_module) |
| 200 | + target_path = os.path.join(mns_dir, filename) |
| 201 | + symlink(mns_module, target_path) |
| 202 | + |
| 203 | + included_mns = [x for x in os.listdir(mns_dir) if x not in ['__init__.py']] |
| 204 | + _log.debug("Included module naming schemes: %s", included_mns) |
| 205 | + |
| 206 | + # inject path into Python search path, and reload modules to get it 'registered' in sys.modules |
| 207 | + sys.path.insert(0, mns_path) |
| 208 | + reload(easybuild.tools.module_naming_scheme) |
| 209 | + |
| 210 | + # sanity check: verify that included module naming schemes can be imported (from expected location) |
| 211 | + verify_imports([os.path.splitext(mns)[0] for mns in included_mns], 'easybuild.tools.module_naming_scheme', mns_dir) |
| 212 | + |
| 213 | + return mns_path |
| 214 | + |
| 215 | + |
| 216 | +def include_toolchains(tmpdir, paths): |
| 217 | + """Include toolchains and toolchain components at specified locations.""" |
| 218 | + toolchains_path = os.path.join(tmpdir, 'included-toolchains') |
| 219 | + toolchain_subpkgs = ['compiler', 'fft', 'linalg', 'mpi'] |
| 220 | + |
| 221 | + set_up_eb_package(toolchains_path, 'easybuild.toolchains', subpkgs=toolchain_subpkgs) |
| 222 | + |
| 223 | + tcs_dir = os.path.join(toolchains_path, 'easybuild', 'toolchains') |
| 224 | + |
| 225 | + allpaths = expand_glob_paths(paths) |
| 226 | + for toolchain_module in allpaths: |
| 227 | + filename = os.path.basename(toolchain_module) |
| 228 | + |
| 229 | + parent_dir = os.path.basename(os.path.dirname(toolchain_module)) |
| 230 | + |
| 231 | + # toolchain components are expected to be in a directory named according to the type of component |
| 232 | + if parent_dir in toolchain_subpkgs: |
| 233 | + target_path = os.path.join(tcs_dir, parent_dir, filename) |
| 234 | + else: |
| 235 | + target_path = os.path.join(tcs_dir, filename) |
| 236 | + |
| 237 | + symlink(toolchain_module, target_path) |
| 238 | + |
| 239 | + included_toolchains = [x for x in os.listdir(tcs_dir) if x not in ['__init__.py'] + toolchain_subpkgs] |
| 240 | + _log.debug("Included toolchains: %s", included_toolchains) |
| 241 | + |
| 242 | + included_subpkg_modules = {} |
| 243 | + for subpkg in toolchain_subpkgs: |
| 244 | + included_subpkg_modules[subpkg] = [x for x in os.listdir(os.path.join(tcs_dir, subpkg)) if x != '__init__.py'] |
| 245 | + _log.debug("Included toolchain %s components: %s", subpkg, included_subpkg_modules[subpkg]) |
| 246 | + |
| 247 | + # inject path into Python search path, and reload modules to get it 'registered' in sys.modules |
| 248 | + sys.path.insert(0, toolchains_path) |
| 249 | + reload(easybuild.toolchains) |
| 250 | + for subpkg in toolchain_subpkgs: |
| 251 | + reload(sys.modules['easybuild.toolchains.%s' % subpkg]) |
| 252 | + |
| 253 | + # sanity check: verify that included toolchain modules can be imported (from expected location) |
| 254 | + verify_imports([os.path.splitext(mns)[0] for mns in included_toolchains], 'easybuild.toolchains', tcs_dir) |
| 255 | + for subpkg in toolchain_subpkgs: |
| 256 | + pkg = '.'.join(['easybuild', 'toolchains', subpkg]) |
| 257 | + loc = os.path.join(tcs_dir, subpkg) |
| 258 | + verify_imports([os.path.splitext(tcmod)[0] for tcmod in included_subpkg_modules[subpkg]], pkg, loc) |
| 259 | + |
| 260 | + return toolchains_path |
0 commit comments