Module Files

In this tutorial, we’ll introduce a few concepts that are fundamental to the generation of module files with Spack, and we’ll guide you through the customization of both module files content and their layout on disk. In the end you should have a clear understanding of:

  • What are module files and how they work
  • How Spack generates them
  • Which commands are available to ease their maintenance
  • How it is possible to customize them in all aspects

Modules at a glance

Let’s start by summarizing what module files are and how you can use them to modify your environment. The idea is to give enough information so that people without any previous exposure to them will be able to follow the tutorial later on. We’ll also give a high-level view of how module files are generated in Spack. If you are already familiar with these topics you can quickly skim through this section or move directly to Setup for the tutorial.

What are module files?

Module files are an easy way to modify your environment in a controlled manner during a shell session. In general, they contain the information needed to run an application or use a library, and they work in conjunction with a tool that interprets them. Typical module files instruct this tool to modify the environment variables when a module file is loaded:

$ module show zlib
-------------------------------------------------------------------
/home/mculpo/PycharmProjects/spack/share/spack/modules/linux-ubuntu14.04-x86_64/zlib/1.2.11-gcc-7.2.0-linux-ubuntu14.04-x86_64-co2px3k:

module-whatis       A free, general-purpose, legally unencumbered lossless data-compression library.
prepend-path        MANPATH /home/mculpo/PycharmProjects/spack/opt/spack/linux-ubuntu14.04-x86_64/gcc-7.2.0/zlib-1.2.11-co2px3k53m76lm6tofylh2mur2hnicux/share/man
prepend-path        LIBRARY_PATH /home/mculpo/PycharmProjects/spack/opt/spack/linux-ubuntu14.04-x86_64/gcc-7.2.0/zlib-1.2.11-co2px3k53m76lm6tofylh2mur2hnicux/lib
prepend-path        LD_LIBRARY_PATH /home/mculpo/PycharmProjects/spack/opt/spack/linux-ubuntu14.04-x86_64/gcc-7.2.0/zlib-1.2.11-co2px3k53m76lm6tofylh2mur2hnicux/lib
prepend-path        CPATH /home/mculpo/PycharmProjects/spack/opt/spack/linux-ubuntu14.04-x86_64/gcc-7.2.0/zlib-1.2.11-co2px3k53m76lm6tofylh2mur2hnicux/include
prepend-path        PKG_CONFIG_PATH /home/mculpo/PycharmProjects/spack/opt/spack/linux-ubuntu14.04-x86_64/gcc-7.2.0/zlib-1.2.11-co2px3k53m76lm6tofylh2mur2hnicux/lib/pkgconfig
prepend-path        CMAKE_PREFIX_PATH /home/mculpo/PycharmProjects/spack/opt/spack/linux-ubuntu14.04-x86_64/gcc-7.2.0/zlib-1.2.11-co2px3k53m76lm6tofylh2mur2hnicux/
-------------------------------------------------------------------

$ echo $LD_LIBRARY_PATH

$ module load zlib
$ echo $LD_LIBRARY_PATH
/home/mculpo/PycharmProjects/spack/opt/spack/linux-ubuntu14.04-x86_64/gcc-7.2.0/zlib-1.2.11-co2px3k53m76lm6tofylh2mur2hnicux/lib

and to undo the modifications when the same module file is unloaded:

$ module unload zlib
$ echo $LD_LIBRARY_PATH

$

Different formats exist for module files, and different tools provide various levels of support for them. Spack can natively generate:

  1. Non-hierarchical module files written in TCL
  2. Hierarchical module files written in Lua

and can build environment-modules and lmod as support tools. Which of the formats or tools best suits one’s needs depends on each particular use-case. For the sake of illustration, we’ll be working on both formats using lmod.

See also

Environment modules
This is the original tool that provided modules support. Its first version was coded in C in the early ’90s and was later substituted by a version completely coded in TCL - the one Spack is distributing. More details on its features are given in the homepage of the project or in its github page. The tool is able to interpret the non-hierarchical TCL modulefiles written by Spack.
Lmod
Lmod is a module system written in Lua, designed to easily handle hierarchies of module files. It’s a drop-in replacement of Environment Modules and works with both of the module file formats generated by Spack. Despite being fully compatible with Environment Modules there are many features that are unique to Lmod. These features are either targeted towards safety or meant to extend the module system functionality.

How do we generate module files?

Before we dive into the hands-on sections it’s worth spending a couple of words to explain how module files are generated by Spack. The following diagram provides a high-level view of the process:

_images/module_file_generation.svg

The red dashed line above represents Spack’s boundaries, the blue one Spack’s dependencies [1]. Module files are generated by combining:

  • the configuration details in config.yaml and modules.yaml
  • the information contained in Spack packages (and processed by the module subpackage)
  • a set of template files

with Jinja2, an external template engine that stamps out each particular module file. As Spack serves very diverse needs this process has many points of customization, and we’ll explore most of them in the next sections.

[1]Spack vendors its dependencies! This means that Spack comes with a copy of each one of its dependencies, including Jinja2, and is already configured to use them.

Setup for the tutorial

In order to showcase the capabilities of Spack’s module file generation, we need a representative set of software to work with. This set must include different flavors of the same packages installed alongside each other and some external packages.

The purpose of this setup is not to make our life harder but to demonstrate how Spack can help with similar situations, as they will happen on real HPC clusters. For instance, it’s often preferable for Spack to use vendor-provided MPI implementations than to build one itself.

The best way to follow along is to use a Docker image, which comes with Spack and all the software used in the following parts already pre-installed. If you want to proceed this way, read Use a Docker image.

If you don’t have Docker installed or for any other reason you prefer to work locally, follow instead Work in a local folder to know how to clone Spack and install the software. Be aware that the set-up will take longer and that the details of the snippets below assume the Docker image and may need changes to work in your particular environment.

Use a Docker image

The fastest way to set-up your environment is to use a Docker image:

$ docker pull alalazo/spack:module_tutorial
$ docker run --rm -h module-file-tutorial -it alalazo/spack:module_tutorial
root@module-file-tutorial:/#

If you arrived at this point you should be ready to start, as all the software needed is pre-installed in the image:

root@module-file-tutorial:/# which spack
/usr/local/bin/spack
root@module-file-tutorial:/# spack find
==> 46 installed packages.
-- linux-ubuntu16.04-x86_64 / gcc@5.4.0 -------------------------
autoconf@2.69    gcc@7.2.0  git@2.9.4  isl@0.18         libtool@2.4.6  lua@5.3.4                lua-luaposix@33.4.0  mpc@1.0.3   ncurses@6.0  pkg-config@0.29.2  tcl@8.6.6
automake@1.15.1  gdbm@1.13  gmp@6.1.2  libsigsegv@2.11  lmod@7.7       lua-luafilesystem@1_6_3  m4@1.4.18            mpfr@3.1.5  perl@5.24.1  readline@7.0       zlib@1.2.11

-- linux-ubuntu16.04-x86_64 / gcc@7.2.0 -------------------------
bzip2@1.0.6  ncurses@6.0             netlib-scalapack@2.0.2  openblas@0.2.20  pkg-config@0.29.2  py-packaging@16.8   py-setuptools@35.0.2  readline@7.0
cmake@3.9.4  netlib-lapack@3.6.1     netlib-scalapack@2.0.2  openmpi@1.10.2   py-appdirs@1.4.3   py-pyparsing@2.2.0  py-six@1.10.0         sqlite@3.20.0
mpich@3.2    netlib-scalapack@2.0.2  netlib-scalapack@2.0.2  openssl@1.0.2k   py-numpy@1.13.1    py-scipy@0.19.1     python@2.7.14         zlib@1.2.11

Go to Non-hierarchical module files to proceed with the tutorial.

Note

Dockerfile for this image
Those of you that want to build a similar container themselves can find the Dockerfile and the other resources in Spack’s share/spack/docs/docker folder.

Work in a local folder

If you don’t feel like using a container, you can set-up your environment locally. Let’s start by cloning the Spack repository and moving to the directory where it was checked out:

$ git clone https://github.com/spack/spack.git
$ cd spack

From here we’ll be building the required stack of software.

Build a module tool

The first thing that we need is the module tool. In this case we choose lmod as it can work with both hierarchical and non-hierarchical module file layouts.

$ bin/spack install lmod

Once the module tool is installed we need to have it available in the current shell. As the installation directories are definitely not easy to remember, we’ll employ the command spack location to retrieve the lmod prefix directly from Spack:

$ . $(spack location -i lmod)/lmod/lmod/init/bash

Now we can source the setup file and activate the shell support:

$ . share/spack/setup_env.sh

Add a new compiler

The second step is to build a recent compiler. On first use, Spack scans the environment and automatically locates the compiler(s) already available on the system. This is what you’ll see on Ubuntu 14.04:

$ uname -a
Linux nuvolari 4.4.0-45-generic #66~14.04.1-Ubuntu SMP Wed Oct 19 15:05:38 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux

$ spack compilers
==> Available compilers
-- gcc ----------------------------------------------------------
gcc@4.8

Let’s bootstrap a more recent compiler with the one that was automatically detected:

$ spack install gcc@7.2.0
...
Wait a long time
...

Once gcc is installed we can use shell support to load it and make it readily available:

$ spack load gcc@7.2.0

It may not be apparent, but the last command employed the module files generated automatically by Spack. What happens under the hood when you use the spack load command is:

  1. the spec passed as argument is translated into a module file name
  2. the current module tool is used to load that module file

You can use this command to double check:

$ module list
Currently Loaded Modules:
1) lmod-7.7-gcc-4.8-okcwjgw 2) gcc-7.2.0-gcc-4.8-twd5nqg

Note that the 7-digit hash at the end of the generated module may vary depending on architecture or package version. Now that we have gcc@7.2.0 in PATH we can finally add it to the list of compilers known to Spack:

$ spack compiler add
==> Added 1 new compiler to ~/.spack/linux/compilers.yaml
    gcc@7.2.0

$ spack compilers
==> Available compilers
-- gcc ----------------------------------------------------------
gcc@7.2.0  gcc@4.8

Build the software that will be used in the tutorial

The last step is to install the software stack needed later on. To mimic an external installation of an MPI provider we’ll install openmpi on the system we are working on. On Ubuntu 14.04 it boils down to:

$ sudo apt-get install openmpi-bin openmpi-common libopenmpi-dev
...

but the exact command varies according to your OS. Then we need to prepare a packages.yaml file that instructs Spack to use an externally provided MPI:

packages:
  openmpi:
    buildable: False
    paths:
      openmpi@1.6: /usr

Finally, we should use Spack to install the packages used in the examples:

$ spack install netlib-scalapack ^openmpi ^openblas
$ spack install netlib-scalapack ^mpich ^openblas
$ spack install netlib-scalapack ^openmpi ^netlib-lapack
$ spack install netlib-scalapack ^mpich ^netlib-lapack
$ spack install py-scipy ^openblas

Non-hierarchical module files

If you arrived to this point you should have an environment that looks similar to:

root@module-file-tutorial:/# module avail

----------------------------------------------------------------------------- /usr/local/share/spack/modules/linux-ubuntu16.04-x86_64 -----------------------------------------------------------------------------
   autoconf-2.69-gcc-5.4.0-bvabhji      libtool-2.4.6-gcc-5.4.0-o2pfwjf              ncurses-6.0-gcc-7.2.0-oh6pqty               pkg-config-0.29.2-gcc-5.4.0-ae2hwm7       readline-7.0-gcc-5.4.0-gizxpch
   automake-1.15.1-gcc-5.4.0-kaiefe4    lmod-7.7-gcc-5.4.0-okcwjgw                   netlib-lapack-3.6.1-gcc-7.2.0-5sywztc       pkg-config-0.29.2-gcc-7.2.0-76z7ehw       readline-7.0-gcc-7.2.0-eqos6rz
   bzip2-1.0.6-gcc-7.2.0-mwamumj        lua-5.3.4-gcc-5.4.0-ytxw2gq                  netlib-scalapack-2.0.2-gcc-7.2.0-5lb2j5p    py-appdirs-1.4.3-gcc-7.2.0-7ncu7zr        sqlite-3.20.0-gcc-7.2.0-hfmjilk
   cmake-3.9.4-gcc-7.2.0-6bxdr6h        lua-luafilesystem-1_6_3-gcc-5.4.0-5dzzlt4    netlib-scalapack-2.0.2-gcc-7.2.0-ax6aza6    py-numpy-1.13.1-gcc-7.2.0-22n5oub         tcl-8.6.6-gcc-5.4.0-767ls4i
   gcc-7.2.0-gcc-5.4.0-go3z4hb          lua-luaposix-33.4.0-gcc-5.4.0-w5jpnwm        netlib-scalapack-2.0.2-gcc-7.2.0-c4v5l7j    py-packaging-16.8-gcc-7.2.0-c37cjmq       zlib-1.2.11-gcc-5.4.0-swly52a
   gdbm-1.13-gcc-5.4.0-vdhoris          m4-1.4.18-gcc-5.4.0-r5envx3                  netlib-scalapack-2.0.2-gcc-7.2.0-m7rzcmh    py-pyparsing-2.2.0-gcc-7.2.0-ahdh5cx      zlib-1.2.11-gcc-7.2.0-lv5fabl
   git-2.9.4-gcc-5.4.0-atwjs4i          mpc-1.0.3-gcc-5.4.0-tumbpsh                  openblas-0.2.20-gcc-7.2.0-kvddide           py-scipy-0.19.1-gcc-7.2.0-7hi7r5j
   gmp-6.1.2-gcc-5.4.0-qc4qcfz          mpfr-3.1.5-gcc-5.4.0-mdi6irz                 openmpi-1.10.2-gcc-7.2.0-ufw7pdi            py-setuptools-35.0.2-gcc-7.2.0-cvasi7i
   isl-0.18-gcc-5.4.0-vttqout           mpich-3.2-gcc-7.2.0-7gxffhv                  openssl-1.0.2k-gcc-7.2.0-pxv3dh4            py-six-1.10.0-gcc-7.2.0-3xk5mod
   libsigsegv-2.11-gcc-5.4.0-fypapcp    ncurses-6.0-gcc-5.4.0-ukq4tcc                perl-5.24.1-gcc-5.4.0-mfzwy6y               python-2.7.14-gcc-7.2.0-555u7ea

Use "module spider" to find all possible modules.
Use "module keyword key1 key2 ..." to search for all possible modules matching any of the "keys".

The non-hierarchical module files that have been generated so far follow the default rules for module generation. Taking a look at the gcc module you’ll see, for example:

root@module-file-tutorial:/# module show gcc-7.2.0-gcc-5.4.0-go3z4hb
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   /usr/local/share/spack/modules/linux-ubuntu16.04-x86_64/gcc-7.2.0-gcc-5.4.0-go3z4hb:
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
whatis("The GNU Compiler Collection includes front ends for C, C++, Objective-C, Fortran, Ada, and Go, as well as libraries for these languages. ")
prepend_path("PATH","/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-5.4.0/gcc-7.2.0-go3z4hbsa6wycoaedr3fforx5qnazdhd/bin")
prepend_path("MANPATH","/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-5.4.0/gcc-7.2.0-go3z4hbsa6wycoaedr3fforx5qnazdhd/share/man")
prepend_path("LIBRARY_PATH","/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-5.4.0/gcc-7.2.0-go3z4hbsa6wycoaedr3fforx5qnazdhd/lib")
prepend_path("LD_LIBRARY_PATH","/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-5.4.0/gcc-7.2.0-go3z4hbsa6wycoaedr3fforx5qnazdhd/lib")
prepend_path("LIBRARY_PATH","/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-5.4.0/gcc-7.2.0-go3z4hbsa6wycoaedr3fforx5qnazdhd/lib64")
prepend_path("LD_LIBRARY_PATH","/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-5.4.0/gcc-7.2.0-go3z4hbsa6wycoaedr3fforx5qnazdhd/lib64")
prepend_path("CPATH","/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-5.4.0/gcc-7.2.0-go3z4hbsa6wycoaedr3fforx5qnazdhd/include")
prepend_path("CMAKE_PREFIX_PATH","/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-5.4.0/gcc-7.2.0-go3z4hbsa6wycoaedr3fforx5qnazdhd/")
help([[The GNU Compiler Collection includes front ends for C, C++, Objective-C,
Fortran, Ada, and Go, as well as libraries for these languages.
]])

As expected, a few environment variables representing paths will be modified by the module file according to the default prefix inspection rules.

Filter unwanted modifications to the environment

Now consider the case that your site has decided that CPATH and LIBRARY_PATH modifications should not be present in module files. What you can do to abide by the rules is to create a configuration file ~/.spack/modules.yaml with the following content:

modules:
  tcl:
    all:
      filter:
        environment_blacklist: ['CPATH', 'LIBRARY_PATH']

Next you should regenerate all the module files:

root@module-file-tutorial:/# spack module refresh --module-type tcl
==> You are about to regenerate tcl module files for:

-- linux-ubuntu16.04-x86_64 / gcc@5.4.0 -------------------------
bvabhji autoconf@2.69    vdhoris gdbm@1.13  vttqout isl@0.18         okcwjgw lmod@7.7                 w5jpnwm lua-luaposix@33.4.0  mdi6irz mpfr@3.1.5   ae2hwm7 pkg-config@0.29.2  swly52a zlib@1.2.11
kaiefe4 automake@1.15.1  atwjs4i git@2.9.4  fypapcp libsigsegv@2.11  ytxw2gq lua@5.3.4                r5envx3 m4@1.4.18            ukq4tcc ncurses@6.0  gizxpch readline@7.0
go3z4hb gcc@7.2.0        qc4qcfz gmp@6.1.2  o2pfwjf libtool@2.4.6    5dzzlt4 lua-luafilesystem@1_6_3  tumbpsh mpc@1.0.3            mfzwy6y perl@5.24.1  767ls4i tcl@8.6.6

-- linux-ubuntu16.04-x86_64 / gcc@7.2.0 -------------------------
mwamumj bzip2@1.0.6  5sywztc netlib-lapack@3.6.1     m7rzcmh netlib-scalapack@2.0.2  76z7ehw pkg-config@0.29.2  ahdh5cx py-pyparsing@2.2.0    555u7ea python@2.7.14
6bxdr6h cmake@3.9.4  ax6aza6 netlib-scalapack@2.0.2  kvddide openblas@0.2.20         7ncu7zr py-appdirs@1.4.3   7hi7r5j py-scipy@0.19.1       eqos6rz readline@7.0
7gxffhv mpich@3.2    c4v5l7j netlib-scalapack@2.0.2  ufw7pdi openmpi@1.10.2          22n5oub py-numpy@1.13.1    cvasi7i py-setuptools@35.0.2  hfmjilk sqlite@3.20.0
oh6pqty ncurses@6.0  5lb2j5p netlib-scalapack@2.0.2  pxv3dh4 openssl@1.0.2k          c37cjmq py-packaging@16.8  3xk5mod py-six@1.10.0         lv5fabl zlib@1.2.11

==> Do you want to proceed? [y/n] y
==> Regenerating tcl module files

If you take a look now at the module for gcc you’ll see that the unwanted paths have disappeared:

root@module-file-tutorial:/# module show gcc-7.2.0-gcc-5.4.0-go3z4hb
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   /usr/local/share/spack/modules/linux-ubuntu16.04-x86_64/gcc-7.2.0-gcc-5.4.0-go3z4hb:
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
whatis("The GNU Compiler Collection includes front ends for C, C++, Objective-C, Fortran, Ada, and Go, as well as libraries for these languages. ")
prepend_path("PATH","/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-5.4.0/gcc-7.2.0-go3z4hbsa6wycoaedr3fforx5qnazdhd/bin")
prepend_path("MANPATH","/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-5.4.0/gcc-7.2.0-go3z4hbsa6wycoaedr3fforx5qnazdhd/share/man")
prepend_path("LD_LIBRARY_PATH","/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-5.4.0/gcc-7.2.0-go3z4hbsa6wycoaedr3fforx5qnazdhd/lib")
prepend_path("LD_LIBRARY_PATH","/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-5.4.0/gcc-7.2.0-go3z4hbsa6wycoaedr3fforx5qnazdhd/lib64")
prepend_path("CMAKE_PREFIX_PATH","/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-5.4.0/gcc-7.2.0-go3z4hbsa6wycoaedr3fforx5qnazdhd/")
help([[The GNU Compiler Collection includes front ends for C, C++, Objective-C,
Fortran, Ada, and Go, as well as libraries for these languages.
]])

Prevent some module files from being generated

Another common request at many sites is to avoid exposing software that is only needed as an intermediate step when building a newer stack. Let’s try to prevent the generation of module files for anything that is compiled with gcc@5.4.0 (the OS provided compiler).

To do this you should add a blacklist keyword to ~/.spack/modules.yaml:

modules:
  tcl:
    blacklist:
      -  '%gcc@5.4.0'
    all:
      filter:
        environment_blacklist: ['CPATH', 'LIBRARY_PATH']

and regenerate the module files:

root@module-file-tutorial:/# spack module refresh --module-type tcl --delete-tree
==> You are about to regenerate tcl module files for:

-- linux-ubuntu16.04-x86_64 / gcc@5.4.0 -------------------------
bvabhji autoconf@2.69    vdhoris gdbm@1.13  vttqout isl@0.18         okcwjgw lmod@7.7                 w5jpnwm lua-luaposix@33.4.0  mdi6irz mpfr@3.1.5   ae2hwm7 pkg-config@0.29.2  swly52a zlib@1.2.11
kaiefe4 automake@1.15.1  atwjs4i git@2.9.4  fypapcp libsigsegv@2.11  ytxw2gq lua@5.3.4                r5envx3 m4@1.4.18            ukq4tcc ncurses@6.0  gizxpch readline@7.0
go3z4hb gcc@7.2.0        qc4qcfz gmp@6.1.2  o2pfwjf libtool@2.4.6    5dzzlt4 lua-luafilesystem@1_6_3  tumbpsh mpc@1.0.3            mfzwy6y perl@5.24.1  767ls4i tcl@8.6.6

-- linux-ubuntu16.04-x86_64 / gcc@7.2.0 -------------------------
mwamumj bzip2@1.0.6  5sywztc netlib-lapack@3.6.1     m7rzcmh netlib-scalapack@2.0.2  76z7ehw pkg-config@0.29.2  ahdh5cx py-pyparsing@2.2.0    555u7ea python@2.7.14
6bxdr6h cmake@3.9.4  ax6aza6 netlib-scalapack@2.0.2  kvddide openblas@0.2.20         7ncu7zr py-appdirs@1.4.3   7hi7r5j py-scipy@0.19.1       eqos6rz readline@7.0
7gxffhv mpich@3.2    c4v5l7j netlib-scalapack@2.0.2  ufw7pdi openmpi@1.10.2          22n5oub py-numpy@1.13.1    cvasi7i py-setuptools@35.0.2  hfmjilk sqlite@3.20.0
oh6pqty ncurses@6.0  5lb2j5p netlib-scalapack@2.0.2  pxv3dh4 openssl@1.0.2k          c37cjmq py-packaging@16.8  3xk5mod py-six@1.10.0         lv5fabl zlib@1.2.11

==> Do you want to proceed? [y/n] y
==> Regenerating tcl module files

root@module-file-tutorial:/# module avail

----------------------------------------------------------------------------- /usr/local/share/spack/modules/linux-ubuntu16.04-x86_64 -----------------------------------------------------------------------------
   bzip2-1.0.6-gcc-7.2.0-mwamumj            netlib-scalapack-2.0.2-gcc-7.2.0-5lb2j5p    openmpi-1.10.2-gcc-7.2.0-ufw7pdi       py-packaging-16.8-gcc-7.2.0-c37cjmq       python-2.7.14-gcc-7.2.0-555u7ea
   cmake-3.9.4-gcc-7.2.0-6bxdr6h            netlib-scalapack-2.0.2-gcc-7.2.0-ax6aza6    openssl-1.0.2k-gcc-7.2.0-pxv3dh4       py-pyparsing-2.2.0-gcc-7.2.0-ahdh5cx      readline-7.0-gcc-7.2.0-eqos6rz
   mpich-3.2-gcc-7.2.0-7gxffhv              netlib-scalapack-2.0.2-gcc-7.2.0-c4v5l7j    pkg-config-0.29.2-gcc-7.2.0-76z7ehw    py-scipy-0.19.1-gcc-7.2.0-7hi7r5j         sqlite-3.20.0-gcc-7.2.0-hfmjilk
   ncurses-6.0-gcc-7.2.0-oh6pqty            netlib-scalapack-2.0.2-gcc-7.2.0-m7rzcmh    py-appdirs-1.4.3-gcc-7.2.0-7ncu7zr     py-setuptools-35.0.2-gcc-7.2.0-cvasi7i    zlib-1.2.11-gcc-7.2.0-lv5fabl
   netlib-lapack-3.6.1-gcc-7.2.0-5sywztc    openblas-0.2.20-gcc-7.2.0-kvddide           py-numpy-1.13.1-gcc-7.2.0-22n5oub      py-six-1.10.0-gcc-7.2.0-3xk5mod

Use "module spider" to find all possible modules.
Use "module keyword key1 key2 ..." to search for all possible modules matching any of the "keys".

This time it is convenient to pass the option --delete-tree to the command that regenerates the module files to instruct it to delete the existing tree and regenerate a new one instead of overwriting the files in the existing directory.

If you look closely you’ll see though that we went too far in blacklisting modules: the module for gcc@7.2.0 disappeared as it was bootstrapped with gcc@5.4.0. To specify exceptions to the blacklist rules you can use whitelist:

modules:
  tcl:
    whitelist:
      -  gcc
    blacklist:
      -  '%gcc@5.4.0'
    all:
      filter:
        environment_blacklist: ['CPATH', 'LIBRARY_PATH']

whitelist rules always have precedence over blacklist rules. If you regenerate the modules again:

root@module-file-tutorial:/# spack module refresh --module-type tcl -y
==> Regenerating tcl module files

you’ll see that now the module for gcc@7.2.0 has reappeared:

root@module-file-tutorial:/# module avail gcc-7.2.0-gcc-5.4.0-go3z4hb

----------------------------------------------------------------------------- /usr/local/share/spack/modules/linux-ubuntu16.04-x86_64 -----------------------------------------------------------------------------
   gcc-7.2.0-gcc-5.4.0-go3z4hb

Use "module spider" to find all possible modules.
Use "module keyword key1 key2 ..." to search for all possible modules matching any of the "keys".

Change module file naming

The next step in making module files more user-friendly is to improve their naming scheme. To reduce the length of the hash or remove it altogether you can use the hash_length keyword in the configuration file:

modules:
  tcl:
    hash_length: 0
    whitelist:
      -  gcc
    blacklist:
      -  '%gcc@5.4.0'
    all:
      filter:
        environment_blacklist: ['CPATH', 'LIBRARY_PATH']

If you try to regenerate the module files now you will get an error:

root@module-file-tutorial:/# spack module refresh --module-type tcl --delete-tree -y
==> Error: Name clashes detected in module files:

file: /usr/local/share/spack/modules/linux-ubuntu16.04-x86_64/netlib-scalapack-2.0.2-gcc-7.2.0
spec: netlib-scalapack@2.0.2%gcc@7.2.0 build_type=RelWithDebInfo ~pic+shared arch=linux-ubuntu16.04-x86_64
spec: netlib-scalapack@2.0.2%gcc@7.2.0 build_type=RelWithDebInfo ~pic+shared arch=linux-ubuntu16.04-x86_64
spec: netlib-scalapack@2.0.2%gcc@7.2.0 build_type=RelWithDebInfo ~pic+shared arch=linux-ubuntu16.04-x86_64
spec: netlib-scalapack@2.0.2%gcc@7.2.0 build_type=RelWithDebInfo ~pic+shared arch=linux-ubuntu16.04-x86_64

==> Error: Operation aborted

Note

We try to check for errors upfront!
In Spack we check for errors upfront whenever possible, so don’t worry about your module files: as a name clash was detected nothing has been changed on disk.

The problem here is that without the hashes the four different flavors of netlib-scalapack map to the same module file name. We can add suffixes to differentiate them:

 modules:
   tcl:
     hash_length: 0
     whitelist:
       -  gcc
     blacklist:
       -  '%gcc@5.4.0'
     all:
       suffixes:
         '^openblas': openblas
         '^netlib-lapack': netlib
       filter:
         environment_blacklist: ['CPATH', 'LIBRARY_PATH']
     netlib-scalapack:
       suffixes:
         '^openmpi': openmpi
         '^mpich': mpich

As you can see it is possible to specify rules that apply only to a restricted set of packages using anonymous specs. Regenerating module files now we obtain:

root@module-file-tutorial:/# spack module refresh --module-type tcl --delete-tree -y
==> Regenerating tcl module files

root@module-file-tutorial:/# module avail

----------------------------------------------------------------------------- /usr/local/share/spack/modules/linux-ubuntu16.04-x86_64 -----------------------------------------------------------------------------
   bzip2-1.0.6-gcc-7.2.0    netlib-lapack-3.6.1-gcc-7.2.0                        openblas-0.2.20-gcc-7.2.0      py-numpy-1.13.1-gcc-7.2.0-openblas    py-six-1.10.0-gcc-7.2.0
   cmake-3.9.4-gcc-7.2.0    netlib-scalapack-2.0.2-gcc-7.2.0-netlib-mpich        openmpi-1.10.2-gcc-7.2.0       py-packaging-16.8-gcc-7.2.0           python-2.7.14-gcc-7.2.0
   gcc-7.2.0-gcc-5.4.0      netlib-scalapack-2.0.2-gcc-7.2.0-netlib-openmpi      openssl-1.0.2k-gcc-7.2.0       py-pyparsing-2.2.0-gcc-7.2.0          readline-7.0-gcc-7.2.0
   mpich-3.2-gcc-7.2.0      netlib-scalapack-2.0.2-gcc-7.2.0-openblas-mpich      pkg-config-0.29.2-gcc-7.2.0    py-scipy-0.19.1-gcc-7.2.0-openblas    sqlite-3.20.0-gcc-7.2.0
   ncurses-6.0-gcc-7.2.0    netlib-scalapack-2.0.2-gcc-7.2.0-openblas-openmpi    py-appdirs-1.4.3-gcc-7.2.0     py-setuptools-35.0.2-gcc-7.2.0        zlib-1.2.11-gcc-7.2.0

Use "module spider" to find all possible modules.
Use "module keyword key1 key2 ..." to search for all possible modules matching any of the "keys".

Finally we can set a naming_scheme to prevent users from loading modules that refer to different flavors of the same library/application:

modules:
  tcl:
    hash_length: 0
    naming_scheme: '${PACKAGE}/${VERSION}-${COMPILERNAME}-${COMPILERVER}'
    whitelist:
      -  gcc
    blacklist:
      -  '%gcc@5.4.0'
    all:
      conflict:
        - '${PACKAGE}'
      suffixes:
        '^openblas': openblas
        '^netlib-lapack': netlib
      filter:
        environment_blacklist: ['CPATH', 'LIBRARY_PATH']
    netlib-scalapack:
      suffixes:
        '^openmpi': openmpi
        '^mpich': mpich

The final result should look like:

root@module-file-tutorial:/# spack module refresh --module-type tcl --delete-tree -y
==> Regenerating tcl module files
root@module-file-tutorial:/# module avail

----------------------------------------------------------------------------- /usr/local/share/spack/modules/linux-ubuntu16.04-x86_64 -----------------------------------------------------------------------------
   bzip2/1.0.6-gcc-7.2.0    netlib-lapack/3.6.1-gcc-7.2.0                            openblas/0.2.20-gcc-7.2.0      py-numpy/1.13.1-gcc-7.2.0-openblas    py-six/1.10.0-gcc-7.2.0
   cmake/3.9.4-gcc-7.2.0    netlib-scalapack/2.0.2-gcc-7.2.0-netlib-mpich            openmpi/1.10.2-gcc-7.2.0       py-packaging/16.8-gcc-7.2.0           python/2.7.14-gcc-7.2.0
   gcc/7.2.0-gcc-5.4.0      netlib-scalapack/2.0.2-gcc-7.2.0-netlib-openmpi          openssl/1.0.2k-gcc-7.2.0       py-pyparsing/2.2.0-gcc-7.2.0          readline/7.0-gcc-7.2.0
   mpich/3.2-gcc-7.2.0      netlib-scalapack/2.0.2-gcc-7.2.0-openblas-mpich          pkg-config/0.29.2-gcc-7.2.0    py-scipy/0.19.1-gcc-7.2.0-openblas    sqlite/3.20.0-gcc-7.2.0
   ncurses/6.0-gcc-7.2.0    netlib-scalapack/2.0.2-gcc-7.2.0-openblas-openmpi (D)    py-appdirs/1.4.3-gcc-7.2.0     py-setuptools/35.0.2-gcc-7.2.0        zlib/1.2.11-gcc-7.2.0

  Where:
   D:  Default Module

Use "module spider" to find all possible modules.
Use "module keyword key1 key2 ..." to search for all possible modules matching any of the "keys".

Note

TCL specific directive
The directives naming_scheme and conflict are TCL specific and can’t be used in the lmod section of the configuration file.

Add custom environment modifications

At many sites it is customary to set an environment variable in a package’s module file that points to the folder in which the package is installed. You can achieve this with Spack by adding an environment directive to the configuration file:

modules:
  tcl:
    hash_length: 0
    naming_scheme: '${PACKAGE}/${VERSION}-${COMPILERNAME}-${COMPILERVER}'
    whitelist:
      -  gcc
    blacklist:
      -  '%gcc@5.4.0'
    all:
      conflict:
        - '${PACKAGE}'
      suffixes:
        '^openblas': openblas
        '^netlib-lapack': netlib
      filter:
        environment_blacklist: ['CPATH', 'LIBRARY_PATH']
      environment:
        set:
          '${PACKAGE}_ROOT': '${PREFIX}'
    netlib-scalapack:
      suffixes:
        '^openmpi': openmpi
        '^mpich': mpich

Under the hood Spack uses the format() API to substitute tokens in either environment variable names or values. There are two caveats though:

  • The set of allowed tokens in variable names is restricted to PACKAGE, VERSION, COMPILER, COMPILERNAME, COMPILERVER, ARCHITECTURE
  • Any token expanded in a variable name is made uppercase, but other than that case sensitivity is preserved

Regenerating the module files results in something like:

root@module-file-tutorial:/# spack module refresh -y --module-type tcl
==> Regenerating tcl module files

root@module-file-tutorial:/# module show gcc
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   /usr/local/share/spack/modules/linux-ubuntu16.04-x86_64/gcc/7.2.0-gcc-5.4.0:
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
whatis("The GNU Compiler Collection includes front ends for C, C++, Objective-C, Fortran, Ada, and Go, as well as libraries for these languages. ")
conflict("gcc")
prepend_path("PATH","/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-5.4.0/gcc-7.2.0-go3z4hbsa6wycoaedr3fforx5qnazdhd/bin")
prepend_path("MANPATH","/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-5.4.0/gcc-7.2.0-go3z4hbsa6wycoaedr3fforx5qnazdhd/share/man")
prepend_path("LD_LIBRARY_PATH","/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-5.4.0/gcc-7.2.0-go3z4hbsa6wycoaedr3fforx5qnazdhd/lib")
prepend_path("LD_LIBRARY_PATH","/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-5.4.0/gcc-7.2.0-go3z4hbsa6wycoaedr3fforx5qnazdhd/lib64")
prepend_path("CMAKE_PREFIX_PATH","/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-5.4.0/gcc-7.2.0-go3z4hbsa6wycoaedr3fforx5qnazdhd/")
setenv("GCC_ROOT","/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-5.4.0/gcc-7.2.0-go3z4hbsa6wycoaedr3fforx5qnazdhd")
help([[The GNU Compiler Collection includes front ends for C, C++, Objective-C,
Fortran, Ada, and Go, as well as libraries for these languages.
]])

As you can see, the gcc module has the environment variable GCC_ROOT set.

Sometimes it’s also useful to apply environment modifications selectively and target only certain packages. You can, for instance set the common variables CC, CXX, etc. in the gcc module file and apply other custom modifications to the openmpi modules as follows:

modules:
  tcl:
    hash_length: 0
    naming_scheme: '${PACKAGE}/${VERSION}-${COMPILERNAME}-${COMPILERVER}'
    whitelist:
      - gcc
    blacklist:
      - '%gcc@5.4.0'
    all:
      conflict:
        - '${PACKAGE}'
      suffixes:
        '^openblas': openblas
        '^netlib-lapack': netlib
      filter:
        environment_blacklist: ['CPATH', 'LIBRARY_PATH']
      environment:
        set:
          '${PACKAGE}_ROOT': '${PREFIX}'
    gcc:
      environment:
        set:
          CC: gcc
          CXX: g++
          FC: gfortran
          F90: gfortran
          F77: gfortran
    openmpi:
      environment:
        set:
          SLURM_MPI_TYPE: pmi2
          OMPI_MCA_btl_openib_warn_default_gid_prefix: '0'
    netlib-scalapack:
      suffixes:
        '^openmpi': openmpi
        '^mpich': mpich

This time we will be more selective and regenerate only the gcc and openmpi module files:

root@module-file-tutorial:/#  spack module refresh -y --module-type tcl gcc
==> Regenerating tcl module files

root@module-file-tutorial:/# spack module refresh -y --module-type tcl openmpi
==> Regenerating tcl module files

root@module-file-tutorial:/# module show gcc
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   /usr/local/share/spack/modules/linux-ubuntu16.04-x86_64/gcc/7.2.0-gcc-5.4.0:
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
whatis("The GNU Compiler Collection includes front ends for C, C++, Objective-C, Fortran, Ada, and Go, as well as libraries for these languages. ")
conflict("gcc")
prepend_path("PATH","/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-5.4.0/gcc-7.2.0-go3z4hbsa6wycoaedr3fforx5qnazdhd/bin")
prepend_path("MANPATH","/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-5.4.0/gcc-7.2.0-go3z4hbsa6wycoaedr3fforx5qnazdhd/share/man")
prepend_path("LD_LIBRARY_PATH","/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-5.4.0/gcc-7.2.0-go3z4hbsa6wycoaedr3fforx5qnazdhd/lib")
prepend_path("LD_LIBRARY_PATH","/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-5.4.0/gcc-7.2.0-go3z4hbsa6wycoaedr3fforx5qnazdhd/lib64")
prepend_path("CMAKE_PREFIX_PATH","/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-5.4.0/gcc-7.2.0-go3z4hbsa6wycoaedr3fforx5qnazdhd/")
setenv("GCC_ROOT","/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-5.4.0/gcc-7.2.0-go3z4hbsa6wycoaedr3fforx5qnazdhd")
setenv("CC","gcc")
setenv("CXX","g++")
setenv("FC","gfortran")
setenv("F90","gfortran")
setenv("F77","gfortran")
help([[The GNU Compiler Collection includes front ends for C, C++, Objective-C,
Fortran, Ada, and Go, as well as libraries for these languages.
]])

root@module-file-tutorial:/# module show openmpi
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   /usr/local/share/spack/modules/linux-ubuntu16.04-x86_64/openmpi/1.10.2-gcc-7.2.0:
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
whatis("The Open MPI Project is an open source Message Passing Interface implementation that is developed and maintained by a consortium of academic, research, and industry partners. Open MPI is therefore able t
o combine the expertise, technologies, and resources from all across the High Performance Computing community in order to build the best MPI library available. Open MPI offers advantages for system and software
vendors, application developers and computer science researchers.  ")
conflict("openmpi")
prepend_path("MANPATH","/usr/share/man")
prepend_path("ACLOCAL_PATH","/usr/share/aclocal")
prepend_path("PKG_CONFIG_PATH","/usr/lib/pkgconfig")
setenv("OPENMPI_ROOT","/usr")
setenv("SLURM_MPI_TYPE","pmi2")
setenv("OMPI_MCA_btl_openib_warn_default_gid_prefix","0")
help([[The Open MPI Project is an open source Message Passing Interface
implementation that is developed and maintained by a consortium of
academic, research, and industry partners. Open MPI is therefore able to
combine the expertise, technologies, and resources from all across the
High Performance Computing community in order to build the best MPI
library available. Open MPI offers advantages for system and software
vendors, application developers and computer science researchers.
]])

Autoload dependencies

Spack can also generate module files that contain code to load the dependencies automatically. You can, for instance generate python modules that load their dependencies by adding the autoload directive and assigning it the value direct:

modules:
  tcl:
    verbose: True
    hash_length: 0
    naming_scheme: '${PACKAGE}/${VERSION}-${COMPILERNAME}-${COMPILERVER}'
    whitelist:
      - gcc
    blacklist:
      - '%gcc@5.4.0'
    all:
      conflict:
        - '${PACKAGE}'
      suffixes:
        '^openblas': openblas
        '^netlib-lapack': netlib
      filter:
        environment_blacklist: ['CPATH', 'LIBRARY_PATH']
      environment:
        set:
          '${PACKAGE}_ROOT': '${PREFIX}'
    gcc:
      environment:
        set:
          CC: gcc
          CXX: g++
          FC: gfortran
          F90: gfortran
          F77: gfortran
    openmpi:
      environment:
        set:
          SLURM_MPI_TYPE: pmi2
          OMPI_MCA_btl_openib_warn_default_gid_prefix: '0'
    netlib-scalapack:
      suffixes:
        '^openmpi': openmpi
        '^mpich': mpich
    ^python:
      autoload:  'direct'

and regenerating the module files for every package that depends on python:

root@module-file-tutorial:/# spack module refresh -y --module-type tcl ^python
==> Regenerating tcl module files

Now the py-scipy module will be:

#%Module1.0
## Module file created by spack (https://github.com/spack/spack) on 2017-10-07 15:02:14.974937
##
## py-scipy@0.19.1%gcc@7.2.0 arch=linux-ubuntu16.04-x86_64 /7hi7r5j
##


module-whatis "SciPy (pronounced 'Sigh Pie') is a Scientific Library for Python. It provides many user-friendly and efficient numerical routines such as routines for numerical integration and optimization."

proc ModulesHelp { } {
puts stderr "SciPy (pronounced 'Sigh Pie') is a Scientific Library for Python. It"
puts stderr "provides many user-friendly and efficient numerical routines such as"
puts stderr "routines for numerical integration and optimization."
}

if ![ is-loaded python/2.7.14-gcc-7.2.0 ] {
    puts stderr "Autoloading python/2.7.14-gcc-7.2.0"
    module load python/2.7.14-gcc-7.2.0
}
if ![ is-loaded openblas/0.2.20-gcc-7.2.0 ] {
    puts stderr "Autoloading openblas/0.2.20-gcc-7.2.0"
    module load openblas/0.2.20-gcc-7.2.0
}
if ![ is-loaded py-numpy/1.13.1-gcc-7.2.0-openblas ] {
    puts stderr "Autoloading py-numpy/1.13.1-gcc-7.2.0-openblas"
    module load py-numpy/1.13.1-gcc-7.2.0-openblas
}
conflict py-scipy

prepend-path LD_LIBRARY_PATH "/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-7.2.0/py-scipy-0.19.1-7hi7r5jri7bmohh4oontvfxo7rgj4hef/lib"
prepend-path CMAKE_PREFIX_PATH "/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-7.2.0/py-scipy-0.19.1-7hi7r5jri7bmohh4oontvfxo7rgj4hef/"
prepend-path PYTHONPATH "/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-7.2.0/py-scipy-0.19.1-7hi7r5jri7bmohh4oontvfxo7rgj4hef/lib/python2.7/site-packages"
setenv PY_SCIPY_ROOT "/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-7.2.0/py-scipy-0.19.1-7hi7r5jri7bmohh4oontvfxo7rgj4hef"

and will contain code to autoload all the dependencies:

root@module-file-tutorial:/# module load py-scipy
Autoloading python/2.7.14-gcc-7.2.0
Autoloading openblas/0.2.20-gcc-7.2.0
Autoloading py-numpy/1.13.1-gcc-7.2.0-openblas

In case messages are unwanted during the autoload procedure, it will be sufficient to omit the line setting verbose: True in the configuration file above.

Hierarchical module files

So far we worked with non-hierarchical module files, i.e. with module files that are all generated in the same root directory and don’t attempt to dynamically modify the MODULEPATH. This results in a flat module structure where all the software is visible at the same time:

root@module-file-tutorial:/# module avail

----------------------------------------------------------------------------- /usr/local/share/spack/modules/linux-ubuntu16.04-x86_64 -----------------------------------------------------------------------------
   bzip2/1.0.6-gcc-7.2.0    netlib-lapack/3.6.1-gcc-7.2.0                            openblas/0.2.20-gcc-7.2.0      py-numpy/1.13.1-gcc-7.2.0-openblas    py-six/1.10.0-gcc-7.2.0
   cmake/3.9.4-gcc-7.2.0    netlib-scalapack/2.0.2-gcc-7.2.0-netlib-mpich            openmpi/1.10.2-gcc-7.2.0       py-packaging/16.8-gcc-7.2.0           python/2.7.14-gcc-7.2.0
   gcc/7.2.0-gcc-5.4.0      netlib-scalapack/2.0.2-gcc-7.2.0-netlib-openmpi          openssl/1.0.2k-gcc-7.2.0       py-pyparsing/2.2.0-gcc-7.2.0          readline/7.0-gcc-7.2.0
   mpich/3.2-gcc-7.2.0      netlib-scalapack/2.0.2-gcc-7.2.0-openblas-mpich          pkg-config/0.29.2-gcc-7.2.0    py-scipy/0.19.1-gcc-7.2.0-openblas    sqlite/3.20.0-gcc-7.2.0
   ncurses/6.0-gcc-7.2.0    netlib-scalapack/2.0.2-gcc-7.2.0-openblas-openmpi (D)    py-appdirs/1.4.3-gcc-7.2.0     py-setuptools/35.0.2-gcc-7.2.0        zlib/1.2.11-gcc-7.2.0

  Where:
   D:  Default Module

Use "module spider" to find all possible modules.
Use "module keyword key1 key2 ..." to search for all possible modules matching any of the "keys".

This layout is quite simple to deploy, but you can see from the above snippet that nothing prevents users from loading incompatible sets of modules:

root@module-file-tutorial:/# module load netlib-lapack/3.6.1-gcc-7.2.0 openblas/0.2.20-gcc-7.2.0
root@module-file-tutorial:/# module list

Currently Loaded Modules:
  1) netlib-lapack/3.6.1-gcc-7.2.0   2) openblas/0.2.20-gcc-7.2.0

Even if conflicts directives are carefully placed in module files, they:

  • won’t enforce a consistent environment, but will just report an error
  • need constant updates, for instance as soon as a new compiler or MPI library is installed

Hierarchical module files try to overcome these shortcomings by showing at start-up only a restricted view of what is available on the system: more specifically only the software that has been installed with OS provided compilers. Among this software there will be other - usually more recent - compilers that, once loaded, will prepend new directories to MODULEPATH unlocking all the software that was compiled with them. This “unlocking” idea can then be extended arbitrarily to virtual dependencies, as we’ll see in the following section.

Core/Compiler/MPI

The most widely used hierarchy is the so called Core/Compiler/MPI where, on top of the compilers, different MPI libraries also unlock software linked to them. There are just a few steps needed to adapt the modules.yaml file we used previously:

  1. enable the lmod file generator
  2. change the tcl tag to lmod
  3. remove tcl specific directives (naming_scheme and conflict)
  4. declare which compilers are considered core_compilers
  5. remove the mpi related suffixes (as they will be substituted by hierarchies)

After these modifications your configuration file should look like:

modules:
  enable::
    - lmod
  lmod:
    core_compilers:
      - 'gcc@5.4.0'
    hierarchy:
      - mpi
    hash_length: 0
    whitelist:
      - gcc
    blacklist:
      - '%gcc@5.4.0'
    all:
      suffixes:
        '^openblas': openblas
        '^netlib-lapack': netlib
      filter:
        environment_blacklist: ['CPATH', 'LIBRARY_PATH']
      environment:
        set:
          '${PACKAGE}_ROOT': '${PREFIX}'
    gcc:
      environment:
        set:
          CC: gcc
          CXX: g++
          FC: gfortran
          F90: gfortran
          F77: gfortran
    openmpi:
      environment:
        set:
          SLURM_MPI_TYPE: pmi2
          OMPI_MCA_btl_openib_warn_default_gid_prefix: '0'

Note

Double colon in configuration files
The double colon after enable is intentional and it serves the purpose of overriding the default list of enabled generators so that only lmod will be active (see Overriding entire sections for more details).

The directive core_compilers accepts a list of compilers. Everything built using these compilers will create a module in the Core part of the hierarchy, which is the entry point for hierarchical module files. It is common practice to put the OS provided compilers in the list and only build common utilities and other compilers with them.

If we now regenerate the module files:

root@module-file-tutorial:/# spack module refresh --module-type lmod --delete-tree -y
==> Regenerating lmod module files

and update MODULEPATH to point to the Core:

root@module-file-tutorial:/# module unuse /usr/local/share/spack/modules/linux-ubuntu16.04-x86_64
root@module-file-tutorial:/# module use /usr/local/share/spack/lmod/linux-ubuntu16.04-x86_64/Core

asking for the available modules will return:

root@module-file-tutorial:/# module avail

---------------------------------------------------------------------------- /usr/local/share/spack/lmod/linux-ubuntu16.04-x86_64/Core ----------------------------------------------------------------------------
   gcc/7.2.0

Use "module spider" to find all possible modules.
Use "module keyword key1 key2 ..." to search for all possible modules matching any of the "keys".

Unsurprisingly, the only visible module is gcc. Loading that we’ll unlock the Compiler part of the hierarchy:

root@module-file-tutorial:/# module load gcc
root@module-file-tutorial:/# module avail

------------------------------------------------------------------------- /usr/local/share/spack/lmod/linux-ubuntu16.04-x86_64/gcc/7.2.0 --------------------------------------------------------------------------
   bzip2/1.0.6    mpich/3.2      netlib-lapack/3.6.1    openmpi/1.10.2    pkg-config/0.29.2    py-numpy/1.13.1-openblas    py-pyparsing/2.2.0          py-setuptools/35.0.2    python/2.7.14    sqlite/3.20.0
   cmake/3.9.4    ncurses/6.0    openblas/0.2.20        openssl/1.0.2k    py-appdirs/1.4.3     py-packaging/16.8           py-scipy/0.19.1-openblas    py-six/1.10.0           readline/7.0     zlib/1.2.11

---------------------------------------------------------------------------- /usr/local/share/spack/lmod/linux-ubuntu16.04-x86_64/Core ----------------------------------------------------------------------------
   gcc/7.2.0 (L)

  Where:
   L:  Module is loaded

Use "module spider" to find all possible modules.
Use "module keyword key1 key2 ..." to search for all possible modules matching any of the "keys".

The same holds true also for the MPI part, that you can enable by loading either mpich or openmpi. Let’s start by loading mpich:

root@module-file-tutorial:/# module load mpich
root@module-file-tutorial:/# module avail

---------------------------------------------------------------- /usr/local/share/spack/lmod/linux-ubuntu16.04-x86_64/mpich/3.2-7gxffhv/gcc/7.2.0 -----------------------------------------------------------------
   netlib-scalapack/2.0.2-netlib    netlib-scalapack/2.0.2-openblas (D)

------------------------------------------------------------------------- /usr/local/share/spack/lmod/linux-ubuntu16.04-x86_64/gcc/7.2.0 --------------------------------------------------------------------------
   bzip2/1.0.6    mpich/3.2   (L)    netlib-lapack/3.6.1    openmpi/1.10.2    pkg-config/0.29.2    py-numpy/1.13.1-openblas    py-pyparsing/2.2.0          py-setuptools/35.0.2    python/2.7.14    sqlite/3.20.0
   cmake/3.9.4    ncurses/6.0        openblas/0.2.20        openssl/1.0.2k    py-appdirs/1.4.3     py-packaging/16.8           py-scipy/0.19.1-openblas    py-six/1.10.0           readline/7.0     zlib/1.2.11

---------------------------------------------------------------------------- /usr/local/share/spack/lmod/linux-ubuntu16.04-x86_64/Core ----------------------------------------------------------------------------
   gcc/7.2.0 (L)

  Where:
   L:  Module is loaded
   D:  Default Module

Use "module spider" to find all possible modules.
Use "module keyword key1 key2 ..." to search for all possible modules matching any of the "keys".


root@module-file-tutorial:/# module load openblas netlib-scalapack/2.0.2-openblas
root@module-file-tutorial:/# module list

Currently Loaded Modules:
  1) gcc/7.2.0   2) mpich/3.2   3) openblas/0.2.20   4) netlib-scalapack/2.0.2-openblas

At this point we can showcase the improved consistency that a hierarchical layout provides over a non-hierarchical one:

root@module-file-tutorial:/# module load openmpi

Lmod is automatically replacing "mpich/3.2" with "openmpi/1.10.2".

Due to MODULEPATH changes, the following have been reloaded:
  1) netlib-scalapack/2.0.2-openblas

Lmod took care of swapping the MPI provider for us, and it also substituted the netlib-scalapack module to conform to the change in the MPI. In this way we can’t accidentally pull-in two different MPI providers at the same time or load a module file for a package linked to openmpi when mpich is also loaded. Consistency for compilers and MPI is ensured by the tool.

Add LAPACK to the hierarchy

The hierarchy just shown is already a great improvement over non-hierarchical layouts, but it still has an asymmetry: LAPACK providers cover the same semantic role as MPI providers, but yet they are not part of the hierarchy.

To be more practical, this means that although we have gained an improved consistency in our environment when it comes to MPI, we still have the same problems as we had before for LAPACK implementations:

root@module-file-tutorial:/# module list

Currently Loaded Modules:
  1) gcc/7.2.0   2) openblas/0.2.20   3) openmpi/1.10.2   4) netlib-scalapack/2.0.2-openblas

root@module-file-tutorial:/# module load netlib-scalapack/2.0.2-netlib
Autoloading netlib-lapack/3.6.1

The following have been reloaded with a version change:
  1) netlib-scalapack/2.0.2-openblas => netlib-scalapack/2.0.2-netlib

root@module-file-tutorial:/# module list

Currently Loaded Modules:
  1) gcc/7.2.0   2) openblas/0.2.20   3) openmpi/1.10.2   4) netlib-lapack/3.6.1   5) netlib-scalapack/2.0.2-netlib

Hierarchies that are deeper than Core/Compiler/MPI are probably still considered “unusual” or “impractical” at many sites, mainly because module files are written manually and keeping track of the combinations among multiple providers quickly becomes quite involved.

For instance, having both MPI and LAPACK in the hierarchy means we must classify software into one of four categories:

  1. Software that doesn’t depend on MPI or LAPACK
  2. Software that depends only on MPI
  3. Software that depends only on LAPACK
  4. Software that depends on both

to decide when to show it to the user. The situation becomes more involved as the number of virtual dependencies in the hierarchy increases.

We can take advantage of the DAG that Spack maintains for the installed software and solve this combinatorial problem in a clean and automated way. In some sense Spack’s ability to manage this combinatorial complexity makes deeper hierarchies feasible.

Coming back to our example, let’s add lapack to the hierarchy and remove any remaining suffix:

modules:
  enable::
    - lmod
  lmod:
    core_compilers:
      - 'gcc@5.4.0'
    hierarchy:
      - mpi
      - lapack
    hash_length: 0
    whitelist:
      - gcc
    blacklist:
      - '%gcc@5.4.0'
      - readline
    all:
      filter:
        environment_blacklist: ['CPATH', 'LIBRARY_PATH']
      environment:
        set:
          '${PACKAGE}_ROOT': '${PREFIX}'
    gcc:
      environment:
        set:
          CC: gcc
          CXX: g++
          FC: gfortran
          F90: gfortran
          F77: gfortran
    openmpi:
      environment:
        set:
          SLURM_MPI_TYPE: pmi2
          OMPI_MCA_btl_openib_warn_default_gid_prefix: '0'

After module files have been regenerated as usual:

root@module-file-tutorial:/# module purge

root@module-file-tutorial:/# spack module refresh --delete-tree -y -m lmod
==> Regenerating lmod module files

we can see that now we have additional components in the hierarchy:

root@module-file-tutorial:/# module load gcc
root@module-file-tutorial:/# module load openblas
root@module-file-tutorial:/# module avail

------------------------------------------------------------- /usr/local/share/spack/lmod/linux-ubuntu16.04-x86_64/openblas/0.2.20-kvddide/gcc/7.2.0 --------------------------------------------------------------
   py-numpy/1.13.1    py-scipy/0.19.1

------------------------------------------------------------------------- /usr/local/share/spack/lmod/linux-ubuntu16.04-x86_64/gcc/7.2.0 --------------------------------------------------------------------------
   bzip2/1.0.6    mpich/3.2      netlib-lapack/3.6.1        openmpi/1.10.2    pkg-config/0.29.2    py-packaging/16.8     py-setuptools/35.0.2    python/2.7.14    sqlite/3.20.0
   cmake/3.9.4    ncurses/6.0    openblas/0.2.20     (L)    openssl/1.0.2k    py-appdirs/1.4.3     py-pyparsing/2.2.0    py-six/1.10.0           readline/7.0     zlib/1.2.11

---------------------------------------------------------------------------- /usr/local/share/spack/lmod/linux-ubuntu16.04-x86_64/Core ----------------------------------------------------------------------------
   gcc/7.2.0 (L)

  Where:
   L:  Module is loaded

Use "module spider" to find all possible modules.
Use "module keyword key1 key2 ..." to search for all possible modules matching any of the "keys".


root@module-file-tutorial:/# module load openmpi
root@module-file-tutorial:/# module avail

-------------------------------------------------- /usr/local/share/spack/lmod/linux-ubuntu16.04-x86_64/openmpi/1.10.2-ufw7pdi/openblas/0.2.20-kvddide/gcc/7.2.0 --------------------------------------------------
   netlib-scalapack/2.0.2

------------------------------------------------------------- /usr/local/share/spack/lmod/linux-ubuntu16.04-x86_64/openblas/0.2.20-kvddide/gcc/7.2.0 --------------------------------------------------------------
   py-numpy/1.13.1    py-scipy/0.19.1

------------------------------------------------------------------------- /usr/local/share/spack/lmod/linux-ubuntu16.04-x86_64/gcc/7.2.0 --------------------------------------------------------------------------
   bzip2/1.0.6    mpich/3.2      netlib-lapack/3.6.1        openmpi/1.10.2 (L)    pkg-config/0.29.2    py-packaging/16.8     py-setuptools/35.0.2    python/2.7.14    sqlite/3.20.0
   cmake/3.9.4    ncurses/6.0    openblas/0.2.20     (L)    openssl/1.0.2k        py-appdirs/1.4.3     py-pyparsing/2.2.0    py-six/1.10.0           readline/7.0     zlib/1.2.11

---------------------------------------------------------------------------- /usr/local/share/spack/lmod/linux-ubuntu16.04-x86_64/Core ----------------------------------------------------------------------------
   gcc/7.2.0 (L)

  Where:
   L:  Module is loaded

Use "module spider" to find all possible modules.
Use "module keyword key1 key2 ..." to search for all possible modules matching any of the "keys".

Both MPI and LAPACK providers will now benefit from the same safety features:

root@module-file-tutorial:/# module load py-numpy netlib-scalapack
root@module-file-tutorial:/# module load mpich

Lmod is automatically replacing "openmpi/1.10.2" with "mpich/3.2".


Due to MODULEPATH changes, the following have been reloaded:
  1) netlib-scalapack/2.0.2

root@module-file-tutorial:/# module load netlib-lapack

Lmod is automatically replacing "openblas/0.2.20" with "netlib-lapack/3.6.1".


Inactive Modules:
  1) py-numpy

Due to MODULEPATH changes, the following have been reloaded:
  1) netlib-scalapack/2.0.2

Because we only compiled py-numpy with openblas the module is made inactive when we switch the LAPACK provider. The user environment is now consistent by design!

Working with templates

As briefly mentioned in the introduction, Spack uses Jinja2 to generate each individual module file. This means that you have all of its flexibility and power when it comes to customizing what gets generated!

Module file templates

The templates that Spack uses to generate module files are stored in the share/spack/templates/module directory within the Spack prefix, and they all share the same common structure. Usually, they start with a header that identifies the type of module being generated. In the case of hierarchical module files it’s:

-- -*- lua -*-
-- Module file created by spack (https://github.com/spack/spack) on {{ timestamp }}
--
-- {{ spec.short_spec }}
--

The statements within double curly brackets {{ ... }} denote expressions that will be evaluated and substituted at module generation time. The rest of the file is then divided into blocks that can be overridden or extended by users, if need be. Control structures , delimited by {% ... %}, are also permitted in the template language:

{% block environment %}
{% for command_name, cmd in environment_modifications %}
{% if command_name == 'PrependPath' %}
prepend_path("{{ cmd.name }}", "{{ cmd.value }}")
{% elif command_name == 'AppendPath' %}
append_path("{{ cmd.name }}", "{{ cmd.value }}")
{% elif command_name == 'RemovePath' %}
remove_path("{{ cmd.name }}", "{{ cmd.value }}")
{% elif command_name == 'SetEnv' %}
setenv("{{ cmd.name }}", "{{ cmd.value }}")
{% elif command_name == 'UnsetEnv' %}
unsetenv("{{ cmd.name }}")
{% endif %}
{% endfor %}
{% endblock %}

The locations where Spack looks for templates are specified in config.yaml:

  # Locations where templates should be found
  template_dirs:
    - $spack/share/spack/templates

and can be extended by users to employ custom templates, as we’ll see next.

Extend the default templates

Let’s assume one of our software is protected by group membership: allowed users belong to the same linux group, and access is granted at group level. Wouldn’t it be nice if people that are not yet entitled to use it could receive a helpful message at module load time that tells them who to contact in your organization to be inserted in the group?

To automate the generation of module files with such site-specific behavior we’ll start by extending the list of locations where Spack looks for module files. Let’s create the file ~/.spack/config.yaml with the content:

config:
  template_dirs:
    - $HOME/.spack/templates

This tells Spack to also search another location when looking for template files. Next, we need to create our custom template extension in the folder listed above:

{% extends "modules/modulefile.lua" %}
{% block footer %}
-- Access is granted only to specific groups
if not isDir("{{ spec.prefix }}") then
    LmodError (
        "You don't have the necessary rights to run \"{{ spec.name }}\".\n\n",
        "\tPlease write an e-mail to 1234@foo.com if you need further information on how to get access to it.\n"
    )
end
{% endblock %}

Let’s name this file group-restricted.lua. The line:

{% extends "modules/modulefile.lua" %}

tells Jinja2 that we are reusing the standard template for hierarchical module files. The section:

{% block footer %}
-- Access is granted only to specific groups
if not isDir("{{ spec.prefix }}") then
    LmodError (
        "You don't have the necessary rights to run \"{{ spec.name }}\".\n\n",
        "\tPlease write an e-mail to 1234@foo.com if you need further information on how to get access to it.\n"
    )
end
{% endblock %}

overrides the footer block. Finally, we need to add a couple of lines in modules.yaml to tell Spack which specs need to use the new custom template. For the sake of illustration let’s assume it’s netlib-scalapack:

modules:
  enable::
    - lmod
  lmod:
    core_compilers:
      - 'gcc@5.4.0'
    hierarchy:
      - mpi
      - lapack
    hash_length: 0
    whitelist:
      - gcc
    blacklist:
      - '%gcc@5.4.0'
      - readline
    all:
      filter:
        environment_blacklist: ['CPATH', 'LIBRARY_PATH']
      environment:
        set:
          '${PACKAGE}_ROOT': '${PREFIX}'
    gcc:
      environment:
        set:
          CC: gcc
          CXX: g++
          FC: gfortran
          F90: gfortran
          F77: gfortran
    openmpi:
      environment:
        set:
          SLURM_MPI_TYPE: pmi2
          OMPI_MCA_btl_openib_warn_default_gid_prefix: '0'
    netlib-scalapack:
      template: 'group-restricted.lua'

If we regenerate the module files one last time:

root@module-file-tutorial:/# spack  module refresh -y -m lmod netlib-scalapack
==> Regenerating lmod module files

we’ll find the following at the end of each netlib-scalapack module file:

-- Access is granted only to specific groups
if not isDir("/usr/local/opt/spack/linux-ubuntu16.04-x86_64/gcc-7.2.0/netlib-scalapack-2.0.2-ax6aza6vyepceyr3fihewp7rbr2vp7ym") then
    LmodError (
        "You don't have the necessary rights to run \"netlib-scalapack\".\n\n",
        "\tPlease write an e-mail to 1234@foo.com if you need further information on how to get access to it.\n"
    )
end

and every user that doesn’t have access to the software will now be redirected to the right e-mail address where to ask for it!