Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow user-local modules with hierarchical naming schemes #1472

Merged
merged 25 commits into from
Jan 20, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b0bdb17
Add 'subdir-user-modules' option
geimer Nov 12, 2015
67c39ec
Extend module naming scheme to provide user-specific module path exte…
geimer Nov 12, 2015
2335cce
Generate 'module use' statements for user-specific module paths
geimer Nov 12, 2015
60ea159
Make unit tests work again
geimer Nov 12, 2015
7562542
Include det_user_modpath_extensions in naming scheme tests
geimer Nov 12, 2015
9bd6a1e
Add unit test for guarded 'module use' statements with prefix
geimer Nov 12, 2015
df718d0
Fix typo
geimer Nov 12, 2015
3e3d19c
add unit test for make_module_extend_modpath, incl. test case for use…
boegel Jan 16, 2016
db102a4
define det_home via get_env, add unit tests
boegel Jan 16, 2016
26c9061
Merge pull request #5 from boegel/user_modules
geimer Jan 16, 2016
a647c1e
Merge branch 'develop' into user_modules
boegel Jan 16, 2016
9c8a093
Merge pull request #6 from boegel/user_modules
geimer Jan 16, 2016
6168a58
Fix typo in test
geimer Jan 16, 2016
99e9a61
Use comma rather than percent in self.log.debug calls
geimer Jan 16, 2016
c412d83
Rename `get_env` to `getenv_cmd` to avoid confusion,
geimer Jan 16, 2016
9b86106
Use `conditional_statement` helper routine
geimer Jan 16, 2016
b2570bd
Move quoting of path to `module_generator.use`
geimer Jan 16, 2016
352e936
Some commenting
geimer Jan 16, 2016
2a74925
fix broken test (spacing in conditional statement)
boegel Jan 16, 2016
de3b55d
fix bug: join --subdir-user-modules with user modpath extensions
boegel Jan 16, 2016
ce1aed5
Merge pull request #7 from boegel/user_modules
geimer Jan 18, 2016
9d08d07
Improved comments
geimer Jan 18, 2016
c862e4e
Adjusted unit tests
geimer Jan 18, 2016
e43787e
Adjusted regex to account for quoted "module use" statements
geimer Jan 20, 2016
3506ee1
Simplified regex
geimer Jan 20, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion easybuild/framework/easyblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -1036,12 +1036,21 @@ def make_module_extend_modpath(self):
txt = ''
if self.cfg['include_modpath_extensions']:
modpath_exts = ActiveMNS().det_modpath_extensions(self.cfg)
self.log.debug("Including module path extensions returned by module naming scheme: %s" % modpath_exts)
self.log.debug("Including module path extensions returned by module naming scheme: %s", modpath_exts)
full_path_modpath_extensions = [os.path.join(self.installdir_mod, ext) for ext in modpath_exts]
# module path extensions must exist, otherwise loading this module file will fail
for modpath_extension in full_path_modpath_extensions:
mkdir(modpath_extension, parents=True)
txt = self.module_generator.use(full_path_modpath_extensions)

# add user-specific module path; use statement will be guarded so no need to create the directories
user_modpath = build_option('subdir_user_modules')
if user_modpath:
user_modpath_exts = ActiveMNS().det_user_modpath_extensions(self.cfg)
user_modpath_exts = [os.path.join(user_modpath, e) for e in user_modpath_exts]
self.log.debug("Including user module path extensions returned by naming scheme: %s", user_modpath_exts)
txt += self.module_generator.use(user_modpath_exts, prefix=self.module_generator.getenv_cmd('HOME'),
guarded=True)
else:
self.log.debug("Not including module path extensions, as specified.")
return txt
Expand Down
7 changes: 7 additions & 0 deletions easybuild/framework/easyconfig/easyconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -1279,6 +1279,13 @@ def det_modpath_extensions(self, ec):
self.log.debug("Obtained modulepath extensions: %s" % modpath_extensions)
return modpath_extensions

def det_user_modpath_extensions(self, ec):
"""Determine user-specific modulepath extensions according to module naming scheme."""
self.log.debug("Determining user modulepath extensions for %s", ec)
modpath_extensions = self.mns.det_user_modpath_extensions(self.check_ec_type(ec))
self.log.debug("Obtained user modulepath extensions: %s", modpath_extensions)
return modpath_extensions

def det_init_modulepaths(self, ec):
"""Determine initial modulepaths according to module naming scheme."""
self.log.debug("Determining initial module paths for %s" % ec)
Expand Down
1 change: 1 addition & 0 deletions easybuild/tools/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ def mk_full_default_path(name, prefix=DEFAULT_PREFIX):
'regtest_output_dir',
'skip',
'stop',
'subdir_user_modules',
'test_report_env_filter',
'testoutput',
'umask',
Expand Down
61 changes: 54 additions & 7 deletions easybuild/tools/module_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@ def conditional_statement(self, condition, body, negative=False):
"""Return formatted conditional statement, with given condition and body."""
raise NotImplementedError

def getenv_cmd(self, envvar):
"""
Return module-syntax specific code to get value of specific environment variable.
"""
raise NotImplementedError

def load_module(self, mod_name, recursive_unload=False, unload_modules=None):
"""
Generate load statement for specified module.
Expand Down Expand Up @@ -286,13 +292,26 @@ def prepend_paths(self, key, paths, allow_abs=False, expand_relpaths=True):
statements = [template % (key, p) for p in abspaths]
return ''.join(statements)

def use(self, paths):
def use(self, paths, prefix=None, guarded=False):
"""
Generate module use statements for given list of module paths.
@param paths: list of module path extensions to generate use statements for; paths will be quoted
@param prefix: optional path prefix; not quoted, i.e., can be a statement
@param guarded: use statements will be guarded to only apply if path exists
"""
use_statements = []
for path in paths:
use_statements.append("module use %s\n" % path)
quoted_path = quote_str(path)
if prefix:
full_path = '[ file join %s %s ]' % (prefix, quoted_path)
else:
full_path = quoted_path
if guarded:
cond_statement = self.conditional_statement('file isdirectory %s' % full_path,
'module use %s' % full_path)
use_statements.append(cond_statement)
else:
use_statements.append("module use %s\n" % full_path)
return ''.join(use_statements)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use '\n'.join here, and don't include \n everywhere above


def set_environment(self, key, value, relpath=False):
Expand Down Expand Up @@ -325,6 +344,12 @@ def set_alias(self, key, value):
# quotes are needed, to ensure smooth working of EBDEVEL* modulefiles
return 'set-alias\t%s\t\t%s\n' % (key, quote_str(value))

def getenv_cmd(self, envvar):
"""
Return module-syntax specific code to get value of specific environment variable.
"""
return '$env(%s)' % envvar


class ModuleGeneratorLua(ModuleGenerator):
"""
Expand All @@ -339,7 +364,7 @@ class ModuleGeneratorLua(ModuleGenerator):
LOAD_TEMPLATE = 'load("%(mod_name)s")'

PATH_JOIN_TEMPLATE = 'pathJoin(root, "%s")'
PREPEND_PATH_TEMPLATE = 'prepend_path("%s", %s)\n'
PREPEND_PATH_TEMPLATE = 'prepend_path("%s", %s)'

def __init__(self, *args, **kwargs):
"""ModuleGeneratorLua constructor."""
Expand Down Expand Up @@ -462,14 +487,30 @@ def prepend_paths(self, key, paths, allow_abs=False, expand_relpaths=True):
abspaths.append('root')

statements = [self.PREPEND_PATH_TEMPLATE % (key, p) for p in abspaths]
return ''.join(statements)
statements.append('')
return '\n'.join(statements)

def use(self, paths):
def use(self, paths, prefix=None, guarded=False):
"""
Generate module use statements for given list of module paths.
@param paths: list of module path extensions to generate use statements for
@param paths: list of module path extensions to generate use statements for; paths will be quoted
@param prefix: optional path prefix; not quoted, i.e., can be a statement
@param guarded: use statements will be guarded to only apply if path exists
"""
return ''.join([self.PREPEND_PATH_TEMPLATE % ('MODULEPATH', quote_str(p)) for p in paths])
use_statements = []
for path in paths:
quoted_path = quote_str(path)
if prefix:
full_path = 'pathJoin(%s, %s)' % (prefix, quoted_path)
else:
full_path = quoted_path
if guarded:
cond_statement = self.conditional_statement('isDir(%s)' % full_path,
self.PREPEND_PATH_TEMPLATE % ('MODULEPATH', full_path))
use_statements.append(cond_statement)
else:
use_statements.append(self.PREPEND_PATH_TEMPLATE % ('MODULEPATH', full_path) + '\n')
return ''.join(use_statements)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'\n'.join please


def set_environment(self, key, value, relpath=False):
"""
Expand Down Expand Up @@ -497,6 +538,12 @@ def set_alias(self, key, value):
# quotes are needed, to ensure smooth working of EBDEVEL* modulefiles
return 'setalias("%s", %s)\n' % (key, quote_str(value))

def getenv_cmd(self, envvar):
"""
Return module-syntax specific code to get value of specific environment variable.
"""
return 'os.getenv("%s")' % envvar


def avail_module_generators():
"""
Expand Down
11 changes: 11 additions & 0 deletions easybuild/tools/module_naming_scheme/categorized_hmns.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,17 @@ def det_modpath_extensions(self, ec):

return self.categorize_paths(basepaths)

def det_user_modpath_extensions(self, ec):
"""
Determine user module path extensions, if any. As typical users are not expected to have many local modules,
further categorizing them using module classes is considered overkill. Thus, we are using a plain hierarchical
scheme for user modules instead.

Examples: Compiler/GCC/4.8.3 (for GCC/4.8.3 module), MPI/GCC/4.8.3/OpenMPI/1.6.5 (for OpenMPI/1.6.5 module)
"""
# Use "system" module path extensions of hierarchical MNS (i.e., w/o module class)
return super(CategorizedHMNS, self).det_modpath_extensions(ec)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is exactly the same as the default implementation? what's the point of defining it then, just let inheritance take care of it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is called because of inheritance. det_modpath_extensions has been overwritten in this class to take the moduleclass into account, but this seems overkill for user module path extensions. Thus, the implementation provided by the superclass is called (intentionally).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, maybe make that clear with a comment in the code?


def det_init_modulepaths(self, ec):
"""
Determine list of initial module paths (i.e., top of the hierarchy).
Expand Down
12 changes: 12 additions & 0 deletions easybuild/tools/module_naming_scheme/mns.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,18 @@ def det_modpath_extensions(self, ec):
# by default: an empty list of subdirectories to extend $MODULEPATH with
return []

def det_user_modpath_extensions(self, ec):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@geimer @boegel This does not seem to be respecting EASYBUILD_SUFFIX_MODULES_PATH (which defaults to all):

conflict("GCCcore")
prepend_path("MODULEPATH", "/usr/local/software/jureca/Stages/Devel-2017a/modules/all/Compiler/GCCcore/5.4.0")
if isDir(pathJoin(os.getenv("HOME"), ".local/EasyBuild/modules/Compiler/GCCcore/5.4.0")) then
    prepend_path("MODULEPATH", pathJoin(os.getenv("HOME"), ".local/EasyBuild/modules/Compiler/GCCcore/5.4.0"))
end

Notice the missing all in the local path.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ocaisa what was the full EasyBuild configuration here (eb --show-config)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-bash-4.2$ eb --show-config
#
# Current EasyBuild configuration
# (C: command line argument, D: default value, E: environment variable, F: configuration file)
#
buildpath                      (E) = /dev/shm/ocaisa
experimental                   (E) = True
fixed-installdir-naming-scheme (E) = True
group-writable-installdir      (E) = True
hide-deps                      (E) = ANTLR, APR, APR-util, AT-SPI2-ATK, AT-SPI2-core, ATK, Autoconf, Automake, adwaita-icon-theme, ant, assimp, Bison, babl, binutils, byacc, bzip2, CUSP, Coreutils, cairo, configurable-http-proxy, DB, DBus, DocBook-XML, Dyninst, dbus-glib, damageproto, ETSF_IO, Exiv2, eudev, expat, FFmpeg, FLTK, FTGL, fixesproto, fontsproto, fontconfig, freeglut, freetype, GCCcore, GDAL, GEGL, GL2PS, GLEW, GLib, GLPK, GPC, GObject-Introspection, GTI, GTK+, GTS, Gdk-Pixbuf, Ghostscript, GraphicsMagick, GtkSourceView, g2clib, g2lib, gc, gexiv2, glproto, gperf, guile, grib_api, gsettings-desktop-schemas, gettext, HarfBuzz, icc, ifort, inputproto, intltool, itstool, JUnit, JSON-C, JSON-GLib, JasPer, jhbuild, kbproto, LZO, LibTIFF, LibUUID, Libint, LittleCMS, libGLU, libICE, libSM, libX11, libXau, libXaw, libXcursor, libXdamage, libXdmcp, libXext, libXfixes, libXfont, libXft, libXi, libXinerama, libXmu, libXpm, libXrandr, libXrender, libXt, libXtst, libcerf, libcroco, libctl, libdap, libdrm, libdwarf, libelf, libepoxy, libevent, libffi, libfontenc, libgd, libgeotiff, libglade, libidn, libjpeg-turbo, libmatheval, libmypaint, libpng, libpciaccess, libpthread-stubs, libreadline, librsvg, libtool, libunistring, libunwind, libyaml, libxcb, libxkbcommon, libxml2, libxslt, libyuv, M4, Mesa, makedepend, motif, msgpack-c, NASM, ncurses, nettle, nodejs, nvenc_sdk, nvidia, OPARI2, OTF2, PCRE, PDT, PROJ, Pango, Pmw, PnMPI, PyCairo, PyGObject, Python-Xpra, pixman, pkg-config, pkgconfig, popt, pscom, Qhull, Qt, Qt5, qrupdate, randrproto, recordproto, renderproto, S-Lang, SCons, SIP, SQLite, SWIG, Serf, Szip, scrollkeeper, Tcl, Tk, texinfo, UDUNITS, util-linux, vpx, wxPropertyGrid, wxWidgets, XML-Parser, XZ, XKeyboardConfig, x264, x265, xbitmaps, xcb-proto, xcb-util, xcb-util-image, xcb-util-keysyms, xcb-util-renderutil, xcb-util-wm, xextproto, xineramaproto, xorg-macros, xprop, xproto, xtrans, YAXT, Yasm, zlib
hide-toolchains                (E) = GCCcore
include-easyblocks             (E) = /work/zam/swmanage/EasyBuild/Custom_EasyBlocks/Devel-2017a/*.py, /work/zam/swmanage/EasyBuild/Custom_EasyBlocks/Devel-2017a/generic/*.py
include-module-naming-schemes  (E) = /work/zam/swmanage/EasyBuild/Custom_MNS/Devel-2017a/*.py
include-toolchains             (E) = /work/zam/swmanage/EasyBuild/Custom_Toolchains/Devel-2017a/*.py
installpath                    (E) = /usr/local/software/jureca/Stages/Devel-2017a
job-backend-config             (E) = /usr/local/software/FZJ/gc3pie.cfg
job-cores                      (E) = 48
job-max-walltime               (E) = 1
minimal-toolchains             (E) = True
module-naming-scheme           (E) = CustomHierarchicalMNS
packagepath                    (E) = /usr/local/software/jureca/Stages/Devel-2017a/packages
prefix                         (E) = /usr/local/software/jureca/Stages/Devel-2017a
repositorypath                 (E) = /usr/local/software/jureca/Stages/Devel-2017a/eb_repo
robot                          (E) = /work/zam/swmanage/EasyBuild/Golden_Repo/Devel-2017a, /usr/local/software/jureca/Stages/Devel-2017a/eb_repo, /work/zam/swmanage/EasyBuild/Golden_Repo/Devel-2017a
robot-paths                    (E) = /work/zam/swmanage/EasyBuild/Golden_Repo/Devel-2017a, /usr/local/software/jureca/Stages/Devel-2017a/eb_repo, /work/zam/swmanage/EasyBuild/Golden_Repo/Devel-2017a
set-gid-bit                    (E) = True
sourcepath                     (E) = /work/zam/swmanage/EasyBuild/sources
subdir-user-modules            (E) = .local/EasyBuild/modules
umask                          (E) = 002
use-existing-modules           (E) = True

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get the same effect with using HierarchicalMNS

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This occurs with a vanilla instance and the command line:

eb --module-naming-scheme=HierarchicalMNS --subdir-user-modules=.local/EasyBuild/modules GCCcore-5.4.0.eb -x

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix implemented in #2250

"""
Determine list of subdirectories relative to the user-specific modules directory for which to extend
$MODULEPATH with when this module is loaded (if any).

@param ec: dict-like object with easyconfig parameter values; for now only the 'name',
'version', 'versionsuffix' and 'toolchain' parameters are guaranteed to be available
@return: A list of $MODULEPATH subdirectories.
"""
# by default: use "system" module path extensions of naming scheme
return self.det_modpath_extensions(ec)

def det_init_modulepaths(self, ec):
"""
Determine initial module paths, where the modules that are top of the hierarchy (if any) live.
Expand Down
2 changes: 1 addition & 1 deletion easybuild/tools/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,7 @@ def modpath_extensions_for(self, mod_names):
modpath_exts = {}
for mod_name in mod_names:
modtxt = self.read_module_file(mod_name)
useregex = re.compile(r"^\s*module\s+use\s+(\S+)", re.M)
useregex = re.compile(r'^\s*module\s+use\s+"?([^"\s]+)"?', re.M)
exts = useregex.findall(modtxt)

self.log.debug("Found $MODULEPATH extensions for %s: %s" % (mod_name, exts))
Expand Down
1 change: 1 addition & 0 deletions easybuild/tools/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ def config_options(self):
'subdir-modules': ("Installpath subdir for modules", None, 'store', DEFAULT_PATH_SUBDIRS['subdir_modules']),
'subdir-software': ("Installpath subdir for software",
None, 'store', DEFAULT_PATH_SUBDIRS['subdir_software']),
'subdir-user-modules': ("Base path of user-specific modules relative to their $HOME", None, 'store', None),
'suffix-modules-path': ("Suffix for module files install path", None, 'store', GENERAL_CLASS),
# this one is sort of an exception, it's something jobscripts can set,
# has no real meaning for regular eb usage
Expand Down
65 changes: 62 additions & 3 deletions test/framework/easyblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,60 @@ def test_fake_module_load(self):
eb.close_log()
os.remove(eb.logfile)

def test_make_module_extend_modpath(self):
"""Test for make_module_extend_modpath"""
self.contents = '\n'.join([
'easyblock = "ConfigureMake"',
'name = "pi"',
'version = "3.14"',
'homepage = "http://example.com"',
'description = "test easyconfig"',
'toolchain = {"name":"dummy", "version": "dummy"}',
'moduleclass = "compiler"',
])
self.writeEC()
eb = EasyBlock(EasyConfig(self.eb_file))
eb.installdir = config.install_path()

# no $MODULEPATH extensions for default module naming scheme (EasyBuildMNS)
self.assertEqual(eb.make_module_extend_modpath(), '')

usermodsdir = 'my/own/modules'
modclasses = ['compiler', 'tools']
os.environ['EASYBUILD_MODULE_NAMING_SCHEME'] = 'CategorizedHMNS'
build_options = {
'subdir_user_modules': usermodsdir,
'valid_module_classes': modclasses,
}
init_config(build_options=build_options)
eb = EasyBlock(EasyConfig(self.eb_file))
eb.installdir = config.install_path()

txt = eb.make_module_extend_modpath()
if get_module_syntax() == 'Tcl':
regexs = [r'^module use ".*/modules/all/Compiler/pi/3.14/%s"$' % c for c in modclasses]
home = r'\$env\(HOME\)'
regexs.extend([
# extension for user modules is guarded
r'if { \[ file isdirectory \[ file join %s "%s/Compiler/pi/3.14" \] \] } {$' % (home, usermodsdir),
# no per-moduleclass extension for user modules
r'^\s+module use \[ file join %s "%s/Compiler/pi/3.14"\ ]$' % (home, usermodsdir),
])
elif get_module_syntax() == 'Lua':
regexs = [r'^prepend_path\("MODULEPATH", ".*/modules/all/Compiler/pi/3.14/%s"\)$' % c for c in modclasses]
home = r'os.getenv\("HOME"\)'
regexs.extend([
# extension for user modules is guarded
r'if isDir\(pathJoin\(%s, "%s/Compiler/pi/3.14"\)\) then' % (home, usermodsdir),
# no per-moduleclass extension for user modules
r'\s+prepend_path\("MODULEPATH", pathJoin\(%s, "%s/Compiler/pi/3.14"\)\)' % (home, usermodsdir),
])
else:
self.assertTrue(False, "Unknown module syntax: %s" % get_module_syntax())
for regex in regexs:
regex = re.compile(regex, re.M)
self.assertTrue(regex.search(txt), "Pattern '%s' found in: %s" % (regex.pattern, txt))

def test_make_module_req(self):
"""Testcase for make_module_req"""
self.contents = '\n'.join([
Expand Down Expand Up @@ -764,10 +818,15 @@ def test_exclude_path_to_top_of_module_tree(self):
modfile_path = os.path.join(modpath, modfile_path)
modtxt = read_file(modfile_path)

for imkl_dep in excluded_deps:
tup = (imkl_dep, modfile_path, modtxt)
for dep in excluded_deps:
tup = (dep, modfile_path, modtxt)
failmsg = "No 'module load' statement found for '%s' not found in module %s: %s" % tup
self.assertFalse(re.search("module load %s" % imkl_dep, modtxt), failmsg)
if get_module_syntax() == 'Tcl':
self.assertFalse(re.search('module load %s' % dep, modtxt), failmsg)
elif get_module_syntax() == 'Lua':
self.assertFalse(re.search('load("%s")' % dep, modtxt), failmsg)
else:
self.assertTrue(False, "Unknown module syntax: %s" % get_module_syntax())

os.environ['EASYBUILD_MODULE_NAMING_SCHEME'] = self.orig_module_naming_scheme
init_config(build_options=build_options)
Expand Down
Loading