|
1 | 1 | # Hooks to customize how EasyBuild installs software in EESSI
|
2 | 2 | # see https://docs.easybuild.io/en/latest/Hooks.html
|
| 3 | +import ast |
3 | 4 | import datetime
|
4 | 5 | import glob
|
| 6 | +import json |
5 | 7 | import os
|
6 | 8 | import re
|
7 | 9 |
|
8 | 10 | import easybuild.tools.environment as env
|
9 | 11 | from easybuild.easyblocks.generic.configuremake import obtain_config_guess
|
10 | 12 | from easybuild.framework.easyconfig.constants import EASYCONFIG_CONSTANTS
|
| 13 | +from easybuild.framework.easyconfig.easyconfig import get_toolchain_hierarchy |
11 | 14 | from easybuild.tools import config
|
12 | 15 | from easybuild.tools.build_log import EasyBuildError, print_msg, print_warning
|
13 | 16 | from easybuild.tools.config import build_option, install_path, update_build_option
|
14 | 17 | from easybuild.tools.filetools import apply_regex_substitutions, copy_dir, copy_file, remove_file, symlink, which
|
15 | 18 | from easybuild.tools.run import run_cmd
|
16 | 19 | from easybuild.tools.systemtools import AARCH64, POWER, X86_64, get_cpu_architecture, get_cpu_features
|
17 | 20 | from easybuild.tools.toolchain.compiler import OPTARCH_GENERIC
|
| 21 | +from easybuild.tools.toolchain.toolchain import is_system_toolchain |
18 | 22 | from easybuild.tools.version import VERSION as EASYBUILD_VERSION
|
19 | 23 | from easybuild.tools.modules import get_software_root_env_var_name
|
20 | 24 |
|
|
50 | 54 |
|
51 | 55 | STACK_REPROD_SUBDIR = 'reprod'
|
52 | 56 |
|
| 57 | +EESSI_SUPPORTED_TOP_LEVEL_TOOLCHAINS = { |
| 58 | + '2023.06': [ |
| 59 | + {'name': 'foss', 'version': '2022b'}, |
| 60 | + {'name': 'foss', 'version': '2023a'}, |
| 61 | + {'name': 'foss', 'version': '2023b'}, |
| 62 | + ], |
| 63 | + '2025.06': [ |
| 64 | + {'name': 'foss', 'version': '2024a'}, |
| 65 | + {'name': 'foss', 'version': '2025a'}, |
| 66 | + ], |
| 67 | +} |
| 68 | + |
53 | 69 |
|
54 | 70 | def is_gcccore_1220_based(**kwargs):
|
55 | 71 | # ecname, ecversion, tcname, tcversion):
|
@@ -128,14 +144,73 @@ def parse_hook(ec, *args, **kwargs):
|
128 | 144 | ec = inject_gpu_property(ec)
|
129 | 145 |
|
130 | 146 |
|
| 147 | +def parse_list_of_dicts_env(var_name): |
| 148 | + """Parse a list of dicts that are stored in an environment variable string""" |
| 149 | + |
| 150 | + # Check if the environment variable name is valid (letters, numbers, underscores, and doesn't start with a digit) |
| 151 | + if not re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', var_name): |
| 152 | + raise ValueError(f"Invalid environment variable name: {var_name}") |
| 153 | + list_string = os.getenv(var_name, '[]') |
| 154 | + |
| 155 | + list_of_dicts = [] |
| 156 | + try: |
| 157 | + # Try JSON format first |
| 158 | + list_of_dicts = json.loads(list_string) |
| 159 | + except json.JSONDecodeError: |
| 160 | + try: |
| 161 | + # Fall back to Python literal format |
| 162 | + list_of_dicts = ast.literal_eval(list_string) |
| 163 | + except (ValueError, SyntaxError): |
| 164 | + raise ValueError(f"Environment variable '{var_name}' does not contain a valid list of dictionaries.") |
| 165 | + |
| 166 | + return list_of_dicts |
| 167 | + |
| 168 | + |
| 169 | +def verify_toolchains_supported_by_eessi_version(easyconfigs): |
| 170 | + """Each EESSI version supports a limited set of toolchains, sanity check the easyconfigs for toolchain support.""" |
| 171 | + eessi_version = get_eessi_envvar('EESSI_VERSION') |
| 172 | + supported_eessi_toolchains = [] |
| 173 | + # Environment variable can't have a '.' so replace by '_' |
| 174 | + site_top_level_toolchains_envvar = 'EESSI_SITE_TOP_LEVEL_TOOLCHAINS_' + eessi_version.replace('.', '_') |
| 175 | + site_top_level_toolchains = parse_list_of_dicts_env(site_top_level_toolchains_envvar) |
| 176 | + for top_level_toolchain in EESSI_SUPPORTED_TOP_LEVEL_TOOLCHAINS[eessi_version] + site_top_level_toolchains: |
| 177 | + supported_eessi_toolchains += get_toolchain_hierarchy(top_level_toolchain) |
| 178 | + for ec in easyconfigs: |
| 179 | + toolchain = ec['ec']['toolchain'] |
| 180 | + # if it is a system toolchain or appears in the list, we are all good |
| 181 | + if is_system_toolchain(toolchain['name']): |
| 182 | + continue |
| 183 | + # This check verifies that the toolchain dict is in the list of supported toolchains. |
| 184 | + # It uses <= as there may be other dict entries in the values returned from get_toolchain_hierarchy() |
| 185 | + # but we only care that the toolchain dict (which has 'name' and 'version') appear. |
| 186 | + elif not any(toolchain.items() <= supported.items() for supported in supported_eessi_toolchains): |
| 187 | + raise EasyBuildError( |
| 188 | + f"Toolchain {toolchain} (required by {ec['full_mod_name']}) is not supported in EESSI/{eessi_version}\n" |
| 189 | + f"Supported toolchains are:\n" + "\n".join(sorted(" " + str(tc) for tc in supported_eessi_toolchains)) |
| 190 | + ) |
| 191 | + |
| 192 | + |
| 193 | +def pre_build_and_install_loop_hook(easyconfigs): |
| 194 | + """Main pre_build_and_install_loop hook: trigger custom functions before beginning installation loop.""" |
| 195 | + |
| 196 | + # Always check that toolchain supported by the EESSI version (unless overridden) |
| 197 | + if os.getenv("EESSI_OVERRIDE_TOOLCHAIN_CHECK"): |
| 198 | + print_warning("Overriding the check that the toolchains are supported by the EESSI version.") |
| 199 | + else: |
| 200 | + verify_toolchains_supported_by_eessi_version(easyconfigs) |
| 201 | + |
| 202 | + |
131 | 203 | def post_ready_hook(self, *args, **kwargs):
|
132 | 204 | """
|
133 | 205 | Post-ready hook: limit parallellism for selected builds based on software name and CPU target.
|
134 | 206 | parallelism needs to be limited because some builds require a lot of memory per used core.
|
135 | 207 | """
|
136 | 208 | # 'parallel' easyconfig parameter (EB4) or the parallel property (EB5) is set via EasyBlock.set_parallel
|
137 | 209 | # in ready step based on available cores
|
138 |
| - parallel = getattr(self, 'parallel', self.cfg['parallel']) |
| 210 | + if hasattr(self, 'parallel'): |
| 211 | + parallel = self.parallel |
| 212 | + else: |
| 213 | + parallel = self.cfg['parallel'] |
139 | 214 |
|
140 | 215 | if parallel == 1:
|
141 | 216 | return # no need to limit if already using 1 core
|
@@ -167,7 +242,7 @@ def post_ready_hook(self, *args, **kwargs):
|
167 | 242 |
|
168 | 243 | # apply the limit if it's different from current
|
169 | 244 | if new_parallel != parallel:
|
170 |
| - if EASYBUILD_VERSION >= '5': |
| 245 | + if hasattr(self, 'parallel'): |
171 | 246 | self.cfg.parallel = new_parallel
|
172 | 247 | else:
|
173 | 248 | self.cfg['parallel'] = new_parallel
|
@@ -394,7 +469,7 @@ def parse_hook_freeimage_aarch64(ec, *args, **kwargs):
|
394 | 469 | https://github.com/EESSI/software-layer/pull/736#issuecomment-2373261889
|
395 | 470 | """
|
396 | 471 | if ec.name == 'FreeImage' and ec.version in ('3.18.0',):
|
397 |
| - if os.getenv('EESSI_CPU_FAMILY') == 'aarch64': |
| 472 | + if get_eessi_envvar('EESSI_CPU_FAMILY') == 'aarch64': |
398 | 473 | # Make sure the toolchainopts key exists, and the value is a dict,
|
399 | 474 | # before we add the option to enable PIC and disable PNG_ARM_NEON_OPT
|
400 | 475 | if 'toolchainopts' not in ec or ec['toolchainopts'] is None:
|
@@ -1230,7 +1305,7 @@ def replace_non_distributable_files_with_symlinks(log, install_dir, pkg_name, al
|
1230 | 1305 | # CUDA and cu* libraries themselves don't care about compute capability so remove this
|
1231 | 1306 | # duplication from under host_injections (symlink to a single CUDA or cu* library
|
1232 | 1307 | # installation for all compute capabilities)
|
1233 |
| - accel_subdir = os.getenv("EESSI_ACCELERATOR_TARGET") |
| 1308 | + accel_subdir = get_eessi_envvar("EESSI_ACCELERATOR_TARGET") |
1234 | 1309 | if accel_subdir:
|
1235 | 1310 | host_inj_path = host_inj_path.replace("/accel/%s" % accel_subdir, '')
|
1236 | 1311 | # make sure source and target of symlink are not the same
|
@@ -1326,7 +1401,7 @@ def post_easyblock_hook(self, *args, **kwargs):
|
1326 | 1401 |
|
1327 | 1402 | # Always trigger this one for EESSI CVMFS/site installations and version 2025.06 or newer, regardless of self.name
|
1328 | 1403 | if os.getenv('EESSI_CVMFS_INSTALL') or os.getenv('EESSI_SITE_INSTALL'):
|
1329 |
| - if os.getenv('EESSI_VERSION') and LooseVersion(os.getenv('EESSI_VERSION')) >= '2025.06': |
| 1404 | + if get_eessi_envvar('EESSI_VERSION') and LooseVersion(get_eessi_envvar('EESSI_VERSION')) >= '2025.06': |
1330 | 1405 | post_easyblock_hook_copy_easybuild_subdir(self, *args, **kwargs)
|
1331 | 1406 | else:
|
1332 | 1407 | self.log.debug("No CVMFS/site installation requested, not running post_easyblock_hook_copy_easybuild_subdir.")
|
|
0 commit comments