7474from easybuild .tools .build_log import print_error , print_msg , print_warning
7575from 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
@@ -1399,43 +1399,40 @@ def make_module_pythonpath(self):
13991399 Add lines for module file to update $PYTHONPATH or $EBPYTHONPREFIXES,
14001400 if they aren't already present and the standard lib/python*/site-packages subdirectory exists
14011401 """
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 )]
1402+ if os .path .isfile (os .path .join (self .installdir , 'bin' , 'python' )): # only needed when not a python install
1403+ return []
14071404
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 )]
1405+ python_subdir_pattern = os .path .join (self .installdir , 'lib' , 'python*' , 'site-packages' )
1406+ candidate_paths = (os .path .relpath (path , self .installdir ) for path in glob .glob (python_subdir_pattern ))
1407+ python_paths = [path for path in candidate_paths if re .match (r'lib/python\d+\.\d+/site-packages' , path )]
1408+ if not python_paths :
1409+ return []
14111410
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' ]
1411+ # determine whether Python is a runtime dependency;
1412+ # if so, we assume it was installed with EasyBuild, and hence is aware of $EBPYTHONPREFIXES
1413+ runtime_deps = [ dep [ 'name' ] for dep in self .cfg . dependencies ( runtime_only = True ) ]
14151414
1416- if 'Python' in runtime_deps :
1417- self .log .info ("Found Python runtime dependency, so considering $EBPYTHONPREFIXES..." )
1415+ # don't use $EBPYTHONPREFIXES unless we can and it's preferred or necesary (due to use of multi_deps)
1416+ use_ebpythonprefixes = False
1417+ multi_deps = self .cfg ['multi_deps' ]
14181418
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
1419+ if 'Python' in runtime_deps :
1420+ self .log .info ("Found Python runtime dependency, so considering $EBPYTHONPREFIXES..." )
14221421
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 " )
1422+ if build_option ( 'prefer_python_search_path' ) == EBPYTHONPREFIXES :
1423+ self .log .info ("Preferred Python search path is $EBPYTHONPREFIXES , so using that " )
14251424 use_ebpythonprefixes = True
14261425
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 ))
1426+ elif multi_deps and 'Python' in multi_deps :
1427+ self .log .info ("Python is listed in 'multi_deps', so using $EBPYTHONPREFIXES instead of $PYTHONPATH" )
1428+ use_ebpythonprefixes = True
14371429
1438- return lines
1430+ if use_ebpythonprefixes :
1431+ path = '' # EBPYTHONPREFIXES are relative to the install dir
1432+ lines = self .module_generator .prepend_paths (EBPYTHONPREFIXES , path , warn_exists = False )
1433+ else :
1434+ lines = self .module_generator .prepend_paths (PYTHONPATH , python_paths , warn_exists = False )
1435+ return [lines ] if lines else []
14391436
14401437 def make_module_extra (self , altroot = None , altversion = None ):
14411438 """
@@ -1469,22 +1466,27 @@ def make_module_extra(self, altroot=None, altversion=None):
14691466 for (key , value ) in self .cfg ['modextravars' ].items ():
14701467 lines .append (self .module_generator .set_environment (key , value ))
14711468
1472- for (key , value ) in self .cfg ['modextrapaths' ].items ():
1473- if isinstance (value , str ):
1474- value = [value ]
1475- elif not isinstance (value , (tuple , list )):
1476- raise EasyBuildError ("modextrapaths dict value %s (type: %s) is not a list or tuple" ,
1477- value , type (value ))
1478- lines .append (self .module_generator .prepend_paths (key , value , allow_abs = self .cfg ['allow_prepend_abs_path' ]))
1479-
1480- for (key , value ) in self .cfg ['modextrapaths_append' ].items ():
1481- if isinstance (value , str ):
1482- value = [value ]
1483- elif not isinstance (value , (tuple , list )):
1484- raise EasyBuildError ("modextrapaths_append dict value %s (type: %s) is not a list or tuple" ,
1485- value , type (value ))
1486- lines .append (self .module_generator .append_paths (key , value , allow_abs = self .cfg ['allow_append_abs_path' ]))
1469+ for extrapaths_type , prepend in [('modextrapaths' , True ), ('modextrapaths_append' , False )]:
1470+ allow_abs = self .cfg ['allow_prepend_abs_path' ] if prepend else self .cfg ['allow_append_abs_path' ]
1471+
1472+ for (key , value ) in self .cfg [extrapaths_type ].items ():
1473+ if not isinstance (value , (tuple , list , dict , str )):
1474+ raise EasyBuildError (
1475+ f"{ extrapaths_type } dict value '{ value } ' (type { type (value )} ) is not a 'list, dict or str'"
1476+ )
14871477
1478+ try :
1479+ paths = value ['paths' ]
1480+ delim = value ['delimiter' ]
1481+ except KeyError :
1482+ raise EasyBuildError (f'{ extrapaths_type } dict "{ value } " lacks "paths" or "delimiter" items' )
1483+ except TypeError :
1484+ paths = value
1485+ delim = ':'
1486+
1487+ lines .append (
1488+ self .module_generator .update_paths (key , paths , prepend = prepend , delim = delim , allow_abs = allow_abs )
1489+ )
14881490 # add lines to update $PYTHONPATH or $EBPYTHONPREFIXES
14891491 lines .extend (self .make_module_pythonpath ())
14901492
@@ -1881,7 +1883,6 @@ def skip_extensions_parallel(self, exts_filter):
18811883 Skip already installed extensions (checking in parallel),
18821884 by removing them from list of Extension instances to install (self.ext_instances).
18831885 """
1884- self .log .experimental ("Skipping installed extensions in parallel" )
18851886 print_msg ("skipping installed extensions (in parallel)" , log = self .log )
18861887
18871888 installed_exts_ids = []
@@ -1938,13 +1939,12 @@ def install_all_extensions(self, install=True):
19381939 self .log .debug ("List of loaded modules: %s" , self .modules_tool .list ())
19391940
19401941 if build_option ('parallel_extensions_install' ):
1941- self .log .experimental ("installing extensions in parallel" )
19421942 try :
19431943 self .install_extensions_parallel (install = install )
19441944 except NotImplementedError :
19451945 # If parallel extension install is not supported for this type of extension then install sequentially
19461946 msg = "Parallel extensions install not supported for %s - using sequential install" % self .name
1947- self .log .experimental (msg )
1947+ self .log .info (msg )
19481948 self .install_extensions_sequential (install = install )
19491949 else :
19501950 self .install_extensions_sequential (install = install )
@@ -2118,20 +2118,33 @@ def update_exts_progress_bar_helper(running_exts, progress_size):
21182118 # if some of the required dependencies are not installed yet, requeue this extension
21192119 elif pending_deps :
21202120
2121- # make sure all required dependencies are actually going to be installed,
2122- # 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;
21232123 missing_deps = [x for x in required_deps if x not in all_ext_names ]
21242124 if missing_deps :
2125- raise EasyBuildError ("Missing required dependencies for %s are not going to be installed: %s" ,
2126- ext .name , ', ' .join (missing_deps ))
2127- else :
2128- self .log .info ("Required dependencies missing for extension %s (%s), adding it back to queue..." ,
2129- 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 )
21302141 # purposely adding extension back in the queue at Nth place rather than at the end,
21312142 # since we assume that the required dependencies will be installed soon...
21322143 exts_queue .insert (max_iter , ext )
21332144
2134- 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 :
21352148 tup = (ext .name , ext .version or '' )
21362149 print_msg ("starting installation of extension %s %s..." % tup , silent = self .silent , log = self .log )
21372150
@@ -3190,15 +3203,21 @@ def sanity_check_rpath(self, rpath_dirs=None, check_readelf_rpath=True):
31903203
31913204 fails = []
31923205
3193- # hard reset $LD_LIBRARY_PATH before running RPATH sanity check
3194- orig_env = env .unset_env_vars (['LD_LIBRARY_PATH' ])
3206+ if build_option ('strict_rpath_sanity_check' ):
3207+ self .log .info ("Unsetting $LD_LIBRARY_PATH since strict RPATH sanity check is enabled..." )
3208+ # hard reset $LD_LIBRARY_PATH before running RPATH sanity check
3209+ orig_env = env .unset_env_vars (['LD_LIBRARY_PATH' ])
3210+ else :
3211+ self .log .info ("Not unsetting $LD_LIBRARY_PATH since strict RPATH sanity check is disabled..." )
3212+ orig_env = None
31953213
31963214 ld_library_path = os .getenv ('LD_LIBRARY_PATH' , '(empty)' )
31973215 self .log .debug (f"$LD_LIBRARY_PATH during RPATH sanity check: { ld_library_path } " )
31983216 modules_list = self .modules_tool .list ()
31993217 self .log .debug (f"List of loaded modules: { modules_list } " )
32003218
32013219 not_found_regex = re .compile (r'(\S+)\s*\=\>\s*not found' )
3220+ lib_path_regex = re .compile (r'\S+\s*\=\>\s*(\S+)' )
32023221 readelf_rpath_regex = re .compile ('(RPATH)' , re .M )
32033222
32043223 # List of libraries that should be exempt from the RPATH sanity check;
@@ -3244,6 +3263,15 @@ def sanity_check_rpath(self, rpath_dirs=None, check_readelf_rpath=True):
32443263 fail_msg = f"Library { match } not found for { path } "
32453264 self .log .warning (fail_msg )
32463265 fails .append (fail_msg )
3266+
3267+ # if any libraries were not found, log whether dependency libraries have an RPATH section
3268+ if fails :
3269+ lib_paths = re .findall (lib_path_regex , out )
3270+ for lib_path in lib_paths :
3271+ self .log .info (f"Checking whether dependency library { lib_path } has RPATH section" )
3272+ res = run_shell_cmd (f"readelf -d { lib_path } " , fail_on_error = False )
3273+ if res .exit_code :
3274+ self .log .info (f"No RPATH section found in { lib_path } " )
32473275 else :
32483276 self .log .debug (f"Output of 'ldd { path } ' checked, looks OK" )
32493277
@@ -3266,7 +3294,8 @@ def sanity_check_rpath(self, rpath_dirs=None, check_readelf_rpath=True):
32663294 else :
32673295 self .log .debug (f"Not sanity checking files in non-existing directory { dirpath } " )
32683296
3269- env .restore_env_vars (orig_env )
3297+ if orig_env :
3298+ env .restore_env_vars (orig_env )
32703299
32713300 return fails
32723301
@@ -4413,7 +4442,9 @@ def build_and_install_one(ecdict, init_env):
44134442 def ensure_writable_log_dir (log_dir ):
44144443 """Make sure we can write into the log dir"""
44154444 if build_option ('read_only_installdir' ):
4416- # temporarily re-enable write permissions for copying log/easyconfig to install dir
4445+ # temporarily re-enable write permissions for copying log/easyconfig to install dir,
4446+ # ensuring that we resolve symlinks
4447+ log_dir = os .path .realpath (log_dir )
44174448 if os .path .exists (log_dir ):
44184449 adjust_permissions (log_dir , stat .S_IWUSR , add = True , recursive = True )
44194450 else :
0 commit comments