From bb358dd637cc1f4e9cb47e2cb112d72da97a9d61 Mon Sep 17 00:00:00 2001 From: zapdos26 Date: Mon, 16 Feb 2026 22:18:55 -0500 Subject: [PATCH 1/2] perf: use d_type/stat from readdir to pre-compute is_dir Extend getFilesInDirectory (C extension) to return triplets {path hidden is_dir} instead of pairs {path hidden}. The is_dir flag is determined from d_type when available, falling back to stat() for DT_UNKNOWN (NFS v3) and DT_LNK (symlinks to directories). This feeds directly into the existing fknown_arr/dknown_arr mechanism in findModulesFromDirsAndFiles, eliminating redundant per-file 'file isdirectory' stat calls. Also update the Tcl fallback __getFilesInDirectory to return the same triplet format using 'file isdirectory' (which follows symlinks). Signed-off-by: zapdos26 --- lib/envmodules.c | 22 +++++++++++++++++++++- tcl/modfind.tcl.in | 29 +++++++++++++++++++++++------ 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/lib/envmodules.c b/lib/envmodules.c index 14b03b3a3..4ff800919 100644 --- a/lib/envmodules.c +++ b/lib/envmodules.c @@ -19,13 +19,14 @@ ************************************************************************/ #define _ISOC99_SOURCE -#define _XOPEN_SOURCE +#define _XOPEN_SOURCE 500 #include #include #include #include #include +#include #include #include #include @@ -84,7 +85,9 @@ Envmodules_GetFilesInDirectoryObjCmd( int have_modulerc = 0; int have_version = 0; int is_hidden; + int is_dir; char path[PATH_MAX]; + struct stat st; /* Parse arguments. */ if (objc == 3) { @@ -129,6 +132,21 @@ Envmodules_GetFilesInDirectoryObjCmd( Tcl_ListObjAppendElement(interp, ltmp, Tcl_NewStringObj(path, -1)); is_hidden = (direntry->d_name[0] == '.') ? 1 : 0; Tcl_ListObjAppendElement(interp, ltmp, Tcl_NewIntObj(is_hidden)); + /* Determine if entry is a directory using d_type when available, + * falling back to stat() for DT_UNKNOWN/DT_LNK (symlinks to + * dirs and NFS v3 where d_type is always DT_UNKNOWN) */ +#ifdef DT_DIR + if (direntry->d_type == DT_DIR) { + is_dir = 1; + } else if (direntry->d_type != DT_UNKNOWN + && direntry->d_type != DT_LNK) { + is_dir = 0; + } else +#endif + { + is_dir = (!stat(path, &st) && S_ISDIR(st.st_mode)) ? 1 : 0; + } + Tcl_ListObjAppendElement(interp, ltmp, Tcl_NewIntObj(is_dir)); } } /* Do not treat error happening during read to send list of valid files. */ @@ -150,11 +168,13 @@ Envmodules_GetFilesInDirectoryObjCmd( snprintf(path, sizeof(path), "%s/%s", dir, ".modulerc"); Tcl_ListObjAppendElement(interp, lres, Tcl_NewStringObj(path, -1)); Tcl_ListObjAppendElement(interp, lres, Tcl_NewIntObj(0)); + Tcl_ListObjAppendElement(interp, lres, Tcl_NewIntObj(0)); } if (have_version) { snprintf(path, sizeof(path), "%s/%s", dir, ".version"); Tcl_ListObjAppendElement(interp, lres, Tcl_NewStringObj(path, -1)); Tcl_ListObjAppendElement(interp, lres, Tcl_NewIntObj(0)); + Tcl_ListObjAppendElement(interp, lres, Tcl_NewIntObj(0)); } /* Then append regular elements. */ Tcl_ListObjAppendList(interp, lres, ltmp); diff --git a/tcl/modfind.tcl.in b/tcl/modfind.tcl.in index ec54eb4d2..f05eba6db 100644 --- a/tcl/modfind.tcl.in +++ b/tcl/modfind.tcl.in @@ -2904,7 +2904,7 @@ proc __getFilesInDirectory {dir fetch_dotversion} { # Add each element in the current directory to the list foreach elt $elt_list { - lappend dir_list $elt 0 + lappend dir_list $elt 0 [file isdirectory $elt] } # search for hidden files @@ -2914,11 +2914,11 @@ proc __getFilesInDirectory {dir fetch_dotversion} { .modulerc - .version { if {($fetch_dotversion || $elt ne {.version}) && [file readable\ $dir/$elt]} { - lappend dir_list $dir/$elt 0 + lappend dir_list $dir/$elt 0 0 } } default { - lappend dir_list $dir/$elt 1 + lappend dir_list $dir/$elt 1 [file isdirectory $dir/$elt] } } } @@ -2993,12 +2993,19 @@ proc findModulesFromDirsAndFiles {dir full_list depthlvl fetch_mtime\ $element] $element] } else { # Add each element in the current directory to the list - foreach {fpelt hid} $elt_list { + foreach {fpelt hid isdir} $elt_list { lappend full_list $fpelt # Flag hidden files if {$hid} { set hidden_list($fpelt) 1 } + # Record file type to avoid later stat calls + set fpeltname [getModuleNameFromModulepath $fpelt $dir] + if {$isdir} { + set dknown_arr($fpeltname) 1 + } else { + set fknown_arr($fpeltname) 1 + } } } } @@ -3083,11 +3090,20 @@ proc findModules {dir mod depthlvl fetch_mtime} { # use catch protection to handle non-readable and non-existent dir if {[catch { set full_list {} - foreach {fpelt hid} [getFilesInDirectory $dir 0] { + array set init_fknown {} + array set init_dknown {} + foreach {fpelt hid isdir} [getFilesInDirectory $dir 0] { set elt [file tail $fpelt] # include any .modulerc file found at the modulepath root if {$elt eq {.modulerc} || $findall || [modEqStatic $elt match]} { lappend full_list $fpelt + # record file type to avoid later stat calls + set eltname [getModuleNameFromModulepath $fpelt $dir] + if {$isdir} { + set init_dknown($eltname) 1 + } else { + set init_fknown($eltname) 1 + } } } }]} { @@ -3095,7 +3111,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 {} {} init_fknown init_dknown reportDebug "found [array names mod_list]" From d9f7839ec46536768ccb4f1a1006f7c9a6fb988d Mon Sep 17 00:00:00 2001 From: zapdos26 Date: Mon, 16 Feb 2026 23:52:25 -0500 Subject: [PATCH 2/2] test: add symlink directory traversal tests Verify that symlinked directories are correctly identified as directories and their modules are discoverable via avail and load. This exercises the d_type/DT_LNK handling in the C extension. Signed-off-by: zapdos26 --- testsuite/modulefiles.deep/plainlink | 1 + .../modules.80-deep/025-symlink-deep.exp | 47 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 120000 testsuite/modulefiles.deep/plainlink create mode 100644 testsuite/modules.80-deep/025-symlink-deep.exp diff --git a/testsuite/modulefiles.deep/plainlink b/testsuite/modulefiles.deep/plainlink new file mode 120000 index 000000000..f8dc9f27b --- /dev/null +++ b/testsuite/modulefiles.deep/plainlink @@ -0,0 +1 @@ +plain \ No newline at end of file diff --git a/testsuite/modules.80-deep/025-symlink-deep.exp b/testsuite/modules.80-deep/025-symlink-deep.exp new file mode 100644 index 000000000..26eaba522 --- /dev/null +++ b/testsuite/modules.80-deep/025-symlink-deep.exp @@ -0,0 +1,47 @@ +############################################################################## +# Modules Revision 3.0 +# Providing a flexible user environment +# +# Description: Deep modulefile directories - symlink directory handling +# Command: avail, load +# Modulefiles: plainlink (symlink to plain) +# +# Test that symlinked directories are correctly identified as directories +# when the C extension uses d_type/stat() to pre-compute is_dir. Symlinks +# have d_type=DT_LNK and must be resolved via stat() to detect if target +# is a directory. +# +############################################################################## + +# ensure auto symbolic versions are not set for these tests +setenv_var MODULES_ADVANCED_VERSION_SPEC 0 + +# plainlink is a symlink to the plain directory within modulefiles.deep +# it should be traversable and its modules should be available + +# test avail on symlinked deep module (terse output) +set deepmodpath "$env(TESTSUITEDIR)/modulefiles.deep" +set ts_sh "$deepmodpath:\nplainlink/dir1/1.0\nplainlink/dir1/2.0\nplainlink/dir2/1.0\nplainlink/dir2/2.0" +setenv_path_var MODULEPATH $deepmodpath +testouterr_cmd "sh" "avail -t plainlink" "OK" $ts_sh + +skip_if_quick_mode + +# test load of a specific module through the symlink +set ans [list] +lappend ans [list set TEST "plain/dir1/1.0"] +lappend ans [list set __MODULES_LMCONFLICT \ + "plainlink/dir1/1.0&plain/dir1"] +lappend ans [list set _LMFILES_ \ + "$deepmodpath/plainlink/dir1/1.0"] +lappend ans [list set LOADEDMODULES "plainlink/dir1/1.0"] +test_cmd_re "sh" "load plainlink/dir1/1.0" $ans + +# test avail on a deeper path within the symlink +set ts_sh "$deepmodpath:\nplainlink/dir2/1.0\nplainlink/dir2/2.0" +testouterr_cmd "sh" "avail -t plainlink/dir2" "OK" $ts_sh + +# cleanup +setenv_path_var MODULEPATH $modpath +unsetenv_var MODULES_ADVANCED_VERSION_SPEC +unset ts_sh ans deepmodpath