Skip to content

Commit 49c0f3c

Browse files
committed
Merge pull request #1301 from boegel/include
add support for --include-* (REVIEW)
2 parents 8930c91 + 6f0c94a commit 49c0f3c

File tree

22 files changed

+724
-39
lines changed

22 files changed

+724
-39
lines changed

easybuild/main.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import copy
3939
import os
4040
import sys
41+
import tempfile
4142
import traceback
4243

4344
# IMPORTANT this has to be the first easybuild import as it customises the logging
@@ -51,7 +52,7 @@
5152
from easybuild.framework.easyconfig.tools import alt_easyconfig_paths, dep_graph, det_easyconfig_paths
5253
from easybuild.framework.easyconfig.tools import get_paths_for, parse_easyconfigs, skip_available
5354
from easybuild.framework.easyconfig.tweak import obtain_ec_for, tweak
54-
from easybuild.tools.config import get_repository, get_repositorypath, set_tmpdir
55+
from easybuild.tools.config import get_repository, get_repositorypath
5556
from easybuild.tools.filetools import cleanup, write_file
5657
from easybuild.tools.options import process_software_build_specs
5758
from easybuild.tools.robot import det_robot_path, dry_run, resolve_dependencies, search_easyconfigs
@@ -163,8 +164,8 @@ def main(testing_data=(None, None, None)):
163164
new_umask = int(options.umask, 8)
164165
old_umask = os.umask(new_umask)
165166

166-
# set temporary directory to use
167-
eb_tmpdir = set_tmpdir(options.tmpdir)
167+
# set by option parsers via set_tmpdir
168+
eb_tmpdir = tempfile.gettempdir()
168169

169170
# initialise logging for main
170171
global _log
@@ -328,7 +329,7 @@ def main(testing_data=(None, None, None)):
328329
if 'original_spec' in ec and os.path.isfile(ec['spec']):
329330
os.remove(ec['spec'])
330331

331-
# stop logging and cleanup tmp log file, unless one build failed (individual logs are located in eb_tmpdir path)
332+
# stop logging and cleanup tmp log file, unless one build failed (individual logs are located in eb_tmpdir)
332333
stop_logging(logfile, logtostdout=options.logtostdout)
333334
if overall_success:
334335
cleanup(logfile, eb_tmpdir, testing)

easybuild/tools/filetools.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@
4343
import urllib2
4444
import zlib
4545
from vsc.utils import fancylogger
46+
from vsc.utils.missing import nub
4647

47-
import easybuild.tools.environment as env
4848
from easybuild.tools.build_log import EasyBuildError, print_msg # import build_log must stay, to use of EasyBuildLog
4949
from easybuild.tools.config import build_option
5050
from easybuild.tools import run
@@ -851,6 +851,24 @@ def mkdir(path, parents=False, set_gid=None, sticky=None):
851851
_log.debug("Not creating existing path %s" % path)
852852

853853

854+
def expand_glob_paths(glob_paths):
855+
"""Expand specified glob paths to a list of unique non-glob paths to only files."""
856+
paths = []
857+
for glob_path in glob_paths:
858+
paths.extend([f for f in glob.glob(glob_path) if os.path.isfile(f)])
859+
860+
return nub(paths)
861+
862+
863+
def symlink(source_path, symlink_path):
864+
"""Create a symlink at the specified path to the given path."""
865+
try:
866+
os.symlink(os.path.abspath(source_path), symlink_path)
867+
_log.info("Symlinked %s to %s", source_path, symlink_path)
868+
except OSError as err:
869+
raise EasyBuildError("Symlinking %s to %s failed: %s", source_path, symlink_path, err)
870+
871+
854872
def path_matches(path, paths):
855873
"""Check whether given path matches any of the provided paths."""
856874
if not os.path.exists(path):

easybuild/tools/include.py

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
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

Comments
 (0)