1# Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
2# file Copyright.txt or https://cmake.org/licensing for details.
3
4#[=======================================================================[.rst:
5FindEnvModules
6--------------
7
8.. versionadded:: 3.15
9
10Locate an environment module implementation and make commands available to
11CMake scripts to use them.  This is compatible with both Lua-based Lmod
12and TCL-based EnvironmentModules.
13
14This module is intended for the use case of setting up the compiler and library
15environment within a :ref:`CTest Script <CTest Script>` (``ctest -S``).  It can
16also be used in a :ref:`CMake Script <Script Processing Mode>` (``cmake -P``).
17
18.. note::
19
20  The loaded environment will not survive past the end of the calling process.
21  Do not use this module in project code (``CMakeLists.txt`` files) to load
22  a compiler environment; it will not be available during the build.  Instead
23  load the environment manually before running CMake or using the generated
24  build system.
25
26Example Usage
27^^^^^^^^^^^^^
28
29.. code-block:: cmake
30
31  set(CTEST_BUILD_NAME "CrayLinux-CrayPE-Cray-dynamic")
32  set(CTEST_BUILD_CONFIGURATION Release)
33  set(CTEST_BUILD_FLAGS "-k -j8")
34  set(CTEST_CMAKE_GENERATOR "Unix Makefiles")
35
36  ...
37
38  find_package(EnvModules REQUIRED)
39
40  env_module(purge)
41  env_module(load modules)
42  env_module(load craype)
43  env_module(load PrgEnv-cray)
44  env_module(load craype-knl)
45  env_module(load cray-mpich)
46  env_module(load cray-libsci)
47
48  set(ENV{CRAYPE_LINK_TYPE} dynamic)
49
50  ...
51
52Result Variables
53^^^^^^^^^^^^^^^^
54
55This module will set the following variables in your project:
56
57``EnvModules_FOUND``
58  True if a compatible environment modules framework was found.
59
60Cache Variables
61^^^^^^^^^^^^^^^
62
63The following cache variable will be set:
64
65``EnvModules_COMMAND``
66  The low level module command to use.  Currently supported
67  implementations are the Lua based Lmod and TCL based EnvironmentModules.
68
69Environment Variables
70^^^^^^^^^^^^^^^^^^^^^
71
72``ENV{MODULESHOME}``
73  Usually set by the module environment implementation, used as a hint to
74  locate the module command to execute.
75
76Provided Functions
77^^^^^^^^^^^^^^^^^^
78
79This defines the following CMake functions for interacting with environment
80modules:
81
82.. command:: env_module
83
84  Execute an aribitrary module command:
85
86  .. code-block:: cmake
87
88    env_module(cmd arg1 ... argN)
89    env_module(
90      COMMAND cmd arg1 ... argN
91      [OUTPUT_VARIABLE <out-var>]
92      [RESULT_VARIABLE <ret-var>]
93    )
94
95  The options are:
96
97  ``cmd arg1 ... argN``
98    The module sub-command and arguments to execute as if they were
99    passed directly to the module command in your shell environment.
100
101  ``OUTPUT_VARIABLE <out-var>``
102    The standard output from executing the module command.
103
104  ``RESULT_VARIABLE <ret-var>``
105    The return code from executing the module command.
106
107.. command:: env_module_swap
108
109  Swap one module for another:
110
111  .. code-block:: cmake
112
113    env_module_swap(out_mod in_mod
114      [OUTPUT_VARIABLE <out-var>]
115      [RESULT_VARIABLE <ret-var>]
116    )
117
118  This is functionally equivalent to the ``module swap out_mod in_mod`` shell
119  command.  The options are:
120
121  ``OUTPUT_VARIABLE <out-var>``
122    The standard output from executing the module command.
123
124  ``RESULT_VARIABLE <ret-var>``
125    The return code from executing the module command.
126
127.. command:: env_module_list
128
129  Retrieve the list of currently loaded modules:
130
131  .. code-block:: cmake
132
133    env_module_list(<out-var>)
134
135  This is functionally equivalent to the ``module list`` shell command.
136  The result is stored in ``<out-var>`` as a properly formatted CMake
137  :ref:`semicolon-separated list <CMake Language Lists>` variable.
138
139.. command:: env_module_avail
140
141  Retrieve the list of available modules:
142
143  .. code-block:: cmake
144
145    env_module_avail([<mod-prefix>] <out-var>)
146
147  This is functionally equivalent to the ``module avail <mod-prefix>`` shell
148  command.  The result is stored in ``<out-var>`` as a properly formatted
149  CMake :ref:`semicolon-separated list <CMake Language Lists>` variable.
150
151#]=======================================================================]
152
153function(env_module)
154  if(NOT EnvModules_COMMAND)
155    message(FATAL_ERROR "Failed to process module command.  EnvModules_COMMAND not found")
156    return()
157  endif()
158
159  set(options)
160  set(oneValueArgs OUTPUT_VARIABLE RESULT_VARIABLE)
161  set(multiValueArgs COMMAND)
162  cmake_parse_arguments(MOD_ARGS
163    "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGV}
164  )
165  if(NOT MOD_ARGS_COMMAND)
166    # If no explicit command argument was given, then treat the calling syntax
167    # as: module(cmd args...)
168    set(exec_cmd ${ARGV})
169  else()
170    set(exec_cmd ${MOD_ARGS_COMMAND})
171  endif()
172
173  if(MOD_ARGS_OUTPUT_VARIABLE)
174    set(err_var_args ERROR_VARIABLE err_var)
175  endif()
176
177  execute_process(
178    COMMAND mktemp -t module.cmake.XXXXXXXXXXXX
179    OUTPUT_VARIABLE tempfile_name
180  )
181  string(STRIP "${tempfile_name}" tempfile_name)
182
183  # If the $MODULESHOME/init/cmake file exists then assume that the CMake
184  # "shell" functionality exits
185  if(EXISTS "$ENV{MODULESHOME}/init/cmake")
186    execute_process(
187      COMMAND ${EnvModules_COMMAND} cmake ${exec_cmd}
188      OUTPUT_FILE ${tempfile_name}
189      ${err_var_args}
190      RESULT_VARIABLE ret_var
191    )
192
193  else() # fallback to the sh shell and manually convert to CMake
194    execute_process(
195      COMMAND ${EnvModules_COMMAND} sh ${exec_cmd}
196      OUTPUT_VARIABLE out_var
197      ${err_var_args}
198      RESULT_VARIABLE ret_var
199    )
200  endif()
201
202  # If we executed successfully then process and cleanup the temp file
203  if(ret_var EQUAL 0)
204    # No CMake shell so we need to process the sh output into CMake code
205    if(NOT EXISTS "$ENV{MODULESHOME}/init/cmake")
206      file(WRITE ${tempfile_name} "")
207      string(REPLACE "\n" ";" out_var "${out_var}")
208      foreach(sh_cmd IN LISTS out_var)
209        if(sh_cmd MATCHES "^ *unset *([^ ]*)")
210          set(cmake_cmd "unset(ENV{${CMAKE_MATCH_1}})")
211        elseif(sh_cmd MATCHES "^ *export *([^ ]*)")
212          set(cmake_cmd "set(ENV{${CMAKE_MATCH_1}} \"\${${CMAKE_MATCH_1}}\")")
213        elseif(sh_cmd MATCHES " *([^ =]*) *= *(.*)")
214          set(var_name "${CMAKE_MATCH_1}")
215          set(var_value "${CMAKE_MATCH_2}")
216          if(var_value MATCHES "^\"(.*[^\\])\"")
217            # If it's in quotes, take the value as is
218            set(var_value "${CMAKE_MATCH_1}")
219          else()
220            # Otherwise, strip trailing spaces
221            string(REGEX REPLACE "([^\\])? +$" "\\1" var_value "${var_value}")
222          endif()
223          string(REPLACE "\\ " " " var_value "${var_value}")
224          set(cmake_cmd "set(${var_name} \"${var_value}\")")
225        else()
226          continue()
227        endif()
228        file(APPEND ${tempfile_name} "${cmake_cmd}\n")
229      endforeach()
230    endif()
231
232    # Process the change in environment variables
233    include(${tempfile_name})
234    file(REMOVE ${tempfile_name})
235  endif()
236
237  # Push the output back out to the calling scope
238  if(MOD_ARGS_OUTPUT_VARIABLE)
239    set(${MOD_ARGS_OUTPUT_VARIABLE} "${err_var}" PARENT_SCOPE)
240  endif()
241  if(MOD_ARGS_RESULT_VARIABLE)
242    set(${MOD_ARGS_RESULT_VARIABLE} ${ret_var} PARENT_SCOPE)
243  endif()
244endfunction(env_module)
245
246#------------------------------------------------------------------------------
247function(env_module_swap out_mod in_mod)
248  set(options)
249  set(oneValueArgs OUTPUT_VARIABLE RESULT_VARIABLE)
250  set(multiValueArgs)
251
252  cmake_parse_arguments(MOD_ARGS
253    "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGV}
254  )
255
256  env_module(COMMAND -t swap ${out_mod} ${in_mod}
257    OUTPUT_VARIABLE tmp_out
258    RETURN_VARIABLE tmp_ret
259  )
260
261  if(MOD_ARGS_OUTPUT_VARIABLE)
262    set(${MOD_ARGS_OUTPUT_VARIABLE} "${err_var}" PARENT_SCOPE)
263  endif()
264  if(MOD_ARGS_RESULT_VARIABLE)
265    set(${MOD_ARGS_RESULT_VARIABLE} ${tmp_ret} PARENT_SCOPE)
266  endif()
267endfunction()
268
269#------------------------------------------------------------------------------
270function(env_module_list out_var)
271  cmake_policy(SET CMP0007 NEW)
272  env_module(COMMAND -t list OUTPUT_VARIABLE tmp_out)
273
274  # Convert output into a CMake list
275  string(REPLACE "\n" ";" ${out_var} "${tmp_out}")
276
277  # Remove title headers and empty entries
278  list(REMOVE_ITEM ${out_var} "No modules loaded")
279  if(${out_var})
280    list(FILTER ${out_var} EXCLUDE REGEX "^(.*:)?$")
281  endif()
282  list(FILTER ${out_var} EXCLUDE REGEX "^(.*:)?$")
283
284  set(${out_var} ${${out_var}} PARENT_SCOPE)
285endfunction()
286
287#------------------------------------------------------------------------------
288function(env_module_avail)
289  cmake_policy(SET CMP0007 NEW)
290
291  if(ARGC EQUAL 1)
292    set(mod_prefix)
293    set(out_var ${ARGV0})
294  elseif(ARGC EQUAL 2)
295    set(mod_prefix ${ARGV0})
296    set(out_var ${ARGV1})
297  else()
298    message(FATAL_ERROR "Usage: env_module_avail([mod_prefix] out_var)")
299  endif()
300  env_module(COMMAND -t avail ${mod_prefix} OUTPUT_VARIABLE tmp_out)
301
302  # Convert output into a CMake list
303  string(REPLACE "\n" ";" tmp_out "${tmp_out}")
304
305  set(${out_var})
306  foreach(MOD IN LISTS tmp_out)
307    # Remove directory entries and empty values
308    if(MOD MATCHES "^(.*:)?$")
309      continue()
310    endif()
311
312    # Convert default modules
313    if(MOD MATCHES "^(.*)/$" ) # "foo/"
314      list(APPEND ${out_var} ${CMAKE_MATCH_1})
315    elseif(MOD MATCHES "^((.*)/.*)\\(default\\)$") # "foo/1.2.3(default)"
316      list(APPEND ${out_var} ${CMAKE_MATCH_2})
317      list(APPEND ${out_var} ${CMAKE_MATCH_1})
318    else()
319      list(APPEND ${out_var} ${MOD})
320    endif()
321  endforeach()
322
323  set(${out_var} ${${out_var}} PARENT_SCOPE)
324endfunction()
325
326#------------------------------------------------------------------------------
327# Make sure we know where the underlying module command is
328find_program(EnvModules_COMMAND
329  NAMES lmod modulecmd
330  HINTS ENV MODULESHOME
331  PATH_SUFFIXES libexec
332)
333
334include(${CMAKE_CURRENT_LIST_DIR}/FindPackageHandleStandardArgs.cmake)
335find_package_handle_standard_args(EnvModules DEFAULT_MSG EnvModules_COMMAND)
336