Skip to content

Commit 2dc460b

Browse files
authored
Merge pull request #42 from ocaisa/restrict_toolchains
Use EasyBuild hooks to limit toolchains possible for specific EESSI version
2 parents e7f35e5 + 9dabe2c commit 2dc460b

File tree

3 files changed

+156
-24
lines changed

3 files changed

+156
-24
lines changed

.github/workflows/scripts/test_init_scripts.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ for shell in ${SHELLS[@]}; do
2323
echo -e "\033[33mWe don't now how to test the shell '$shell', PRs are Welcome.\033[0m"
2424
else
2525
# TEST 1: Source Script and check Module Output
26-
assert "$shell -c 'source init/lmod/$shell' 2>&1 " "EESSI/$EESSI_VERSION loaded successfully"
26+
assert "$shell -c 'source init/lmod/$shell' 2>&1 " "Module for EESSI/$EESSI_VERSION loaded successfully"
2727
# TEST 2: Check if module overviews first section is the loaded EESSI module
2828
MODULE_SECTIONS=($($shell -c "source init/lmod/$shell 2>/dev/null; module ov 2>&1 | grep -e '---'"))
2929
PATTERN="/cvmfs/software\.eessi\.io/versions/$EESSI_VERSION/software/linux/x86_64/(intel/haswell|amd/zen3)/modules/all"

.github/workflows/test-eb-hooks.yml

Lines changed: 75 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,32 @@
11
# documentation: https://help.github.com/en/articles/workflow-syntax-for-github-actions
2-
name: Check whether eb_hooks.py script is up-to-date
2+
name: Run checks on EasyBuild hooks script
33
on:
44
push:
55
pull_request:
66
workflow_dispatch:
77
permissions:
88
contents: read # to fetch code (actions/checkout)
99
jobs:
10-
check_eb_hooks:
10+
check_eb_hooks_uptodate:
1111
runs-on: ubuntu-24.04
1212
strategy:
1313
matrix:
1414
EESSI_VERSION:
1515
- '2023.06'
16+
- '2025.06'
17+
1618
steps:
1719
- name: Check out software-layer repository
1820
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
1921
with:
2022
fetch-depth: 0 # Fetch all history for all branches and tags
21-
22-
- name: Show host system info
23-
run: |
24-
echo "/proc/cpuinfo:"
25-
cat /proc/cpuinfo
26-
echo
27-
echo "lscpu:"
28-
lscpu
29-
30-
- name: Mount EESSI CernVM-FS pilot repository
31-
uses: cvmfs-contrib/github-action-cvmfs@55899ca74cf78ab874bdf47f5a804e47c198743c # v4.0
23+
- name: Mount EESSI CernVM-FS repository
24+
uses: eessi/github-action-eessi@v3
3225
with:
33-
cvmfs_config_package: https://github.com/EESSI/filesystem-layer/releases/download/latest/cvmfs-config-eessi_latest_all.deb
34-
cvmfs_http_proxy: DIRECT
35-
cvmfs_repositories: software.eessi.io
26+
eessi_stack_version: ${{matrix.EESSI_VERSION}}
27+
use_eessi_module: true
3628

37-
- name: Check that EasyBuild hook is up to date
29+
- name: Check whether eb_hooks.py script is up-to-date
3830
if: ${{ github.event_name == 'pull_request' }}
3931
run: |
4032
FILE="eb_hooks.py"
@@ -56,6 +48,71 @@ jobs:
5648
sed -i "s/<EESSI_VERSION>/${{matrix.EESSI_VERSION}}/g" "${TEMP_FILE}"
5749
5850
# Compare the hooks to what is shipped in the repository
59-
source /cvmfs/software.eessi.io/versions/${{matrix.EESSI_VERSION}}/init/bash
6051
module load EESSI-extend
6152
diff "$TEMP_FILE" "$EASYBUILD_HOOKS"
53+
54+
check_eb_hooks_functionality:
55+
runs-on: ubuntu-24.04
56+
strategy:
57+
matrix:
58+
EESSI_VERSION:
59+
- '2023.06'
60+
- '2025.06'
61+
include:
62+
# For each EESSI version we need to test different modules
63+
- EESSI_VERSION: '2023.06'
64+
COMPATIBLE_EASYCONFIG: 'M4-1.4.19-GCCcore-13.2.0.eb'
65+
INCOMPATIBLE_EASYCONFIG: 'M4-1.4.19-GCCcore-14.2.0.eb'
66+
# Pick a site toolchain that will allow the incompatible easyconfig
67+
# (the name will be modified when exported)
68+
SITE_TOP_LEVEL_TOOLCHAINS: '[{"name": "GCCcore", "version": "14.2.0"}]'
69+
- EESSI_VERSION: '2025.06'
70+
COMPATIBLE_EASYCONFIG: 'M4-1.4.19-GCCcore-14.2.0.eb'
71+
INCOMPATIBLE_EASYCONFIG: 'M4-1.4.19-GCCcore-13.2.0.eb'
72+
# Pick a site toolchain that will allow the incompatible easyconfig
73+
# (the name will be modified when exported)
74+
SITE_TOP_LEVEL_TOOLCHAINS: '[{"name": "GCCcore", "version": "13.2.0"}]'
75+
76+
steps:
77+
- name: Check out software-layer repository
78+
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
79+
80+
- name: Mount EESSI CernVM-FS repository
81+
uses: eessi/github-action-eessi@v3
82+
with:
83+
eessi_stack_version: ${{matrix.EESSI_VERSION}}
84+
use_eessi_module: true
85+
86+
- name: Test that hook toolchain verification check works
87+
if: ${{ github.event_name == 'pull_request' }}
88+
run: |
89+
# Set up some environment variables
90+
export COMPATIBLE_EASYCONFIG=${{matrix.COMPATIBLE_EASYCONFIG}}
91+
export INCOMPATIBLE_EASYCONFIG=${{matrix.INCOMPATIBLE_EASYCONFIG}}
92+
93+
# Load specific EESSI-extend vertsion (proxies a version check)
94+
module load EESSI-extend/${{matrix.EESSI_VERSION}}-easybuild
95+
96+
# Test an easyconfig that should work
97+
eb --hooks=$PWD/eb_hooks.py "$COMPATIBLE_EASYCONFIG" --stop fetch
98+
echo "Success for hook with easyconfig $COMPATIBLE_EASYCONFIG with EESSI/${{matrix.EESSI_VERSION}}"
99+
100+
# Now ensure an incompatible easyconfig does not work
101+
eb --hooks=$PWD/eb_hooks.py "$INCOMPATIBLE_EASYCONFIG" --stop fetch 2>&1 1>/dev/null | grep -q "not supported in EESSI"
102+
echo "Found expected failure for hook with easyconfig $INCOMPATIBLE_EASYCONFIG and EESSI/${{matrix.EESSI_VERSION}}"
103+
104+
# Check the override works
105+
EESSI_OVERRIDE_TOOLCHAIN_CHECK=1 eb --hooks=$PWD/eb_hooks.py "$INCOMPATIBLE_EASYCONFIG" --stop fetch
106+
echo "Hook ignored via EESSI_OVERRIDE_TOOLCHAIN_CHECK with easyconfig $INCOMPATIBLE_EASYCONFIG and EESSI/${{matrix.EESSI_VERSION}}"
107+
108+
# Now check if we can set a site list of supported toolchains
109+
export SANITIZED_EESSI_VERSION=$(echo "${{ matrix.EESSI_VERSION }}" | sed 's/\./_/g')
110+
export EESSI_SITE_TOP_LEVEL_TOOLCHAINS_"$SANITIZED_EESSI_VERSION"='${{matrix.SITE_TOP_LEVEL_TOOLCHAINS}}'
111+
eb --hooks=$PWD/eb_hooks.py "$INCOMPATIBLE_EASYCONFIG" --stop fetch
112+
echo "Site supported toolchain from $EESSI_SITE_TOP_LEVEL_TOOLCHAINS successfully used with easyconfig $INCOMPATIBLE_EASYCONFIG and EESSI/${{matrix.EESSI_VERSION}}"
113+
114+
# Make sure an invalid list of dicts fails
115+
export EESSI_SITE_TOP_LEVEL_TOOLCHAINS_"$SANITIZED_EESSI_VERSION"="Not a list of dicts"
116+
eb --hooks=$PWD/eb_hooks.py "$INCOMPATIBLE_EASYCONFIG" --stop fetch 2>&1 1>/dev/null | grep -q "does not contain a valid list of dictionaries"
117+
echo "Incorrect format for EESSI_SITE_TOP_LEVEL_TOOLCHAINS caught"
118+

eb_hooks.py

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
# Hooks to customize how EasyBuild installs software in EESSI
22
# see https://docs.easybuild.io/en/latest/Hooks.html
3+
import ast
34
import datetime
45
import glob
6+
import json
57
import os
68
import re
79

810
import easybuild.tools.environment as env
911
from easybuild.easyblocks.generic.configuremake import obtain_config_guess
1012
from easybuild.framework.easyconfig.constants import EASYCONFIG_CONSTANTS
13+
from easybuild.framework.easyconfig.easyconfig import get_toolchain_hierarchy
1114
from easybuild.tools import config
1215
from easybuild.tools.build_log import EasyBuildError, print_msg, print_warning
1316
from easybuild.tools.config import build_option, install_path, update_build_option
1417
from easybuild.tools.filetools import apply_regex_substitutions, copy_dir, copy_file, remove_file, symlink, which
1518
from easybuild.tools.run import run_cmd
1619
from easybuild.tools.systemtools import AARCH64, POWER, X86_64, get_cpu_architecture, get_cpu_features
1720
from easybuild.tools.toolchain.compiler import OPTARCH_GENERIC
21+
from easybuild.tools.toolchain.toolchain import is_system_toolchain
1822
from easybuild.tools.version import VERSION as EASYBUILD_VERSION
1923
from easybuild.tools.modules import get_software_root_env_var_name
2024

@@ -50,6 +54,18 @@
5054

5155
STACK_REPROD_SUBDIR = 'reprod'
5256

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+
5369

5470
def is_gcccore_1220_based(**kwargs):
5571
# ecname, ecversion, tcname, tcversion):
@@ -128,14 +144,73 @@ def parse_hook(ec, *args, **kwargs):
128144
ec = inject_gpu_property(ec)
129145

130146

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+
131203
def post_ready_hook(self, *args, **kwargs):
132204
"""
133205
Post-ready hook: limit parallellism for selected builds based on software name and CPU target.
134206
parallelism needs to be limited because some builds require a lot of memory per used core.
135207
"""
136208
# 'parallel' easyconfig parameter (EB4) or the parallel property (EB5) is set via EasyBlock.set_parallel
137209
# 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']
139214

140215
if parallel == 1:
141216
return # no need to limit if already using 1 core
@@ -167,7 +242,7 @@ def post_ready_hook(self, *args, **kwargs):
167242

168243
# apply the limit if it's different from current
169244
if new_parallel != parallel:
170-
if EASYBUILD_VERSION >= '5':
245+
if hasattr(self, 'parallel'):
171246
self.cfg.parallel = new_parallel
172247
else:
173248
self.cfg['parallel'] = new_parallel
@@ -394,7 +469,7 @@ def parse_hook_freeimage_aarch64(ec, *args, **kwargs):
394469
https://github.com/EESSI/software-layer/pull/736#issuecomment-2373261889
395470
"""
396471
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':
398473
# Make sure the toolchainopts key exists, and the value is a dict,
399474
# before we add the option to enable PIC and disable PNG_ARM_NEON_OPT
400475
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
12301305
# CUDA and cu* libraries themselves don't care about compute capability so remove this
12311306
# duplication from under host_injections (symlink to a single CUDA or cu* library
12321307
# installation for all compute capabilities)
1233-
accel_subdir = os.getenv("EESSI_ACCELERATOR_TARGET")
1308+
accel_subdir = get_eessi_envvar("EESSI_ACCELERATOR_TARGET")
12341309
if accel_subdir:
12351310
host_inj_path = host_inj_path.replace("/accel/%s" % accel_subdir, '')
12361311
# make sure source and target of symlink are not the same
@@ -1326,7 +1401,7 @@ def post_easyblock_hook(self, *args, **kwargs):
13261401

13271402
# Always trigger this one for EESSI CVMFS/site installations and version 2025.06 or newer, regardless of self.name
13281403
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':
13301405
post_easyblock_hook_copy_easybuild_subdir(self, *args, **kwargs)
13311406
else:
13321407
self.log.debug("No CVMFS/site installation requested, not running post_easyblock_hook_copy_easybuild_subdir.")

0 commit comments

Comments
 (0)