diff --git a/docs/source/370_irreversible.rst b/docs/source/370_irreversible.rst new file mode 100644 index 000000000..182259a0a --- /dev/null +++ b/docs/source/370_irreversible.rst @@ -0,0 +1,86 @@ +.. _mode_select-label: + +Mode Select: Irreversible Module Actions +====================================== + +Purpose +^^^^^^^ + +The mode select feature allows modulefiles to specify actions that should only be executed in specific modes (load or unload). This is particularly useful for operations that are irreversible or need special handling during module load/unload cycles. + +Usage +^^^^^ + +Mode select is implemented through a table with a ``mode`` field that specifies when the action should be executed:: + + -- Execute only during load + setenv{ + name = "MY_VAR", + value = "some_value", + mode = "load" + } + + -- Execute only during unload + setenv{ + name = "CLEANUP_VAR", + value = "cleanup_value", + mode = "unload" + } + +Supported Modes +^^^^^^^^^^^^^^ + +The following modes are supported: + +* ``load`` - Execute the action only when loading the module +* ``unload`` - Execute the action only when unloading the module + +Important Notes +^^^^^^^^^^^^^ + +1. When using mode select, the specified action becomes irreversible in the opposite mode. For example: + + * If an action is specified with ``mode = "load"``, it will not be automatically reversed during unload + * If an action is specified with ``mode = "unload"``, it will not be automatically reversed during load + +2. To completely remove the effects of a mode-specific module, you may need to: + + * Purge all modules (``module purge``) + * Start a new shell session + * Or manually reverse the changes + +Example Scenario +^^^^^^^^^^^^^^^ + +Consider a module that needs to perform special cleanup during unload:: + + -- cleanup.lua + help([[ + This module demonstrates mode-specific actions + that require special handling during unload. + ]]) + + -- Normal reversible action + prepend_path("PATH", "/path/to/bin") + + -- Special cleanup only during unload + setenv{ + name = "CLEANUP_REQUIRED", + value = "true", + mode = "unload" + } + +In this example: + +* The PATH modification is reversible and handled normally +* The CLEANUP_REQUIRED variable is only set during unload +* Loading the module again will not automatically clear CLEANUP_REQUIRED + +Best Practices +^^^^^^^^^^^^^ + +1. Use mode select sparingly and only when necessary +2. Document any irreversible changes in the module's help text +3. Consider providing helper functions or instructions for users to manually reverse changes +4. Test both load and unload scenarios thoroughly +5. Consider the impact on module collections and module restore operations \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 3cec4551e..df33213d9 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,4 +1,3 @@ - Lmod: A New Environment Module System ===================================== @@ -148,6 +147,7 @@ Advanced Topics 340_inherit 350_community 360_check_syntax + 370_irreversible Internal Structure of Lmod ^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/rt/show/mf/Core/mode_test/1.0.lua b/rt/show/mf/Core/mode_test/1.0.lua new file mode 100644 index 000000000..5d67b8db5 --- /dev/null +++ b/rt/show/mf/Core/mode_test/1.0.lua @@ -0,0 +1,17 @@ +help([[ +Test module for showing mode-specific function definitions +]]) + +whatis("Name: Mode Test") +whatis("Version: 1.0") + +-- Test showing a standard function +setenv("STANDARD_VAR", "value") + +-- Test showing mode-specific functions +setenv{"MODE_VAR", "value", mode="load"} +prepend_path{"MODE_PATH", "/path", mode="unload"} + +-- Test showing a variable used in a mode-specific function +local t = {"VAR", "value", mode="load"} +setenv(t) \ No newline at end of file diff --git a/rt/show/show.tdesc b/rt/show/show.tdesc index 35e73854c..73483f056 100644 --- a/rt/show/show.tdesc +++ b/rt/show/show.tdesc @@ -38,6 +38,7 @@ testdescript = { runLmod --location show a #14 runLmod --terse show a #15 runLmod show showMe #16 + runLmod show mode_test #17 HOME=$ORIG_HOME diff --git a/src/MC_Show.lua b/src/MC_Show.lua index 0a1508620..ce03ab795 100644 --- a/src/MC_Show.lua +++ b/src/MC_Show.lua @@ -69,7 +69,27 @@ M.build_unload = MainControl.do_not_build_unload M.color_banner = MainControl.color_banner local function l_ShowCmd(name,...) - A[#A+1] = ShowCmdStr(name, ...) + local args = pack(...) + dbg.start{"l_ShowCmd(name=\"",name,"\")"} + for i=1,args.n do + dbg.print{" arg[",i,"] type=",type(args[i]),", value=",args[i],"\n"} + if type(args[i]) == "table" then + dbg.print{" table contents:\n"} + for k,v in pairs(args[i]) do + dbg.print{" key=",k,", value=",v,"\n"} + end + end + end + + -- Use ShowCmdTbl for mode-specific functions + if args.n == 1 and type(args[1]) == "table" and args[1].mode then + dbg.print{" Using ShowCmdTbl\n"} + A[#A+1] = ShowCmdTbl(name, args[1]) + else + dbg.print{" Using ShowCmdStr\n"} + A[#A+1] = ShowCmdStr(name, ...) + end + dbg.fini("l_ShowCmd") end local function l_Show_help(...) diff --git a/src/modfuncs.lua b/src/modfuncs.lua index c816e62cc..d8f859e30 100644 --- a/src/modfuncs.lua +++ b/src/modfuncs.lua @@ -69,6 +69,18 @@ local max = math.max local _concatTbl = table.concat local pack = (_VERSION == "Lua 5.1") and argsPack or table.pack -- luacheck: compat local unpack = (_VERSION == "Lua 5.1") and unpack or table.unpack -- luacheck: compat + +-- List of functions that support mode-select +local mode_select_functions = { + setenv = true, + pushenv = true, + unsetenv = true, + prepend_path = true, + append_path = true, + remove_path = true, + load = true +} + -------------------------------------------------------------------------- -- Special table concat function that knows about strings and numbers. -- @param aa Input array @@ -192,16 +204,6 @@ end -- @param cmdName The command which is getting its arguments validated. -- @param t The table containing mode selector input local function l_validateModeSelector(cmdName, t) - if (t.mode == nil) then - mcp:report{msg="e_Mode_Not_Set", fn = myFileName(), cmdName = cmdName} - return false - end - - if (#t.mode == 0) then - mcp:report{msg="e_Mode_Not_Set", fn = myFileName(), cmdName = cmdName} - return false - end - local validModes = {load = true, unload = true} for i = 1, #t.mode do if not validModes[t.mode[i]] then @@ -221,25 +223,64 @@ local function l_list_2_Tbl(first_elem, ...) local my_mcp = nil local t = nil local action = nil - if ( type(first_elem) == "table" )then + + -- First check if this is a mode-select capable function call + local is_mode_select_capable = false + local cmdName = "unknown" + if type(first_elem) == "table" and first_elem.cmdName then + cmdName = first_elem.cmdName + is_mode_select_capable = mode_select_functions[cmdName] + end + + dbg.print{"Mode selection debug:\n"} + dbg.print{"Function name: ", cmdName, "\n"} + dbg.print{"First elem type: ", type(first_elem), "\n"} + dbg.print{"Is mode-select capable: ", is_mode_select_capable, "\n"} + + if type(first_elem) == "table" then t = first_elem t.kind = "table" local my_mode = mode() - local modeA = t.mode or {} - -- Validate mode selector input - if not l_validateModeSelector(t.cmdName or "unknown", t) then - return MCPQ, t - end + -- Only do mode validation for mode-select capable functions using curly brace syntax + if is_mode_select_capable and t.kind == "table" and not t.__waterMark then + dbg.print{"Checking mode for mode-select capable function\n"} + dbg.print{"Current mode: ", my_mode, "\n"} + dbg.print{"Has mode key: ", t.mode ~= nil, "\n"} + + -- For mode-select capable functions using curly brace syntax, mode MUST be specified + if not t.mode then + dbg.print{"Error: Mode-select capable function called with curly brace syntax but no mode key\n"} + mcp:report{msg="e_Mode_Not_Set", fn = myFileName(), cmdName = cmdName} + return MCPQ, t + end - for i = 1,#modeA do - if (my_mode == modeA[i]) then - action = true - my_mcp = MCP - break + -- Mode must be a non-empty table + if type(t.mode) ~= "table" or #t.mode == 0 then + dbg.print{"Error: Mode must be a non-empty table\n"} + mcp:report{msg="e_Mode_Not_Set", fn = myFileName(), cmdName = cmdName} + return MCPQ, t end - end + -- Validate the mode values + if not l_validateModeSelector(cmdName, t) then + return MCPQ, t + end + + -- Check if current mode matches any of the specified modes + local modeA = t.mode + for i = 1,#modeA do + if (my_mode == modeA[i]) then + action = true + my_mcp = MCP + break + end + end + else + -- Non mode-select function or non-curly brace syntax + action = true + my_mcp = mcp + end else t = pack(first_elem, ...) t.kind = "list" diff --git a/src/utils.lua b/src/utils.lua index f97598bb1..a2c6f0753 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -837,6 +837,40 @@ function ShowCmdStr(name, ...) return concatTbl(b,"") end +-------------------------------------------------------------------------- +-- This routine formats table-style module commands to match the modulefile format +-- @param name The command name (e.g. "setenv") +-- @param t The table of arguments +function ShowCmdTbl(name, t) + dbg.start{"ShowCmdTbl(",name,", t)"} + + local a = {} + a[#a + 1] = s_indentString + a[#a + 1] = name + a[#a + 1] = "{" + + -- Handle numeric indices first + for i = 1, #t do + if i > 1 then + a[#a + 1] = "," + end + a[#a + 1] = '"' .. t[i] .. '"' + end + + -- Only add mode field if present + if t.mode then + if #t > 0 then + a[#a + 1] = "," + end + a[#a + 1] = 'mode="' .. t.mode .. '"' + end + + a[#a + 1] = "}\n" + + dbg.fini("ShowCmdTbl") + return concatTbl(a,"") +end + -------------------------------------------------------------------------- -- This routine prints a help message. This is used by MC_Show function ShowHelpStr(...)