diff --git a/tcl/modfind.tcl.in b/tcl/modfind.tcl.in index ec54eb4d2..9cddd8400 100644 --- a/tcl/modfind.tcl.in +++ b/tcl/modfind.tcl.in @@ -2955,7 +2955,7 @@ proc findModulesInMemCache {searchid} { # Walk through provided list of directories and files to find modules proc findModulesFromDirsAndFiles {dir full_list depthlvl fetch_mtime\ res_arrname {indir_arrname {}} {hidden_listname {}} {fknown_arrname {}}\ - {dknown_arrname {}}} { + {dknown_arrname {}} {prunespec {}}} { # link to variables/arrays from upper context upvar $res_arrname mod_list if {$indir_arrname ne {}} { @@ -2971,6 +2971,15 @@ proc findModulesFromDirsAndFiles {dir full_list depthlvl fetch_mtime\ upvar $dknown_arrname dknown_arr } + # build pruning path components from spec (e.g. tools/cat15/pkg10/3.0 -> + # {tools cat15 pkg10 3.0}) to skip non-matching directories during walk + if {$prunespec ne {}} { + set pruneparts [file split $prunespec] + set prunedepth [llength $pruneparts] + } else { + set prunedepth 0 + } + foreach igndir [getConf ignored_dirs] { set ignored_dirs($igndir) 1 } @@ -2986,6 +2995,23 @@ proc findModulesFromDirsAndFiles {dir full_list depthlvl fetch_mtime\ if {[info exists dknown_arr($modulename)] || (![info exists\ fknown_arr($modulename)] && [file isdirectory $element])} { if {![info exists ignored_dirs($tail)]} { + # prune directories that cannot match the query path: read + # dir for .modulerc/.version but skip subdirs and files + if {$prunedepth > 0 && $moddepthlvl <= $prunedepth && [lindex\ + $pruneparts [expr {$moddepthlvl - 1}]] ne $tail} { + if {![catch { + set elt_list [getFilesInDirectory $element 1] + }]} { + foreach {fpelt hid} $elt_list { + set fptail [file tail $fpelt] + if {$fptail eq {.modulerc} || $fptail eq\ + {.version}} { + lappend full_list $fpelt + } + } + } + continue + } if {[catch { set elt_list [getFilesInDirectory $element 1] } errMsg]} { @@ -3051,12 +3077,12 @@ proc findModulesFromDirsAndFiles {dir full_list depthlvl fetch_mtime\ } # finds all module-related files matching mod in the module path dir -proc findModules {dir mod depthlvl fetch_mtime} { +proc findModules {dir mod depthlvl fetch_mtime {prunespec {}}} { reportDebug "finding '$mod' in $dir (depthlvl=$depthlvl,\ fetch_mtime=$fetch_mtime)" # generated search id (for cache search/save) by compacting given args - set searchid $dir:$mod:$depthlvl:$fetch_mtime + set searchid $dir:$mod:$depthlvl:$fetch_mtime:$prunespec # look at memory cache for a compatible result lassign [findModulesInMemCache $searchid] cache_searchid cache_list @@ -3095,7 +3121,8 @@ proc findModules {dir mod depthlvl fetch_mtime} { } # walk through list of dirs and files to find modules - findModulesFromDirsAndFiles $dir $full_list $depthlvl $fetch_mtime mod_list + findModulesFromDirsAndFiles $dir $full_list $depthlvl $fetch_mtime \ + mod_list {} {} {} {} $prunespec reportDebug "found [array names mod_list]" @@ -3201,7 +3228,29 @@ proc getModules {dir {mod {}} {fetch_mtime 0} {search {}} {filter {}}} { # unless EMS need to be performed (findModules should fetch everything) set depthlvl [expr {$indepth || $ems_required ? 0 : $querydepth + 1}] - array set found_list [findModules $dir $findmod $depthlvl $fetch_mtime] + # enable directory pruning when query is a specific deep path without + # wildcards (e.g. tools/cat15/pkg10/3.0) to avoid scanning sibling + # directories that cannot match; verify every path component exists + # on disk so virtual names (symbols/aliases) do not trigger pruning + set prunespec {} + if {$hasmoddir && !$find_all && !$contains && $modqe eq\ + [string map {* {} ? {}} $modqe]} { + set _canprune 1 + set _checkpath $dir + foreach _qp [file split $modqe] { + set _checkpath [file join $_checkpath $_qp] + if {![file isdirectory $_checkpath]} { + set _canprune 0 + break + } + } + if {$_canprune} { + set prunespec $modqe + } + } + + array set found_list [findModules $dir $findmod $depthlvl $fetch_mtime\ + $prunespec] } # Phase #1: consolidate every kind of entries (directory, modulefile, diff --git a/testsuite/modulefiles.deep/prunetest/cat1/.modulerc b/testsuite/modulefiles.deep/prunetest/cat1/.modulerc new file mode 100644 index 000000000..59548db53 --- /dev/null +++ b/testsuite/modulefiles.deep/prunetest/cat1/.modulerc @@ -0,0 +1,2 @@ +#%Module1.0 +module-alias prunetest/shortcut prunetest/cat2/pkg1/1.0 diff --git a/testsuite/modulefiles.deep/prunetest/cat1/pkg1/1.0 b/testsuite/modulefiles.deep/prunetest/cat1/pkg1/1.0 new file mode 100644 index 000000000..b909bf08b --- /dev/null +++ b/testsuite/modulefiles.deep/prunetest/cat1/pkg1/1.0 @@ -0,0 +1,2 @@ +#%Module1.0 +module-whatis "prunetest/$d/$v" diff --git a/testsuite/modulefiles.deep/prunetest/cat1/pkg1/2.0 b/testsuite/modulefiles.deep/prunetest/cat1/pkg1/2.0 new file mode 100644 index 000000000..b909bf08b --- /dev/null +++ b/testsuite/modulefiles.deep/prunetest/cat1/pkg1/2.0 @@ -0,0 +1,2 @@ +#%Module1.0 +module-whatis "prunetest/$d/$v" diff --git a/testsuite/modulefiles.deep/prunetest/cat1/pkg2/1.0 b/testsuite/modulefiles.deep/prunetest/cat1/pkg2/1.0 new file mode 100644 index 000000000..b909bf08b --- /dev/null +++ b/testsuite/modulefiles.deep/prunetest/cat1/pkg2/1.0 @@ -0,0 +1,2 @@ +#%Module1.0 +module-whatis "prunetest/$d/$v" diff --git a/testsuite/modulefiles.deep/prunetest/cat1/pkg2/2.0 b/testsuite/modulefiles.deep/prunetest/cat1/pkg2/2.0 new file mode 100644 index 000000000..b909bf08b --- /dev/null +++ b/testsuite/modulefiles.deep/prunetest/cat1/pkg2/2.0 @@ -0,0 +1,2 @@ +#%Module1.0 +module-whatis "prunetest/$d/$v" diff --git a/testsuite/modulefiles.deep/prunetest/cat2/pkg1/1.0 b/testsuite/modulefiles.deep/prunetest/cat2/pkg1/1.0 new file mode 100644 index 000000000..b909bf08b --- /dev/null +++ b/testsuite/modulefiles.deep/prunetest/cat2/pkg1/1.0 @@ -0,0 +1,2 @@ +#%Module1.0 +module-whatis "prunetest/$d/$v" diff --git a/testsuite/modulefiles.deep/prunetest/cat2/pkg1/2.0 b/testsuite/modulefiles.deep/prunetest/cat2/pkg1/2.0 new file mode 100644 index 000000000..b909bf08b --- /dev/null +++ b/testsuite/modulefiles.deep/prunetest/cat2/pkg1/2.0 @@ -0,0 +1,2 @@ +#%Module1.0 +module-whatis "prunetest/$d/$v" diff --git a/testsuite/modulefiles.deep/prunetest/cat2/pkg2/1.0 b/testsuite/modulefiles.deep/prunetest/cat2/pkg2/1.0 new file mode 100644 index 000000000..b909bf08b --- /dev/null +++ b/testsuite/modulefiles.deep/prunetest/cat2/pkg2/1.0 @@ -0,0 +1,2 @@ +#%Module1.0 +module-whatis "prunetest/$d/$v" diff --git a/testsuite/modulefiles.deep/prunetest/cat2/pkg2/2.0 b/testsuite/modulefiles.deep/prunetest/cat2/pkg2/2.0 new file mode 100644 index 000000000..b909bf08b --- /dev/null +++ b/testsuite/modulefiles.deep/prunetest/cat2/pkg2/2.0 @@ -0,0 +1,2 @@ +#%Module1.0 +module-whatis "prunetest/$d/$v" diff --git a/testsuite/modules.80-deep/026-pruning-deep.exp b/testsuite/modules.80-deep/026-pruning-deep.exp new file mode 100644 index 000000000..f9c9c12db --- /dev/null +++ b/testsuite/modules.80-deep/026-pruning-deep.exp @@ -0,0 +1,60 @@ +############################################################################## +# Modules Revision 3.0 +# Providing a flexible user environment +# +# Description: Directory pruning for deep module queries +# Command: avail, load +# Modulefiles: prunetest/cat{1,2}/pkg{1,2}/{1.0,2.0} +# +# Test that specific deep queries correctly return matching modules +# when directory pruning skips non-matching sibling directories. +# Ensures pruning does not omit valid results and broad queries +# still return everything. +# +############################################################################## + +# use deep modulefiles path for these tests +set deepmodpath "$env(TESTSUITEDIR)/modulefiles.deep" +setenv_path_var MODULEPATH $deepmodpath + +# ensure auto symbolic versions are not set for these tests +setenv_var MODULES_ADVANCED_VERSION_SPEC 0 + +# avail on specific 3-level deep path should return only that pkg +set ts_sh "$deepmodpath:\nprunetest/cat1/pkg1/1.0\nprunetest/cat1/pkg1/2.0" +testouterr_cmd "sh" "avail -t prunetest/cat1/pkg1" "OK" $ts_sh + +# avail on 2-level deep path returns all pkgs under that category +set ts_sh "$deepmodpath:\nprunetest/cat1/pkg1/1.0\nprunetest/cat1/pkg1/2.0\nprunetest/cat1/pkg2/1.0\nprunetest/cat1/pkg2/2.0" +testouterr_cmd "sh" "avail -t prunetest/cat1" "OK" $ts_sh + +skip_if_quick_mode + +# avail on the top level returns all modules (no pruning) +set ts_sh "$deepmodpath:\nprunetest/cat1/pkg1/1.0\nprunetest/cat1/pkg1/2.0\nprunetest/cat1/pkg2/1.0\nprunetest/cat1/pkg2/2.0\nprunetest/cat2/pkg1/1.0\nprunetest/cat2/pkg1/2.0\nprunetest/cat2/pkg2/1.0\nprunetest/cat2/pkg2/2.0" +testouterr_cmd "sh" "avail -t prunetest" "OK" $ts_sh + +# avail on exact version path +set ts_sh "$deepmodpath:\nprunetest/cat2/pkg2/2.0" +testouterr_cmd "sh" "avail -t prunetest/cat2/pkg2/2.0" "OK" $ts_sh + +# load a specific deep module +set ans [list] +lappend ans [list set _LMFILES_ \ + "$deepmodpath/prunetest/cat1/pkg2/1.0"] +lappend ans [list set LOADEDMODULES "prunetest/cat1/pkg2/1.0"] +test_cmd_re "sh" "load prunetest/cat1/pkg2/1.0" $ans + +# wildcard query should not be pruned and return all matches +set ts_sh "$deepmodpath:\nprunetest/cat1/pkg1/1.0\nprunetest/cat1/pkg1/2.0\nprunetest/cat2/pkg1/1.0\nprunetest/cat2/pkg1/2.0" +testouterr_cmd_re "sh" "avail -t prunetest/*/pkg1" "OK" $ts_sh + +# cross-directory alias: cat1/.modulerc defines prunetest/shortcut +# pointing to cat2/pkg1/1.0; pruning cat1 must still find the alias +set ts_sh "$deepmodpath:\nprunetest/shortcut" +testouterr_cmd "sh" "avail -t prunetest/shortcut" "OK" $ts_sh + +# cleanup +setenv_path_var MODULEPATH $modpath +unsetenv_var MODULES_ADVANCED_VERSION_SPEC +unset ts_sh ans deepmodpath