7272from easybuild .tools .build_details import get_build_stats
7373from easybuild .tools .build_log import EasyBuildError , EasyBuildExit , dry_run_msg , dry_run_warning , dry_run_set_dirs
7474from easybuild .tools .build_log import print_error , print_msg , print_warning
75- from easybuild .tools .config import CHECKSUM_PRIORITY_JSON , DEFAULT_ENVVAR_USERS_MODULES
75+ from easybuild .tools .config import CHECKSUM_PRIORITY_JSON , DEFAULT_ENVVAR_USERS_MODULES , PYTHONPATH , EBPYTHONPREFIXES
7676from easybuild .tools .config import FORCE_DOWNLOAD_ALL , FORCE_DOWNLOAD_PATCHES , FORCE_DOWNLOAD_SOURCES
77- from easybuild .tools .config import EASYBUILD_SOURCES_URL # noqa
77+ from easybuild .tools .config import EASYBUILD_SOURCES_URL # noqa
7878from easybuild .tools .config import build_option , build_path , get_log_filename , get_repository , get_repositorypath
7979from easybuild .tools .config import install_path , log_path , package_path , source_paths
8080from easybuild .tools .environment import restore_env , sanitize_env
@@ -1219,6 +1219,8 @@ def make_devel_module(self, create_in_builddir=False):
12191219 # these should be all the dependencies and we should load them
12201220 recursive_unload = self .cfg ['recursive_module_unload' ]
12211221 depends_on = self .cfg ['module_depends_on' ]
1222+ if depends_on is not None :
1223+ self .log .deprecated ("'module_depends_on' easyconfig parameter should not be used anymore" , '6.0' )
12221224 for key in os .environ :
12231225 # legacy support
12241226 if key .startswith (DEVEL_ENV_VAR_NAME_PREFIX ):
@@ -1346,6 +1348,8 @@ def make_module_dep(self, unload_info=None):
13461348 # include load statements for retained dependencies
13471349 recursive_unload = self .cfg ['recursive_module_unload' ]
13481350 depends_on = self .cfg ['module_depends_on' ]
1351+ if depends_on is not None :
1352+ self .log .deprecated ("'module_depends_on' easyconfig parameter should not be used anymore" , '6.0' )
13491353 loads = []
13501354 for dep in deps :
13511355 unload_modules = []
@@ -1390,6 +1394,49 @@ def make_module_description(self):
13901394 """
13911395 return self .module_generator .get_description ()
13921396
1397+ def make_module_pythonpath (self ):
1398+ """
1399+ Add lines for module file to update $PYTHONPATH or $EBPYTHONPREFIXES,
1400+ if they aren't already present and the standard lib/python*/site-packages subdirectory exists
1401+ """
1402+ lines = []
1403+ if not os .path .isfile (os .path .join (self .installdir , 'bin' , 'python' )): # only needed when not a python install
1404+ python_subdir_pattern = os .path .join (self .installdir , 'lib' , 'python*' , 'site-packages' )
1405+ candidate_paths = (os .path .relpath (path , self .installdir ) for path in glob .glob (python_subdir_pattern ))
1406+ python_paths = [path for path in candidate_paths if re .match (r'lib/python\d+\.\d+/site-packages' , path )]
1407+
1408+ # determine whether Python is a runtime dependency;
1409+ # if so, we assume it was installed with EasyBuild, and hence is aware of $EBPYTHONPREFIXES
1410+ runtime_deps = [dep ['name' ] for dep in self .cfg .dependencies (runtime_only = True )]
1411+
1412+ # don't use $EBPYTHONPREFIXES unless we can and it's preferred or necesary (due to use of multi_deps)
1413+ use_ebpythonprefixes = False
1414+ multi_deps = self .cfg ['multi_deps' ]
1415+
1416+ if 'Python' in runtime_deps :
1417+ self .log .info ("Found Python runtime dependency, so considering $EBPYTHONPREFIXES..." )
1418+
1419+ if build_option ('prefer_python_search_path' ) == EBPYTHONPREFIXES :
1420+ self .log .info ("Preferred Python search path is $EBPYTHONPREFIXES, so using that" )
1421+ use_ebpythonprefixes = True
1422+
1423+ elif multi_deps and 'Python' in multi_deps :
1424+ self .log .info ("Python is listed in 'multi_deps', so using $EBPYTHONPREFIXES instead of $PYTHONPATH" )
1425+ use_ebpythonprefixes = True
1426+
1427+ if python_paths :
1428+ # add paths unless they were already added
1429+ if use_ebpythonprefixes :
1430+ path = '' # EBPYTHONPREFIXES are relative to the install dir
1431+ if path not in self .module_generator .added_paths_per_key [EBPYTHONPREFIXES ]:
1432+ lines .append (self .module_generator .prepend_paths (EBPYTHONPREFIXES , path ))
1433+ else :
1434+ for python_path in python_paths :
1435+ if python_path not in self .module_generator .added_paths_per_key [PYTHONPATH ]:
1436+ lines .append (self .module_generator .prepend_paths (PYTHONPATH , python_path ))
1437+
1438+ return lines
1439+
13931440 def make_module_extra (self , altroot = None , altversion = None ):
13941441 """
13951442 Set extra stuff in module file, e.g. $EBROOT*, $EBVERSION*, etc.
@@ -1438,6 +1485,9 @@ def make_module_extra(self, altroot=None, altversion=None):
14381485 value , type (value ))
14391486 lines .append (self .module_generator .append_paths (key , value , allow_abs = self .cfg ['allow_append_abs_path' ]))
14401487
1488+ # add lines to update $PYTHONPATH or $EBPYTHONPREFIXES
1489+ lines .extend (self .make_module_pythonpath ())
1490+
14411491 modloadmsg = self .cfg ['modloadmsg' ]
14421492 if modloadmsg :
14431493 # add trailing newline to prevent that shell prompt is 'glued' to module load message
@@ -2068,20 +2118,33 @@ def update_exts_progress_bar_helper(running_exts, progress_size):
20682118 # if some of the required dependencies are not installed yet, requeue this extension
20692119 elif pending_deps :
20702120
2071- # make sure all required dependencies are actually going to be installed,
2072- # to avoid getting stuck in an infinite loop!
2121+ # check whether all required dependency extensions are actually going to be installed;
2122+ # if not, we assume that they are provided by dependencies;
20732123 missing_deps = [x for x in required_deps if x not in all_ext_names ]
20742124 if missing_deps :
2075- raise EasyBuildError ("Missing required dependencies for %s are not going to be installed: %s" ,
2076- ext .name , ', ' .join (missing_deps ))
2077- else :
2078- self .log .info ("Required dependencies missing for extension %s (%s), adding it back to queue..." ,
2079- ext .name , ', ' .join (pending_deps ))
2125+ msg = f"Missing required extensions for { ext .name } not found "
2126+ msg += "in list of extensions being installed, let's assume they are provided by "
2127+ msg += "dependencies and proceed: " + ', ' .join (missing_deps )
2128+ self .log .info (msg )
2129+
2130+ msg = f"Pending dependencies for { ext .name } before taking into account missing dependencies: "
2131+ self .log .debug (msg + ', ' .join (pending_deps ))
2132+ pending_deps = [x for x in pending_deps if x not in missing_deps ]
2133+ msg = f"Pending dependencies for { ext .name } after taking into account missing dependencies: "
2134+ self .log .debug (msg + ', ' .join (pending_deps ))
2135+
2136+ if pending_deps :
2137+ msg = f"Required dependencies not installed yet for extension { ext .name } ("
2138+ msg += ', ' .join (pending_deps )
2139+ msg += "), adding it back to queue..."
2140+ self .log .info (msg )
20802141 # purposely adding extension back in the queue at Nth place rather than at the end,
20812142 # since we assume that the required dependencies will be installed soon...
20822143 exts_queue .insert (max_iter , ext )
20832144
2084- else :
2145+ # list of pending dependencies may be empty now after taking into account required extensions
2146+ # that are not being installed above, so extension may be ready to install
2147+ if not pending_deps :
20852148 tup = (ext .name , ext .version or '' )
20862149 print_msg ("starting installation of extension %s %s..." % tup , silent = self .silent , log = self .log )
20872150
@@ -3137,15 +3200,21 @@ def sanity_check_rpath(self, rpath_dirs=None, check_readelf_rpath=True):
31373200
31383201 fails = []
31393202
3140- # hard reset $LD_LIBRARY_PATH before running RPATH sanity check
3141- orig_env = env .unset_env_vars (['LD_LIBRARY_PATH' ])
3203+ if build_option ('strict_rpath_sanity_check' ):
3204+ self .log .info ("Unsetting $LD_LIBRARY_PATH since strict RPATH sanity check is enabled..." )
3205+ # hard reset $LD_LIBRARY_PATH before running RPATH sanity check
3206+ orig_env = env .unset_env_vars (['LD_LIBRARY_PATH' ])
3207+ else :
3208+ self .log .info ("Not unsetting $LD_LIBRARY_PATH since strict RPATH sanity check is disabled..." )
3209+ orig_env = None
31423210
31433211 ld_library_path = os .getenv ('LD_LIBRARY_PATH' , '(empty)' )
31443212 self .log .debug (f"$LD_LIBRARY_PATH during RPATH sanity check: { ld_library_path } " )
31453213 modules_list = self .modules_tool .list ()
31463214 self .log .debug (f"List of loaded modules: { modules_list } " )
31473215
31483216 not_found_regex = re .compile (r'(\S+)\s*\=\>\s*not found' )
3217+ lib_path_regex = re .compile (r'\S+\s*\=\>\s*(\S+)' )
31493218 readelf_rpath_regex = re .compile ('(RPATH)' , re .M )
31503219
31513220 # List of libraries that should be exempt from the RPATH sanity check;
@@ -3191,6 +3260,15 @@ def sanity_check_rpath(self, rpath_dirs=None, check_readelf_rpath=True):
31913260 fail_msg = f"Library { match } not found for { path } "
31923261 self .log .warning (fail_msg )
31933262 fails .append (fail_msg )
3263+
3264+ # if any libraries were not found, log whether dependency libraries have an RPATH section
3265+ if fails :
3266+ lib_paths = re .findall (lib_path_regex , out )
3267+ for lib_path in lib_paths :
3268+ self .log .info (f"Checking whether dependency library { lib_path } has RPATH section" )
3269+ res = run_shell_cmd (f"readelf -d { lib_path } " , fail_on_error = False )
3270+ if res .exit_code :
3271+ self .log .info (f"No RPATH section found in { lib_path } " )
31943272 else :
31953273 self .log .debug (f"Output of 'ldd { path } ' checked, looks OK" )
31963274
@@ -3213,7 +3291,8 @@ def sanity_check_rpath(self, rpath_dirs=None, check_readelf_rpath=True):
32133291 else :
32143292 self .log .debug (f"Not sanity checking files in non-existing directory { dirpath } " )
32153293
3216- env .restore_env_vars (orig_env )
3294+ if orig_env :
3295+ env .restore_env_vars (orig_env )
32173296
32183297 return fails
32193298
@@ -4360,7 +4439,9 @@ def build_and_install_one(ecdict, init_env):
43604439 def ensure_writable_log_dir (log_dir ):
43614440 """Make sure we can write into the log dir"""
43624441 if build_option ('read_only_installdir' ):
4363- # temporarily re-enable write permissions for copying log/easyconfig to install dir
4442+ # temporarily re-enable write permissions for copying log/easyconfig to install dir,
4443+ # ensuring that we resolve symlinks
4444+ log_dir = os .path .realpath (log_dir )
43644445 if os .path .exists (log_dir ):
43654446 adjust_permissions (log_dir , stat .S_IWUSR , add = True , recursive = True )
43664447 else :
0 commit comments