diff --git a/easybuild/tools/module_generator.py b/easybuild/tools/module_generator.py index 3711c7605f..9fc54800a5 100644 --- a/easybuild/tools/module_generator.py +++ b/easybuild/tools/module_generator.py @@ -49,6 +49,65 @@ _log = fancylogger.getLogger('module_generator', fname=False) +def avail_module_generators(): + """ + Return all known module syntaxes. + """ + return dict([(k.SYNTAX, k) for k in get_subclasses(ModuleGenerator)]) + + +def module_generator(app, fake=False): + """ + Return ModuleGenerator instance that matches the selected module file syntax to be used + """ + module_syntax = get_module_syntax() + available_mod_gens = avail_module_generators() + + if module_syntax not in available_mod_gens: + raise EasyBuildError("No module generator available for specified syntax '%s' (available: %s)", + module_syntax, available_mod_gens) + + module_generator_class = available_mod_gens[module_syntax] + return module_generator_class(app, fake=fake) + + +def module_load_regex(modfilepath): + """ + Return the correct (compiled) regex to extract dependencies, depending on the module file type (Lua vs Tcl) + """ + if modfilepath.endswith(ModuleGeneratorLua.MODULE_FILE_EXTENSION): + regex = ModuleGeneratorLua.LOAD_REGEX + else: + regex = ModuleGeneratorTcl.LOAD_REGEX + return re.compile(regex, re.M) + + +def dependencies_for(mod_name, modtool, depth=sys.maxint): + """ + Obtain a list of dependencies for the given module, determined recursively, up to a specified depth (optionally) + :param depth: recursion depth (default is sys.maxint, which should be equivalent to infinite recursion depth) + """ + mod_filepath = modtool.modulefile_path(mod_name) + modtxt = read_file(mod_filepath) + loadregex = module_load_regex(mod_filepath) + mods = loadregex.findall(modtxt) + + if depth > 0: + # recursively determine dependencies for these dependency modules, until depth is non-positive + moddeps = [dependencies_for(mod, modtool, depth=depth - 1) for mod in mods] + else: + # ignore any deeper dependencies + moddeps = [] + + # add dependencies of dependency modules only if they're not there yet + for moddepdeps in moddeps: + for dep in moddepdeps: + if not dep in mods: + mods.append(dep) + + return mods + + class ModuleGenerator(object): """ Class for generating module files. @@ -70,6 +129,37 @@ def __init__(self, application, fake=False): self.log = fancylogger.getLogger(self.__class__.__name__, fname=False) self.fake_mod_path = tempfile.mkdtemp() + def create_symlinks(self, mod_symlink_paths, fake=False): + """Create moduleclass symlink(s) to actual module file.""" + mod_filepath = self.get_module_filepath(fake=fake) + class_mod_files = [self.get_module_filepath(fake=fake, mod_path_suffix=p) for p in mod_symlink_paths] + try: + for class_mod_file in class_mod_files: + # remove symlink if its there (even if it's broken) + if os.path.lexists(class_mod_file): + self.log.debug("Removing existing symlink %s", class_mod_file) + os.remove(class_mod_file) + + mkdir(os.path.dirname(class_mod_file), parents=True) + os.symlink(mod_filepath, class_mod_file) + + except OSError, err: + raise EasyBuildError("Failed to create symlinks from %s to %s: %s", class_mod_files, mod_filepath, err) + + def define_env_var(self, env_var): + """ + Determine whether environment variable with specified name should be defined or not. + + :param env_var: name of environment variable to check + """ + return env_var not in (build_option('filter_env_vars') or []) + + def get_module_filepath(self, fake=False, mod_path_suffix=None): + """Return path to module file.""" + mod_path = self.get_modules_path(fake=fake, mod_path_suffix=mod_path_suffix) + full_mod_name = self.app.full_mod_name + self.MODULE_FILE_EXTENSION + return os.path.join(mod_path, full_mod_name) + def get_modules_path(self, fake=False, mod_path_suffix=None): """Return path to directory where module files should be generated in.""" mod_path = install_path('mod') @@ -82,12 +172,6 @@ def get_modules_path(self, fake=False, mod_path_suffix=None): return os.path.join(mod_path, mod_path_suffix) - def get_module_filepath(self, fake=False, mod_path_suffix=None): - """Return path to module file.""" - mod_path = self.get_modules_path(fake=fake, mod_path_suffix=mod_path_suffix) - full_mod_name = self.app.full_mod_name + self.MODULE_FILE_EXTENSION - return os.path.join(mod_path, full_mod_name) - def prepare(self, fake=False): """ Prepare for generating module file: Creates the absolute filename for the module. @@ -106,22 +190,7 @@ def prepare(self, fake=False): return mod_path - def create_symlinks(self, mod_symlink_paths, fake=False): - """Create moduleclass symlink(s) to actual module file.""" - mod_filepath = self.get_module_filepath(fake=fake) - class_mod_files = [self.get_module_filepath(fake=fake, mod_path_suffix=p) for p in mod_symlink_paths] - try: - for class_mod_file in class_mod_files: - # remove symlink if its there (even if it's broken) - if os.path.lexists(class_mod_file): - self.log.debug("Removing existing symlink %s", class_mod_file) - os.remove(class_mod_file) - - mkdir(os.path.dirname(class_mod_file), parents=True) - os.symlink(mod_filepath, class_mod_file) - - except OSError, err: - raise EasyBuildError("Failed to create symlinks from %s to %s: %s", class_mod_files, mod_filepath, err) + # From this point on just not implemented methods def comment(self, msg): """Return given string formatted as a comment.""" @@ -138,21 +207,31 @@ def conditional_statement(self, condition, body, negative=False, else_body=None) """ raise NotImplementedError - def define_env_var(self, env_var): + def get_description(self, conflict=True): """ - Determine whether environment variable with specified name should be defined or not. + Generate a description. + """ + raise NotImplementedError - :param env_var: name of environment variable to check + def getenv_cmd(self, envvar): """ - return env_var not in (build_option('filter_env_vars') or []) + Return module-syntax specific code to get value of specific environment variable. + """ + raise NotImplementedError - def set_environment(self, key, value, relpath=False): + def load_module(self, mod_name, recursive_unload=False, unload_modules=None): """ - Generate a quoted setenv statement for the given key/value pair. + Generate load statement for specified module. - :param key: name of environment variable to define - :param value: value to define environment variable with - :param relpath: value is path relative to installation prefix + :param mod_name: name of module to generate load statement for + :param recursive_unload: boolean indicating whether the 'load' statement should be reverted on unload + :param unload_modules: name(s) of module to unload first + """ + raise NotImplementedError + + def msg_on_load(self, msg): + """ + Add a message that should be printed when loading the module. """ raise NotImplementedError @@ -167,31 +246,38 @@ def prepend_paths(self, key, paths, allow_abs=False, expand_relpaths=True): """ raise NotImplementedError - def msg_on_load(self, msg): + def set_alias(self, key, value): """ - Add a message that should be printed when loading the module. + Generate set-alias statement in modulefile for the given key/value pair. """ raise NotImplementedError - def set_alias(self, key, value): + def set_as_default(self, module_folder_path, module_version): """ - Generate set-alias statement in modulefile for the given key/value pair. + Set generated module as default module + + :param module_folder_path: module folder path, e.g. $HOME/easybuild/modules/all/Bison + :param module_version: module version, e.g. 3.0.4 """ raise NotImplementedError - def getenv_cmd(self, envvar): + def set_environment(self, key, value, relpath=False): """ - Return module-syntax specific code to get value of specific environment variable. + Generate a quoted setenv statement for the given key/value pair. + + :param key: name of environment variable to define + :param value: value to define environment variable with + :param relpath: value is path relative to installation prefix """ raise NotImplementedError - def load_module(self, mod_name, recursive_unload=False, unload_modules=None): + def swap_module(self, mod_name_out, mod_name_in, guarded=True): """ - Generate load statement for specified module. + Generate swap statement for specified module names. - :param mod_name: name of module to generate load statement for - :param recursive_unload: boolean indicating whether the 'load' statement should be reverted on unload - :param unload_modules: name(s) of module to unload first + :param mod_name_out: name of module to unload (swap out) + :param mod_name_in: name of module to load (swap in) + :param guarded: guard 'swap' statement, fall back to 'load' if module being swapped out is not loaded """ raise NotImplementedError @@ -203,19 +289,15 @@ def unload_module(self, mod_name): """ raise NotImplementedError - def swap_module(self, mod_name_out, mod_name_in, guarded=True): + def use(self, paths, prefix=None, guarded=False): """ - Generate swap statement for specified module names. - - :param mod_name_out: name of module to unload (swap out) - :param mod_name_in: name of module to load (swap in) - :param guarded: guard 'swap' statement, fall back to 'load' if module being swapped out is not loaded + 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 """ raise NotImplementedError - def set_as_default(self, module_folder_path, module_version): - raise NotImplementedError - class ModuleGeneratorTcl(ModuleGenerator): """ @@ -304,6 +386,12 @@ def get_description(self, conflict=True): return txt + def getenv_cmd(self, envvar): + """ + Return module-syntax specific code to get value of specific environment variable. + """ + return '$env(%s)' % envvar + def load_module(self, mod_name, recursive_unload=False, unload_modules=None): """ Generate load statement for specified module. @@ -327,30 +415,14 @@ def load_module(self, mod_name, recursive_unload=False, unload_modules=None): return '\n'.join([''] + load_statement) % {'mod_name': mod_name} - def unload_module(self, mod_name): - """ - Generate unload statement for specified module. - - :param mod_name: name of module to generate unload statement for - """ - return '\n'.join(['', "module unload %s" % mod_name]) - - def swap_module(self, mod_name_out, mod_name_in, guarded=True): + def msg_on_load(self, msg): """ - Generate swap statement for specified module names. - - :param mod_name_out: name of module to unload (swap out) - :param mod_name_in: name of module to load (swap in) - :param guarded: guard 'swap' statement, fall back to 'load' if module being swapped out is not loaded + Add a message that should be printed when loading the module. """ - body = "module swap %s %s" % (mod_name_out, mod_name_in) - if guarded: - alt_body = self.LOAD_TEMPLATE % {'mod_name': mod_name_in} - swap_statement = [self.conditional_statement("is-loaded %s" % mod_name_out, body, else_body=alt_body)] - else: - swap_statement = [body, ''] - - return '\n'.join([''] + swap_statement) + # escape any (non-escaped) characters with special meaning by prefixing them with a backslash + msg = re.sub(r'((? 0: - # recursively determine dependencies for these dependency modules, until depth is non-positive - moddeps = [dependencies_for(mod, modtool, depth=depth - 1) for mod in mods] - else: - # ignore any deeper dependencies - moddeps = [] - - # add dependencies of dependency modules only if they're not there yet - for moddepdeps in moddeps: - for dep in moddepdeps: - if not dep in mods: - mods.append(dep) - - return mods + 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)