diff --git a/.github/workflows/container_tests.yml b/.github/workflows/container_tests.yml
index 540fafcea1..4521186580 100644
--- a/.github/workflows/container_tests.yml
+++ b/.github/workflows/container_tests.yml
@@ -17,10 +17,10 @@ jobs:
python: [3.7]
fail-fast: false
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
- name: set up Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
with:
python-version: ${{matrix.python}}
architecture: x64
@@ -28,7 +28,7 @@ jobs:
- name: install OS & Python packages
run: |
# for modules tool
- APT_PKGS="lua5.2 liblua5.2-dev lua-filesystem lua-posix tcl tcl-dev"
+ APT_PKGS="lua5.3 liblua5.3-dev lua-filesystem lua-posix tcl tcl-dev"
# for building Singularity images
APT_PKGS+=" rpm dnf"
@@ -74,7 +74,7 @@ jobs:
ls dist
export PREFIX=/tmp/$USER/$GITHUB_SHA
pip install --prefix $PREFIX dist/easybuild[-_]framework*tar.gz
- pip install --prefix $PREFIX https://github.com/easybuilders/easybuild-easyblocks/archive/develop.tar.gz
+ pip install --prefix $PREFIX https://github.com/easybuilders/easybuild-easyblocks/archive/5.0.x.tar.gz
- name: run test
run: |
@@ -95,7 +95,7 @@ jobs:
echo '%_dbpath %{_var}/lib/rpm' >> $HOME/.rpmmacros
# build CentOS 7 container image for bzip2 1.0.8 using EasyBuild;
# see https://docs.easybuild.io/en/latest/Containers.html
- curl -OL https://raw.githubusercontent.com/easybuilders/easybuild-easyconfigs/develop/easybuild/easyconfigs/b/bzip2/bzip2-1.0.8.eb
+ curl -OL https://raw.githubusercontent.com/easybuilders/easybuild-easyconfigs/5.0.x/easybuild/easyconfigs/b/bzip2/bzip2-1.0.8.eb
export EASYBUILD_CONTAINERPATH=$PWD
export EASYBUILD_CONTAINER_CONFIG='bootstrap=docker,from=ghcr.io/easybuilders/centos-7.9-python3-amd64'
eb bzip2-1.0.8.eb --containerize --experimental --container-build-image
diff --git a/.github/workflows/container_tests_apptainer.yml b/.github/workflows/container_tests_apptainer.yml
index f7040000f9..1f8417ea7d 100644
--- a/.github/workflows/container_tests_apptainer.yml
+++ b/.github/workflows/container_tests_apptainer.yml
@@ -15,13 +15,13 @@ jobs:
strategy:
matrix:
python: [3.7]
- apptainer: [1.0.0, 1.1.7]
+ apptainer: [1.0.0, 1.1.7, 1.3.6]
fail-fast: false
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
- name: set up Python
- uses: actions/setup-python@v3
+ uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
with:
python-version: ${{matrix.python}}
architecture: x64
@@ -31,7 +31,7 @@ jobs:
# for building CentOS 7 container images
APT_PKGS="rpm dnf"
# for modules tool
- APT_PKGS+=" lua5.2 liblua5.2-dev lua-filesystem lua-posix tcl tcl-dev"
+ APT_PKGS+=" lua5.3 liblua5.3-dev lua-filesystem lua-posix tcl tcl-dev"
# Avoid apt-get update, as we don't really need it,
# and it does more harm than good (it's fairly expensive, and it results in flaky test runs)
@@ -41,12 +41,6 @@ jobs:
sudo apt-get install $APT_PKGS
fi
- # fix for lua-posix packaging issue, see https://bugs.launchpad.net/ubuntu/+source/lua-posix/+bug/1752082
- # needed for Ubuntu 18.04, but not for Ubuntu 20.04, so skipping symlinking if posix.so already exists
- if [ ! -e /usr/lib/x86_64-linux-gnu/lua/5.2/posix.so ] ; then
- sudo ln -s /usr/lib/x86_64-linux-gnu/lua/5.2/posix_c.so /usr/lib/x86_64-linux-gnu/lua/5.2/posix.so
- fi
-
- name: install Lmod
run: |
# avoid downloading modules tool sources into easybuild-framework dir
@@ -74,7 +68,7 @@ jobs:
ls dist
export PREFIX=/tmp/$USER/$GITHUB_SHA
pip install --prefix $PREFIX dist/easybuild[-_]framework*tar.gz
- pip install --prefix $PREFIX https://github.com/easybuilders/easybuild-easyblocks/archive/develop.tar.gz
+ pip install --prefix $PREFIX https://github.com/easybuilders/easybuild-easyblocks/archive/5.0.x.tar.gz
- name: run test
run: |
@@ -95,7 +89,7 @@ jobs:
echo '%_dbpath %{_var}/lib/rpm' >> $HOME/.rpmmacros
# build CentOS 7 container image for bzip2 1.0.8 using EasyBuild;
# see https://docs.easybuild.io/en/latest/Containers.html
- curl -OL https://raw.githubusercontent.com/easybuilders/easybuild-easyconfigs/develop/easybuild/easyconfigs/b/bzip2/bzip2-1.0.8.eb
+ curl -OL https://raw.githubusercontent.com/easybuilders/easybuild-easyconfigs/5.0.x/easybuild/easyconfigs/b/bzip2/bzip2-1.0.8.eb
export EASYBUILD_CONTAINERPATH=$PWD
export EASYBUILD_CONTAINER_CONFIG='bootstrap=docker,from=ghcr.io/easybuilders/centos-7.9-python3-amd64'
export EASYBUILD_CONTAINER_TYPE='apptainer'
diff --git a/.github/workflows/eb_command.yml b/.github/workflows/eb_command.yml
index 220258cd9f..a2ef7c6c1d 100644
--- a/.github/workflows/eb_command.yml
+++ b/.github/workflows/eb_command.yml
@@ -11,16 +11,19 @@ concurrency:
jobs:
test-eb:
- runs-on: ubuntu-20.04
strategy:
matrix:
- python: [3.6, 3.7, 3.8, 3.9, '3.10', '3.11']
+ python: [3.8, 3.9, '3.10', '3.11', '3.12', '3.13']
+ include:
+ - python: 3.7
+ os: ubuntu-22.04
fail-fast: false
+ runs-on: ${{matrix.os || 'ubuntu-24.04'}}
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
- name: set up Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
with:
python-version: ${{matrix.python}}
architecture: x64
@@ -32,9 +35,13 @@ jobs:
# update to latest pip, check version
pip install --upgrade pip
pip --version
+ if ! python -c "import distutils" 2> /dev/null; then
+ # we need setuptools for distutils in Python 3.12+, needed for python setup.py sdist
+ pip install --upgrade setuptools
+ fi
# for modules tool
- APT_PKGS="lua5.2 liblua5.2-dev lua-filesystem lua-posix tcl tcl-dev"
+ APT_PKGS="lua5.3 liblua5.3-dev lua-filesystem lua-posix tcl tcl-dev"
# Avoid apt-get update, as we don't really need it,
# and it does more harm than good (it's fairly expensive, and it results in flaky test runs)
@@ -44,12 +51,6 @@ jobs:
sudo apt-get install $APT_PKGS
fi
- # fix for lua-posix packaging issue, see https://bugs.launchpad.net/ubuntu/+source/lua-posix/+bug/1752082
- # needed for Ubuntu 18.04, but not for Ubuntu 20.04, so skipping symlinking if posix.so already exists
- if [ ! -e /usr/lib/x86_64-linux-gnu/lua/5.2/posix.so ] ; then
- sudo ln -s /usr/lib/x86_64-linux-gnu/lua/5.2/posix_c.so /usr/lib/x86_64-linux-gnu/lua/5.2/posix.so
- fi
-
- name: install modules tool
run: |
# avoid downloading modules tool sources into easybuild-framework dir
@@ -91,7 +92,7 @@ jobs:
pymajver=$(python -c 'import sys; print(sys.version_info[0])')
pymajminver=$(python -c 'import sys; print(".".join(str(x) for x in sys.version_info[:2]))')
# check patterns in verbose output
- for pattern in "^>> Considering .python.\.\.\." "^>> .python. version: ${pymajminver}\.[0-9]\+, which matches Python ${pymajver} version requirement" "^>> 'python' is able to import 'easybuild.framework', so retaining it" "^>> Selected Python command: python \(.*/bin/python\)" "^This is EasyBuild 4\.[0-9.]\+"; do
+ for pattern in "^>> Considering .python3.\.\.\." "^>> .python3. version: ${pymajminver}\.[0-9]\+, which matches Python ${pymajver} version requirement" "^>> 'python3' is able to import 'easybuild.framework', so retaining it" "^>> Selected Python command: python3 \(.*/bin/python3\)" "^This is EasyBuild 5\.[0-9.]\+"; do
echo "Looking for pattern \"${pattern}\" in eb_version.out..."
grep "$pattern" eb_version.out
done
@@ -103,7 +104,7 @@ jobs:
for eb_python in "python${pymajver}" "python${pymajminver}"; do
export EB_PYTHON="${eb_python}"
eb --version | tee eb_version.out 2>&1
- for pattern in "^>> Considering .${eb_python}.\.\.\." "^>> .${eb_python}. version: ${pymajminver}\.[0-9]\+, which matches Python ${pymajver} version requirement" "^>> '${eb_python}' is able to import 'easybuild.framework', so retaining it" "^>> Selected Python command: ${eb_python} \(.*/bin/${eb_python}\)" "^This is EasyBuild 4\.[0-9.]\+"; do
+ for pattern in "^>> Considering .${eb_python}.\.\.\." "^>> .${eb_python}. version: ${pymajminver}\.[0-9]\+, which matches Python ${pymajver} version requirement" "^>> '${eb_python}' is able to import 'easybuild.framework', so retaining it" "^>> Selected Python command: ${eb_python} \(.*/bin/${eb_python}\)" "^This is EasyBuild 5\.[0-9.]\+"; do
echo "Looking for pattern \"${pattern}\" in eb_version.out..."
grep "$pattern" eb_version.out
done
diff --git a/.github/workflows/end2end.yml b/.github/workflows/end2end.yml
index e74b65cc79..b2bf320d94 100644
--- a/.github/workflows/end2end.yml
+++ b/.github/workflows/end2end.yml
@@ -7,29 +7,28 @@ jobs:
strategy:
matrix:
container:
- - centos-7.9
- centos-8.5
- - fedora-36
+ - fedora-41
- opensuse-15.4
- - rockylinux-8.8
- - rockylinux-9.2
+ - rockylinux-8.10
+ - rockylinux-9.5
- ubuntu-20.04
- ubuntu-22.04
+ - ubuntu-24.04
fail-fast: false
container:
image: ghcr.io/easybuilders/${{ matrix.container }}-amd64
- env: {ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true} # Allow using Node16 actions
steps:
- name: Check out the repo
- uses: actions/checkout@v3
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
- name: download and unpack easyblocks and easyconfigs repositories
run: |
cd $HOME
for pkg in easyblocks easyconfigs; do
- curl -OL https://github.com/easybuilders/easybuild-${pkg}/archive/develop.tar.gz
- tar xfz develop.tar.gz
- rm -f develop.tar.gz
+ curl -OL https://github.com/easybuilders/easybuild-${pkg}/archive/5.0.x.tar.gz
+ tar xfz 5.0.x.tar.gz
+ rm -f 5.0.x.tar.gz
done
- name: Set up environment
@@ -37,7 +36,7 @@ jobs:
run: |
# collect environment variables to be set in subsequent steps in script that can be sourced
echo "export PATH=$PWD:$PATH" > /tmp/eb_env
- echo "export PYTHONPATH=$PWD:$HOME/easybuild-easyblocks-develop:$HOME/easybuild-easyconfigs-develop" >> /tmp/eb_env
+ echo "export PYTHONPATH=$PWD:$HOME/easybuild-easyblocks-5.0.x:$HOME/easybuild-easyconfigs-5.0.x" >> /tmp/eb_env
- name: Run commands to check test environment
shell: bash
@@ -50,6 +49,7 @@ jobs:
"eb --show-system-info"
"eb --check-eb-deps"
"eb --show-config"
+ "eb -x bzip2-1.0.8.eb"
)
for cmd in "${cmds[@]}"; do
echo ">>> $cmd"
@@ -59,4 +59,8 @@ jobs:
- name: End-to-end test of installing bzip2 with EasyBuild
shell: bash
run: |
- sudo -u easybuild bash -l -c "source /tmp/eb_env; eb bzip2-1.0.8.eb --trace --robot"
+ EB_ARGS=''
+ if [[ "${{ matrix.container }}" == "fedora-41" ]] || [[ "${{ matrix.container }}" == "ubuntu-24.04" ]]; then
+ EB_ARGS='--filter-deps=binutils'
+ fi
+ sudo -u easybuild bash -l -c "source /tmp/eb_env; eb bzip2-1.0.8.eb --trace --robot ${EB_ARGS}"
diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml
index e7c25b22d7..d5a7163f70 100644
--- a/.github/workflows/linting.yml
+++ b/.github/workflows/linting.yml
@@ -10,16 +10,19 @@ concurrency:
jobs:
python-linting:
- runs-on: ubuntu-20.04
strategy:
matrix:
- python-version: [3.6, 3.7, 3.8, 3.9, '3.10', '3.11']
-
+ python: [3.8, 3.9, '3.10', '3.11', '3.12', '3.13']
+ include:
+ - python: 3.7
+ os: ubuntu-22.04
+ fail-fast: false
+ runs-on: ${{matrix.os || 'ubuntu-24.04'}}
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
- name: set up Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
with:
python-version: ${{ matrix.python-version }}
@@ -30,10 +33,4 @@ jobs:
- name: Run flake8 to verify PEP8-compliance of Python code
run: |
- # don't check py2vs3/py3.py when testing with Python 2, and vice versa
- if [[ "${{ matrix.python-version }}" =~ "2." ]]; then
- py_excl=py3
- else
- py_excl=py2
- fi
- flake8 --exclude ./easybuild/tools/py2vs3/${py_excl}.py
+ flake8
diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml
index 8a8b13af96..45f32bd866 100644
--- a/.github/workflows/unit_tests.yml
+++ b/.github/workflows/unit_tests.yml
@@ -11,53 +11,47 @@ concurrency:
jobs:
setup:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-latest
outputs:
- lmod7: Lmod-7.8.22
- lmod8: Lmod-8.7.6
- modulesTcl: modules-tcl-1.147
- modules3: modules-3.2.10
- modules4: modules-4.1.4
+ lmod8: Lmod-8.7.58
+ modules4: modules-4.5.3
+ modules5: modules-5.3.1
steps:
- run: "true"
build:
needs: setup
- runs-on: ubuntu-20.04
+ runs-on: ${{matrix.os || 'ubuntu-24.04'}}
strategy:
matrix:
- python: [3.6]
+ # Python 3.10 is default in Ubuntu 22.04
+ python: ['3.10']
modules_tool:
# use variables defined by 'setup' job above, see also
# https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#needs-context
- - ${{needs.setup.outputs.lmod7}}
- ${{needs.setup.outputs.lmod8}}
- - ${{needs.setup.outputs.modulesTcl}}
- - ${{needs.setup.outputs.modules3}}
- ${{needs.setup.outputs.modules4}}
- lc_all: [""]
+ - ${{needs.setup.outputs.modules5}}
include:
- # Test different Python 3 versions with Lmod 8.x
- - python: 3.7
+ # Test different Python 3 versions with Lmod 8.x (with both Lua and Tcl module syntax)
+ - python: '3.7'
modules_tool: ${{needs.setup.outputs.lmod8}}
- - python: 3.8
+ os: ubuntu-22.04
+ - python: '3.8'
modules_tool: ${{needs.setup.outputs.lmod8}}
- - python: 3.9
- modules_tool: ${{needs.setup.outputs.lmod8}}
- - python: '3.10'
+ - python: '3.9'
modules_tool: ${{needs.setup.outputs.lmod8}}
- python: '3.11'
modules_tool: ${{needs.setup.outputs.lmod8}}
- # There may be encoding errors in Python 3 which are hidden when an UTF-8 encoding is set
- # Hence run the tests (again) with LC_ALL=C and Python 3.6 (or any < 3.7)
- - python: 3.6
+ - python: '3.12'
+ modules_tool: ${{needs.setup.outputs.lmod8}}
+ - python: '3.13'
modules_tool: ${{needs.setup.outputs.lmod8}}
- lc_all: C
fail-fast: false
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
- name: set up Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
with:
python-version: ${{matrix.python}}
architecture: x64
@@ -65,7 +59,7 @@ jobs:
- name: install OS & Python packages
run: |
# for modules tool
- APT_PKGS="lua5.2 liblua5.2-dev lua-filesystem lua-posix tcl tcl-dev"
+ APT_PKGS="lua5.3 liblua5.3-dev lua-filesystem lua-posix tcl tcl-dev"
# for GitPython, python-hglib
APT_PKGS+=" git mercurial"
# dep for GC3Pie
@@ -79,19 +73,18 @@ jobs:
sudo apt-get install $APT_PKGS
fi
- # fix for lua-posix packaging issue, see https://bugs.launchpad.net/ubuntu/+source/lua-posix/+bug/1752082
- # needed for Ubuntu 18.04, but not for Ubuntu 20.04, so skipping symlinking if posix.so already exists
- if [ ! -e /usr/lib/x86_64-linux-gnu/lua/5.2/posix.so ] ; then
- sudo ln -s /usr/lib/x86_64-linux-gnu/lua/5.2/posix_c.so /usr/lib/x86_64-linux-gnu/lua/5.2/posix.so
- fi
# Python packages
pip --version
pip install --upgrade pip
pip --version
pip install -r requirements.txt
+ if ! python -c "import distutils" 2> /dev/null; then
+ # we need setuptools for distutils in Python 3.12+, needed for python setup.py sdist
+ pip install --upgrade setuptools
+ fi
# git config is required to make actual git commits (cfr. tests for GitRepository)
- git config --global user.name "Travis CI"
- git config --global user.email "travis@travis-ci.org"
+ git config --global user.name "Github Actions"
+ git config --global user.email "actions@github.com"
git config --get-regexp 'user.*'
- name: install GitHub token (if available)
@@ -101,9 +94,9 @@ jobs:
# and are only run after the PR gets merged
GITHUB_TOKEN: ${{secrets.CI_UNIT_TESTS_GITHUB_TOKEN}}
run: |
- # only install GitHub token when testing with Lmod 8.x + Python 3.6 or 3.9, to avoid hitting GitHub rate limit
+ # only install GitHub token when testing with Lmod 8.x + Python 3.9, to avoid hitting GitHub rate limit
# tests that require a GitHub token are skipped automatically when no GitHub token is available
- if [[ "${{matrix.modules_tool}}" =~ 'Lmod-8' ]] && [[ "${{matrix.python}}" =~ 3.[69] ]]; then
+ if [[ "${{matrix.modules_tool}}" =~ 'Lmod-8' ]] && [[ "${{matrix.python}}" =~ 3.9 ]]; then
if [ ! -z $GITHUB_TOKEN ]; then
SET_KEYRING="import keyrings.alt.file; keyring.set_keyring(keyrings.alt.file.PlaintextKeyring())"
python -c "import keyring; $SET_KEYRING; keyring.set_password('github_token', 'easybuild_test', '$GITHUB_TOKEN')"
@@ -129,7 +122,7 @@ jobs:
run: |
# make sure there are no (top-level) "import setuptools" or "import pkg_resources" statements,
# since EasyBuild should not have a runtime requirement on setuptools
- SETUPTOOLS_IMPORTS=$(egrep -RI '^(from|import)[ ]*pkg_resources|^(from|import)[ ]*setuptools' * || true)
+ SETUPTOOLS_IMPORTS=$(egrep --exclude setup.py -RI '^(from|import)[ ]*pkg_resources|^(from|import)[ ]*setuptools' * || true)
test "x$SETUPTOOLS_IMPORTS" = "x" || (echo "Found setuptools and/or pkg_resources imports in easybuild/:\n${SETUPTOOLS_IMPORTS}" && exit 1)
- name: install sources
@@ -143,14 +136,16 @@ jobs:
- name: run test suite
env:
EB_VERBOSE: 1
- LC_ALL: ${{matrix.lc_all}}
+ LC_ALL: ""
run: |
# run tests *outside* of checked out easybuild-framework directory,
# to ensure we're testing installed version (see previous step)
cd $HOME
# initialize environment for modules tool
if [ -f $HOME/moduleshome ]; then export MODULESHOME=$(cat $HOME/moduleshome); fi
- source $(cat $HOME/mod_init); type module
+ source $(cat $HOME/mod_init)
+ type module
+ module --version
# make sure 'eb' is available via $PATH, and that $PYTHONPATH is set (some tests expect that);
# also pick up changes to $PATH set by sourcing $MOD_INIT
export PREFIX=/tmp/$USER/$GITHUB_SHA
@@ -158,11 +153,7 @@ jobs:
export PYTHONPATH=$PREFIX/lib/python${{matrix.python}}/site-packages:$PYTHONPATH
eb --version
# tell EasyBuild which modules tool is available
- if [[ ${{matrix.modules_tool}} =~ ^modules-tcl- ]]; then
- export EASYBUILD_MODULES_TOOL=EnvironmentModulesTcl
- elif [[ ${{matrix.modules_tool}} =~ ^modules-3 ]]; then
- export EASYBUILD_MODULES_TOOL=EnvironmentModulesC
- elif [[ ${{matrix.modules_tool}} =~ ^modules-4 ]]; then
+ if [[ ${{matrix.modules_tool}} =~ ^modules- ]]; then
export EASYBUILD_MODULES_TOOL=EnvironmentModules
else
export EASYBUILD_MODULES_TOOL=Lmod
@@ -195,9 +186,8 @@ jobs:
IGNORE_PATTERNS+="|skipping SvnRepository test"
IGNORE_PATTERNS+="|requires Lmod as modules tool"
IGNORE_PATTERNS+="|stty: 'standard input': Inappropriate ioctl for device"
- IGNORE_PATTERNS+="|CryptographyDeprecationWarning: Python 3.[56]"
+ IGNORE_PATTERNS+="|CryptographyDeprecationWarning: Python 3.7"
IGNORE_PATTERNS+="|from cryptography.* import "
- IGNORE_PATTERNS+="|CryptographyDeprecationWarning: Python 2"
IGNORE_PATTERNS+="|Blowfish"
IGNORE_PATTERNS+="|GC3Pie not available, skipping test"
IGNORE_PATTERNS+="|CryptographyDeprecationWarning: TripleDES has been moved"
diff --git a/.github/workflows/unit_tests_python2.yml b/.github/workflows/unit_tests_python2.yml
deleted file mode 100644
index aa00f9a2fd..0000000000
--- a/.github/workflows/unit_tests_python2.yml
+++ /dev/null
@@ -1,82 +0,0 @@
-# documentation: https://help.github.com/en/articles/workflow-syntax-for-github-actions
-name: EasyBuild framework unit tests (python2)
-on: [push, pull_request]
-
-permissions:
- contents: read # to fetch code (actions/checkout)
-
-concurrency:
- group: ${{format('{0}:{1}:{2}', github.repository, github.ref, github.workflow)}}
- cancel-in-progress: true
-
-jobs:
- test_python2:
- runs-on: ubuntu-20.04
- container:
- # CentOS 7.9 container that already includes Lmod & co,
- # see https://github.com/easybuilders/easybuild-containers
- image: ghcr.io/easybuilders/centos-7.9-amd64
- env: {ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true} # Allow using Node16 actions
- steps:
- - uses: actions/checkout@v3
-
- - name: install Python packages
- run: |
- # Python packages
- python2 -V
- python2 -m pip --version
- python2 -m pip install --upgrade pip
- python2 -m pip --version
- # strip out GC3Pie since installation with ancient setuptools (0.9.8) fails
- sed -i '/GC3Pie/d' requirements.txt
- python2 -m pip install -r requirements.txt
- # git config is required to make actual git commits (cfr. tests for GitRepository)
- sudo -u easybuild git config --global user.name "GitHub Actions"
- sudo -u easybuild git config --global user.email "actions@github.com"
- sudo -u easybuild git config --get-regexp 'user.*'
-
- - name: install GitHub token (if available)
- env:
- # token (owned by @boegelbot) with gist permissions (required for some of the tests for GitHub integration);
- # this token is not available in pull requests, so tests that require it are skipped in PRs,
- # and are only run after the PR gets merged
- GITHUB_TOKEN: ${{secrets.CI_UNIT_TESTS_GITHUB_TOKEN}}
- run: |
- # tests that require a GitHub token are skipped automatically when no GitHub token is available
- if [ ! -z $GITHUB_TOKEN ]; then
- sudo -u easybuild python2 -c "import keyring; import keyrings.alt.file; keyring.set_keyring(keyrings.alt.file.PlaintextKeyring()); keyring.set_password('github_token', 'easybuild_test', '$GITHUB_TOKEN')";
- echo "GitHub token installed!"
- else
- echo "Installation of GitHub token skipped!"
- fi
-
- - name: install sources
- run: |
- # install from source distribution tarball, to test release as published on PyPI
- python2 setup.py sdist
- ls dist
- export PREFIX=/tmp/$USER/$GITHUB_SHA
- python2 -m pip install --prefix $PREFIX dist/easybuild-framework*tar.gz
-
- - name: run test suite
- run: |
- # run tests *outside* of checked out easybuild-framework directory,
- # to ensure we're testing installed version (see previous step)
- cd $HOME
- # make sure 'eb' is available via $PATH, and that $PYTHONPATH is set (some tests expect that)
- export PREFIX=/tmp/$USER/$GITHUB_SHA
- ENV_CMDS="export PATH=$PREFIX/bin:$PATH; export PYTHONPATH=$PREFIX/lib/python2.7/site-packages:$PYTHONPATH"
- ENV_CMDS="${ENV_CMDS}; export EB_VERBOSE=1; export EB_PYTHON=python2; export TEST_EASYBUILD_SILENCE_DEPRECATION_WARNINGS=python2"
- # run EasyBuild command via (non-root) easybuild user + login shell
- sudo -u easybuild bash -l -c "${ENV_CMDS}; module --version; eb --version"
- # show active EasyBuild configuration
- sudo -u easybuild bash -l -c "${ENV_CMDS}; eb --show-config"
- # gather some useful info on test system
- sudo -u easybuild bash -l -c "${ENV_CMDS}; eb --show-system-info"
- # check GitHub configuration
- sudo -u easybuild bash -l -c "${ENV_CMDS}; eb --check-github --github-user=easybuild_test"
- # create file owned by root but writable by anyone (used by test_copy_file)
- sudo touch /tmp/file_to_overwrite_for_easybuild_test_copy_file.txt
- sudo chmod o+w /tmp/file_to_overwrite_for_easybuild_test_copy_file.txt
- # run test suite (via easybuild user + login shell)
- sudo -u easybuild bash -l -c "${ENV_CMDS}; python2 -O -m test.framework.suite"
diff --git a/RELEASE_NOTES b/RELEASE_NOTES
index 4eed2c680c..9375667477 100644
--- a/RELEASE_NOTES
+++ b/RELEASE_NOTES
@@ -1,10 +1,150 @@
This file contains a description of the major changes to the easybuild-framework EasyBuild package.
For more detailed information, please see the git log.
-These release notes can also be consulted at https://easybuild.readthedocs.io/en/latest/Release_notes.html.
+These release notes can also be consulted at https://docs.easybuild.io/release-notes .
-v4.9.4 (22 september 2024)
+v5.0.0 (18 March 2025)
+----------------------
+
+- remove support for Python 2.7 and 3.5 (#4229, #4270, #4306, #4473, #4477, #4476, #4478, #4524, #4607, #4756, #4810, #4811)
+ - also run unit test suite with Python 3.12 + 3.13 (#4484, #4674)
+- changed defaults in EasyBuild configuration:
+ - enable --trace by default (#4250, #4310, #4311, #4491)
+ - enable RPATH linking by default (#4448, #4515, #4779)
+ - disable strict RPATH sanity check by default, allow re-enabling it via `--strict-rpath-sanity-check` configuration option (#4475)
+ - enable `module-depends-on` by default (#4500)
+ - enable `module-extensions` by default (#4501)
+ - set default maximum level of parallellism to `16` via `max-parallel` configuration option (#4606, #4777, #4816)
+ - use Slurm as the default job backend (#4659)
+ - enable keeping of symbolic links by default when copying (set `keepsymlinks` easyconfig parameter to `True`) (#4773)
+- changed behaviour:
+ - change default for `change_into_dir` option in `extract_file` function to to `False` (#4246)
+ - enforce correct `.patch(.*)` extension for patch files (#4247)
+ - create `lib` -> `lib64` symlink (or vice versa) *before* running `postinstallcmds` (#4435)
+ - don't allow unresolved templates in easyconfig parameters by default + add support for `--allow-unresolved-templates` configuration option (#4516, #4725, #4726, #4727)
+ - switch default checksum to `sha256` (#4523)
+ - only allow use of `rpath` toolchain option when `system` toolchain is used (#4585, #4808)
+ - use default value `$XDG_CONFIG_DIRS` from XDG basedir spec: `/etc/xdg` (instead of `/etc`) (#4591)
+ - reverse order for parsing files in `XDG_CONFIG_DIRS` (#4630)
+ - move verifying of checksums from `source` to `fetch` step, to include it with `--fetch` (#4624, #4729)
+ - drop support for pep8 package (was used for `--check-contrib` + `--check-style`) (#4634)
+ - specify changes that should be made by generated module files via `module_load_environment` attribute of `EasyBlock` class (#4653, #4754, #4761, #4774, #4799, #4784, #4801, #4802, #4803, #4809)
+ - let jobs retweak easyconfigs themselves by passing down `--try-*` options (#4669)
+ - refactor `make_extension_string` method in `EasyBlock` class (#4690)
+ - change `Toolchain.get_flag` so it doesn't automatically prepend a dash (`-`) to compiler flags (#4698)
+ - rename `Compiler.COMPILER*_FLAGS` to `Compiler.COMPILER*_OPTIONS` (#4698)
+ - change semantics of `--dry-run`, so it doesn't imply `--robot` (#4704)
+ - run sanity checks commands from an empty temporary directory (rather than the software install directory) (#4723)
+ - add context manager for allowing unresolved templates and make the state members private (#4735)
+ - also remove support for directly setting `enable_templating` and `expect_resolved_template_values`
+- various enhancements:
+ - `run_shell_cmd` function to run shell commands which replaces both the (now deprecated) `run_cmd` and `run_cmd_qa` functions (#4284, #4314, #4321, #4322, #4327, #4334, #4335, #4336, #4351, #4356, #4378, #4380, #4383, #4390, #4422, #4423, #4427, #4428, #4430, #4431, #4432, #4441, #4443, #4444, #4453, #4454, #4471, #4504, #4509, #4544, #4612, #4617, #4664, #4721, #4728, #4731, #4755, #4757)
+ - detect Fortran `.mod` files for installations using `GCCcore` toolchain (#4389)
+ - create `env.sh` and `cmd.sh` helper scripts in `run_shell_cmd` to allow starting interactive shell to debug failing shell commands (#4486, #4611, #4662, #4666, #4685, #4792)
+ - add support for alternate easyconfig parameters/templates/constants (#4511, #4514, #4549, #4555)
+ - create reproducible tarballs for sources created via `git_config` (requires `.tar.xz` + Python 3.9+) (#4248, #4517, #4522, #4660, #4733, #4797, #4798, #4813)
+ - use more granular exit codes when `EasyBuildError` is raised (#4534)
+ - prepend to `$PYTHONPATH` or `$EBPYTHONPREFIXES` in generated module files by automatically scanning for Python site package directories (as configured via `prefer-python-search-path`) (#4539, #4686)
+ - copy build directory and/or log file(s) if installation failed to path specified via `--failed-install-build-dirs-path` or `--failed-install-logs-path` (#4601)
+ - add `--search-path-cpp-headers` configuration option to control how EasyBuild sets paths to headers at build time (#4645)
+ - add `module-search-path-headers` configuration option to control how modules set search paths to header files (#4655)
+ - add `--keep-debug-symbols` configuration option to set default value of '`debug`' toolchain option (#4688, #4764)
+ - add `--search-path-linker` option to control linker options at build time (#4697)
+ - don't raise error when required extensions are not found when installing extensions in parallel (#4671)
+ - mark support for installing extensions in parallel as being mature, since it's no longer experimental (#4672)
+ - mark easystack support as being mature, since it's no longer experimental (#4673)
+ - and several additional small enhancements:
+ - enhance download instructions by mentioning active source path (#4459)
+ - add CUDA compute capability integer format templates (`cuda_int_*_sep`) (#4463)
+ - add new `get_cwd` function to `tools.filetools` to retrieve current working directory (#4525)
+ - use `dict.items()` instead of repeatedly getting the value (#4533)
+ - set `usedforsecurity` to `False` when calling `hashlib.md5` with Python >= 3.9 (#4550)
+ - add `GNU_FTP_SOURCE` template constant (#4598)
+ - allow using `amend/try-amend` multiple times in an easystack file entry (#4667)
+ - add support for `%(rpath_enabled)s` template value (#4670)
+ - add `resolve_template` method to `EasyConfig` class (#4677)
+ - allow templates in `custom_paths` & `custom_commands` sanity-check arguments (#4679)
+ - updates and fixes for `findPythonDeps` script (#4682, #4740)
+ - allow use of custom delimiter for paths in module generator (#4687)
+ - enhance `get_software_libdir` to return full paths if requested (#4699)
+ - enhance `EasyBlock` class to allow passing in `logfile` (#4707)
+ - avoid checking loaded module twice in `findUpdatedEcs.sh` script (#4710)
+ - allow nesting values in checksum dicts (#4711) Flamefire:non-dict MERGED 2024-12-02T12:14:12Z
+ - use `enumerate` where applicable + fix for `ModuleGenerator._generate_multi_deps_list` (#4720)
+ - faster `nub` function (#4737)
+ - enhance `apply_regex_substitutions` to support use of multi-line patterns, requiring matching all patterns in each file, and use pre-compiled regular expressions (#4758)
+ - ignore other classes if software specific easyblock class was found (#4769)
+ - also allow trailing whitespaces for `examples` and `citing` easyconfig parameters (#4796)
+ - enhance `get_gpu_info` to also use `amd-smi` for AMD GPUs if possible (#4805)
+- various changes, improvemnents, and fixes for the supported modules tools (Environment Modules + Lmod)
+ - drop load storm safe guard for Environment Modules v4.2.4+ (#4373)
+ - run unit tests on an updated versions of Environment Modules: v4.5.3 + v5.3.1 (#4415)
+ - add `check_group` support for module files in Tcl syntax (#4418)
+ - bump minimum required Lmod to 8.0.0 (#4424)
+ - bump minimum required Tmod (4.x) to 4.3.0 (#4425)
+ - use `getenv` modulefile command with `EnvironmentModules` >= 4.2.0 (#4614)
+ - add module cache build support in `EnvironmentModules` class (#4615)
+ - derive `EnvironmentModules` class directly from `ModulesTool` rather than from to be deprecated `EnvironmentModulesTcl` (#4625)
+ - adapt `module show` command run to cope with non-zero exit code for non-existing module (required for Environment Modules v5.5+ and Lmod 8.7.56+) (#4739)
+- various improvements for the framework test suite:
+ - allow test case filtering by class name (#4788)
+ - improve test failure output for `assertEqual` (#4807)
+ - fix `test_modulerc` by taking into account that wrapper module may also be shown as being loaded (#4778)
+ - (temporarily) use 5.0.x branch for easyblocks + easyconfigs in CI workflows (#4358)
+ - fix GitHub Actions for CentOS 7.9 container (#4712)
+ - stop using Ubuntu 20.4 in GitHub Actions workflows, use Ubuntu 22.04 instead (#4783)
+ - update GitHub actions workflows to use `ubuntu-24.04` where possible (#4795)
+- various small bug fixes:
+ - silence output of included easyblocks for `--terse` (#4765)
+ - avoid processing the same EasyConfig multiple times (#4767)
+ - fix easyconfig parameter deprecation (#4479, #4480)
+ - switch from `ls` to `bash` in tests that are expecting this to be a binary (#4492)
+ - fix the checksum type check (#4578)
+ - fix `to_checksums` with `None` values in dicts and recursion (#4579)
+ - fix `test_toy_lock_cleanup_signals` (#4600)
+ - resolve symlink when making log dir writable (#4658)
+ - fix typo in `veryloose` toolchain option for RISC-V (#4668)
+ - fix dry-run output when using `multi_deps` (#4678)
+ - make `LooseVersion('1.0') == LooseVersion('1')` (#4691)
+ - fix FFT entry in `--list-toolchains` output for Cray toolchains (#4719)
+ - avoid making build directory read-only (#4736)
+- deprecated functionality:
+ - `easybuild.tools.py2vs3` module (#4229)
+ - rename unclear `*run*` methods to `*install_extension*` + rename `install_extensions` to `install_all_extensions` (#4400)
+ - deprecate `run_cmd` and `run_cmd_qa` & co, move them to `easybuild._deprecated` module (#4433)
+ - deprecate support for `EnvironmentModulesC` and `EnvironmentModulesTcl` module tools (#4439)
+ - deprecate old checksum types (incl. md5) (#4526, #4545)
+ - deprecate use of `parallel` easyconfig parameter and fix updating the template value (#4580)
+ - convert template constant lists to dicts and export the constants by name (#4595)
+ - rename '`source`' step to '`extract`' (affects `skipsteps` easyconfig parameter + `--stop` option) (#4629)
+ - deprecate `make_module_req_guess` method in `EasyBlock` class (#4653, #4763)
+ - deprecate support for GC3Pie as job backend (#4659)
+ - add deprecation warning for `optarch` value without leading dash (#4698)
+ - deprecate `post_install_step` method in `EasyBlock`, was renamed to `post_processing_step` (#4715)
+- remove functionality that was deprecated in EasyBuild v4.x, including:
+ - EasyBuild bootstrap script (#4233)
+ - support for YAML-based easyconfig format (.yeb) (#4237)
+ - `wait-on-lock` configuration setting (#4239)
+ - `dummy` toolchain (#4240)
+ - `accept-eula` configuration setting (#4242)
+ - `is_generic_easyblock` function from `easybuild.framework.easyconfig.easyconfig` (#4243)
+ - `use_git_am` option for `apply_patch` function (#4244)
+ - `fetch_extension_sources` method in `EasyBlock` class (#4245)
+ - support for 32-bit targets (#4272)
+ - `descr` option for `simple_option` function (#4273)
+ - `Toolchain.add_dependencies` method (#4274)
+ - `copytree`, `rmtree2` functions from `easybuild.filetools` (#4275)
+ - `skip_symlinks` option for `adjust_permissions` function (#4275)
+ - `log_error` option from `which` function (#4276)
+ - `skip_lower` option from `template_constant_dict` (#4277)
+ - `disable_templating` + `default_fallback` options in `get_easyblock_class` (#4278)
+ - `mod_exists_regex_template` options in `ModulesTool.exist` (#4279)
+- other changes
+ - take into account that `VERBOSE_VERSION` imported from `easybuild.easyblocks` is now a string value (#4357)
+
+
+v4.9.4 (22 September 2024)
--------------------------
update/bugfix release
diff --git a/easybuild/__init__.py b/easybuild/__init__.py
index 3637c9f395..fa668dfd4b 100644
--- a/easybuild/__init__.py
+++ b/easybuild/__init__.py
@@ -1,5 +1,5 @@
##
-# Copyright 2011-2024 Ghent University
+# Copyright 2011-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/_deprecated.py b/easybuild/_deprecated.py
new file mode 100644
index 0000000000..2beebd8174
--- /dev/null
+++ b/easybuild/_deprecated.py
@@ -0,0 +1,854 @@
+# #
+# Copyright 2023-2023 Ghent University
+#
+# This file is part of EasyBuild,
+# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
+# with support of Ghent University (http://ugent.be/hpc),
+# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
+# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
+# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
+#
+# https://github.com/easybuilders/easybuild
+#
+# EasyBuild is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation v2.
+#
+# EasyBuild is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with EasyBuild. If not, see .
+# #
+"""
+Deprecated functionality, which will be removed with next major EasyBuild version
+
+Authors:
+
+* Kenneth Hoste (Ghent University)
+"""
+import contextlib
+import functools
+import os
+import re
+import signal
+import subprocess
+import sys
+import tempfile
+import time
+from datetime import datetime
+
+import easybuild.tools.asyncprocess as asyncprocess
+from easybuild.base import fancylogger
+from easybuild.tools.build_log import EasyBuildError, dry_run_msg, print_msg, time_str_since
+from easybuild.tools.config import ERROR, IGNORE, WARN, build_option
+from easybuild.tools.hooks import RUN_SHELL_CMD, load_hooks, run_hook
+from easybuild.tools.utilities import nub, trace_msg
+
+
+_log = fancylogger.getLogger('_deprecated', fname=False)
+
+
+errors_found_in_log = 0
+
+# default strictness level
+strictness = WARN
+
+
+CACHED_COMMANDS = [
+ "sysctl -n hw.cpufrequency_max", # used in get_cpu_speed (OS X)
+ "sysctl -n hw.memsize", # used in get_total_memory (OS X)
+ "sysctl -n hw.ncpu", # used in get_avail_core_count (OS X)
+ "sysctl -n machdep.cpu.brand_string", # used in get_cpu_model (OS X)
+ "sysctl -n machdep.cpu.vendor", # used in get_cpu_vendor (OS X)
+ "type module", # used in ModulesTool.check_module_function
+ "type _module_raw", # used in EnvironmentModules.check_module_function
+ "ulimit -u", # used in det_parallelism
+]
+
+
+def run_cmd_cache(func):
+ """Function decorator to cache (and retrieve cached) results of running commands."""
+ cache = {}
+
+ @functools.wraps(func)
+ def cache_aware_func(cmd, *args, **kwargs):
+ """Retrieve cached result of selected commands, or run specified and collect & cache result."""
+
+ # cache key is combination of command and input provided via stdin ('inp' named option)
+ key = (cmd, kwargs.get('inp', None))
+ # fetch from cache if available, cache it if it's not, but only on cmd strings
+ if isinstance(cmd, str) and key in cache:
+ _log.debug("Using cached value for command '%s': %s", cmd, cache[key])
+ return cache[key]
+ else:
+ res = func(cmd, *args, **kwargs)
+ if cmd in CACHED_COMMANDS:
+ cache[key] = res
+ return res
+
+ # expose clear/update methods of cache to wrapped function
+ cache_aware_func.clear_cache = cache.clear
+ cache_aware_func.update_cache = cache.update
+
+ return cache_aware_func
+
+
+def get_output_from_process(proc, read_size=None, asynchronous=False, print_deprecation_warning=True):
+ """
+ Get output from running process (that was opened with subprocess.Popen).
+
+ :param proc: process to get output from
+ :param read_size: number of bytes of output to read (if None: read all output)
+ :param asynchronous: get output asynchronously
+ """
+
+ if print_deprecation_warning:
+ _log.deprecated("get_output_from_process is deprecated, you should stop using it", '6.0')
+
+ if asynchronous:
+ # e=False is set to avoid raising an exception when command has completed;
+ # that's needed to ensure we get all output,
+ # see https://github.com/easybuilders/easybuild-framework/issues/3593
+ output = asyncprocess.recv_some(proc, e=False)
+ elif read_size:
+ output = proc.stdout.read(read_size)
+ else:
+ output = proc.stdout.read()
+
+ # need to be careful w.r.t. encoding since we want to obtain a string value,
+ # and the output may include non UTF-8 characters
+ # * in Python 2, .decode() returns a value of type 'unicode',
+ # but we really want a regular 'str' value (which is also why we use 'ignore' for encoding errors)
+ # * in Python 3, .decode() returns a 'str' value when called on the 'bytes' value obtained from .read()
+ output = str(output.decode('ascii', 'ignore'))
+
+ return output
+
+
+@run_cmd_cache
+def run_cmd(cmd, log_ok=True, log_all=False, simple=False, inp=None, regexp=True, log_output=False, path=None,
+ force_in_dry_run=False, verbose=True, shell=None, trace=True, stream_output=None, asynchronous=False,
+ with_hooks=True, with_sysroot=True):
+ """
+ Run specified command (in a subshell)
+ :param cmd: command to run
+ :param log_ok: only run output/exit code for failing commands (exit code non-zero)
+ :param log_all: always log command output and exit code
+ :param simple: if True, just return True/False to indicate success, else return a tuple: (output, exit_code)
+ :param inp: the input given to the command via stdin
+ :param regexp: regex used to check the output for errors; if True it will use the default (see parse_log_for_error)
+ :param log_output: indicate whether all output of command should be logged to a separate temporary logfile
+ :param path: path to execute the command in; current working directory is used if unspecified
+ :param force_in_dry_run: force running the command during dry run
+ :param verbose: include message on running the command in dry run output
+ :param shell: allow commands to not run in a shell (especially useful for cmd lists), defaults to True
+ :param trace: print command being executed as part of trace output
+ :param stream_output: enable streaming command output to stdout
+ :param asynchronous: run command asynchronously (returns subprocess.Popen instance if set to True)
+ :param with_hooks: trigger pre/post run_shell_cmd hooks (if defined)
+ :param with_sysroot: prepend sysroot to exec_cmd (if defined)
+ """
+
+ _log.deprecated("run_cmd is deprecated, use run_shell_cmd from easybuild.tools.run instead", '6.0')
+
+ cwd = os.getcwd()
+
+ if isinstance(cmd, str):
+ cmd_msg = cmd.strip()
+ elif isinstance(cmd, list):
+ cmd_msg = ' '.join(cmd)
+ else:
+ raise EasyBuildError("Unknown command type ('%s'): %s", type(cmd), cmd)
+
+ if shell is None:
+ shell = True
+ if isinstance(cmd, list):
+ raise EasyBuildError("When passing cmd as a list then `shell` must be set explictely! "
+ "Note that all elements of the list but the first are treated as arguments "
+ "to the shell and NOT to the command to be executed!")
+
+ if log_output or (trace and build_option('trace')):
+ # collect output of running command in temporary log file, if desired
+ fd, cmd_log_fn = tempfile.mkstemp(suffix='.log', prefix='easybuild-run_cmd-')
+ os.close(fd)
+ try:
+ cmd_log = open(cmd_log_fn, 'w')
+ except IOError as err:
+ raise EasyBuildError("Failed to open temporary log file for output of command: %s", err)
+ _log.debug('run_cmd: Output of "%s" will be logged to %s' % (cmd, cmd_log_fn))
+ else:
+ cmd_log_fn, cmd_log = None, None
+
+ # auto-enable streaming of command output under --logtostdout/-l, unless it was disabled explicitely
+ if stream_output is None and build_option('logtostdout'):
+ _log.info("Auto-enabling streaming output of '%s' command because logging to stdout is enabled", cmd_msg)
+ stream_output = True
+
+ if stream_output:
+ print_msg("(streaming) output for command '%s':" % cmd_msg)
+
+ start_time = datetime.now()
+ if trace:
+ trace_txt = "running command:\n"
+ trace_txt += "\t[started at: %s]\n" % start_time.strftime('%Y-%m-%d %H:%M:%S')
+ trace_txt += "\t[working dir: %s]\n" % (path or os.getcwd())
+ if inp:
+ trace_txt += "\t[input: %s]\n" % inp
+ trace_txt += "\t[output logged in %s]\n" % cmd_log_fn
+ trace_msg(trace_txt + '\t' + cmd_msg)
+
+ # early exit in 'dry run' mode, after printing the command that would be run (unless running the command is forced)
+ if not force_in_dry_run and build_option('extended_dry_run'):
+ if path is None:
+ path = cwd
+ if verbose:
+ dry_run_msg(" running command \"%s\"" % cmd_msg, silent=build_option('silent'))
+ dry_run_msg(" (in %s)" % path, silent=build_option('silent'))
+
+ # make sure we get the type of the return value right
+ if simple:
+ return True
+ else:
+ # output, exit code
+ return ('', 0)
+
+ try:
+ if path:
+ os.chdir(path)
+
+ _log.debug("run_cmd: running cmd %s (in %s)" % (cmd, os.getcwd()))
+ except OSError as err:
+ _log.warning("Failed to change to %s: %s" % (path, err))
+ _log.info("running cmd %s in non-existing directory, might fail!", cmd)
+
+ if cmd_log:
+ cmd_log.write("# output for command: %s\n\n" % cmd_msg)
+
+ exec_cmd = "/bin/bash"
+
+ # if EasyBuild is configured to use an alternate sysroot,
+ # we should also run shell commands using the bash shell provided in there,
+ # since /bin/bash may not be compatible with the alternate sysroot
+ if with_sysroot:
+ sysroot = build_option('sysroot')
+ if sysroot:
+ sysroot_bin_bash = os.path.join(sysroot, 'bin', 'bash')
+ if os.path.exists(sysroot_bin_bash):
+ exec_cmd = sysroot_bin_bash
+
+ if not shell:
+ if isinstance(cmd, list):
+ exec_cmd = None
+ cmd.insert(0, '/usr/bin/env')
+ elif isinstance(cmd, str):
+ cmd = '/usr/bin/env %s' % cmd
+ else:
+ raise EasyBuildError("Don't know how to prefix with /usr/bin/env for commands of type %s", type(cmd))
+
+ _log.info("Using %s as shell for running cmd: %s", exec_cmd, cmd)
+
+ if with_hooks:
+ hooks = load_hooks(build_option('hooks'))
+ hook_res = run_hook(RUN_SHELL_CMD, hooks, pre_step_hook=True, args=[cmd], kwargs={'work_dir': os.getcwd()})
+ if isinstance(hook_res, str):
+ cmd, old_cmd = hook_res, cmd
+ _log.info("Command to run was changed by pre-%s hook: '%s' (was: '%s')", RUN_SHELL_CMD, cmd, old_cmd)
+
+ _log.info('running cmd: %s ' % cmd)
+ try:
+ proc = subprocess.Popen(cmd, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
+ stdin=subprocess.PIPE, close_fds=True, executable=exec_cmd)
+ except OSError as err:
+ raise EasyBuildError("run_cmd init cmd %s failed:%s", cmd, err)
+
+ if inp:
+ proc.stdin.write(inp.encode())
+ proc.stdin.close()
+
+ if asynchronous:
+ return (proc, cmd, cwd, start_time, cmd_log)
+ else:
+ return complete_cmd(proc, cmd, cwd, start_time, cmd_log, log_ok=log_ok, log_all=log_all, simple=simple,
+ regexp=regexp, stream_output=stream_output, trace=trace, with_hook=with_hooks,
+ print_deprecation_warning=False)
+
+
+def check_async_cmd(proc, cmd, owd, start_time, cmd_log, fail_on_error=True, output_read_size=1024, output=''):
+ """
+ Check status of command that was started asynchronously.
+
+ :param proc: subprocess.Popen instance representing asynchronous command
+ :param cmd: command being run
+ :param owd: original working directory
+ :param start_time: start time of command (datetime instance)
+ :param cmd_log: log file to print command output to
+ :param fail_on_error: raise EasyBuildError when command exited with an error
+ :param output_read_size: number of bytes to read from output
+ :param output: already collected output for this command
+
+ :result: dict value with result of the check (boolean 'done', 'exit_code', 'output')
+ """
+
+ _log.deprecated("check_async_cmd is deprecated, you should stop using it", '6.0')
+
+ # use small read size, to avoid waiting for a long time until sufficient output is produced
+ if output_read_size:
+ if not isinstance(output_read_size, int) or output_read_size < 0:
+ raise EasyBuildError("Number of output bytes to read should be a positive integer value (or zero)")
+ add_out = get_output_from_process(proc, read_size=output_read_size, print_deprecation_warning=False)
+ _log.debug("Additional output from asynchronous command '%s': %s" % (cmd, add_out))
+ output += add_out
+
+ exit_code = proc.poll()
+ if exit_code is None:
+ _log.debug("Asynchronous command '%s' still running..." % cmd)
+ done = False
+ else:
+ _log.debug("Asynchronous command '%s' completed!", cmd)
+ output, _ = complete_cmd(proc, cmd, owd, start_time, cmd_log, output=output,
+ simple=False, trace=False, log_ok=fail_on_error,
+ print_deprecation_warning=False)
+ done = True
+
+ res = {
+ 'done': done,
+ 'exit_code': exit_code,
+ 'output': output,
+ }
+ return res
+
+
+def complete_cmd(proc, cmd, owd, start_time, cmd_log, log_ok=True, log_all=False, simple=False,
+ regexp=True, stream_output=None, trace=True, output='', with_hook=True,
+ print_deprecation_warning=True):
+ """
+ Complete running of command represented by passed subprocess.Popen instance.
+
+ :param proc: subprocess.Popen instance representing running command
+ :param cmd: command being run
+ :param owd: original working directory
+ :param start_time: start time of command (datetime instance)
+ :param cmd_log: log file to print command output to
+ :param log_ok: only run output/exit code for failing commands (exit code non-zero)
+ :param log_all: always log command output and exit code
+ :param simple: if True, just return True/False to indicate success, else return a tuple: (output, exit_code)
+ :param regexp: regex used to check the output for errors; if True it will use the default (see parse_log_for_error)
+ :param stream_output: enable streaming command output to stdout
+ :param trace: print command being executed as part of trace output
+ :param with_hook: trigger post run_shell_cmd hooks (if defined)
+ """
+
+ if print_deprecation_warning:
+ _log.deprecated("complete_cmd is deprecated, you should stop using it", '6.0')
+
+ # use small read size when streaming output, to make it stream more fluently
+ # read size should not be too small though, to avoid too much overhead
+ if stream_output:
+ read_size = 128
+ else:
+ read_size = 1024 * 8
+
+ stdouterr = output
+
+ try:
+ ec = proc.poll()
+ while ec is None:
+ # need to read from time to time.
+ # - otherwise the stdout/stderr buffer gets filled and it all stops working
+ output = get_output_from_process(proc, read_size=read_size, print_deprecation_warning=False)
+ if cmd_log:
+ cmd_log.write(output)
+ if stream_output:
+ sys.stdout.write(output)
+ stdouterr += output
+ ec = proc.poll()
+
+ # read remaining data (all of it)
+ output = get_output_from_process(proc, print_deprecation_warning=False)
+ finally:
+ proc.stdout.close()
+
+ if cmd_log:
+ cmd_log.write(output)
+ cmd_log.close()
+ if stream_output:
+ sys.stdout.write(output)
+ stdouterr += output
+
+ if with_hook:
+ hooks = load_hooks(build_option('hooks'))
+ run_hook_kwargs = {
+ 'exit_code': ec,
+ 'output': stdouterr,
+ 'work_dir': os.getcwd(),
+ }
+ run_hook(RUN_SHELL_CMD, hooks, post_step_hook=True, args=[cmd], kwargs=run_hook_kwargs)
+
+ if trace:
+ trace_msg("command completed: exit %s, ran in %s" % (ec, time_str_since(start_time)))
+
+ try:
+ os.chdir(owd)
+ except OSError as err:
+ raise EasyBuildError("Failed to return to %s after executing command: %s", owd, err)
+
+ return parse_cmd_output(cmd, stdouterr, ec, simple, log_all, log_ok, regexp, print_deprecation_warning=False)
+
+
+def run_cmd_qa(cmd, qa, no_qa=None, log_ok=True, log_all=False, simple=False, regexp=True, std_qa=None, path=None,
+ maxhits=50, trace=True):
+ """
+ Run specified interactive command (in a subshell)
+ :param cmd: command to run
+ :param qa: dictionary which maps question to answers
+ :param no_qa: list of patters that are not questions
+ :param log_ok: only run output/exit code for failing commands (exit code non-zero)
+ :param log_all: always log command output and exit code
+ :param simple: if True, just return True/False to indicate success, else return a tuple: (output, exit_code)
+ :param regexp: regex used to check the output for errors; if True it will use the default (see parse_log_for_error)
+ :param std_qa: dictionary which maps question regex patterns to answers
+ :param path: path to execute the command is; current working directory is used if unspecified
+ :param maxhits: maximum number of cycles (seconds) without being able to find a known question
+ :param trace: print command being executed as part of trace output
+ """
+
+ _log.deprecated("run_cmd_qa is deprecated, use run_shell_cmd from easybuild.tools.run instead", '6.0')
+
+ cwd = os.getcwd()
+
+ if not isinstance(cmd, str) and len(cmd) > 1:
+ # We use shell=True and hence we should really pass the command as a string
+ # When using a list then every element past the first is passed to the shell itself, not the command!
+ raise EasyBuildError("The command passed must be a string!")
+
+ if log_all or (trace and build_option('trace')):
+ # collect output of running command in temporary log file, if desired
+ fd, cmd_log_fn = tempfile.mkstemp(suffix='.log', prefix='easybuild-run_cmd_qa-')
+ os.close(fd)
+ try:
+ cmd_log = open(cmd_log_fn, 'w')
+ except IOError as err:
+ raise EasyBuildError("Failed to open temporary log file for output of interactive command: %s", err)
+ _log.debug('run_cmd_qa: Output of "%s" will be logged to %s' % (cmd, cmd_log_fn))
+ else:
+ cmd_log_fn, cmd_log = None, None
+
+ start_time = datetime.now()
+ if trace:
+ trace_txt = "running interactive command:\n"
+ trace_txt += "\t[started at: %s]\n" % start_time.strftime('%Y-%m-%d %H:%M:%S')
+ trace_txt += "\t[working dir: %s]\n" % (path or os.getcwd())
+ trace_txt += "\t[output logged in %s]\n" % cmd_log_fn
+ trace_msg(trace_txt + '\t' + cmd.strip())
+
+ # early exit in 'dry run' mode, after printing the command that would be run
+ if build_option('extended_dry_run'):
+ if path is None:
+ path = cwd
+ dry_run_msg(" running interactive command \"%s\"" % cmd, silent=build_option('silent'))
+ dry_run_msg(" (in %s)" % path, silent=build_option('silent'))
+ if cmd_log:
+ cmd_log.close()
+ if simple:
+ return True
+ else:
+ # output, exit code
+ return ('', 0)
+
+ try:
+ if path:
+ os.chdir(path)
+
+ _log.debug("run_cmd_qa: running cmd %s (in %s)" % (cmd, os.getcwd()))
+ except OSError as err:
+ _log.warning("Failed to change to %s: %s" % (path, err))
+ _log.info("running cmd %s in non-existing directory, might fail!" % cmd)
+
+ # Part 1: process the QandA dictionary
+ # given initial set of Q and A (in dict), return dict of reg. exp. and A
+ #
+ # make regular expression that matches the string with
+ # - replace whitespace
+ # - replace newline
+
+ def escape_special(string):
+ return re.sub(r"([\+\?\(\)\[\]\*\.\\\$])", r"\\\1", string)
+
+ split = r'[\s\n]+'
+ regSplit = re.compile(r"" + split)
+
+ def process_QA(q, a_s):
+ splitq = [escape_special(x) for x in regSplit.split(q)]
+ regQtxt = split.join(splitq) + split.rstrip('+') + "*$"
+ # add optional split at the end
+ for i in [idx for idx, a in enumerate(a_s) if not a.endswith('\n')]:
+ a_s[i] += '\n'
+ regQ = re.compile(r"" + regQtxt)
+ if regQ.search(q):
+ return (a_s, regQ)
+ else:
+ raise EasyBuildError("runqanda: Question %s converted in %s does not match itself", q, regQtxt)
+
+ def check_answers_list(answers):
+ """Make sure we have a list of answers (as strings)."""
+ if isinstance(answers, str):
+ answers = [answers]
+ elif not isinstance(answers, list):
+ if cmd_log:
+ cmd_log.close()
+ raise EasyBuildError("Invalid type for answer on %s, no string or list: %s (%s)",
+ question, type(answers), answers)
+ # list is manipulated when answering matching question, so return a copy
+ return answers[:]
+
+ new_qa = {}
+ _log.debug("new_qa: ")
+ for question, answers in qa.items():
+ answers = check_answers_list(answers)
+ (answers, regQ) = process_QA(question, answers)
+ new_qa[regQ] = answers
+ _log.debug("new_qa[%s]: %s" % (regQ.pattern, new_qa[regQ]))
+
+ new_std_qa = {}
+ if std_qa:
+ for question, answers in std_qa.items():
+ regQ = re.compile(r"" + question + r"[\s\n]*$")
+ answers = check_answers_list(answers)
+ for i in [idx for idx, a in enumerate(answers) if not a.endswith('\n')]:
+ answers[i] += '\n'
+ new_std_qa[regQ] = answers
+ _log.debug("new_std_qa[%s]: %s" % (regQ.pattern, new_std_qa[regQ]))
+
+ new_no_qa = []
+ if no_qa:
+ # simple statements, can contain wildcards
+ new_no_qa = [re.compile(r"" + x + r"[\s\n]*$") for x in no_qa]
+
+ _log.debug("New noQandA list is: %s" % [x.pattern for x in new_no_qa])
+
+ # Part 2: Run the command and answer questions
+ # - this needs asynchronous stdout
+
+ hooks = load_hooks(build_option('hooks'))
+ run_hook_kwargs = {
+ 'interactive': True,
+ 'work_dir': os.getcwd(),
+ }
+ hook_res = run_hook(RUN_SHELL_CMD, hooks, pre_step_hook=True, args=[cmd], kwargs=run_hook_kwargs)
+ if isinstance(hook_res, str):
+ cmd, old_cmd = hook_res, cmd
+ _log.info("Interactive command to run was changed by pre-%s hook: '%s' (was: '%s')",
+ RUN_SHELL_CMD, cmd, old_cmd)
+
+ # # Log command output
+ if cmd_log:
+ cmd_log.write("# output for interactive command: %s\n\n" % cmd)
+
+ # Make sure we close the proc handles and the cmd_log file
+ @contextlib.contextmanager
+ def get_proc():
+ try:
+ proc = asyncprocess.Popen(cmd, shell=True, stdout=asyncprocess.PIPE, stderr=asyncprocess.STDOUT,
+ stdin=asyncprocess.PIPE, close_fds=True, executable='/bin/bash')
+ except OSError as err:
+ if cmd_log:
+ cmd_log.close()
+ raise EasyBuildError("run_cmd_qa init cmd %s failed:%s", cmd, err)
+ try:
+ yield proc
+ finally:
+ if proc.stdout:
+ proc.stdout.close()
+ if proc.stdin:
+ proc.stdin.close()
+ if cmd_log:
+ cmd_log.close()
+
+ with get_proc() as proc:
+ ec = proc.poll()
+ stdout_err = ''
+ old_len_out = -1
+ hit_count = 0
+
+ while ec is None:
+ # need to read from time to time.
+ # - otherwise the stdout/stderr buffer gets filled and it all stops working
+ try:
+ out = get_output_from_process(proc, asynchronous=True, print_deprecation_warning=False)
+
+ if cmd_log:
+ cmd_log.write(out)
+ stdout_err += out
+ # recv_some used by get_output_from_process for getting asynchronous output may throw exception
+ except (IOError, Exception) as err:
+ _log.debug("run_cmd_qa cmd %s: read failed: %s", cmd, err)
+ out = None
+
+ hit = False
+ for question, answers in new_qa.items():
+ res = question.search(stdout_err)
+ if out and res:
+ fa = answers[0] % res.groupdict()
+ # cycle through list of answers
+ last_answer = answers.pop(0)
+ answers.append(last_answer)
+ _log.debug("List of answers for question %s after cycling: %s", question.pattern, answers)
+
+ _log.debug("run_cmd_qa answer %s question %s out %s", fa, question.pattern, stdout_err[-50:])
+ asyncprocess.send_all(proc, fa)
+ hit = True
+ break
+ if not hit:
+ for question, answers in new_std_qa.items():
+ res = question.search(stdout_err)
+ if out and res:
+ fa = answers[0] % res.groupdict()
+ # cycle through list of answers
+ last_answer = answers.pop(0)
+ answers.append(last_answer)
+ _log.debug("List of answers for question %s after cycling: %s", question.pattern, answers)
+
+ _log.debug("run_cmd_qa answer %s std question %s out %s",
+ fa, question.pattern, stdout_err[-50:])
+ asyncprocess.send_all(proc, fa)
+ hit = True
+ break
+ if not hit:
+ if len(stdout_err) > old_len_out:
+ old_len_out = len(stdout_err)
+ else:
+ noqa = False
+ for r in new_no_qa:
+ if r.search(stdout_err):
+ _log.debug("runqanda: noQandA found for out %s", stdout_err[-50:])
+ noqa = True
+ if not noqa:
+ hit_count += 1
+ else:
+ hit_count = 0
+ else:
+ hit_count = 0
+
+ if hit_count > maxhits:
+ # explicitly kill the child process before exiting
+ try:
+ os.killpg(proc.pid, signal.SIGKILL)
+ os.kill(proc.pid, signal.SIGKILL)
+ except OSError as err:
+ _log.debug("run_cmd_qa exception caught when killing child process: %s", err)
+ _log.debug("run_cmd_qa: full stdouterr: %s", stdout_err)
+ raise EasyBuildError("run_cmd_qa: cmd %s : Max nohits %s reached: end of output %s",
+ cmd, maxhits, stdout_err[-500:])
+
+ # the sleep below is required to avoid exiting on unknown 'questions' too early (see above)
+ time.sleep(1)
+ ec = proc.poll()
+
+ # Process stopped. Read all remaining data
+ try:
+ if proc.stdout:
+ out = get_output_from_process(proc, print_deprecation_warning=False)
+ stdout_err += out
+ if cmd_log:
+ cmd_log.write(out)
+ except IOError as err:
+ _log.debug("runqanda cmd %s: remaining data read failed: %s", cmd, err)
+
+ run_hook_kwargs.update({
+ 'interactive': True,
+ 'exit_code': ec,
+ 'output': stdout_err,
+ })
+ run_hook(RUN_SHELL_CMD, hooks, post_step_hook=True, args=[cmd], kwargs=run_hook_kwargs)
+
+ if trace:
+ trace_msg("interactive command completed: exit %s, ran in %s" % (ec, time_str_since(start_time)))
+
+ try:
+ os.chdir(cwd)
+ except OSError as err:
+ raise EasyBuildError("Failed to return to %s after executing command: %s", cwd, err)
+
+ return parse_cmd_output(cmd, stdout_err, ec, simple, log_all, log_ok, regexp, print_deprecation_warning=False)
+
+
+def parse_cmd_output(cmd, stdouterr, ec, simple, log_all, log_ok, regexp, print_deprecation_warning=True):
+ """
+ Parse command output and construct return value.
+ :param cmd: executed command
+ :param stdouterr: combined stdout/stderr of executed command
+ :param ec: exit code of executed command
+ :param simple: if True, just return True/False to indicate success, else return a tuple: (output, exit_code)
+ :param log_all: always log command output and exit code
+ :param log_ok: only run output/exit code for failing commands (exit code non-zero)
+ :param regexp: regex used to check the output for errors; if True it will use the default (see parse_log_for_error)
+ """
+
+ if print_deprecation_warning:
+ _log.deprecated("parse_cmd_output is deprecated, you should stop using it", '6.0')
+
+ if strictness == IGNORE:
+ check_ec = False
+ fail_on_error_match = False
+ elif strictness == WARN:
+ check_ec = True
+ fail_on_error_match = False
+ elif strictness == ERROR:
+ check_ec = True
+ fail_on_error_match = True
+ else:
+ raise EasyBuildError("invalid strictness setting: %s", strictness)
+
+ # allow for overriding the regexp setting
+ if not regexp:
+ fail_on_error_match = False
+
+ if ec and (log_all or log_ok):
+ # We don't want to error if the user doesn't care
+ if check_ec:
+ raise EasyBuildError('cmd "%s" exited with exit code %s and output:\n%s', cmd, ec, stdouterr)
+ else:
+ _log.warning('cmd "%s" exited with exit code %s and output:\n%s' % (cmd, ec, stdouterr))
+ elif not ec:
+ if log_all:
+ _log.info('cmd "%s" exited with exit code %s and output:\n%s' % (cmd, ec, stdouterr))
+ else:
+ _log.debug('cmd "%s" exited with exit code %s and output:\n%s' % (cmd, ec, stdouterr))
+
+ # parse the stdout/stderr for errors when strictness dictates this or when regexp is passed in
+ if fail_on_error_match or regexp:
+ res = parse_log_for_error(stdouterr, regexp, stdout=False, print_deprecation_warning=False)
+ if res:
+ errors = "\n\t" + "\n\t".join([r[0] for r in res])
+ error_str = "error" if len(res) == 1 else "errors"
+ if fail_on_error_match:
+ raise EasyBuildError("Found %s %s in output of %s:%s", len(res), error_str, cmd, errors)
+ else:
+ _log.warning("Found %s potential %s (some may be harmless) in output of %s:%s",
+ len(res), error_str, cmd, errors)
+
+ if simple:
+ if ec:
+ # If the user does not care -> will return true
+ return not check_ec
+ else:
+ return True
+ else:
+ # Because we are not running in simple mode, we return the output and ec to the user
+ return (stdouterr, ec)
+
+
+def parse_log_for_error(txt, regExp=None, stdout=True, msg=None, print_deprecation_warning=True):
+ """
+ txt is multiline string.
+ - in memory
+ regExp is a one-line regular expression
+ - default
+ """
+
+ if print_deprecation_warning:
+ _log.deprecated("parse_log_for_error is deprecated, you should stop using it", '6.0')
+
+ global errors_found_in_log
+
+ if regExp and isinstance(regExp, bool):
+ regExp = r"(?= loose_mv.version[:depth]:
self.raiseException("DEPRECATED (since v%s) functionality used: %s" % (max_ver, msg), exception=exception)
else:
- deprecation_msg = "Deprecated functionality, will no longer work in v%s: %s" % (max_ver, msg)
+ deprecation_msg = "Deprecated functionality, will no longer work in EasyBuild v%s: %s" % (max_ver, msg)
log_callback(deprecation_msg)
def _handleFunction(self, function, levelno, **kwargs):
@@ -588,7 +587,7 @@ def logToFile(filename, enable=True, filehandler=None, name=None, max_bytes=MAX_
os.makedirs(directory)
except Exception as ex:
exc, detail, tb = sys.exc_info()
- raise_with_traceback(exc, "Cannot create logdirectory %s: %s \n detail: %s" % (directory, ex, detail), tb)
+ raise exc("Cannot create logdirectory %s: %s \n detail: %s" % (directory, ex, detail)).with_traceback(tb)
return _logToSomething(
logging.handlers.RotatingFileHandler,
@@ -741,7 +740,7 @@ def setLogLevel(level):
"""
Set a global log level for all FancyLoggers
"""
- if isinstance(level, string_type):
+ if isinstance(level, str):
level = getLevelInt(level)
logger = getLogger(fname=False, clsname=False)
logger.setLevel(level)
diff --git a/easybuild/base/frozendict.py b/easybuild/base/frozendict.py
index 6bfe91a82b..5d0205687e 100644
--- a/easybuild/base/frozendict.py
+++ b/easybuild/base/frozendict.py
@@ -21,10 +21,10 @@
It can be used as a drop-in replacement for dictionaries where immutability is desired.
"""
import operator
+from collections.abc import Mapping
from functools import reduce
from easybuild.base import fancylogger
-from easybuild.tools.py2vs3 import Mapping
# minor adjustments:
diff --git a/easybuild/base/generaloption.py b/easybuild/base/generaloption.py
index c3660f7cbf..820033a420 100644
--- a/easybuild/base/generaloption.py
+++ b/easybuild/base/generaloption.py
@@ -1,5 +1,5 @@
#
-# Copyright 2011-2024 Ghent University
+# Copyright 2011-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -31,6 +31,7 @@
* Jens Timmerman (Ghent University)
"""
+import configparser
import copy
import difflib
import inspect
@@ -39,13 +40,15 @@
import re
import sys
import textwrap
+from configparser import ConfigParser
from functools import reduce
+from io import StringIO
from optparse import Option, OptionGroup, OptionParser, OptionValueError, Values
from optparse import SUPPRESS_HELP as nohelp # supported in optparse of python v2.4
from easybuild.base.fancylogger import getLogger, setroot, setLogLevel, getDetailsLogLevels
from easybuild.base.optcomplete import autocomplete, CompleterOption
-from easybuild.tools.py2vs3 import StringIO, configparser, ConfigParser, string_type, subprocess_popen_text
+from easybuild.tools.run import subprocess_popen_text
from easybuild.tools.utilities import mk_md_table, mk_rst_table, nub, shell_quote
try:
@@ -138,7 +141,7 @@ def get_empty_add_flex(allvalues, self=None):
empty = None
if isinstance(allvalues, (list, tuple)):
- if isinstance(allvalues[0], string_type):
+ if isinstance(allvalues[0], str):
empty = ''
if empty is None:
@@ -471,7 +474,7 @@ def is_value_a_commandline_option(self, opt, value, index=None):
# --longopt=value, so no issues there either.
# following checks assume that value is a string (not a store_or_None)
- if not isinstance(value, string_type):
+ if not isinstance(value, str):
return None
cmdline_index = None
@@ -1198,7 +1201,7 @@ def add_group_parser(self, opt_dict, description, prefix=None, otherdefaults=Non
# choices
nameds['choices'] = [str(x) for x in extra_detail] # force to strings
hlp += ' (choices: %s)' % ', '.join(nameds['choices'])
- elif isinstance(extra_detail, string_type) and len(extra_detail) == 1:
+ elif isinstance(extra_detail, str) and len(extra_detail) == 1:
args.insert(0, "-%s" % extra_detail)
elif isinstance(extra_detail, (dict,)):
# extract any optcomplete completer hints
@@ -1715,7 +1718,7 @@ class SimpleOption(GeneralOption):
PARSER = SimpleOptionParser
SETROOTLOGGER = True
- def __init__(self, go_dict=None, descr=None, short_groupdescr=None, long_groupdescr=None, config_files=None):
+ def __init__(self, go_dict=None, short_groupdescr=None, long_groupdescr=None, config_files=None):
"""Initialisation
:param go_dict: General Option option dict
:param short_groupdescr: short description of main options
@@ -1744,18 +1747,13 @@ def __init__(self, go_dict=None, descr=None, short_groupdescr=None, long_groupde
super(SimpleOption, self).__init__(**kwargs)
- if descr is not None:
- # TODO: as there is no easy/clean way to access the version of the vsc-base package,
- # this is equivalent to a warning
- self.log.deprecated('SimpleOption descr argument', '2.5.0', '3.0.0')
-
def main_options(self):
if self.go_dict is not None:
prefix = None
self.add_group_parser(self.go_dict, self.descr, prefix=prefix)
-def simple_option(go_dict=None, descr=None, short_groupdescr=None, long_groupdescr=None, config_files=None):
+def simple_option(go_dict=None, short_groupdescr=None, long_groupdescr=None, config_files=None):
"""A function that returns a single level GeneralOption option parser
:param go_dict: General Option option dict
@@ -1769,5 +1767,5 @@ def simple_option(go_dict=None, descr=None, short_groupdescr=None, long_groupdes
the generated help will include the docstring
"""
- return SimpleOption(go_dict=go_dict, descr=descr, short_groupdescr=short_groupdescr,
- long_groupdescr=long_groupdescr, config_files=config_files)
+ return SimpleOption(go_dict=go_dict, short_groupdescr=short_groupdescr, long_groupdescr=long_groupdescr,
+ config_files=config_files)
diff --git a/easybuild/base/optcomplete.py b/easybuild/base/optcomplete.py
index 3fa75a6635..ba0f075cf2 100644
--- a/easybuild/base/optcomplete.py
+++ b/easybuild/base/optcomplete.py
@@ -107,7 +107,7 @@
from optparse import OptionParser, Option
from pprint import pformat
-from easybuild.tools.py2vs3 import string_type
+from easybuild.tools.filetools import get_cwd
from easybuild.tools.utilities import shell_quote
debugfn = None # for debugging only
@@ -211,7 +211,7 @@ class FileCompleter(Completer):
CALL_ARGS_OPTIONAL = ['prefix']
def __init__(self, endings=None):
- if isinstance(endings, string_type):
+ if isinstance(endings, str):
endings = [endings]
elif endings is None:
endings = []
@@ -282,11 +282,11 @@ class RegexCompleter(Completer):
def __init__(self, regexlist, always_dirs=True):
self.always_dirs = always_dirs
- if isinstance(regexlist, string_type):
+ if isinstance(regexlist, str):
regexlist = [regexlist]
self.regexlist = []
for regex in regexlist:
- if isinstance(regex, string_type):
+ if isinstance(regex, str):
regex = re.compile(regex)
self.regexlist.append(regex)
@@ -538,7 +538,7 @@ def autocomplete(parser, arg_completer=None, opt_completer=None, subcmd_complete
# Note: this will get filtered properly below.
completer_kwargs = {
- 'pwd': os.getcwd(),
+ 'pwd': get_cwd(),
'cline': cline,
'cpoint': cpoint,
'prefix': prefix,
@@ -547,7 +547,7 @@ def autocomplete(parser, arg_completer=None, opt_completer=None, subcmd_complete
# File completion.
if completer and (not prefix or not prefix.startswith('-')):
# Call appropriate completer depending on type.
- if isinstance(completer, (string_type, list, tuple)):
+ if isinstance(completer, (str, list, tuple)):
completer = FileCompleter(completer)
elif not isinstance(completer, (types.FunctionType, types.LambdaType, types.ClassType, types.ObjectType)):
# TODO: what to do here?
@@ -555,7 +555,7 @@ def autocomplete(parser, arg_completer=None, opt_completer=None, subcmd_complete
completions = completer(**completer_kwargs)
- if isinstance(completions, string_type):
+ if isinstance(completions, str):
# is a bash command, just run it
if SHELL in (BASH,): # TODO: zsh
print(completions)
diff --git a/easybuild/base/rest.py b/easybuild/base/rest.py
index ed4c187436..4d97eb6183 100644
--- a/easybuild/base/rest.py
+++ b/easybuild/base/rest.py
@@ -40,9 +40,10 @@
import copy
import json
from functools import partial
+from urllib.parse import urlencode
+from urllib.request import HTTPSHandler, Request, build_opener
from easybuild.base import fancylogger
-from easybuild.tools.py2vs3 import HTTPSHandler, Request, build_opener, json_loads, string_type, urlencode
class Client(object):
@@ -180,7 +181,7 @@ def request(self, method, url, body, headers, content_type=None):
else:
body = conn.read()
try:
- pybody = json_loads(body)
+ pybody = json.loads(body)
except ValueError:
pybody = body
fancylogger.getLogger().debug('reponse len: %s ', len(pybody))
@@ -203,10 +204,8 @@ def get_connection(self, method, url, body, headers):
else:
sep = ''
- # value passed to 'data' must be a 'bytes' value (not 'str') in Python 3.x, but a string value in Python 2
- # hence, we encode the value obtained (if needed)
- # this doesn't affect the value type in Python 2, and makes it a 'bytes' value in Python 3
- if isinstance(body, string_type):
+ # value passed to 'data' must be a 'bytes' value (not 'str') hence, we encode the value obtained (if needed)
+ if isinstance(body, str):
body = body.encode('utf-8')
request = Request(self.url + sep + url, data=body)
diff --git a/easybuild/base/testing.py b/easybuild/base/testing.py
index 4b3df7e563..8e6f635b12 100644
--- a/easybuild/base/testing.py
+++ b/easybuild/base/testing.py
@@ -1,5 +1,5 @@
#
-# Copyright 2014-2024 Ghent University
+# Copyright 2014-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -39,16 +39,10 @@
import re
import sys
from contextlib import contextmanager
-
-try:
- from cStringIO import StringIO # Python 2
-except ImportError:
- from io import StringIO # Python 3
+from io import StringIO
from unittest import TestCase as OrigTestCase
-from easybuild.tools.py2vs3 import string_type
-
def nicediff(txta, txtb, offset=5):
"""
@@ -61,15 +55,12 @@ def nicediff(txta, txtb, offset=5):
"""
diff = list(difflib.ndiff(txta.splitlines(1), txtb.splitlines(1)))
different_idx = [idx for idx, line in enumerate(diff) if not line.startswith(' ')]
- res_idx = []
+ res_idx = set()
# very bruteforce
for didx in different_idx:
- for idx in range(max(didx - offset, 0), min(didx + offset, len(diff) - 1)):
- if idx not in res_idx:
- res_idx.append(idx)
- res_idx.sort()
+ res_idx.update(range(max(didx - offset, 0), min(didx + offset, len(diff))))
# insert linenumbers too? what are the linenumbers in ndiff?
- newdiff = [diff[idx] for idx in res_idx]
+ newdiff = [diff[idx] for idx in sorted(res_idx)]
return newdiff
@@ -82,30 +73,34 @@ class TestCase(OrigTestCase):
ASSERT_MAX_DIFF = 100
DIFF_OFFSET = 5 # lines of text around changes
- def is_string(self, x):
- """test if the variable x is a string)"""
- try:
- return isinstance(x, string_type)
- except NameError:
- return isinstance(x, str)
+ def _is_diffable(self, x):
+ """Test if it makes sense to show a diff for x"""
+ if isinstance(x, (int, float, bool, type(None))):
+ return False
+ if isinstance(x, str) and '\n' not in x:
+ return False
+ return True
- # pylint: disable=arguments-differ
+ # pylint: disable=arguments-differ,arguments-renamed
def assertEqual(self, a, b, msg=None):
"""Make assertEqual always print useful messages"""
try:
super(TestCase, self).assertEqual(a, b)
except AssertionError as e:
+ if not self._is_diffable(a) or not self._is_diffable(b):
+ raise
+
if msg is None:
msg = str(e)
else:
- msg = "%s: %s" % (msg, e)
+ msg = "%s: %s" % (msg, str(e))
- if self.is_string(a):
+ if isinstance(a, str):
txta = a
else:
txta = pprint.pformat(a)
- if self.is_string(b):
+ if isinstance(b, str):
txtb = b
else:
txtb = pprint.pformat(b)
@@ -116,7 +111,7 @@ def assertEqual(self, a, b, msg=None):
else:
limit = ''
- raise AssertionError("%s:\nDIFF%s:\n%s" % (msg, limit, ''.join(diff[:self.ASSERT_MAX_DIFF])))
+ raise AssertionError("%s:\nDIFF%s:\n%s" % (msg, limit, ''.join(diff[:self.ASSERT_MAX_DIFF]))) from None
def assertExists(self, path, msg=None):
"""Assert the given path exists"""
@@ -130,6 +125,11 @@ def assertNotExists(self, path, msg=None):
msg = "'%s' should not exist" % path
self.assertFalse(os.path.exists(path), msg)
+ def assertAllExist(self, paths, msg=None):
+ """Assert that all paths in the given list exist"""
+ for path in paths:
+ self.assertExists(path, msg)
+
def setUp(self):
"""Prepare test case."""
super(TestCase, self).setUp()
@@ -173,7 +173,7 @@ def assertErrorRegex(self, error, regex, call, *args, **kwargs):
self.fail("Expected errors with %s(%s) call should occur" % (call.__name__, str_args))
except error as err:
msg = self.convert_exception_to_str(err)
- if self.is_string(regex):
+ if isinstance(regex, str):
regex = re.compile(regex)
self.assertTrue(regex.search(msg), "Pattern '%s' is found in '%s'" % (regex.pattern, msg))
diff --git a/easybuild/base/wrapper.py b/easybuild/base/wrapper.py
index e73c8d22c5..bc44d9c494 100644
--- a/easybuild/base/wrapper.py
+++ b/easybuild/base/wrapper.py
@@ -6,7 +6,24 @@
Original code by http://stackoverflow.com/users/416467/kindall from answer 4 of
http://stackoverflow.com/questions/9057669/how-can-i-intercept-calls-to-pythons-magic-methods-in-new-style-classes
"""
-from easybuild.tools.py2vs3 import mk_wrapper_baseclass
+
+
+# based on six's 'with_metaclass' function
+# see also https://stackoverflow.com/questions/18513821/python-metaclass-understanding-the-with-metaclass
+def create_base_metaclass(base_class_name, metaclass, *bases):
+ """Create new class with specified metaclass based on specified base class(es)."""
+ return metaclass(base_class_name, bases, {})
+
+
+def mk_wrapper_baseclass(metaclass):
+
+ class WrapperBase(object, metaclass=metaclass):
+ """
+ Wrapper class that provides proxy access to an instance of some internal instance.
+ """
+ __wraps__ = None
+
+ return WrapperBase
class WrapperMeta(type):
diff --git a/easybuild/framework/__init__.py b/easybuild/framework/__init__.py
index f03298abca..56ffd37de2 100644
--- a/easybuild/framework/__init__.py
+++ b/easybuild/framework/__init__.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py
index 0b76f5bff2..4d9b4fbdd7 100644
--- a/easybuild/framework/easyblock.py
+++ b/easybuild/framework/easyblock.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -40,19 +40,25 @@
* Maxime Boissonneault (Compute Canada)
* Davide Vanzo (Vanderbilt University)
* Caspar van Leeuwen (SURF)
+* Jan Andre Reuter (Juelich Supercomputing Centre)
"""
-
+import concurrent
import copy
import glob
import inspect
import json
import os
+import random
import re
import stat
+import sys
import tempfile
import time
import traceback
+from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
+from string import ascii_letters
+from textwrap import indent
import easybuild.tools.environment as env
import easybuild.tools.toolchain as toolchain
@@ -66,39 +72,43 @@
from easybuild.framework.easyconfig.tools import dump_env_easyblock, get_paths_for
from easybuild.framework.easyconfig.templates import TEMPLATE_NAMES_EASYBLOCK_RUN_STEP, template_constant_dict
from easybuild.framework.extension import Extension, resolve_exts_filter_template
-from easybuild.tools import LooseVersion, config, run
+from easybuild.tools import LooseVersion, config
from easybuild.tools.build_details import get_build_stats
-from easybuild.tools.build_log import EasyBuildError, dry_run_msg, dry_run_warning, dry_run_set_dirs
+from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, dry_run_msg, dry_run_warning, dry_run_set_dirs
from easybuild.tools.build_log import print_error, print_msg, print_warning
from easybuild.tools.config import CHECKSUM_PRIORITY_JSON, DEFAULT_ENVVAR_USERS_MODULES
+from easybuild.tools.config import EASYBUILD_SOURCES_URL, EBPYTHONPREFIXES # noqa
from easybuild.tools.config import FORCE_DOWNLOAD_ALL, FORCE_DOWNLOAD_PATCHES, FORCE_DOWNLOAD_SOURCES
-from easybuild.tools.config import EASYBUILD_SOURCES_URL # noqa
-from easybuild.tools.config import build_option, build_path, get_log_filename, get_repository, get_repositorypath
+from easybuild.tools.config import MOD_SEARCH_PATH_HEADERS, PYTHONPATH, SEARCH_PATH_BIN_DIRS, SEARCH_PATH_LIB_DIRS
+from easybuild.tools.config import build_option, build_path, get_failed_install_build_dirs_path
+from easybuild.tools.config import get_failed_install_logs_path, get_log_filename, get_repository, get_repositorypath
from easybuild.tools.config import install_path, log_path, package_path, source_paths
from easybuild.tools.environment import restore_env, sanitize_env
-from easybuild.tools.filetools import CHECKSUM_TYPE_MD5, CHECKSUM_TYPE_SHA256
+from easybuild.tools.filetools import CHECKSUM_TYPE_SHA256
from easybuild.tools.filetools import adjust_permissions, apply_patch, back_up_file, change_dir, check_lock
-from easybuild.tools.filetools import compute_checksum, convert_name, copy_file, create_lock, create_patch_info
-from easybuild.tools.filetools import derive_alt_pypi_url, diff_files, dir_contains_files, download_file
-from easybuild.tools.filetools import encode_class_name, extract_file
-from easybuild.tools.filetools import find_backup_name_candidate, get_source_tarball_from_git, is_alt_pypi_url
-from easybuild.tools.filetools import is_binary, is_sha256_checksum, mkdir, move_file, move_logs, read_file, remove_dir
-from easybuild.tools.filetools import remove_file, remove_lock, verify_checksum, weld_paths, write_file, symlink
-from easybuild.tools.hooks import BUILD_STEP, CLEANUP_STEP, CONFIGURE_STEP, EXTENSIONS_STEP, FETCH_STEP, INSTALL_STEP
-from easybuild.tools.hooks import MODULE_STEP, MODULE_WRITE, PACKAGE_STEP, PATCH_STEP, PERMISSIONS_STEP, POSTITER_STEP
-from easybuild.tools.hooks import POSTPROC_STEP, PREPARE_STEP, READY_STEP, SANITYCHECK_STEP, SOURCE_STEP
-from easybuild.tools.hooks import SINGLE_EXTENSION, TEST_STEP, TESTCASES_STEP, load_hooks, run_hook
-from easybuild.tools.run import check_async_cmd, run_cmd
+from easybuild.tools.filetools import compute_checksum, convert_name, copy_dir, copy_file, create_lock
+from easybuild.tools.filetools import create_non_existing_paths, create_patch_info, derive_alt_pypi_url, diff_files
+from easybuild.tools.filetools import dir_contains_files, download_file, encode_class_name, extract_file
+from easybuild.tools.filetools import find_backup_name_candidate, get_cwd, get_source_tarball_from_git, is_alt_pypi_url
+from easybuild.tools.filetools import is_binary, is_parent_path, is_sha256_checksum, mkdir, move_file, move_logs
+from easybuild.tools.filetools import read_file, remove_dir, remove_file, remove_lock, symlink, verify_checksum
+from easybuild.tools.filetools import weld_paths, write_file
+from easybuild.tools.hooks import (
+ BUILD_STEP, CLEANUP_STEP, CONFIGURE_STEP, EXTENSIONS_STEP, EXTRACT_STEP, FETCH_STEP, INSTALL_STEP, MODULE_STEP,
+ MODULE_WRITE, PACKAGE_STEP, PATCH_STEP, PERMISSIONS_STEP, POSTITER_STEP, POSTPROC_STEP, PREPARE_STEP, READY_STEP,
+ SANITYCHECK_STEP, SINGLE_EXTENSION, TEST_STEP, TESTCASES_STEP, load_hooks, run_hook,
+)
+from easybuild.tools.run import RunShellCmdError, raise_run_shell_cmd_error, run_shell_cmd
from easybuild.tools.jenkins import write_to_xml
from easybuild.tools.module_generator import ModuleGeneratorLua, ModuleGeneratorTcl, module_generator, dependencies_for
from easybuild.tools.module_naming_scheme.utilities import det_full_ec_version
from easybuild.tools.modules import ROOT_ENV_VAR_NAME_PREFIX, VERSION_ENV_VAR_NAME_PREFIX, DEVEL_ENV_VAR_NAME_PREFIX
-from easybuild.tools.modules import Lmod, curr_module_paths, invalidate_module_caches_for, get_software_root
+from easybuild.tools.modules import Lmod, ModEnvVarType, ModuleLoadEnvironment, MODULE_LOAD_ENV_HEADERS
+from easybuild.tools.modules import curr_module_paths, invalidate_module_caches_for, get_software_root
from easybuild.tools.modules import get_software_root_env_var_name, get_software_version_env_var_name
from easybuild.tools.output import PROGRESS_BAR_DOWNLOAD_ALL, PROGRESS_BAR_EASYCONFIG, PROGRESS_BAR_EXTENSIONS
from easybuild.tools.output import show_progress_bars, start_progress_bar, stop_progress_bar, update_progress_bar
from easybuild.tools.package.utilities import package
-from easybuild.tools.py2vs3 import extract_method_name, string_type
from easybuild.tools.repository.repository import init_repository
from easybuild.tools.systemtools import check_linked_shared_libs, det_parallelism, get_linked_libs_raw
from easybuild.tools.systemtools import get_shared_lib_ext, pick_system_specific_value, use_group
@@ -106,7 +116,7 @@
from easybuild.tools.utilities import remove_unwanted_chars, time2str, trace_msg
from easybuild.tools.version import this_is_easybuild, VERBOSE_VERSION, VERSION
-DEFAULT_BIN_LIB_SUBDIRS = ('bin', 'lib', 'lib64')
+DEFAULT_BIN_LIB_SUBDIRS = SEARCH_PATH_BIN_DIRS + SEARCH_PATH_LIB_DIRS
MODULE_ONLY_STEPS = [MODULE_STEP, PREPARE_STEP, READY_STEP, POSTITER_STEP, SANITYCHECK_STEP]
@@ -141,14 +151,15 @@ def extra_options(extra=None):
#
# INIT
#
- def __init__(self, ec):
+ def __init__(self, ec, logfile=None):
"""
Initialize the EasyBlock instance.
:param ec: a parsed easyconfig file (EasyConfig instance)
+ :param logfile: pass logfile from other EasyBlock. If not passed, create logfile (optional)
"""
# keep track of original working directory, so we can go back there
- self.orig_workdir = os.getcwd()
+ self.orig_workdir = get_cwd()
# dict of all hooks (mapping of name to function)
self.hooks = load_hooks(build_option('hooks'))
@@ -200,6 +211,21 @@ def __init__(self, ec):
if modules_header_path is not None:
self.modules_header = read_file(modules_header_path)
+ # environment variables on module load
+ mod_load_aliases = {}
+ # apply --module-search-path-headers: easyconfig parameter has precedence
+ mod_load_cpp_headers = self.cfg['module_search_path_headers'] or build_option('module_search_path_headers')
+
+ try:
+ mod_load_aliases[MODULE_LOAD_ENV_HEADERS] = MOD_SEARCH_PATH_HEADERS[mod_load_cpp_headers]
+ except KeyError as err:
+ raise EasyBuildError(
+ f"Unknown value selected for option module-search-path-headers: {mod_load_cpp_headers}. "
+ f"Choose one of: {', '.join(MOD_SEARCH_PATH_HEADERS)}"
+ ) from err
+
+ self.module_load_environment = ModuleLoadEnvironment(aliases=mod_load_aliases)
+
# determine install subdirectory, based on module name
self.install_subdir = None
@@ -214,7 +240,8 @@ def __init__(self, ec):
# logging
self.log = None
- self.logfile = None
+ self.logfile = logfile
+ self.external_logfile = logfile is not None
self.logdebug = build_option('debug')
self.postmsg = '' # allow a post message to be set, which can be shown as last output
self.current_step = None
@@ -294,6 +321,7 @@ def post_init(self):
# but needs to be correct if the build is performed in the installation directory
self.log.info("Changing build dir to %s", self.installdir)
self.builddir = self.installdir
+ self.set_parallel()
# INIT/CLOSE LOG
def _init_log(self):
@@ -303,11 +331,11 @@ def _init_log(self):
if self.log is not None:
return
- self.logfile = get_log_filename(self.name, self.version, add_salt=True)
- fancylogger.logToFile(self.logfile, max_bytes=0)
+ if self.logfile is None:
+ self.logfile = get_log_filename(self.name, self.version, add_salt=True)
+ fancylogger.logToFile(self.logfile, max_bytes=0)
self.log = fancylogger.getLogger(name=self.__class__.__name__, fname=False)
-
self.log.info(this_is_easybuild())
this_module = inspect.getmodule(self)
@@ -323,6 +351,9 @@ def close_log(self):
"""
Shutdown the logger.
"""
+ # only close log if we created a logfile
+ if self.external_logfile:
+ return
self.log.info("Closing log for application name %s version %s" % (self.name, self.version))
fancylogger.logToFile(self.logfile, enable=False)
@@ -357,34 +388,57 @@ def get_checksum_for(self, checksums, filename=None, index=None):
:param filename: name of the file to obtain checksum for
:param index: index of file in list
"""
- checksum = None
-
- # sometimes, filename are specified as a dict
+ chksum_input = filename
+ chksum_input_git = None
+ # if filename is provided as dict, take 'filename' key
if isinstance(filename, dict):
- filename = filename['filename']
+ chksum_input = filename.get('filename', None)
+ chksum_input_git = filename.get('git_config', None)
+ # early return if no filename given
+ if chksum_input is None:
+ self.log.debug("Cannot get checksum without a file name")
+ return None
+ checksum = None
# if checksums are provided as a dict, lookup by source filename as key
if isinstance(checksums, dict):
- if filename is not None and filename in checksums:
- checksum = checksums[filename]
- else:
- checksum = None
- elif isinstance(checksums, (list, tuple)):
- if index is not None and index < len(checksums) and (index >= 0 or abs(index) <= len(checksums)):
+ try:
+ checksum = checksums[chksum_input]
+ except KeyError:
+ self.log.debug("Checksum not found for file: %s", chksum_input)
+ elif isinstance(checksums, (list, tuple)) and index is not None:
+ try:
checksum = checksums[index]
- else:
- checksum = None
- elif checksums is None:
- checksum = None
- else:
+ except IndexError:
+ self.log.debug("Checksum not found for index list: %s", index)
+ elif checksums is not None:
raise EasyBuildError("Invalid type for checksums (%s), should be dict, list, tuple or None.",
type(checksums))
if checksum is None or build_option("checksum_priority") == CHECKSUM_PRIORITY_JSON:
json_checksums = self.get_checksums_from_json()
- return json_checksums.get(filename, None)
- else:
- return checksum
+ checksum = json_checksums.get(chksum_input, None)
+
+ if checksum and chksum_input_git is not None:
+ # ignore any checksum for given filename due to changes in https://github.com/python/cpython/issues/90021
+ # tarballs made for git repos are not reproducible when created with Python < 3.9
+ if sys.version_info[0] >= 3 and sys.version_info[1] < 9:
+ self.log.deprecated(
+ "Reproducible tarballs of Git repos are only possible when using Python 3.9+ to run EasyBuild. "
+ f"Skipping checksum verification of {chksum_input} since Python < 3.9 is used.",
+ '6.0'
+ )
+ return None
+ # not all archives formats of git repos are reproducible
+ # warn users that checksum might fail for non-reproducible archives
+ _, file_ext = os.path.splitext(chksum_input)
+ if file_ext not in ['', '.tar', '.txz', '.xz']:
+ print_warning(
+ f"Checksum verification may fail! Archive file '{chksum_input}' contains sources of a git repo "
+ "in a non-reproducible format. Please re-create that archive with XZ compression instead."
+ )
+
+ return checksum
def get_checksums_from_json(self, always_read=False):
"""
@@ -418,7 +472,7 @@ def fetch_source(self, source, checksum=None, extension=False, download_instruct
if source is None:
raise EasyBuildError("fetch_source called with empty 'source' argument")
- elif isinstance(source, string_type):
+ elif isinstance(source, str):
filename = source
elif isinstance(source, dict):
# Making a copy to avoid modifying the object with pops
@@ -476,7 +530,7 @@ def fetch_sources(self, sources=None, checksums=None):
# Single source should be re-wrapped as a list, and checksums with it
if isinstance(sources, dict):
sources = [sources]
- if isinstance(checksums, string_type):
+ if isinstance(checksums, str):
checksums = [checksums]
# Loop over the list of sources; list of checksums must match >= in size
@@ -532,14 +586,6 @@ def fetch_patches(self, patch_specs=None, extension=False, checksums=None):
else:
self.log.info("Added patches: %s", self.patches)
- def fetch_extension_sources(self, skip_checksums=False):
- """
- Fetch source and patch files for extensions (DEPRECATED, use collect_exts_file_info instead).
- """
- depr_msg = "EasyBlock.fetch_extension_sources is deprecated, use EasyBlock.collect_exts_file_info instead"
- self.log.deprecated(depr_msg, '5.0')
- return self.collect_exts_file_info(fetch_files=True, verify_checksums=not skip_checksums)
-
def collect_exts_file_info(self, fetch_files=True, verify_checksums=True):
"""
Collect information on source and patch files for extensions.
@@ -597,19 +643,16 @@ def collect_exts_file_info(self, fetch_files=True, verify_checksums=True):
template_values = copy.deepcopy(self.cfg.template_values)
template_values.update(template_constant_dict(ext_src))
- # resolve templates in extension options
- ext_options = resolve_template(ext_options, template_values)
+ source_urls = resolve_template(ext_options.get('source_urls', []), template_values)
+ checksums = resolve_template(ext_options.get('checksums', []), template_values)
- source_urls = ext_options.get('source_urls', [])
- checksums = ext_options.get('checksums', [])
-
- download_instructions = ext_options.get('download_instructions')
+ download_instructions = resolve_template(ext_options.get('download_instructions'), template_values)
if ext_options.get('nosource', None):
self.log.debug("No sources for extension %s, as indicated by 'nosource'", ext_name)
elif ext_options.get('sources', None):
- sources = ext_options['sources']
+ sources = resolve_template(ext_options['sources'], template_values)
# only a single source file is supported for extensions currently,
# see https://github.com/easybuilders/easybuild-framework/issues/3463
@@ -624,7 +667,7 @@ def collect_exts_file_info(self, fetch_files=True, verify_checksums=True):
# always pass source spec as dict value to fetch_source method,
# mostly so we can inject stuff like source URLs
- if isinstance(source, string_type):
+ if isinstance(source, str):
source = {'filename': source}
elif not isinstance(source, dict):
raise EasyBuildError("Incorrect value type for source of extension %s: %s",
@@ -646,17 +689,18 @@ def collect_exts_file_info(self, fetch_files=True, verify_checksums=True):
})
else:
- # use default template for name of source file if none is specified
- default_source_tmpl = resolve_template('%(name)s-%(version)s.tar.gz', template_values)
# if no sources are specified via 'sources', fall back to 'source_tmpl'
src_fn = ext_options.get('source_tmpl')
if src_fn is None:
- src_fn = default_source_tmpl
- elif not isinstance(src_fn, string_type):
+ # use default template for name of source file if none is specified
+ src_fn = '%(name)s-%(version)s.tar.gz'
+ elif not isinstance(src_fn, str):
error_msg = "source_tmpl value must be a string! (found value of type '%s'): %s"
raise EasyBuildError(error_msg, type(src_fn).__name__, src_fn)
+ src_fn = resolve_template(src_fn, template_values)
+
if fetch_files:
src_path = self.obtain_file(src_fn, extension=True, urls=source_urls,
force_download=force_download,
@@ -671,9 +715,8 @@ def collect_exts_file_info(self, fetch_files=True, verify_checksums=True):
src_path = ext_src['src']
src_fn = os.path.basename(src_path)
- # report both MD5 and SHA256 checksums, since both are valid default checksum types
src_checksums = {}
- for checksum_type in (CHECKSUM_TYPE_MD5, CHECKSUM_TYPE_SHA256):
+ for checksum_type in [CHECKSUM_TYPE_SHA256]:
src_checksum = compute_checksum(src_path, checksum_type=checksum_type)
src_checksums[checksum_type] = src_checksum
self.log.info("%s checksum for %s: %s", checksum_type, src_path, src_checksum)
@@ -686,10 +729,13 @@ def collect_exts_file_info(self, fetch_files=True, verify_checksums=True):
elif build_option('ignore_checksums'):
print_warning("Ignoring failing checksum verification for %s" % src_fn)
else:
- raise EasyBuildError('Checksum verification for extension source %s failed', src_fn)
+ raise EasyBuildError(
+ 'Checksum verification for extension source %s failed', src_fn,
+ exit_code=EasyBuildExit.FAIL_CHECKSUM
+ )
# locate extension patches (if any), and verify checksums
- ext_patches = ext_options.get('patches', [])
+ ext_patches = resolve_template(ext_options.get('patches', []), template_values)
if fetch_files:
ext_patches = self.fetch_patches(patch_specs=ext_patches, extension=True)
else:
@@ -704,9 +750,7 @@ def collect_exts_file_info(self, fetch_files=True, verify_checksums=True):
for patch in ext_patches:
patch = patch['path']
computed_checksums[patch] = {}
- # report both MD5 and SHA256 checksums,
- # since both are valid default checksum types
- for checksum_type in (CHECKSUM_TYPE_MD5, CHECKSUM_TYPE_SHA256):
+ for checksum_type in [CHECKSUM_TYPE_SHA256]:
checksum = compute_checksum(patch, checksum_type=checksum_type)
computed_checksums[patch][checksum_type] = checksum
self.log.info("%s checksum for %s: %s", checksum_type, patch, checksum)
@@ -723,14 +767,16 @@ def collect_exts_file_info(self, fetch_files=True, verify_checksums=True):
elif build_option('ignore_checksums'):
print_warning("Ignoring failing checksum verification for %s" % patch_fn)
else:
- raise EasyBuildError("Checksum verification for extension patch %s failed",
- patch_fn)
+ raise EasyBuildError(
+ "Checksum verification for extension patch %s failed", patch_fn,
+ exit_code=EasyBuildExit.FAIL_CHECKSUM
+ )
else:
self.log.debug('No patches found for extension %s.' % ext_name)
exts_sources.append(ext_src)
- elif isinstance(ext, string_type):
+ elif isinstance(ext, str):
exts_sources.append({'name': ext})
else:
@@ -795,12 +841,11 @@ def obtain_file(self, filename, extension=False, urls=None, download_filename=No
return fullpath
except IOError as err:
+ msg = f"Downloading file {filename} from url {url} to {fullpath} failed: {err}"
if not warning_only:
- raise EasyBuildError("Downloading file %s "
- "from url %s to %s failed: %s", filename, url, fullpath, err)
+ raise EasyBuildError(msg, exit_code=EasyBuildExit.FAIL_DOWNLOAD)
else:
- self.log.warning("Downloading file %s "
- "from url %s to %s failed: %s", filename, url, fullpath, err)
+ self.log.warning(msg)
return None
else:
@@ -873,116 +918,124 @@ def obtain_file(self, filename, extension=False, urls=None, download_filename=No
if self.dry_run:
self.dry_run_msg(" * %s found at %s", filename, foundfile)
return foundfile
- elif no_download:
+
+ if no_download:
if self.dry_run:
self.dry_run_msg(" * %s (MISSING)", filename)
return filename
- else:
- if not warning_only:
- raise EasyBuildError("Couldn't find file %s anywhere, and downloading it is disabled... "
- "Paths attempted (in order): %s ", filename, ', '.join(failedpaths))
- else:
- self.log.warning("Couldn't find file %s anywhere, and downloading it is disabled... "
- "Paths attempted (in order): %s ", filename, ', '.join(failedpaths))
- return None
- elif git_config:
- return get_source_tarball_from_git(filename, targetdir, git_config)
- else:
- # try and download source files from specified source URLs
- if urls:
- source_urls = urls[:]
- else:
- source_urls = []
- source_urls.extend(self.cfg['source_urls'])
- # Add additional URLs as configured.
- for url in build_option("extra_source_urls"):
- url += "/" + name_letter + "/" + location
- source_urls.append(url)
+ failedpaths_msg = "\n * ".join([""]+failedpaths)
+ file_notfound_msg = (
+ f"Couldn't find file '{filename}' anywhere, and downloading it is disabled... "
+ f"Paths attempted (in order): {failedpaths_msg}"
+ )
- mkdir(targetdir, parents=True)
+ if warning_only:
+ self.log.warning(file_notfound_msg)
+ return None
- for url in source_urls:
+ raise EasyBuildError(file_notfound_msg, exit_code=EasyBuildExit.MISSING_SOURCES)
- if extension:
- targetpath = os.path.join(targetdir, "extensions", filename)
- else:
- targetpath = os.path.join(targetdir, filename)
+ if git_config:
+ return get_source_tarball_from_git(filename, targetdir, git_config)
- url_filename = download_filename or filename
+ # try and download source files from specified source URLs
+ if urls:
+ source_urls = urls[:]
+ else:
+ source_urls = []
+ source_urls.extend(self.cfg['source_urls'])
- if isinstance(url, string_type):
- if url[-1] in ['=', '/']:
- fullurl = "%s%s" % (url, url_filename)
- else:
- fullurl = "%s/%s" % (url, url_filename)
- elif isinstance(url, tuple):
- # URLs that require a suffix, e.g., SourceForge download links
- # e.g. http://sourceforge.net/projects/math-atlas/files/Stable/3.8.4/atlas3.8.4.tar.bz2/download
- fullurl = "%s/%s/%s" % (url[0], url_filename, url[1])
- else:
- self.log.warning("Source URL %s is of unknown type, so ignoring it." % url)
- continue
+ # Add additional URLs as configured.
+ for url in build_option("extra_source_urls"):
+ url += "/" + name_letter + "/" + location
+ source_urls.append(url)
- # PyPI URLs may need to be converted due to change in format of these URLs,
- # cfr. https://bitbucket.org/pypa/pypi/issues/438
- if PYPI_PKG_URL_PATTERN in fullurl and not is_alt_pypi_url(fullurl):
- alt_url = derive_alt_pypi_url(fullurl)
- if alt_url:
- _log.debug("Using alternate PyPI URL for %s: %s", fullurl, alt_url)
- fullurl = alt_url
- else:
- _log.debug("Failed to derive alternate PyPI URL for %s, so retaining the original", fullurl)
+ mkdir(targetdir, parents=True)
- if self.dry_run:
- self.dry_run_msg(" * %s will be downloaded to %s", filename, targetpath)
- if extension and urls:
- # extensions typically have custom source URLs specified, only mention first
- self.dry_run_msg(" (from %s, ...)", fullurl)
- downloaded = True
+ for url in source_urls:
- else:
- self.log.debug("Trying to download file %s from %s to %s ..." % (filename, fullurl, targetpath))
- downloaded = False
- try:
- if download_file(filename, fullurl, targetpath):
- downloaded = True
+ if extension:
+ targetpath = os.path.join(targetdir, "extensions", filename)
+ else:
+ targetpath = os.path.join(targetdir, filename)
- except IOError as err:
- self.log.debug("Failed to download %s from %s: %s" % (filename, url, err))
- failedpaths.append(fullurl)
- continue
+ url_filename = download_filename or filename
- if downloaded:
- # if fetching from source URL worked, we're done
- self.log.info("Successfully downloaded source file %s from %s" % (filename, fullurl))
- return targetpath
+ if isinstance(url, str):
+ if url[-1] in ['=', '/']:
+ fullurl = "%s%s" % (url, url_filename)
else:
- failedpaths.append(fullurl)
+ fullurl = "%s/%s" % (url, url_filename)
+ elif isinstance(url, tuple):
+ # URLs that require a suffix, e.g., SourceForge download links
+ # e.g. http://sourceforge.net/projects/math-atlas/files/Stable/3.8.4/atlas3.8.4.tar.bz2/download
+ fullurl = "%s/%s/%s" % (url[0], url_filename, url[1])
+ else:
+ self.log.warning("Source URL %s is of unknown type, so ignoring it." % url)
+ continue
+
+ # PyPI URLs may need to be converted due to change in format of these URLs,
+ # cfr. https://bitbucket.org/pypa/pypi/issues/438
+ if PYPI_PKG_URL_PATTERN in fullurl and not is_alt_pypi_url(fullurl):
+ alt_url = derive_alt_pypi_url(fullurl)
+ if alt_url:
+ _log.debug("Using alternative PyPI URL for %s: %s", fullurl, alt_url)
+ fullurl = alt_url
+ else:
+ _log.debug("Failed to derive alternative PyPI URL for %s, so retaining the original",
+ fullurl)
if self.dry_run:
- self.dry_run_msg(" * %s (MISSING)", filename)
- return filename
+ self.dry_run_msg(" * %s will be downloaded to %s", filename, targetpath)
+ if extension and urls:
+ # extensions typically have custom source URLs specified, only mention first
+ self.dry_run_msg(" (from %s, ...)", fullurl)
+ downloaded = True
+
else:
- error_msg = "Couldn't find file %s anywhere, "
- if download_instructions is None:
- download_instructions = self.cfg['download_instructions']
- if download_instructions is not None and download_instructions != "":
- msg = "\nDownload instructions:\n\n" + download_instructions + '\n'
- print_msg(msg, prefix=False, stderr=True)
- error_msg += "please follow the download instructions above, and make the file available "
- error_msg += "in the active source path (%s)" % ':'.join(source_paths())
- else:
- # flatten list to string with '%' characters escaped (literal '%' desired in 'sprintf')
- failedpaths_msg = ', '.join(failedpaths).replace('%', '%%')
- error_msg += "and downloading it didn't work either... "
- error_msg += "Paths attempted (in order): %s " % failedpaths_msg
+ self.log.debug("Trying to download file %s from %s to %s ..." % (filename, fullurl, targetpath))
+ downloaded = False
+ try:
+ if download_file(filename, fullurl, targetpath):
+ downloaded = True
- if not warning_only:
- raise EasyBuildError(error_msg, filename)
- else:
- self.log.warning(error_msg, filename)
- return None
+ except IOError as err:
+ self.log.debug("Failed to download %s from %s: %s" % (filename, url, err))
+ failedpaths.append(fullurl)
+ continue
+
+ if downloaded:
+ # if fetching from source URL worked, we're done
+ self.log.info("Successfully downloaded source file %s from %s" % (filename, fullurl))
+ return targetpath
+ else:
+ failedpaths.append(fullurl)
+
+ if self.dry_run:
+ self.dry_run_msg(" * %s (MISSING)", filename)
+ return filename
+ else:
+ error_msg = "Couldn't find file %s anywhere, "
+ if download_instructions is None:
+ download_instructions = self.cfg['download_instructions']
+ if download_instructions is not None and download_instructions != "":
+ msg = "\nDownload instructions:\n\n" + indent(download_instructions, ' ') + '\n\n'
+ msg += "Make the files available in the active source path: %s\n" % ':'.join(source_paths())
+ print_msg(msg, prefix=False, stderr=True)
+ error_msg += "please follow the download instructions above, and make the file available "
+ error_msg += "in the active source path (%s)" % ':'.join(source_paths())
+ else:
+ # flatten list to string with '%' characters escaped (literal '%' desired in 'sprintf')
+ failedpaths_msg = ', '.join(failedpaths).replace('%', '%%')
+ error_msg += "and downloading it didn't work either... "
+ error_msg += "Paths attempted (in order): %s " % failedpaths_msg
+
+ if not warning_only:
+ raise EasyBuildError(error_msg, filename, exit_code=EasyBuildExit.FAIL_DOWNLOAD)
+ else:
+ self.log.warning(error_msg, filename)
+ return None
#
# GETTER/SETTER UTILITY FUNCTIONS
@@ -1217,6 +1270,8 @@ def make_devel_module(self, create_in_builddir=False):
# these should be all the dependencies and we should load them
recursive_unload = self.cfg['recursive_module_unload']
depends_on = self.cfg['module_depends_on']
+ if depends_on is not None:
+ self.log.deprecated("'module_depends_on' easyconfig parameter should not be used anymore", '6.0')
for key in os.environ:
# legacy support
if key.startswith(DEVEL_ENV_VAR_NAME_PREFIX):
@@ -1344,6 +1399,8 @@ def make_module_dep(self, unload_info=None):
# include load statements for retained dependencies
recursive_unload = self.cfg['recursive_module_unload']
depends_on = self.cfg['module_depends_on']
+ if depends_on is not None:
+ self.log.deprecated("'module_depends_on' easyconfig parameter should not be used anymore", '6.0')
loads = []
for dep in deps:
unload_modules = []
@@ -1368,8 +1425,7 @@ def make_module_dep(self, unload_info=None):
multi_dep_mod_names = {}
for deplist in self.cfg.multi_deps:
for dep in deplist:
- multi_dep_mod_names.setdefault(dep['name'], [])
- multi_dep_mod_names[dep['name']].append(dep['short_mod_name'])
+ multi_dep_mod_names.setdefault(dep['name'], []).append(dep['short_mod_name'])
multi_dep_load_defaults = []
for _, depmods in sorted(multi_dep_mod_names.items()):
@@ -1388,6 +1444,46 @@ def make_module_description(self):
"""
return self.module_generator.get_description()
+ def make_module_pythonpath(self):
+ """
+ Add lines for module file to update $PYTHONPATH or $EBPYTHONPREFIXES,
+ if they aren't already present and the standard lib/python*/site-packages subdirectory exists
+ """
+ if os.path.isfile(os.path.join(self.installdir, 'bin', 'python')): # only needed when not a python install
+ return []
+
+ python_subdir_pattern = os.path.join(self.installdir, 'lib', 'python*', 'site-packages')
+ candidate_paths = (os.path.relpath(path, self.installdir) for path in glob.glob(python_subdir_pattern))
+ python_paths = [path for path in candidate_paths if re.match(r'lib/python\d+\.\d+/site-packages', path)]
+ if not python_paths:
+ return []
+
+ # determine whether Python is a runtime dependency;
+ # if so, we assume it was installed with EasyBuild, and hence is aware of $EBPYTHONPREFIXES
+ runtime_deps = [dep['name'] for dep in self.cfg.dependencies(runtime_only=True)]
+
+ # don't use $EBPYTHONPREFIXES unless we can and it's preferred or necesary (due to use of multi_deps)
+ use_ebpythonprefixes = False
+ multi_deps = self.cfg['multi_deps']
+
+ if 'Python' in runtime_deps:
+ self.log.info("Found Python runtime dependency, so considering $EBPYTHONPREFIXES...")
+
+ if build_option('prefer_python_search_path') == EBPYTHONPREFIXES:
+ self.log.info("Preferred Python search path is $EBPYTHONPREFIXES, so using that")
+ use_ebpythonprefixes = True
+
+ elif multi_deps and 'Python' in multi_deps:
+ self.log.info("Python is listed in 'multi_deps', so using $EBPYTHONPREFIXES instead of $PYTHONPATH")
+ use_ebpythonprefixes = True
+
+ if use_ebpythonprefixes:
+ path = '' # EBPYTHONPREFIXES are relative to the install dir
+ lines = self.module_generator.prepend_paths(EBPYTHONPREFIXES, path, warn_exists=False)
+ else:
+ lines = self.module_generator.prepend_paths(PYTHONPATH, python_paths, warn_exists=False)
+ return [lines] if lines else []
+
def make_module_extra(self, altroot=None, altversion=None):
"""
Set extra stuff in module file, e.g. $EBROOT*, $EBVERSION*, etc.
@@ -1420,21 +1516,8 @@ def make_module_extra(self, altroot=None, altversion=None):
for (key, value) in self.cfg['modextravars'].items():
lines.append(self.module_generator.set_environment(key, value))
- for (key, value) in self.cfg['modextrapaths'].items():
- if isinstance(value, string_type):
- value = [value]
- elif not isinstance(value, (tuple, list)):
- raise EasyBuildError("modextrapaths dict value %s (type: %s) is not a list or tuple",
- value, type(value))
- lines.append(self.module_generator.prepend_paths(key, value, allow_abs=self.cfg['allow_prepend_abs_path']))
-
- for (key, value) in self.cfg['modextrapaths_append'].items():
- if isinstance(value, string_type):
- value = [value]
- elif not isinstance(value, (tuple, list)):
- raise EasyBuildError("modextrapaths_append dict value %s (type: %s) is not a list or tuple",
- value, type(value))
- lines.append(self.module_generator.append_paths(key, value, allow_abs=self.cfg['allow_append_abs_path']))
+ # add lines to update $PYTHONPATH or $EBPYTHONPREFIXES
+ lines.extend(self.make_module_pythonpath())
modloadmsg = self.cfg['modloadmsg']
if modloadmsg:
@@ -1468,8 +1551,8 @@ def make_module_extra_extensions(self):
# set environment variable that specifies list of extensions
# We need only name and version, so don't resolve templates
exts_list = self.make_extension_string(ext_sep=',', sort=False)
- env_var_name = convert_name(self.name, upper=True)
- lines.append(self.module_generator.set_environment('EBEXTSLIST%s' % env_var_name, exts_list))
+ env_var_name = 'EBEXTSLIST' + convert_name(self.name, upper=True)
+ lines.append(self.module_generator.set_environment(env_var_name, exts_list))
return ''.join(lines)
@@ -1553,108 +1636,186 @@ def make_module_group_check(self):
return txt
- def make_module_req(self):
+ def make_module_req(self, fake=False):
"""
- Generate the environment-variables to run the module.
+ Generate the environment-variables required to run the module.
"""
- requirements = self.make_module_req_guess()
+ if self.make_module_req_guess.__qualname__ != "EasyBlock.make_module_req_guess":
+ # Deprecated make_module_req_guess method used in child Easyblock
+ # adjust environment with custom make_module_req_guess
+ self.log.deprecated(
+ "make_module_req_guess() is deprecated, use EasyBlock.module_load_environment instead.",
+ "6.0",
+ )
+ self.module_load_environment.replace(self.make_module_req_guess())
- lines = ['\n']
- if os.path.isdir(self.installdir):
- old_dir = change_dir(self.installdir)
- else:
- old_dir = None
+ # update module load environment with extra paths defined in easyconfig
+ self.inject_module_extra_paths()
+
+ # Expand and inject path-like environment variables into module file
+ env_var_requirements = {
+ envar_name: envar_val
+ for envar_name, envar_val in sorted(self.module_load_environment.items())
+ if envar_val.is_path
+ }
+ self.log.debug(f"Tentative module environment requirements before path expansion: {env_var_requirements}")
+ # TODO: handle non-path variables in make_module_extra
+ # in the meantime, just report if any is found
+ non_path_envars = set(self.module_load_environment) - set(env_var_requirements)
+ if non_path_envars:
+ self.log.warning(
+ f"Non-path variables found in module load environment: {non_path_envars}."
+ "This is not yet supported by this version of EasyBuild."
+ )
+
+ mod_lines = ['\n']
if self.dry_run:
self.dry_run_msg("List of paths that would be searched and added to module file:\n")
note = "note: glob patterns are not expanded and existence checks "
note += "for paths are skipped for the statements below due to dry run"
- lines.append(self.module_generator.comment(note))
-
- # For these environment variables, the corresponding directory must include at least one file.
- # The values determine if detection is done recursively, i.e. if it accepts directories where files
- # are only in subdirectories.
- keys_requiring_files = {
- 'PATH': False,
- 'LD_LIBRARY_PATH': False,
- 'LIBRARY_PATH': True,
- 'CPATH': True,
- 'CMAKE_PREFIX_PATH': True,
- 'CMAKE_LIBRARY_PATH': True,
- }
+ mod_lines.append(self.module_generator.comment(note))
- for key, reqs in sorted(requirements.items()):
- if isinstance(reqs, string_type):
- self.log.warning("Hoisting string value %s into a list before iterating over it", reqs)
- reqs = [reqs]
- if self.dry_run:
- self.dry_run_msg(" $%s: %s" % (key, ', '.join(reqs)))
- # Don't expand globs or do any filtering below for dry run
- paths = reqs
+ for env_var, search_paths in env_var_requirements.items():
+ if self.dry_run or fake:
+ # Don't expand globs or do any filtering for dry run
+ mod_req_paths = search_paths
+ if self.dry_run:
+ self.dry_run_msg(f" ${env_var}:{', '.join(mod_req_paths)}")
else:
- # Expand globs but only if the string is non-empty
- # empty string is a valid value here (i.e. to prepend the installation prefix, cfr $CUDA_HOME)
- paths = sum((glob.glob(path) if path else [path] for path in reqs), []) # sum flattens to list
-
- # If lib64 is just a symlink to lib we fixup the paths to avoid duplicates
- lib64_is_symlink = (all(os.path.isdir(path) for path in ['lib', 'lib64']) and
- os.path.samefile('lib', 'lib64'))
- if lib64_is_symlink:
- fixed_paths = []
- for path in paths:
- if (path + os.path.sep).startswith('lib64' + os.path.sep):
- # We only need CMAKE_LIBRARY_PATH if there is a separate lib64 path, so skip symlink
- if key == 'CMAKE_LIBRARY_PATH':
- continue
- path = path.replace('lib64', 'lib', 1)
- fixed_paths.append(path)
- if fixed_paths != paths:
- self.log.info("Fixed symlink lib64 in paths for %s: %s -> %s", key, paths, fixed_paths)
- paths = fixed_paths
- # remove duplicate paths preserving order
- paths = nub(paths)
- if key in keys_requiring_files:
- # only retain paths that contain at least one file
- recursive = keys_requiring_files[key]
- retained_paths = []
- for pth in paths:
- fullpath = os.path.join(self.installdir, pth)
- if os.path.isdir(fullpath) and dir_contains_files(fullpath, recursive=recursive):
- retained_paths.append(pth)
- if retained_paths != paths:
- self.log.info("Only retaining paths for %s that contain at least one file: %s -> %s",
- key, paths, retained_paths)
- paths = retained_paths
-
- if paths:
- lines.append(self.module_generator.prepend_paths(key, paths))
+ mod_req_paths = [
+ expanded_path for unexpanded_path in search_paths
+ for expanded_path in self.expand_module_search_path(unexpanded_path, path_type=search_paths.type)
+ ]
+
+ if mod_req_paths:
+ mod_req_paths = nub(mod_req_paths) # remove duplicates
+ extra_mod_lines = self.module_generator.update_paths(env_var, mod_req_paths, allow_abs=True,
+ prepend=search_paths.mod_prepend,
+ delim=search_paths.delimiter)
+ mod_lines.append(extra_mod_lines)
+
if self.dry_run:
self.dry_run_msg('')
- if old_dir is not None:
- change_dir(old_dir)
+ return ''.join(mod_lines)
+
+ def inject_module_extra_paths(self):
+ """
+ Parse search paths and delimiters defined in 'modextrapaths' easyconfig parameter
+ and inject them into module load environment
+ """
+ ec_param = 'modextrapaths'
+ known_aliases = [MODULE_LOAD_ENV_HEADERS]
+
+ for env_var_name, extra_opts in self.cfg[ec_param].items():
+ # default settings for environment variables
+ env_var_opts = {
+ 'delimiter': os.pathsep, # ':'
+ 'var_type': ModEnvVarType.PATH_WITH_FILES,
+ 'prepend': True,
+ }
+
+ if not isinstance(extra_opts, dict):
+ extra_opts = {'paths': extra_opts}
+
+ env_var_opts.update(extra_opts)
+
+ if 'paths' not in env_var_opts:
+ error_msg = f"'paths' key not set for ${env_var_name} in '{ec_param}' easyconfig parameter"
+ raise EasyBuildError(error_msg)
+
+ # make sure that we have a list of paths, even if there's only one
+ if isinstance(env_var_opts['paths'], str):
+ env_var_opts['paths'] = [env_var_opts['paths']]
+
+ existing_env_vars = []
+ if env_var_name in self.module_load_environment:
+ # update existing variable
+ existing_env_vars.append(env_var_name)
+ elif env_var_name in known_aliases:
+ # update existing alias to variables
+ existing_env_vars = self.module_load_environment.alias_vars(env_var_name)
+
+ for existing_env_var in existing_env_vars:
+ env_var = getattr(self.module_load_environment, existing_env_var)
+ env_var.extend(env_var_opts['paths'])
+ env_var.delimiter = env_var_opts['delimiter']
+ env_var.prepend = env_var_opts['prepend']
+ env_var.type = env_var_opts['var_type']
+ msg = f"Variable ${existing_env_var} from '{ec_param}' extended in module load environment with "
+ msg += f"delimiter='{env_var.delimiter}', prepend='{env_var.prepend}', type='{env_var.type}' "
+ msg += f"and paths='{env_var}'"
+ self.log.debug(msg)
+
+ if not existing_env_vars:
+ # rename 'modextrapaths' options to match ModuleEnvironmentVariable constructor parameters
+ env_var_opts['contents'] = env_var_opts.pop('paths')
+ setattr(self.module_load_environment, env_var_name, env_var_opts)
+ env_var = getattr(self.module_load_environment, env_var_name)
+ msg = f"Variable ${env_var_name} from '{ec_param}' added to module load environment with "
+ msg += f"delimiter='{env_var.delimiter}', prepend='{env_var.prepend}', type='{env_var.type}' "
+ msg += f"and paths='{env_var}'"
+ self.log.debug(msg)
+
+ def expand_module_search_path(self, search_path, path_type=ModEnvVarType.PATH_WITH_FILES):
+ """
+ Expand given path glob and return list of suitable paths to be used as search paths:
+ - Paths must point to existing files/directories
+ - Relative paths are relative to installation prefix root and are kept relative after expansion
+ - Absolute paths are kept as absolute paths after expansion
+ - Follow symlinks and resolve their paths (avoids duplicate paths through symlinks)
+ - :path_type: ModEnvVarType that controls requirements for population of directories
+ - PATH: no requirements, can be empty
+ - PATH_WITH_FILES: must contain at least one file in them (default)
+ - PATH_WITH_TOP_FILES: increase stricness to require files in top level directory
+ """
+ if os.path.isabs(search_path):
+ abs_glob = search_path
+ else:
+ real_installdir = os.path.realpath(self.installdir)
+ abs_glob = os.path.join(real_installdir, search_path)
+
+ exp_search_paths = glob.glob(abs_glob, recursive=True)
+
+ retained_search_paths = []
+ for abs_path in exp_search_paths:
+ check_dir_files = path_type in (ModEnvVarType.PATH_WITH_FILES, ModEnvVarType.PATH_WITH_TOP_FILES)
+ if os.path.isdir(abs_path) and check_dir_files:
+ # only retain paths to directories that contain at least one file
+ recursive = path_type == ModEnvVarType.PATH_WITH_FILES
+ if not dir_contains_files(abs_path, recursive=recursive):
+ self.log.debug("Discarded search path to empty directory: %s", abs_path)
+ continue
+
+ if os.path.isabs(search_path):
+ retain_path = abs_path
+ else:
+ # recover relative path
+ retain_path = os.path.relpath(os.path.realpath(abs_path), start=real_installdir)
+ if retain_path == '.':
+ retain_path = '' # use empty string to represent root of install dir
- return ''.join(lines)
+ if retain_path.startswith('..' + os.path.sep):
+ raise EasyBuildError(
+ f"Expansion of search path glob pattern '{search_path}' resulted in a relative path "
+ f"pointing outside of install directory: {retain_path}"
+ )
+
+ retained_search_paths.append(retain_path)
+
+ return retained_search_paths
def make_module_req_guess(self):
"""
- A dictionary of possible directories to look for.
- """
- lib_paths = ['lib', 'lib32', 'lib64']
- return {
- 'PATH': ['bin', 'sbin'],
- 'LD_LIBRARY_PATH': lib_paths,
- 'LIBRARY_PATH': lib_paths,
- 'CPATH': ['include'],
- 'MANPATH': ['man', os.path.join('share', 'man')],
- 'PKG_CONFIG_PATH': [os.path.join(x, 'pkgconfig') for x in lib_paths + ['share']],
- 'ACLOCAL_PATH': [os.path.join('share', 'aclocal')],
- 'CLASSPATH': ['*.jar'],
- 'XDG_DATA_DIRS': ['share'],
- 'GI_TYPELIB_PATH': [os.path.join(x, 'girepository-*') for x in lib_paths],
- 'CMAKE_PREFIX_PATH': [''],
- 'CMAKE_LIBRARY_PATH': ['lib64'], # lib and lib32 are searched through the above
- }
+ A dictionary of common search path variables to be loaded by environment modules
+ Each key contains the list of known directories related to the search path
+ """
+ self.log.deprecated(
+ "make_module_req_guess() is deprecated, use EasyBlock.module_load_environment instead",
+ '6.0',
+ )
+ return self.module_load_environment.as_dict
def load_module(self, mod_paths=None, purge=True, extra_modules=None, verbose=True):
"""
@@ -1748,33 +1909,41 @@ def load_dependency_modules(self):
# EXTENSIONS UTILITY FUNCTIONS
#
- def _make_extension_list(self):
+ def make_extension_string(self, name_version_sep='-', ext_sep=', ', sort=True):
+ """
+ Generate a string with a list of extensions returned by make_extension_list.
+
+ The name and version are separated by name_version_sep and each extension is separated by ext_sep.
+ For customization of extensions the make_extension_list method should be used.
+ """
+ exts_list = (name_version_sep.join(ext) for ext in self.make_extension_list())
+ if sort:
+ exts_list = sorted(exts_list, key=str.lower)
+ return ext_sep.join(exts_list)
+
+ def make_extension_list(self):
"""
Return a list of extension names and their versions included in this installation
- Each entry should be a (name, version) tuple or just (name, ) if no version exists
+ Each entry should be a (name, version) tuple or just (name, ) if no version exists.
+ Custom EasyBlocks may override this to add extensions that cannot be found automatically.
"""
# Each extension in exts_list is either a string or a list/tuple with name, version as first entries
# As name can be a templated value we must resolve templates
+ if hasattr(self, '_make_extension_list'):
+ self.log.nosupport("self._make_extension_list is replaced by self.make_extension_list", '5.0')
+ if type(self).make_extension_string != EasyBlock.make_extension_string:
+ self.log.nosupport("self.make_extension_string should not be overridden", '5.0')
+
exts_list = []
for ext in self.cfg.get_ref('exts_list'):
- if isinstance(ext, string_type):
+ if isinstance(ext, str):
exts_list.append((resolve_template(ext, self.cfg.template_values), ))
else:
- exts_list.append((resolve_template(ext[0], self.cfg.template_values), ext[1]))
+ exts_list.append((resolve_template(ext[0], self.cfg.template_values),
+ resolve_template(ext[1], self.cfg.template_values)))
return exts_list
- def make_extension_string(self, name_version_sep='-', ext_sep=', ', sort=True):
- """
- Generate a string with a list of extensions.
-
- The name and version are separated by name_version_sep and each extension is separated by ext_sep
- """
- exts_list = (name_version_sep.join(ext) for ext in self._make_extension_list())
- if sort:
- exts_list = sorted(exts_list, key=str.lower)
- return ext_sep.join(exts_list)
-
def prepare_for_extensions(self):
"""Ran before installing extensions (eg to set templates)"""
@@ -1807,21 +1976,20 @@ def skip_extensions_sequential(self, exts_filter):
exts_cnt = len(self.ext_instances)
- res = []
+ exts = []
for idx, ext_inst in enumerate(self.ext_instances):
cmd, stdin = resolve_exts_filter_template(exts_filter, ext_inst)
- (out, ec) = run_cmd(cmd, log_all=False, log_ok=False, simple=False, inp=stdin,
- regexp=False, trace=False)
- self.log.info("exts_filter result for %s: exit code %s; output: %s", ext_inst.name, ec, out)
- if ec == 0:
- print_msg("skipping extension %s" % ext_inst.name, silent=self.silent, log=self.log)
+ res = run_shell_cmd(cmd, stdin=stdin, fail_on_error=False, hidden=True)
+ self.log.info(f"exts_filter result for {ext_inst.name}: exit code {res.exit_code}; output: {res.output}")
+ if res.exit_code == EasyBuildExit.SUCCESS:
+ print_msg(f"skipping extension {ext_inst.name}", silent=self.silent, log=self.log)
else:
- self.log.info("Not skipping %s", ext_inst.name)
- res.append(ext_inst)
+ self.log.info(f"Not skipping {ext_inst.name}")
+ exts.append(ext_inst)
- self.update_exts_progress_bar("skipping installed extensions (%d/%d checked)" % (idx + 1, exts_cnt))
+ self.update_exts_progress_bar(f"skipping installed extensions ({idx + 1}/{exts_cnt} checked)")
- self.ext_instances = res
+ self.ext_instances = exts
self.update_exts_progress_bar("already installed extensions filtered out", total=len(self.ext_instances))
def skip_extensions_parallel(self, exts_filter):
@@ -1829,44 +1997,33 @@ def skip_extensions_parallel(self, exts_filter):
Skip already installed extensions (checking in parallel),
by removing them from list of Extension instances to install (self.ext_instances).
"""
- self.log.experimental("Skipping installed extensions in parallel")
print_msg("skipping installed extensions (in parallel)", log=self.log)
- async_cmd_info_cache = {}
- running_checks_ids = []
installed_exts_ids = []
- exts_queue = list(enumerate(self.ext_instances[:]))
checked_exts_cnt = 0
exts_cnt = len(self.ext_instances)
+ cmds = [resolve_exts_filter_template(exts_filter, ext) for ext in self.ext_instances]
- # asynchronously run checks to see whether extensions are already installed
- while exts_queue or running_checks_ids:
+ with ThreadPoolExecutor(max_workers=self.cfg.parallel) as thread_pool:
- # first handle completed checks
- for idx in running_checks_ids[:]:
+ # list of command to run asynchronously
+ async_cmds = [thread_pool.submit(run_shell_cmd, cmd, stdin=stdin, hidden=True, fail_on_error=False,
+ asynchronous=True, task_id=idx) for (idx, (cmd, stdin)) in enumerate(cmds)]
+
+ # process result of commands as they have completed running
+ for done_task in concurrent.futures.as_completed(async_cmds):
+ res = done_task.result()
+ idx = res.task_id
ext_name = self.ext_instances[idx].name
- # don't read any output, just check whether command completed
- async_cmd_info = check_async_cmd(*async_cmd_info_cache[idx], output_read_size=0, fail_on_error=False)
- if async_cmd_info['done']:
- out, ec = async_cmd_info['output'], async_cmd_info['exit_code']
- self.log.info("exts_filter result for %s: exit code %s; output: %s", ext_name, ec, out)
- running_checks_ids.remove(idx)
- if ec == 0:
- print_msg("skipping extension %s" % ext_name, log=self.log)
- installed_exts_ids.append(idx)
-
- checked_exts_cnt += 1
- exts_pbar_label = "skipping installed extensions "
- exts_pbar_label += "(%d/%d checked)" % (checked_exts_cnt, exts_cnt)
- self.update_exts_progress_bar(exts_pbar_label)
-
- # start additional checks asynchronously
- while exts_queue and len(running_checks_ids) < self.cfg['parallel']:
- idx, ext = exts_queue.pop(0)
- cmd, stdin = resolve_exts_filter_template(exts_filter, ext)
- async_cmd_info_cache[idx] = run_cmd(cmd, log_all=False, log_ok=False, simple=False, inp=stdin,
- regexp=False, trace=False, asynchronous=True)
- running_checks_ids.append(idx)
+ self.log.info(f"exts_filter result for {ext_name}: exit code {res.exit_code}; output: {res.output}")
+ if res.exit_code == EasyBuildExit.SUCCESS:
+ print_msg(f"skipping extension {ext_name}", log=self.log)
+ installed_exts_ids.append(idx)
+
+ checked_exts_cnt += 1
+ exts_pbar_label = "skipping installed extensions "
+ exts_pbar_label += "(%d/%d checked)" % (checked_exts_cnt, exts_cnt)
+ self.update_exts_progress_bar(exts_pbar_label)
# compose new list of extensions, skip over the ones that are already installed;
# note: original order in extensions list should be preserved!
@@ -1878,7 +2035,15 @@ def skip_extensions_parallel(self, exts_filter):
self.ext_instances = retained_ext_instances
- def install_extensions(self, install=True):
+ def install_extensions(self, *args, **kwargs):
+ """[DEPRECATED] Install extensions."""
+ self.log.deprecated(
+ "EasyBlock.install_extensions() is deprecated, use EasyBlock.install_all_extensions() instead.",
+ '6.0',
+ )
+ self.install_all_extensions(*args, **kwargs)
+
+ def install_all_extensions(self, install=True):
"""
Install extensions.
@@ -1888,13 +2053,12 @@ def install_extensions(self, install=True):
self.log.debug("List of loaded modules: %s", self.modules_tool.list())
if build_option('parallel_extensions_install'):
- self.log.experimental("installing extensions in parallel")
try:
self.install_extensions_parallel(install=install)
except NotImplementedError:
# If parallel extension install is not supported for this type of extension then install sequentially
msg = "Parallel extensions install not supported for %s - using sequential install" % self.name
- self.log.experimental(msg)
+ self.log.info(msg)
self.install_extensions_sequential(install=install)
else:
self.install_extensions_sequential(install=install)
@@ -1910,7 +2074,6 @@ def install_extensions_sequential(self, install=True):
exts_cnt = len(self.ext_instances)
for idx, ext in enumerate(self.ext_instances):
-
self.log.info("Starting extension %s", ext.name)
run_hook(SINGLE_EXTENSION, self.hooks, pre_step_hook=True, args=[ext])
@@ -1942,15 +2105,15 @@ def install_extensions_sequential(self, install=True):
ext.toolchain.prepare(onlymod=self.cfg['onlytcmod'], silent=True, loadmod=False,
rpath_filter_dirs=self.rpath_filter_dirs)
- # real work
+ # actual installation of the extension
if install:
try:
- ext.prerun()
+ ext.install_extension_substep("pre_install_extension")
with self.module_generator.start_module_creation():
- txt = ext.run()
+ txt = ext.install_extension_substep("install_extension")
if txt:
self.module_extra_extensions += txt
- ext.postrun()
+ ext.install_extension_substep("post_install_extension")
finally:
if not self.dry_run:
ext_duration = datetime.now() - start_time
@@ -1971,6 +2134,8 @@ def install_extensions_parallel(self, install=True):
"""
self.log.info("Installing extensions in parallel...")
+ thread_pool = ThreadPoolExecutor(max_workers=self.cfg.parallel)
+
running_exts = []
installed_ext_names = []
@@ -2007,16 +2172,23 @@ def update_exts_progress_bar_helper(running_exts, progress_size):
# check for extension installations that have completed
if running_exts:
- self.log.info("Checking for completed extension installations (%d running)...", len(running_exts))
+ self.log.info(f"Checking for completed extension installations ({len(running_exts)} running)...")
for ext in running_exts[:]:
- if self.dry_run or ext.async_cmd_check():
- self.log.info("Installation of %s completed!", ext.name)
- ext.postrun()
- running_exts.remove(ext)
- installed_ext_names.append(ext.name)
- update_exts_progress_bar_helper(running_exts, 1)
+ if self.dry_run or ext.async_cmd_task.done():
+ res = ext.async_cmd_task.result()
+ if res.exit_code == EasyBuildExit.SUCCESS:
+ self.log.info(f"Installation of extension {ext.name} completed!")
+ # run post-install method for extension from same working dir as installation of extension
+ cwd = change_dir(res.work_dir)
+ ext.install_extension_substep("post_install_extension")
+ change_dir(cwd)
+ running_exts.remove(ext)
+ installed_ext_names.append(ext.name)
+ update_exts_progress_bar_helper(running_exts, 1)
+ else:
+ raise_run_shell_cmd_error(res)
else:
- self.log.debug("Installation of %s is still running...", ext.name)
+ self.log.debug(f"Installation of extension {ext.name} is still running...")
# try to start as many extension installations as we can, taking into account number of available cores,
# but only consider first 100 extensions still in the queue
@@ -2024,7 +2196,7 @@ def update_exts_progress_bar_helper(running_exts, progress_size):
for _ in range(max_iter):
- if not (exts_queue and len(running_exts) < self.cfg['parallel']):
+ if not (exts_queue and len(running_exts) < self.cfg.parallel):
break
# check whether extension at top of the queue is ready to install
@@ -2060,20 +2232,33 @@ def update_exts_progress_bar_helper(running_exts, progress_size):
# if some of the required dependencies are not installed yet, requeue this extension
elif pending_deps:
- # make sure all required dependencies are actually going to be installed,
- # to avoid getting stuck in an infinite loop!
+ # check whether all required dependency extensions are actually going to be installed;
+ # if not, we assume that they are provided by dependencies;
missing_deps = [x for x in required_deps if x not in all_ext_names]
if missing_deps:
- raise EasyBuildError("Missing required dependencies for %s are not going to be installed: %s",
- ext.name, ', '.join(missing_deps))
- else:
- self.log.info("Required dependencies missing for extension %s (%s), adding it back to queue...",
- ext.name, ', '.join(pending_deps))
+ msg = f"Missing required extensions for {ext.name} not found "
+ msg += "in list of extensions being installed, let's assume they are provided by "
+ msg += "dependencies and proceed: " + ', '.join(missing_deps)
+ self.log.info(msg)
+
+ msg = f"Pending dependencies for {ext.name} before taking into account missing dependencies: "
+ self.log.debug(msg + ', '.join(pending_deps))
+ pending_deps = [x for x in pending_deps if x not in missing_deps]
+ msg = f"Pending dependencies for {ext.name} after taking into account missing dependencies: "
+ self.log.debug(msg + ', '.join(pending_deps))
+
+ if pending_deps:
+ msg = f"Required dependencies not installed yet for extension {ext.name} ("
+ msg += ', '.join(pending_deps)
+ msg += "), adding it back to queue..."
+ self.log.info(msg)
# purposely adding extension back in the queue at Nth place rather than at the end,
# since we assume that the required dependencies will be installed soon...
exts_queue.insert(max_iter, ext)
- else:
+ # list of pending dependencies may be empty now after taking into account required extensions
+ # that are not being installed above, so extension may be ready to install
+ if not pending_deps:
tup = (ext.name, ext.version or '')
print_msg("starting installation of extension %s %s..." % tup, silent=self.silent, log=self.log)
@@ -2082,10 +2267,10 @@ def update_exts_progress_bar_helper(running_exts, progress_size):
ext.toolchain.prepare(onlymod=self.cfg['onlytcmod'], silent=True, loadmod=False,
rpath_filter_dirs=self.rpath_filter_dirs)
if install:
- ext.prerun()
- ext.run_async()
+ ext.install_extension_substep("pre_install_extension")
+ ext.async_cmd_task = ext.install_extension_substep("install_extension_async", thread_pool)
running_exts.append(ext)
- self.log.info("Started installation of extension %s in the background...", ext.name)
+ self.log.info(f"Started installation of extension {ext.name} in the background...")
update_exts_progress_bar_helper(running_exts, 0)
# print progress info after every iteration (unless that info is already shown via progress bar)
@@ -2098,6 +2283,8 @@ def update_exts_progress_bar_helper(running_exts, progress_size):
running_ext_names = ', '.join(x.name for x in running_exts[:3]) + ", ..."
print_msg(msg % (installed_cnt, exts_cnt, queued_cnt, running_cnt, running_ext_names), log=self.log)
+ thread_pool.shutdown()
+
#
# MISCELLANEOUS UTILITY FUNCTIONS
#
@@ -2248,19 +2435,36 @@ def set_parallel(self):
"""Set 'parallel' easyconfig parameter to determine how many cores can/should be used for parallel builds."""
# set level of parallelism for build
par = build_option('parallel')
- cfg_par = self.cfg['parallel']
- if cfg_par is None:
- self.log.debug("Desired parallelism specified via 'parallel' build option: %s", par)
- elif par is None:
- par = cfg_par
- self.log.debug("Desired parallelism specified via 'parallel' easyconfig parameter: %s", par)
+ if par is not None:
+ self.log.debug(f"Desired parallelism specified via 'parallel' build option: {par}")
+
+ # Transitional only in case some easyblocks still set/change cfg['parallel']
+ # Use _parallelLegacy to avoid deprecation warnings
+ par_ec = self.cfg['_parallelLegacy']
+ if par_ec is not None:
+ if par is None:
+ par = par_ec
+ else:
+ par = min(int(par), int(par_ec))
+
+ # --max-parallel specifies global maximum for parallelism
+ max_par_global = int(build_option('max_parallel'))
+ # note: 'max_parallel' and 'maxparallel; are the same easyconfig parameter,
+ # since 'max_parallel' is an alternative name for 'maxparallel'
+ max_par_ec = self.cfg['max_parallel']
+ # take into account that False is a valid value for max_parallel
+ if max_par_ec is False:
+ max_par_ec = 1
+ # if max_parallel is not specified in easyconfig, we take the global value
+ if max_par_ec is None:
+ max_par = max_par_global
+ # take minimum value if both are specified
else:
- par = min(int(par), int(cfg_par))
- self.log.debug("Desired parallelism: minimum of 'parallel' build option/easyconfig parameter: %s", par)
+ max_par = min(int(max_par_ec), max_par_global)
- par = det_parallelism(par, maxpar=self.cfg['maxparallel'])
- self.log.info("Setting parallelism: %s" % par)
- self.cfg['parallel'] = par
+ par = det_parallelism(par=par, maxpar=max_par)
+ self.log.info(f"Setting parallelism: {par}")
+ self.cfg.parallel = par
def remove_module_file(self):
"""Remove module file (if it exists), and check for ghost installation directory (and deal with it)."""
@@ -2306,8 +2510,6 @@ def check_readiness_step(self):
"""
Verify if all is ok to start build.
"""
- self.set_parallel()
-
# check whether modules are loaded
loadedmods = self.modules_tool.loaded_modules()
if len(loadedmods) > 0:
@@ -2412,8 +2614,7 @@ def fetch_step(self, skip_checksums=False):
# compute checksums for all source and patch files
if not (skip_checksums or self.dry_run):
for fil in self.src + self.patches:
- # report both MD5 and SHA256 checksums, since both are valid default checksum types
- for checksum_type in [CHECKSUM_TYPE_MD5, CHECKSUM_TYPE_SHA256]:
+ for checksum_type in [CHECKSUM_TYPE_SHA256]:
fil[checksum_type] = compute_checksum(fil['path'], checksum_type=checksum_type)
self.log.info("%s checksum for %s: %s", checksum_type, fil['path'], fil[checksum_type])
@@ -2472,7 +2673,10 @@ def checksum_step(self):
elif build_option('ignore_checksums'):
print_warning("Ignoring failing checksum verification for %s" % fil['name'])
else:
- raise EasyBuildError("Checksum verification for %s using %s failed.", fil['path'], fil['checksum'])
+ raise EasyBuildError(
+ "Checksum verification for %s using %s failed.", fil['path'], fil['checksum'],
+ exit_code=EasyBuildExit.FAIL_CHECKSUM
+ )
def check_checksums_for(self, ent, sub='', source_cnt=None):
"""
@@ -2481,13 +2685,22 @@ def check_checksums_for(self, ent, sub='', source_cnt=None):
ec_fn = os.path.basename(self.cfg.path)
checksum_issues = []
- sources = ent.get('sources', [])
- patches = ent.get('patches', []) + ent.get('postinstallpatches', [])
- checksums = ent.get('checksums', [])
+ # try to get value with templates resolved, but fall back to not resolving templates;
+ # this is better for error reporting that includes names of source files
+ try:
+ sources = ent.get('sources', [])
+ patches = ent.get('patches', []) + ent.get('postinstallpatches', [])
+ checksums = ent.get('checksums', [])
+ except EasyBuildError:
+ if isinstance(ent, EasyConfig):
+ sources = ent.get_ref('sources')
+ patches = ent.get_ref('patches') + ent.get_ref('postinstallpatches')
+ checksums = ent.get_ref('checksums')
+
# Single source should be re-wrapped as a list, and checksums with it
if isinstance(sources, dict):
sources = [sources]
- if isinstance(checksums, string_type):
+ if isinstance(checksums, str):
checksums = [checksums]
if not checksums:
@@ -2562,10 +2775,10 @@ def check_checksums(self):
checksum_issues.extend(self.check_checksums_for(self.cfg))
# also check checksums for extensions
- for ext in self.cfg['exts_list']:
+ for ext in self.cfg.get_ref('exts_list'):
# just skip extensions for which only a name is specified
# those are just there to check for things that are in the "standard library"
- if not isinstance(ext, string_type):
+ if not isinstance(ext, str):
ext_name = ext[0]
# take into account that extension may be a 2-tuple with just name/version
ext_opts = ext[2] if len(ext) == 3 else {}
@@ -2655,7 +2868,7 @@ def prepare_step(self, start_dir=True, load_tc_deps_modules=True):
if build_option('rpath_override_dirs') is not None:
# make sure we have a list
rpath_overrides = build_option('rpath_override_dirs')
- if isinstance(rpath_overrides, string_type):
+ if isinstance(rpath_overrides, str):
rpath_override_dirs = rpath_overrides.split(':')
# Filter out any empty values
rpath_override_dirs = list(filter(None, rpath_override_dirs))
@@ -2741,18 +2954,19 @@ def test_step(self):
"""Run unit tests provided by software (if any)."""
unit_test_cmd = self.cfg['runtest']
if unit_test_cmd:
-
- self.log.debug("Trying to execute %s as a command for running unit tests...", unit_test_cmd)
- (out, _) = run_cmd(unit_test_cmd, log_all=True, simple=False)
-
- return out
+ self.log.debug(f"Trying to execute {unit_test_cmd} as a command for running unit tests...")
+ res = run_shell_cmd(unit_test_cmd)
+ return res.output
def _test_step(self):
"""Run the test_step and handles failures"""
try:
self.test_step()
- except EasyBuildError as err:
- self.report_test_failure(err)
+ except RunShellCmdError as err:
+ err.print()
+ ec_path = os.path.basename(self.cfg.path)
+ error_msg = f"shell command '{err.cmd_name} ...' failed in test step for {ec_path}"
+ self.report_test_failure(error_msg)
def stage_install_step(self):
"""Install in a stage directory before actual installation."""
@@ -2782,7 +2996,7 @@ def init_ext_instances(self):
# obtain name and module path for default extention class
exts_defaultclass = self.cfg['exts_defaultclass']
- if isinstance(exts_defaultclass, string_type):
+ if isinstance(exts_defaultclass, str):
# proper way: derive module path from specified class name
default_class = exts_defaultclass
default_class_modpath = get_module_path(default_class, generic=True)
@@ -2793,7 +3007,6 @@ def init_ext_instances(self):
exts_cnt = len(self.exts)
self.update_exts_progress_bar("creating internal datastructures for extensions")
-
for idx, ext in enumerate(self.exts):
ext_name = ext['name']
self.log.debug("Creating class instance for extension %s...", ext_name)
@@ -2886,7 +3099,7 @@ def extensions_step(self, fetch=False, install=True):
fake_mod_data = self.load_fake_module(purge=True, extra_modules=build_dep_mods)
- start_progress_bar(PROGRESS_BAR_EXTENSIONS, len(self.cfg['exts_list']))
+ start_progress_bar(PROGRESS_BAR_EXTENSIONS, len(self.cfg.get_ref('exts_list')))
self.prepare_for_extensions()
@@ -2910,7 +3123,7 @@ def extensions_step(self, fetch=False, install=True):
if self.skip:
self.skip_extensions()
- self.install_extensions(install=install)
+ self.install_all_extensions(install=install)
# cleanup (unload fake module, remove fake module dir)
if fake_mod_data:
@@ -2959,7 +3172,7 @@ def fix_shebang(self):
shebang_regex = re.compile(r'^#![ ]*.*[/ ]%s.*' % lang)
fix_shebang_for = self.cfg['fix_%s_shebang_for' % lang]
if fix_shebang_for:
- if isinstance(fix_shebang_for, string_type):
+ if isinstance(fix_shebang_for, str):
fix_shebang_for = [fix_shebang_for]
shebang = '#!%s %s' % (env_for_shebang, lang)
@@ -3001,17 +3214,17 @@ def run_post_install_commands(self, commands=None):
commands = self.cfg['postinstallcmds']
if commands:
- self.log.debug("Specified post install commands: %s", commands)
+ self.log.debug(f"Specified post install commands: {commands}")
# make sure we have a list of commands
if not isinstance(commands, (list, tuple)):
- error_msg = "Invalid value for 'postinstallcmds', should be list or tuple of strings: %s"
- raise EasyBuildError(error_msg, commands)
+ error_msg = f"Invalid value for 'postinstallcmds', should be list or tuple of strings: {commands}"
+ raise EasyBuildError(error_msg)
for cmd in commands:
- if not isinstance(cmd, string_type):
- raise EasyBuildError("Invalid element in 'postinstallcmds', not a string: %s", cmd)
- run_cmd(cmd, simple=True, log_ok=True, log_all=True)
+ if not isinstance(cmd, str):
+ raise EasyBuildError(f"Invalid element in 'postinstallcmds', not a string: {cmd}")
+ run_shell_cmd(cmd)
def apply_post_install_patches(self, patches=None):
"""
@@ -3036,15 +3249,12 @@ def print_post_install_messages(self):
def post_install_step(self):
"""
- Do some postprocessing
+ [DEPRECATED] Do some postprocessing
- run post install commands if any were specified
"""
-
- self.run_post_install_commands()
- self.apply_post_install_patches()
- self.print_post_install_messages()
-
- self.fix_shebang()
+ # even though post_install_step is deprecated in easyblocks we need to keep this here until it is
+ # removed in 6.0 for easyblocks calling super(EB_xxx, self).post_install_step()
+ # The deprecation warning for those is below, in post_processing_step().
lib_dir = os.path.join(self.installdir, 'lib')
lib64_dir = os.path.join(self.installdir, 'lib64')
@@ -3053,18 +3263,44 @@ def post_install_step(self):
# However for each
in $LIBRARY_PATH (where is often /lib) it searches /../lib64 first.
# So we create /lib64 as a symlink to /lib to make it prefer EB installed libraries.
# See https://github.com/easybuilders/easybuild-easyconfigs/issues/5776
- if build_option('lib64_lib_symlink'):
- if os.path.exists(lib_dir) and not os.path.exists(lib64_dir):
- # create *relative* 'lib64' symlink to 'lib';
- # see https://github.com/easybuilders/easybuild-framework/issues/3564
- symlink('lib', lib64_dir, use_abspath_source=False)
+ if build_option('lib64_lib_symlink') and os.path.exists(lib_dir) and not os.path.exists(lib64_dir):
+ # create *relative* 'lib64' symlink to 'lib';
+ # see https://github.com/easybuilders/easybuild-framework/issues/3564
+ symlink('lib', lib64_dir, use_abspath_source=False)
# symlink lib to lib64, which is helpful on OpenSUSE;
# see https://github.com/easybuilders/easybuild-framework/issues/3549
- if build_option('lib_lib64_symlink'):
- if os.path.exists(lib64_dir) and not os.path.exists(lib_dir):
- # create *relative* 'lib' symlink to 'lib64';
- symlink('lib64', lib_dir, use_abspath_source=False)
+ if build_option('lib_lib64_symlink') and os.path.exists(lib64_dir) and not os.path.exists(lib_dir):
+ # create *relative* 'lib' symlink to 'lib64';
+ symlink('lib64', lib_dir, use_abspath_source=False)
+
+ self.run_post_install_commands()
+ self.apply_post_install_patches()
+ self.print_post_install_messages()
+
+ self.fix_shebang()
+
+ def post_processing_step(self):
+ """
+ Do some postprocessing
+ - run post install commands if any were specified
+ """
+ # if post_install_step() is present in the easyblock print a deprecation warning
+ # with EB 6.0, post_install_step() can be renamed to post_processing_step, and this method deleted.
+
+ if self.post_install_step.__qualname__ != "EasyBlock.post_install_step":
+ self.log.deprecated(
+ "EasyBlock.post_install_step() is deprecated, use EasyBlock.post_processing_step() instead.",
+ '6.0',
+ )
+ return self.post_install_step()
+
+ def _dispatch_sanity_check_step(self, *args, **kwargs):
+ """Decide whether to run the dry-run or the real version of the sanity-check step"""
+ if self.dry_run:
+ self._sanity_check_step_dry_run(*args, **kwargs)
+ else:
+ self._sanity_check_step(*args, **kwargs)
def sanity_check_step(self, *args, **kwargs):
"""
@@ -3072,15 +3308,11 @@ def sanity_check_step(self, *args, **kwargs):
- if *any* of the files/subdirectories in the installation directory listed
in sanity_check_paths are non-existent (or empty), the sanity check fails
"""
- if self.dry_run:
- self._sanity_check_step_dry_run(*args, **kwargs)
-
# handling of extensions that were installed for multiple dependency versions is done in ExtensionEasyBlock
- elif self.cfg['multi_deps'] and not self.is_extension:
+ if self.cfg['multi_deps'] and not self.is_extension:
self._sanity_check_step_multi_deps(*args, **kwargs)
-
else:
- self._sanity_check_step(*args, **kwargs)
+ self._dispatch_sanity_check_step(*args, **kwargs)
def _sanity_check_step_multi_deps(self, *args, **kwargs):
"""Perform sanity check for installations that iterate over a list a versions for particular dependencies."""
@@ -3112,7 +3344,7 @@ def _sanity_check_step_multi_deps(self, *args, **kwargs):
self.log.info(info_msg)
kwargs['extra_modules'] = extra_modules
- self._sanity_check_step(*args, **kwargs)
+ self._dispatch_sanity_check_step(*args, **kwargs)
# restore list of lists of build dependencies & stop iterating again
self.cfg['builddependencies'] = builddeps
@@ -3125,46 +3357,52 @@ def sanity_check_rpath(self, rpath_dirs=None, check_readelf_rpath=True):
fails = []
- # hard reset $LD_LIBRARY_PATH before running RPATH sanity check
- orig_env = env.unset_env_vars(['LD_LIBRARY_PATH'])
+ if build_option('strict_rpath_sanity_check'):
+ self.log.info("Unsetting $LD_LIBRARY_PATH since strict RPATH sanity check is enabled...")
+ # hard reset $LD_LIBRARY_PATH before running RPATH sanity check
+ orig_env = env.unset_env_vars(['LD_LIBRARY_PATH'])
+ else:
+ self.log.info("Not unsetting $LD_LIBRARY_PATH since strict RPATH sanity check is disabled...")
+ orig_env = None
- self.log.debug("$LD_LIBRARY_PATH during RPATH sanity check: %s", os.getenv('LD_LIBRARY_PATH', '(empty)'))
- self.log.debug("List of loaded modules: %s", self.modules_tool.list())
+ ld_library_path = os.getenv('LD_LIBRARY_PATH', '(empty)')
+ self.log.debug(f"$LD_LIBRARY_PATH during RPATH sanity check: {ld_library_path}")
+ modules_list = self.modules_tool.list()
+ self.log.debug(f"List of loaded modules: {modules_list}")
not_found_regex = re.compile(r'(\S+)\s*\=\>\s*not found')
- readelf_rpath_regex = re.compile('(RPATH)', re.M)
+ lib_path_regex = re.compile(r'\S+\s*\=\>\s*(\S+)')
+ readelf_rpath_regex = re.compile(r'\(RPATH\)', re.M)
# List of libraries that should be exempt from the RPATH sanity check;
# For example, libcuda.so.1 should never be RPATH-ed by design,
# see https://github.com/easybuilders/easybuild-framework/issues/4095
filter_rpath_sanity_libs = build_option('filter_rpath_sanity_libs')
- msg = "Ignoring the following libraries if they are not found by RPATH sanity check: %s"
- self.log.info(msg, filter_rpath_sanity_libs)
+ self.log.info("Ignoring the following libraries if they are not found by RPATH sanity check: %s",
+ filter_rpath_sanity_libs)
if rpath_dirs is None:
rpath_dirs = self.cfg['bin_lib_subdirs'] or self.bin_lib_subdirs()
if not rpath_dirs:
rpath_dirs = DEFAULT_BIN_LIB_SUBDIRS
- self.log.info("Using default subdirectories for binaries/libraries to verify RPATH linking: %s",
- rpath_dirs)
+ self.log.info(f"Using default subdirs for binaries/libraries to verify RPATH linking: {rpath_dirs}")
else:
- self.log.info("Using specified subdirectories for binaries/libraries to verify RPATH linking: %s",
- rpath_dirs)
+ self.log.info(f"Using specified subdirs for binaries/libraries to verify RPATH linking: {rpath_dirs}")
for dirpath in [os.path.join(self.installdir, d) for d in rpath_dirs]:
if os.path.exists(dirpath):
- self.log.debug("Sanity checking RPATH for files in %s", dirpath)
+ self.log.debug(f"Sanity checking RPATH for files in {dirpath}")
for path in [os.path.join(dirpath, x) for x in os.listdir(dirpath)]:
- self.log.debug("Sanity checking RPATH for %s", path)
+ self.log.debug(f"Sanity checking RPATH for {path}")
out = get_linked_libs_raw(path)
if out is None:
- msg = "Failed to determine dynamically linked libraries for %s, "
+ msg = "Failed to determine dynamically linked libraries for {path}, "
msg += "so skipping it in RPATH sanity check"
- self.log.debug(msg, path)
+ self.log.debug(msg)
else:
# check whether all required libraries are found via 'ldd'
matches = re.findall(not_found_regex, out)
@@ -3172,36 +3410,46 @@ def sanity_check_rpath(self, rpath_dirs=None, check_readelf_rpath=True):
# For each match, check if the library is in the exception list
for match in matches:
if match in filter_rpath_sanity_libs:
- msg = "Library %s not found for %s, but ignored "
- msg += "since it is on the rpath exception list: %s"
- self.log.info(msg, match, path, filter_rpath_sanity_libs)
+ msg = f"Library {match} not found for {path}, but ignored "
+ msg += f"since it is on the rpath exception list: {filter_rpath_sanity_libs}"
+ self.log.info(msg)
else:
- fail_msg = "Library %s not found for %s" % (match, path)
+ fail_msg = f"Library {match} not found for {path}"
self.log.warning(fail_msg)
fails.append(fail_msg)
+
+ # if any libraries were not found, log whether dependency libraries have an RPATH section
+ if fails:
+ lib_paths = re.findall(lib_path_regex, out)
+ for lib_path in lib_paths:
+ self.log.info(f"Checking whether dependency library {lib_path} has RPATH section")
+ res = run_shell_cmd(f"readelf -d {lib_path}", fail_on_error=False)
+ if res.exit_code:
+ self.log.info(f"No RPATH section found in {lib_path}")
else:
- self.log.debug("Output of 'ldd %s' checked, looks OK", path)
+ self.log.debug(f"Output of 'ldd {path}' checked, looks OK")
# check whether RPATH section in 'readelf -d' output is there
if check_readelf_rpath:
fail_msg = None
- out, ec = run_cmd("readelf -d %s" % path, simple=False, trace=False)
- if ec:
- fail_msg = "Failed to run 'readelf %s': %s" % (path, out)
- elif not readelf_rpath_regex.search(out):
- fail_msg = "No '(RPATH)' found in 'readelf -d' output for %s: %s" % (path, out)
+ res = run_shell_cmd(f"readelf -d {path}", fail_on_error=False, hidden=True)
+ if res.exit_code != EasyBuildExit.SUCCESS:
+ fail_msg = f"Failed to run 'readelf -d {path}': {res.output}"
+ elif not readelf_rpath_regex.search(res.output):
+ fail_msg = f"No '(RPATH)' found in 'readelf -d' output for {path}"
if fail_msg:
self.log.warning(fail_msg)
fails.append(fail_msg)
else:
- self.log.debug("Output of 'readelf -d %s' checked, looks OK", path)
+ self.log.debug(f"Output of 'readelf -d {path}' checked, looks OK")
else:
self.log.debug("Skipping the RPATH section check with 'readelf -d', as requested")
else:
- self.log.debug("Not sanity checking files in non-existing directory %s", dirpath)
+ self.log.debug(f"Not sanity checking files in non-existing directory {dirpath}")
- env.restore_env_vars(orig_env)
+ if orig_env:
+ env.restore_env_vars(orig_env)
return fails
@@ -3330,6 +3578,19 @@ def regex_for_lib(lib):
return fail_msg
+ def sanity_check_mod_files(self):
+ """
+ Check installation for Fortran .mod files
+ """
+ self.log.debug(f"Checking for .mod files in install directory {self.installdir}...")
+ mod_files = glob.glob(os.path.join(self.installdir, '**', '*.mod'), recursive=True)
+
+ fail_msg = None
+ if mod_files:
+ fail_msg = f"One or more .mod files found in {self.installdir}: " + ', '.join(mod_files)
+
+ return fail_msg
+
def _sanity_check_step_common(self, custom_paths, custom_commands):
"""
Determine sanity check paths and commands to use.
@@ -3359,7 +3620,7 @@ def _sanity_check_step_common(self, custom_paths, custom_commands):
# if no sanity_check_paths are specified in easyconfig,
# we fall back to the ones provided by the easyblock via custom_paths
if custom_paths:
- paths = custom_paths
+ paths = self.cfg.resolve_template(custom_paths)
self.log.info("Using customized sanity check paths: %s", paths)
# if custom_paths is empty, we fall back to a generic set of paths:
# non-empty bin/ + /lib or /lib64 directories
@@ -3373,14 +3634,13 @@ def _sanity_check_step_common(self, custom_paths, custom_commands):
# if enhance_sanity_check is enabled *and* sanity_check_paths are specified in the easyconfig,
# those paths are used to enhance the paths provided by the easyblock
if enhance_sanity_check and ec_paths:
- for key in ec_paths:
- val = ec_paths[key]
+ for key, val in ec_paths.items():
if isinstance(val, list):
paths[key] = paths.get(key, []) + val
else:
- error_pattern = "Incorrect value type in sanity_check_paths, should be a list: "
- error_pattern += "%s (type: %s)" % (val, type(val))
- raise EasyBuildError(error_pattern)
+ error_msg = "Incorrect value type in sanity_check_paths, should be a list: "
+ error_msg += "%s (type: %s)" % (val, type(val))
+ raise EasyBuildError(error_msg)
self.log.info("Enhanced sanity check paths after taking into account easyconfig file: %s", paths)
sorted_keys = sorted(paths.keys())
@@ -3410,7 +3670,7 @@ def _sanity_check_step_common(self, custom_paths, custom_commands):
self.log.info("Using (only) sanity check commands specified by easyconfig file: %s", commands)
else:
if custom_commands:
- commands = custom_commands
+ commands = self.cfg.resolve_template(custom_commands)
self.log.info("Using customised sanity check commands: %s", commands)
else:
commands = []
@@ -3424,7 +3684,7 @@ def _sanity_check_step_common(self, custom_paths, custom_commands):
for i, command in enumerate(commands):
# set command to default. This allows for config files with
# non-tuple commands
- if isinstance(command, string_type):
+ if isinstance(command, str):
self.log.debug("Using %s as sanity check command" % command)
commands[i] = command
else:
@@ -3584,7 +3844,7 @@ def xs2str(xs):
(typ, check_fn) = path_keys_and_check[key]
for xs in paths[key]:
- if isinstance(xs, string_type):
+ if isinstance(xs, str):
xs = (xs,)
elif not isinstance(xs, tuple):
raise EasyBuildError("Unsupported type %s encountered in '%s', not a string or tuple",
@@ -3608,6 +3868,7 @@ def xs2str(xs):
if not found:
sanity_check_fail_msg = "no %s found at %s in %s" % (typ, xs2str(xs), self.installdir)
self.sanity_check_fail_msgs.append(sanity_check_fail_msg)
+ self.exit_code = EasyBuildExit.FAIL_SANITY_CHECK
self.log.warning("Sanity check: %s", sanity_check_fail_msg)
trace_msg("%s %s found: %s" % (typ, xs2str(xs), ('FAILED', 'OK')[found]))
@@ -3620,24 +3881,26 @@ def xs2str(xs):
if self.toolchain.mpi_family() and self.toolchain.mpi_family() in toolchain.OPENMPI:
env.setvar('OMPI_MCA_rmaps_base_oversubscribe', '1')
- # change to install directory (better environment for running tests)
- if os.path.isdir(self.installdir):
- change_dir(self.installdir)
+ # run sanity checks from an empty temp directory
+ # using the build or installation directory can produce false positives and polute them with files
+ sanity_check_work_dir = tempfile.mkdtemp(prefix='eb-sanity-check-')
# run sanity check commands
- for command in commands:
+ for cmd in commands:
- trace_msg("running command '%s' ..." % command)
+ trace_msg(f"running command '{cmd}' ...")
- out, ec = run_cmd(command, simple=False, log_ok=False, log_all=False, trace=False)
- if ec != 0:
- fail_msg = "sanity check command %s exited with code %s (output: %s)" % (command, ec, out)
+ res = run_shell_cmd(cmd, work_dir=sanity_check_work_dir, fail_on_error=False, hidden=True)
+ if res.exit_code != EasyBuildExit.SUCCESS:
+ fail_msg = f"sanity check command {cmd} failed with exit code {res.exit_code} (output: {res.output})"
self.sanity_check_fail_msgs.append(fail_msg)
- self.log.warning("Sanity check: %s" % self.sanity_check_fail_msgs[-1])
+ self.exit_code = EasyBuildExit.FAIL_SANITY_CHECK
+ self.log.warning(f"Sanity check: {fail_msg}")
else:
- self.log.info("sanity check command %s ran successfully! (output: %s)" % (command, out))
+ self.log.info(f"sanity check command {cmd} ran successfully! (output: {res.output})")
- trace_msg("result for command '%s': %s" % (command, ('FAILED', 'OK')[ec == 0]))
+ cmd_result_str = ('FAILED', 'OK')[res.exit_code == EasyBuildExit.SUCCESS]
+ trace_msg(f"result for command '{cmd}': {cmd_result_str}")
# also run sanity check for extensions (unless we are an extension ourselves)
if not extension:
@@ -3651,6 +3914,16 @@ def xs2str(xs):
self.log.warning("Check for required/banned linked shared libraries failed!")
self.sanity_check_fail_msgs.append(linked_shared_lib_fails)
+ # software installed with GCCcore toolchain should not have Fortran module files (.mod),
+ # unless that's explicitly allowed
+ if self.toolchain.name in ('GCCcore',) and not self.cfg['skip_mod_files_sanity_check']:
+ mod_files_found_msg = self.sanity_check_mod_files()
+ if mod_files_found_msg:
+ if build_option('fail_on_mod_files_gcccore'):
+ self.sanity_check_fail_msgs.append(mod_files_found_msg)
+ else:
+ print_warning(mod_files_found_msg)
+
# cleanup
if self.fake_mod_data:
self.clean_up_fake_module(self.fake_mod_data)
@@ -3667,9 +3940,12 @@ def xs2str(xs):
# pass or fail
if self.sanity_check_fail_msgs:
- raise EasyBuildError("Sanity check failed: %s", '\n'.join(self.sanity_check_fail_msgs))
- else:
- self.log.debug("Sanity check passed!")
+ raise EasyBuildError(
+ "Sanity check failed: " + '\n'.join(self.sanity_check_fail_msgs),
+ exit_code=EasyBuildExit.FAIL_SANITY_CHECK,
+ )
+
+ self.log.debug("Sanity check passed!")
def _set_module_as_default(self, fake=False):
"""
@@ -3699,7 +3975,7 @@ def cleanup_step(self):
# make sure we're out of the dir we're removing
change_dir(self.orig_workdir)
- self.log.info("Cleaning up builddir %s (in %s)", self.builddir, os.getcwd())
+ self.log.info("Cleaning up builddir %s (in %s)", self.builddir, get_cwd())
try:
remove_dir(self.builddir)
@@ -3754,7 +4030,7 @@ def make_module_step(self, fake=False):
txt += self.make_module_deppaths()
txt += self.make_module_dep()
txt += self.make_module_extend_modpath()
- txt += self.make_module_req()
+ txt += self.make_module_req(fake=fake)
txt += self.make_module_extra()
txt += self.make_module_footer()
@@ -3769,8 +4045,13 @@ def make_module_step(self, fake=False):
for line in txt.split('\n'):
self.dry_run_msg(INDENT_4SPACES + line)
else:
- write_file(mod_filepath, txt)
- self.log.info("Module file %s written: %s", mod_filepath, txt)
+ try:
+ write_file(mod_filepath, txt)
+ self.log.info("Module file %s written: %s", mod_filepath, txt)
+ except EasyBuildError:
+ raise EasyBuildError(
+ f"Unable to write Module file {mod_filepath}", exit_code=EasyBuildExit.FAIL_MODULE_WRITE
+ )
# if backup module file is there, print diff with newly generated module file
if self.mod_file_backup and not fake:
@@ -3878,26 +4159,26 @@ def test_cases_step(self):
for test in self.cfg['tests']:
change_dir(self.orig_workdir)
if os.path.isabs(test):
- path = test
+ test_cmd = test
else:
for source_path in source_paths():
- path = os.path.join(source_path, self.name, test)
- if os.path.exists(path):
+ test_cmd = os.path.join(source_path, self.name, test)
+ if os.path.exists(test_cmd):
break
- if not os.path.exists(path):
- raise EasyBuildError("Test specifies invalid path: %s", path)
+ if not os.path.exists(test_cmd):
+ raise EasyBuildError(f"Test specifies invalid path: {test_cmd}")
try:
- self.log.debug("Running test %s" % path)
- run_cmd(path, log_all=True, simple=True)
+ self.log.debug(f"Running test {test_cmd}")
+ run_shell_cmd(test_cmd)
except EasyBuildError as err:
- raise EasyBuildError("Running test %s failed: %s", path, err)
+ raise EasyBuildError(f"Running test {test_cmd} failed: {err}")
def update_config_template_run_step(self):
"""Update the the easyconfig template dictionary with easyconfig.TEMPLATE_NAMES_EASYBLOCK_RUN_STEP names"""
for name in TEMPLATE_NAMES_EASYBLOCK_RUN_STEP:
- self.cfg.template_values[name[0]] = str(getattr(self, name[0], None))
+ self.cfg.template_values[name] = str(getattr(self, name, None))
self.cfg.generate_template_values()
def skip_step(self, step, skippable):
@@ -3966,7 +4247,7 @@ def run_step(self, step, step_methods):
for step_method in step_methods:
# Remove leading underscore from e.g. "_test_step"
- method_name = extract_method_name(step_method).lstrip('_')
+ method_name = '_'.join(step_method.__code__.co_names).lstrip('_')
self.log.info("Running method %s part of step %s", method_name, step)
if self.dry_run:
@@ -4018,15 +4299,6 @@ def ready_step_spec(initial):
return get_step(READY_STEP, "creating build dir, resetting environment", ready_substeps, False,
initial=initial)
- source_substeps = [
- (False, lambda x: x.checksum_step),
- (True, lambda x: x.extract_step),
- ]
-
- def source_step_spec(initial):
- """Return source step specified."""
- return get_step(SOURCE_STEP, "unpacking", source_substeps, True, initial=initial)
-
install_substeps = [
(False, lambda x: x.stage_install_step),
(False, lambda x: x.make_installdir),
@@ -4040,6 +4312,7 @@ def install_step_spec(initial):
# format for step specifications: (step_name, description, list of functions, skippable)
# core steps that are part of the iterated loop
+ extract_step_spec = (EXTRACT_STEP, "unpacking", [lambda x: x.extract_step], True)
patch_step_spec = (PATCH_STEP, 'patching', [lambda x: x.patch_step], True)
prepare_step_spec = (PREPARE_STEP, 'preparing', [lambda x: x.prepare_step], False)
configure_step_spec = (CONFIGURE_STEP, 'configuring', [lambda x: x.configure_step], True)
@@ -4049,9 +4322,10 @@ def install_step_spec(initial):
# part 1: pre-iteration + first iteration
steps_part1 = [
- (FETCH_STEP, 'fetching files', [lambda x: x.fetch_step], False),
+ (FETCH_STEP, "fetching files and verifying checksums",
+ [lambda x: x.fetch_step, lambda x: x.checksum_step], False),
ready_step_spec(True),
- source_step_spec(True),
+ extract_step_spec,
patch_step_spec,
prepare_step_spec,
configure_step_spec,
@@ -4065,7 +4339,7 @@ def install_step_spec(initial):
# not all parts of all steps need to be rerun (see e.g., ready, prepare)
steps_part2 = [
ready_step_spec(False),
- source_step_spec(False),
+ extract_step_spec,
patch_step_spec,
prepare_step_spec,
configure_step_spec,
@@ -4077,7 +4351,7 @@ def install_step_spec(initial):
# part 3: post-iteration part
steps_part3 = [
(POSTITER_STEP, 'restore after iterating', [lambda x: x.post_iter_step], False),
- (POSTPROC_STEP, 'postprocessing', [lambda x: x.post_install_step], True),
+ (POSTPROC_STEP, 'postprocessing', [lambda x: x.post_processing_step], True),
(SANITYCHECK_STEP, 'sanity checking', [lambda x: x.sanity_check_step], True),
(CLEANUP_STEP, 'cleaning up', [lambda x: x.cleanup_step], False),
(MODULE_STEP, 'creating module', [lambda x: x.make_module_step], False),
@@ -4153,6 +4427,17 @@ def run_all_steps(self, run_test_cases):
start_time = datetime.now()
try:
self.run_step(step_name, step_methods)
+ except RunShellCmdError as err:
+ err.print()
+ error_msg = (
+ f"shell command '{err.cmd_name} ...' failed with exit code {err.exit_code} "
+ f"in {step_name} step for {os.path.basename(self.cfg.path)}"
+ )
+ try:
+ step_exit_code = EasyBuildExit[f"FAIL_{step_name.upper()}_STEP"]
+ except KeyError:
+ step_exit_code = EasyBuildExit.ERROR
+ raise EasyBuildError(error_msg, exit_code=step_exit_code) from err
finally:
if not self.dry_run:
step_duration = datetime.now() - start_time
@@ -4190,6 +4475,54 @@ def print_dry_run_note(loc, silent=True):
dry_run_msg(msg, silent=silent)
+def copy_build_dirs_logs_failed_install(application_log, silent, app, easyconfig):
+ """
+ Copy build directories and log files for failed installation (if desired)
+ """
+ logs_path = get_failed_install_logs_path(easyconfig)
+ build_dirs_path = get_failed_install_build_dirs_path(easyconfig)
+
+ # there may be multiple log files, or the file name may be different due to zipping
+ logs = glob.glob(f"{application_log}*")
+
+ timestamp = time.strftime('%Y%m%d-%H%M%S')
+ salt = ''.join(random.choices(ascii_letters, k=5))
+ unique_subdir = f'{timestamp}-{salt}'
+
+ operation_args = []
+
+ if logs_path and logs:
+ logs_path = os.path.join(logs_path, unique_subdir)
+
+ if is_parent_path(app.builddir, logs_path):
+ msg = "Path to copy log files of failed installs to is subdirectory of build directory; not copying"
+ print_warning(msg, log=_log, silent=silent)
+ else:
+ msg = f"Log file(s) of failed installation copied to {logs_path}"
+ operation_args.append((copy_file, logs, logs_path, msg))
+
+ if build_dirs_path and os.path.isdir(app.builddir):
+ build_dirs_path = os.path.join(build_dirs_path, unique_subdir)
+
+ if is_parent_path(app.builddir, build_dirs_path):
+ msg = "Path to copy build dirs of failed installs to is subdirectory of build directory; not copying"
+ print_warning(msg, log=_log, silent=silent)
+ else:
+ msg = f"Build directory of failed installation copied to {build_dirs_path}"
+
+ def operation(src, dest):
+ copy_dir(src, dest, dirs_exist_ok=True)
+
+ operation_args.append((operation, [app.builddir], build_dirs_path, msg))
+
+ persistence_paths = create_non_existing_paths(target_path for (_, _, target_path, _) in operation_args)
+
+ for idx, (operation, paths, _, msg) in enumerate(operation_args):
+ for path in paths:
+ operation(path, persistence_paths[idx])
+ print_msg(msg, log=_log, silent=silent)
+
+
def build_and_install_one(ecdict, init_env):
"""
Build the software
@@ -4220,11 +4553,10 @@ def build_and_install_one(ecdict, init_env):
# restore original environment, and then sanitize it
_log.info("Resetting environment")
- run.errors_found_in_log = 0
restore_env(init_env)
sanitize_env()
- cwd = os.getcwd()
+ cwd = get_cwd()
# load easyblock
easyblock = build_option('easyblock')
@@ -4237,7 +4569,7 @@ def build_and_install_one(ecdict, init_env):
try:
app_class = get_easyblock_class(easyblock, name=name)
app = app_class(ecdict['ec'])
- _log.info("Obtained application instance of for %s (easyblock: %s)" % (name, easyblock))
+ _log.info("Obtained application instance for %s (easyblock: %s)" % (name, easyblock))
except EasyBuildError as err:
print_error("Failed to get application instance for %s (easyblock: %s): %s" % (name, easyblock, err.msg),
silent=silent)
@@ -4254,7 +4586,8 @@ def build_and_install_one(ecdict, init_env):
app.cfg['skip'] = skip
# build easyconfig
- errormsg = '(no error)'
+ error_msg = '(no error)'
+ exit_code = None
# timing info
start_time = time.time()
try:
@@ -4292,9 +4625,11 @@ def build_and_install_one(ecdict, init_env):
adjust_permissions(app.installdir, stat.S_IWUSR, add=False, recursive=True)
except EasyBuildError as err:
- first_n = 300
- errormsg = "build failed (first %d chars): %s" % (first_n, err.msg[:first_n])
- _log.warning(errormsg)
+ error_msg = err.msg
+ try:
+ exit_code = err.exit_code
+ except AttributeError:
+ exit_code = EasyBuildExit.ERROR
result = False
ended = 'ended'
@@ -4309,12 +4644,14 @@ def build_and_install_one(ecdict, init_env):
def ensure_writable_log_dir(log_dir):
"""Make sure we can write into the log dir"""
if build_option('read_only_installdir'):
- # temporarily re-enable write permissions for copying log/easyconfig to install dir
+ # temporarily re-enable write permissions for copying log/easyconfig to install dir,
+ # ensuring that we resolve symlinks
+ log_dir = os.path.realpath(log_dir)
if os.path.exists(log_dir):
adjust_permissions(log_dir, stat.S_IWUSR, add=True, recursive=True)
else:
parent_dir = os.path.dirname(log_dir)
- if os.path.exists(parent_dir):
+ if os.path.exists(parent_dir) and not (os.stat(parent_dir).st_mode & stat.S_IWUSR):
adjust_permissions(parent_dir, stat.S_IWUSR, add=True, recursive=False)
mkdir(log_dir, parents=True)
adjust_permissions(parent_dir, stat.S_IWUSR, add=False, recursive=False)
@@ -4395,7 +4732,7 @@ def ensure_writable_log_dir(log_dir):
copy_file(patch['path'], target)
_log.debug("Copied patch %s to %s", patch['path'], target)
- if build_option('read_only_installdir'):
+ if build_option('read_only_installdir') and not app.cfg['stop']:
# take away user write permissions (again)
perms = stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH
adjust_permissions(new_log_dir, perms, add=False, recursive=True)
@@ -4412,15 +4749,13 @@ def ensure_writable_log_dir(log_dir):
success = True
summary = 'COMPLETED'
succ = 'successfully'
+ exit_code = EasyBuildExit.SUCCESS
else:
# build failed
success = False
summary = 'FAILED'
-
- build_dir = ''
- if app.builddir:
- build_dir = " (build directory: %s)" % (app.builddir)
- succ = "unsuccessfully%s: %s" % (build_dir, errormsg)
+ succ = "unsuccessfully: " + error_msg
+ exit_code = EasyBuildExit.ERROR if exit_code is None else exit_code
# cleanup logs
app.close_log()
@@ -4429,11 +4764,6 @@ def ensure_writable_log_dir(log_dir):
req_time = time2str(end_timestamp - start_timestamp)
print_msg("%s: Installation %s %s (took %s)" % (summary, ended, succ, req_time), log=_log, silent=silent)
- # check for errors
- if run.errors_found_in_log > 0:
- _log.warning("%d possible error(s) were detected in the "
- "build logs, please verify the build.", run.errors_found_in_log)
-
if app.postmsg:
print_msg("\nWARNING: %s\n" % app.postmsg, log=_log, silent=silent)
@@ -4451,9 +4781,12 @@ def ensure_writable_log_dir(log_dir):
logs = glob.glob('%s*' % application_log)
print_msg("Results of the build can be found in the log file(s) %s" % ', '.join(logs), log=_log, silent=silent)
+ if not success:
+ copy_build_dirs_logs_failed_install(application_log, silent, app, ecdict['ec'])
+
del app
- return (success, application_log, errormsg)
+ return (success, application_log, error_msg, exit_code)
def copy_easyblocks_for_reprod(easyblock_instances, reprod_dir):
@@ -4463,7 +4796,7 @@ def copy_easyblocks_for_reprod(easyblock_instances, reprod_dir):
for easyblock_class in inspect.getmro(type(easyblock_instance)):
easyblock_path = inspect.getsourcefile(easyblock_class)
# if we reach EasyBlock, Extension or ExtensionEasyBlock class, we are done
- # (Extension and ExtensionEasyblock are hardcoded to avoid a cyclical import)
+ # (Extension and ExtensionEasyBlock are hardcoded to avoid a cyclical import)
if easyblock_class.__name__ in [EasyBlock.__name__, 'Extension', 'ExtensionEasyBlock']:
break
else:
@@ -4540,7 +4873,7 @@ def build_easyconfigs(easyconfigs, output_dir, test_results):
instance = get_easyblock_instance(ec)
apps.append(instance)
- base_dir = os.getcwd()
+ base_dir = get_cwd()
# keep track of environment right before initiating builds
# note: may be different from ORIG_OS_ENVIRON, since EasyBuild may have defined additional env vars itself by now
@@ -4674,7 +5007,7 @@ def inject_checksums(ecs, checksum_type):
def make_list_lines(values, indent_level):
"""Make lines for list of values."""
def to_str(s):
- if isinstance(s, string_type):
+ if isinstance(s, str):
return "'%s'" % s
else:
return str(s)
diff --git a/easybuild/framework/easyconfig/__init__.py b/easybuild/framework/easyconfig/__init__.py
index b19017e023..47385a8862 100644
--- a/easybuild/framework/easyconfig/__init__.py
+++ b/easybuild/framework/easyconfig/__init__.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/framework/easyconfig/constants.py b/easybuild/framework/easyconfig/constants.py
index 4fe8c9d7b5..e4ba2c3c6f 100644
--- a/easybuild/framework/easyconfig/constants.py
+++ b/easybuild/framework/easyconfig/constants.py
@@ -1,5 +1,5 @@
#
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -37,6 +37,7 @@
from easybuild.base import fancylogger
from easybuild.tools.build_log import print_warning
+from easybuild.tools.modules import MODULE_LOAD_ENV_HEADERS
from easybuild.tools.systemtools import KNOWN_ARCH_CONSTANTS, get_os_name, get_os_type, get_os_version
@@ -67,6 +68,7 @@ def _get_arch_constant():
'ARCH': (_get_arch_constant(), "CPU architecture of current system (aarch64, x86_64, ppc64le, ...)"),
'EXTERNAL_MODULE': (EXTERNAL_MODULE_MARKER, "External module marker"),
'HOME': (os.path.expanduser('~'), "Home directory ($HOME)"),
+ 'MODULE_LOAD_ENV_HEADERS': (MODULE_LOAD_ENV_HEADERS, "Environment variables with search paths to CPP headers"),
'OS_TYPE': (get_os_type(), "System type (e.g. 'Linux' or 'Darwin')"),
'OS_NAME': (get_os_name(), "System name (e.g. 'fedora' or 'RHEL')"),
'OS_VERSION': (get_os_version(), "System version"),
diff --git a/easybuild/framework/easyconfig/default.py b/easybuild/framework/easyconfig/default.py
index ce11d0457b..66ed417b61 100644
--- a/easybuild/framework/easyconfig/default.py
+++ b/easybuild/framework/easyconfig/default.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -110,8 +110,6 @@
'installopts': ['', 'Extra options for installation', BUILD],
'maxparallel': [None, 'Max degree of parallelism', BUILD],
'module_only': [False, 'Only generate module file', BUILD],
- 'parallel': [None, ('Degree of parallelism for e.g. make (default: based on the number of '
- 'cores, active cpuset and restrictions in ulimit)'), BUILD],
'patches': [[], "List of patches to apply", BUILD],
'prebuildopts': ['', 'Extra options pre-passed to build command.', BUILD],
'preconfigopts': ['', 'Extra options pre-passed to configure.', BUILD],
@@ -131,6 +129,7 @@
'sanity_check_paths': [{}, ("List of files and directories to check "
"(format: {'files':, 'dirs':})"), BUILD],
'skip': [False, "Skip existing software", BUILD],
+ 'skip_mod_files_sanity_check': [False, "Skip the check for .mod files in a GCCcore level install", BUILD],
'skipsteps': [[], "Skip these steps", BUILD],
'source_urls': [[], "List of URLs for source files", BUILD],
'sources': [[], "List of source files", BUILD],
@@ -155,8 +154,8 @@
FILEMANAGEMENT],
'keeppreviousinstall': [False, ('Boolean to keep the previous installation with identical '
'name. Experts only!'), FILEMANAGEMENT],
- 'keepsymlinks': [False, ('Boolean to determine whether symlinks are to be kept during copying '
- 'or if the content of the files pointed to should be copied'),
+ 'keepsymlinks': [True, ('Boolean to determine whether symlinks are to be kept during copying '
+ 'or if the content of the files pointed to should be copied'),
FILEMANAGEMENT],
'start_dir': [None, ('Path to start the make in. If the path is absolute, use that path. '
'If not, this is added to the guessed path.'), FILEMANAGEMENT],
@@ -190,12 +189,9 @@
'exts_list': [[], 'List with extensions added to the base installation', EXTENSIONS],
# MODULES easyconfig parameters
- 'allow_append_abs_path': [False, "Allow specifying absolute paths to append in modextrapaths_append", MODULES],
- 'allow_prepend_abs_path': [False, "Allow specifying absolute paths to prepend in modextrapaths", MODULES],
'include_modpath_extensions': [True, "Include $MODULEPATH extensions specified by module naming scheme.", MODULES],
'modaliases': [{}, "Aliases to be defined in module file", MODULES],
'modextrapaths': [{}, "Extra paths to be prepended in module file", MODULES],
- 'modextrapaths_append': [{}, "Extra paths to be appended in module file", MODULES],
'modextravars': [{}, "Extra environment variables to be added to module file", MODULES],
'modloadmsg': [{}, "Message that should be printed when generated module is loaded", MODULES],
'modunloadmsg': [{}, "Message that should be printed when generated module is unloaded", MODULES],
@@ -205,12 +201,13 @@
'moduleclass': [MODULECLASS_BASE, 'Module class to be used for this software', MODULES],
'moduleforceunload': [False, 'Force unload of all modules when loading the extension', MODULES],
'moduleloadnoconflict': [False, "Don't check for conflicts, unload other versions instead ", MODULES],
- 'module_depends_on': [False, 'Use depends_on (Lmod 7.6.1+) for dependencies in generated module '
- '(implies recursive unloading of modules).', MODULES],
+ 'module_depends_on': [None, 'Use depends_on (Lmod 7.6.1+) for dependencies in generated module '
+ '(implies recursive unloading of modules) [DEPRECATED]', MODULES],
+ 'module_search_path_headers': [None, "Environment variable set by modules on load "
+ "with search paths to header files (if None, use $CPATH)", MODULES],
'recursive_module_unload': [None, "Recursive unload of all dependencies when unloading module "
- "(True/False to hard enable/disable; None implies honoring "
- "the --recursive-module-unload EasyBuild configuration setting",
- MODULES],
+ "(True/False to hard enable/disable; None implies honoring the "
+ "--recursive-module-unload EasyBuild configuration setting", MODULES],
# MODULES documentation easyconfig parameters
# (docurls is part of MANDATORY)
diff --git a/easybuild/framework/easyconfig/easyconfig.py b/easybuild/framework/easyconfig/easyconfig.py
index a8c9a488b9..4665a1d1e8 100644
--- a/easybuild/framework/easyconfig/easyconfig.py
+++ b/easybuild/framework/easyconfig/easyconfig.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -51,31 +51,32 @@
import easybuild.tools.filetools as filetools
from easybuild.base import fancylogger
+from easybuild.base.wrapper import create_base_metaclass
from easybuild.framework.easyconfig import MANDATORY
from easybuild.framework.easyconfig.constants import EASYCONFIG_CONSTANTS, EXTERNAL_MODULE_MARKER
from easybuild.framework.easyconfig.default import DEFAULT_CONFIG
from easybuild.framework.easyconfig.format.convert import Dependency
from easybuild.framework.easyconfig.format.format import DEPENDENCY_PARAMETERS
from easybuild.framework.easyconfig.format.one import EB_FORMAT_EXTENSION, retrieve_blocks_in_spec
-from easybuild.framework.easyconfig.format.yeb import YEB_FORMAT_EXTENSION, is_yeb_format
from easybuild.framework.easyconfig.licenses import EASYCONFIG_LICENSES_DICT
-from easybuild.framework.easyconfig.parser import DEPRECATED_PARAMETERS, REPLACED_PARAMETERS
-from easybuild.framework.easyconfig.parser import EasyConfigParser, fetch_parameters_from_easyconfig
+from easybuild.framework.easyconfig.parser import ALTERNATIVE_EASYCONFIG_PARAMETERS, DEPRECATED_EASYCONFIG_PARAMETERS
+from easybuild.framework.easyconfig.parser import REPLACED_PARAMETERS, EasyConfigParser
+from easybuild.framework.easyconfig.parser import fetch_parameters_from_easyconfig
+from easybuild.framework.easyconfig.templates import ALTERNATIVE_EASYCONFIG_TEMPLATES, DEPRECATED_EASYCONFIG_TEMPLATES
from easybuild.framework.easyconfig.templates import TEMPLATE_CONSTANTS, TEMPLATE_NAMES_DYNAMIC, template_constant_dict
from easybuild.tools import LooseVersion
-from easybuild.tools.build_log import EasyBuildError, print_warning, print_msg
+from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, print_warning, print_msg
from easybuild.tools.config import GENERIC_EASYBLOCK_PKG, LOCAL_VAR_NAMING_CHECK_ERROR, LOCAL_VAR_NAMING_CHECK_LOG
from easybuild.tools.config import LOCAL_VAR_NAMING_CHECK_WARN
from easybuild.tools.config import Singleton, build_option, get_module_naming_scheme
from easybuild.tools.filetools import convert_name, copy_file, create_index, decode_class_name, encode_class_name
from easybuild.tools.filetools import find_backup_name_candidate, find_easyconfigs, load_index
from easybuild.tools.filetools import read_file, write_file
-from easybuild.tools.hooks import PARSE, load_hooks, run_hook
+from easybuild.tools.hooks import PARSE, EXTRACT_STEP, STEP_NAMES, load_hooks, run_hook
from easybuild.tools.module_naming_scheme.mns import DEVEL_MODULE_SUFFIX
from easybuild.tools.module_naming_scheme.utilities import avail_module_naming_schemes, det_full_ec_version
from easybuild.tools.module_naming_scheme.utilities import det_hidden_modname, is_valid_module_name
from easybuild.tools.modules import modules_tool, NoModulesTool
-from easybuild.tools.py2vs3 import create_base_metaclass, string_type
from easybuild.tools.systemtools import check_os_dependency, pick_dep_version
from easybuild.tools.toolchain.toolchain import SYSTEM_TOOLCHAIN_NAME, is_system_toolchain
from easybuild.tools.toolchain.toolchain import TOOLCHAIN_CAPABILITIES, TOOLCHAIN_CAPABILITY_CUDA
@@ -119,11 +120,24 @@ def handle_deprecated_or_replaced_easyconfig_parameters(ec_method):
def new_ec_method(self, key, *args, **kwargs):
"""Check whether any replace easyconfig parameters are still used"""
# map deprecated parameters to their replacements, issue deprecation warning(/error)
- if key in DEPRECATED_PARAMETERS:
+ if key == 'parallel':
+ _log.deprecated("Easyconfig parameter 'parallel' is deprecated, "
+ "use 'max_parallel' or the parallel property instead.", '6.0')
+ # This hidden parameter allows easyblocks to continue using self.cfg['parallel'] which contains
+ # the computed parallelism after the ready step but the easyconfig parameter before that step.
+
+ # Easyblocks using `max_parallel` always get the value from the easyconfig unmodified.
+ # Easyblocks should use either the parallel property or `max_parallel` such that the semantic is clear.
+ # In particular writes to self.cfg['parallel'] do NOT update the %(parallel)s template.
+ # It can be removed when the deprecation expires.
+ key = '_parallelLegacy'
+ elif key in ALTERNATIVE_EASYCONFIG_PARAMETERS:
+ key = ALTERNATIVE_EASYCONFIG_PARAMETERS[key]
+ elif key in DEPRECATED_EASYCONFIG_PARAMETERS:
depr_key = key
- key, ver = DEPRECATED_PARAMETERS[depr_key]
- _log.deprecated("Easyconfig parameter '%s' is deprecated, use '%s' instead." % (depr_key, key), ver)
- if key in REPLACED_PARAMETERS:
+ key, ver = DEPRECATED_EASYCONFIG_PARAMETERS[depr_key]
+ _log.deprecated("Easyconfig parameter '%s' is deprecated, use '%s' instead" % (depr_key, key), ver)
+ elif key in REPLACED_PARAMETERS:
_log.nosupport("Easyconfig parameter '%s' is replaced by '%s'" % (key, REPLACED_PARAMETERS[key]), '2.0')
return ec_method(self, key, *args, **kwargs)
@@ -141,7 +155,10 @@ def is_local_var_name(name):
"""
res = False
if name.startswith(LOCAL_VAR_PREFIX) or name.startswith('_'):
- res = True
+ # make exception for '_parallelLegacy' hidden easyconfig parameter,
+ # which is used to deprecate use of 'paralell' easyconfig parameter
+ if name != '_parallelLegacy':
+ res = True
# __builtins__ is always defined as a 'local' variables
# single-letter local variable names are allowed (mainly for use in list comprehensions)
# in Python 2, variables defined in list comprehensions leak to the outside (no longer the case in Python 3)
@@ -180,7 +197,8 @@ def triage_easyconfig_params(variables, ec):
for key in variables:
# validations are skipped, just set in the config
- if key in ec:
+ if any(key in d for d in (ec, DEPRECATED_EASYCONFIG_PARAMETERS.keys(),
+ ALTERNATIVE_EASYCONFIG_PARAMETERS.keys())):
ec_params[key] = variables[key]
_log.debug("setting config option %s: value %s (type: %s)", key, ec_params[key], type(ec_params[key]))
elif key in REPLACED_PARAMETERS:
@@ -236,7 +254,7 @@ def det_subtoolchain_version(current_tc, subtoolchain_names, optional_toolchains
subtoolchain_version = None
# ensure we always have a tuple of alternative subtoolchain names, which makes things easier below
- if isinstance(subtoolchain_names, string_type):
+ if isinstance(subtoolchain_names, str):
subtoolchain_names = (subtoolchain_names,)
system_subtoolchain = False
@@ -248,11 +266,6 @@ def det_subtoolchain_version(current_tc, subtoolchain_names, optional_toolchains
# system toolchain: bottom of the hierarchy
if is_system_toolchain(subtoolchain_name):
add_system_to_minimal_toolchains = build_option('add_system_to_minimal_toolchains')
- if not add_system_to_minimal_toolchains and build_option('add_dummy_to_minimal_toolchains'):
- depr_msg = "Use --add-system-to-minimal-toolchains instead of --add-dummy-to-minimal-toolchains"
- _log.deprecated(depr_msg, '5.0')
- add_system_to_minimal_toolchains = True
-
system_subtoolchain = True
if add_system_to_minimal_toolchains and not incl_capabilities:
@@ -409,20 +422,6 @@ def get_toolchain_hierarchy(parent_toolchain, incl_capabilities=False):
return toolchain_hierarchy
-@contextmanager
-def disable_templating(ec):
- """Temporarily disable templating on the given EasyConfig
-
- Usage:
- with disable_templating(ec):
- # Do what you want without templating
- # Templating set to previous value
- """
- _log.deprecated("disable_templating(ec) was replaced by ec.disable_templating()", '5.0')
- with ec.disable_templating() as old_value:
- yield old_value
-
-
class EasyConfig(object):
"""
Class which handles loading, reading, validation of easyconfigs
@@ -443,7 +442,11 @@ def __init__(self, path, extra_options=None, build_specs=None, validate=True, hi
:param local_var_naming_check: mode to use when checking if local variables use the recommended naming scheme
"""
self.template_values = None
- self.enable_templating = True # a boolean to control templating
+ # a boolean to control templating, can be (temporarily) disabled via disable_templating context manager
+ self._templating_enabled = True
+ # boolean to control whether all template values must be resolvable on access,
+ # can be (temporarily) disabled via allow_unresolved_templates context manager
+ self._expect_resolved_template_values = True
self.log = fancylogger.getLogger(self.__class__.__name__, fname=False)
@@ -514,6 +517,12 @@ def __init__(self, path, extra_options=None, build_specs=None, validate=True, hi
self.iterate_options = []
self.iterating = False
+ # Storage for parallel property. Mark as unset initially
+ self._parallel = None
+ # introduce hidden '_parallelLegacy' easyconfig parameter,
+ # used to deprecate use of 'parallel' easyconfig parameter
+ self._config['_parallelLegacy'] = [None, '', ('', )]
+
# parse easyconfig file
self.build_specs = build_specs
self.parse()
@@ -557,12 +566,48 @@ def disable_templating(self):
# Do what you want without templating
# Templating set to previous value
"""
- old_enable_templating = self.enable_templating
- self.enable_templating = False
+ old_templating_enabled = self._templating_enabled
+ self._templating_enabled = False
+ try:
+ yield old_templating_enabled
+ finally:
+ self._templating_enabled = old_templating_enabled
+
+ @property
+ def templating_enabled(self):
+ """Check whether templating is enabled on this EasyConfig"""
+ return self._templating_enabled
+
+ def _enable_templating(self, *_):
+ self.log.nosupport("self.enable_templating is replaced by self.templating_enabled. "
+ "To disable it use the self.disable_templating context manager", '5.0')
+ enable_templating = property(_enable_templating, _enable_templating)
+
+ @contextmanager
+ def allow_unresolved_templates(self):
+ """Temporarily allow templates to be not (fully) resolved.
+
+ This should only be used when it is intended to use partially resolved templates.
+ Otherwise `ec.get(key, resolve=False)` should be used.
+ See also @ref disable_templating.
+
+ Usage:
+ with ec.allow_unresolved_templates():
+ value = ec.get('key') # This will not raise an error
+ print(value % {'extra_key': exta_value})
+ # Resolving is enforced again if it was before
+ """
+ old_expect_resolved_template_values = self._expect_resolved_template_values
+ self._expect_resolved_template_values = False
try:
- yield old_enable_templating
+ yield old_expect_resolved_template_values
finally:
- self.enable_templating = old_enable_templating
+ self._expect_resolved_template_values = old_expect_resolved_template_values
+
+ @property
+ def expect_resolved_template_values(self):
+ """Check whether resolving all template values on access is enforced."""
+ return self._expect_resolved_template_values
def __str__(self):
"""Return a string representation of this EasyConfig instance"""
@@ -574,10 +619,7 @@ def __str__(self):
def filename(self):
"""Determine correct filename for this easyconfig file."""
- if is_yeb_format(self.path, self.rawtxt):
- ext = YEB_FORMAT_EXTENSION
- else:
- ext = EB_FORMAT_EXTENSION
+ ext = EB_FORMAT_EXTENSION
return '%s-%s%s' % (self.name, det_full_ec_version(self), ext)
@@ -613,6 +655,7 @@ def copy(self, validate=None):
ec = EasyConfig(self.path, validate=validate, hidden=self.hidden, rawtxt=self.rawtxt)
# take a copy of the actual config dictionary (which already contains the extra options)
ec._config = copy.deepcopy(self._config)
+ ec._parallel = self._parallel # Might be already set, e.g. for extensions
# since rawtxt is defined, self.path may not get inherited, make sure it does
if self.path:
ec.path = self.path
@@ -627,7 +670,7 @@ def update(self, key, value, allow_duplicate=True):
Update an easyconfig parameter with the specified value (i.e. append to it).
Note: For dictionary easyconfig parameters, 'allow_duplicate' is ignored (since it's meaningless).
"""
- if isinstance(value, string_type):
+ if isinstance(value, str):
inval = [value]
elif isinstance(value, (list, dict, tuple)):
inval = value
@@ -645,7 +688,7 @@ def update(self, key, value, allow_duplicate=True):
# Grab current parameter value so we can modify it
param_value = copy.deepcopy(self[key])
- if isinstance(param_value, string_type):
+ if isinstance(param_value, str):
for item in inval:
# re.search: only add value to string if it's not there yet (surrounded by whitespace)
if allow_duplicate or (not re.search(r'(^|\s+)%s(\s+|$)' % re.escape(item), param_value)):
@@ -683,7 +726,8 @@ def set_keys(self, params):
with self.disable_templating():
for key in sorted(params.keys()):
# validations are skipped, just set in the config
- if key in self._config.keys():
+ if any(key in x.keys() for x in (self._config, ALTERNATIVE_EASYCONFIG_PARAMETERS,
+ DEPRECATED_EASYCONFIG_PARAMETERS)):
self[key] = params[key]
self.log.info("setting easyconfig parameter %s: value %s (type: %s)",
key, self[key], type(self[key]))
@@ -716,6 +760,12 @@ def parse(self):
if missing_mandatory_keys:
raise EasyBuildError("mandatory parameters not provided in %s: %s", self.path, missing_mandatory_keys)
+ if 'parallel' in ec_vars:
+ # Replace value and issue better warning for easyconfig parameters,
+ # as opposed to warnings meant for easyblocks)
+ self.log.deprecated("Easyconfig parameter 'parallel' is deprecated, use 'max_parallel' instead.", '6.0')
+ ec_vars['_parallelLegacy'] = ec_vars.pop('parallel')
+
# provide suggestions for typos. Local variable names are excluded from this check
possible_typos = [(key, difflib.get_close_matches(key.lower(), self._config.keys(), 1, 0.85))
for key in ec_vars if not is_local_var_name(key) and key not in self]
@@ -782,12 +832,16 @@ def count_files(self):
"""
Determine number of files (sources + patches) required for this easyconfig.
"""
- cnt = len(self['sources']) + len(self['patches'])
- for ext in self['exts_list']:
+ # No need to resolve templates as we only need a count not the names
+ with self.disable_templating():
+ cnt = len(self['sources']) + len(self['patches'])
+ exts = self['exts_list']
+
+ for ext in exts:
if isinstance(ext, tuple) and len(ext) >= 3:
ext_opts = ext[2]
- # check for 'sources' first, since that's also considered first by EasyBlock.fetch_extension_sources
+ # check for 'sources' first, since that's also considered first by EasyBlock.collect_exts_file_info
if 'sources' in ext_opts:
cnt += len(ext_opts['sources'])
elif 'source_tmpl' in ext_opts:
@@ -817,7 +871,7 @@ def local_var_naming(self, local_var_naming_check):
msg = "Use of %d unknown easyconfig parameters detected %s: %s\n" % (cnt, in_fn, unknown_keys_msg)
msg += "If these are just local variables please rename them to start with '%s', " % LOCAL_VAR_PREFIX
msg += "or try using --fix-deprecated-easyconfigs to do this automatically.\nFor more information, see "
- msg += "https://easybuild.readthedocs.io/en/latest/Easyconfig-files-local-variables.html ."
+ msg += "https://docs.easybuild.io/easyconfig-files-local-variables/ ."
# always log a warning if local variable that don't follow recommended naming scheme are found
self.log.warning(msg)
@@ -836,7 +890,7 @@ def check_deprecated(self, path):
deprecated = self['deprecated']
if deprecated:
- if isinstance(deprecated, string_type):
+ if isinstance(deprecated, str):
if 'easyconfig' not in build_option('silence_deprecation_warnings'):
depr_msgs.append("easyconfig file '%s' is marked as deprecated:\n%s\n" % (path, deprecated))
else:
@@ -852,10 +906,10 @@ def check_deprecated(self, path):
if depr_msgs:
depr_msg = ', '.join(depr_msgs)
- depr_maj_ver = int(str(VERSION).split('.')[0]) + 1
+ depr_maj_ver = int(str(VERSION).split('.', maxsplit=1)[0]) + 1
depr_ver = '%s.0' % depr_maj_ver
- more_info_depr_ec = " (see also http://easybuild.readthedocs.org/en/latest/Deprecated-easyconfigs.html)"
+ more_info_depr_ec = " (see also https://docs.easybuild.io/deprecated-easyconfigs)"
self.log.deprecated(depr_msg, depr_ver, more_info=more_info_depr_ec, silent=build_option('silent'))
@@ -867,8 +921,8 @@ def validate(self, check_osdeps=True):
- check license
"""
self.log.info("Validating easyconfig")
- for attr in self.validations:
- self._validate(attr, self.validations[attr])
+ for attr, valid_values in self.validations.items():
+ self._validate(attr, valid_values)
if check_osdeps:
self.log.info("Checking OS dependencies")
@@ -877,9 +931,25 @@ def validate(self, check_osdeps=True):
self.log.info("Not checking OS dependencies")
self.log.info("Checking skipsteps")
- if not isinstance(self._config['skipsteps'][0], (list, tuple,)):
+ skipsteps = self._config['skipsteps'][0]
+ if not isinstance(skipsteps, (list, tuple,)):
raise EasyBuildError('Invalid type for skipsteps. Allowed are list or tuple, got %s (%s)',
- type(self._config['skipsteps'][0]), self._config['skipsteps'][0])
+ type(skipsteps), skipsteps)
+ unknown_step_names = [step for step in skipsteps if step not in STEP_NAMES]
+ if unknown_step_names:
+ error_lines = ["Found one or more unknown step names in 'skipsteps' easyconfig parameter:"]
+ for step in unknown_step_names:
+ error_line = "* %s" % step
+ # try to find close match, may be just a typo in the step name
+ close_matches = difflib.get_close_matches(step, STEP_NAMES, 2, 0.8)
+ # 'source' step was renamed to 'extract' in EasyBuild 5.0, see provide a useful suggestion in that case;
+ # see https://github.com/easybuilders/easybuild-framework/pull/4629
+ if not close_matches and step == 'source':
+ close_matches.append(EXTRACT_STEP)
+ if close_matches:
+ error_line += " (did you mean %s?)" % ', or '.join("'%s'" % s for s in close_matches)
+ error_lines.append(error_line)
+ raise EasyBuildError('\n'.join(error_lines))
self.log.info("Checking build option lists")
self.validate_iterate_opts_lists()
@@ -914,7 +984,7 @@ def validate_os_deps(self):
not_found = []
for dep in self['osdependencies']:
# make sure we have a tuple
- if isinstance(dep, string_type):
+ if isinstance(dep, str):
dep = (dep,)
elif not isinstance(dep, tuple):
raise EasyBuildError("Non-tuple value type for OS dependency specification: %s (type %s)",
@@ -924,9 +994,12 @@ def validate_os_deps(self):
not_found.append(dep)
if not_found:
- raise EasyBuildError("One or more OS dependencies were not found: %s", not_found)
- else:
- self.log.info("OS dependencies ok: %s" % self['osdependencies'])
+ raise EasyBuildError(
+ "One or more OS dependencies were not found: %s", not_found,
+ exit_code=EasyBuildExit.MISSING_SYSTEM_DEPENDENCY
+ )
+
+ self.log.info("OS dependencies ok: %s" % self['osdependencies'])
return True
@@ -1119,15 +1192,19 @@ def filter_deps(self, deps):
return retained_deps
- def dependencies(self, build_only=False):
+ def dependencies(self, build_only=False, runtime_only=False):
"""
Returns an array of parsed dependencies (after filtering, if requested)
dependency = {'name': '', 'version': '', 'system': (False|True), 'versionsuffix': '', 'toolchain': ''}
Iterable builddependencies are flattened when not iterating.
:param build_only: only return build dependencies, discard others
+ :param runtime_only: only return runtime dependencies, discard others
"""
- deps = self.builddependencies()
+ if runtime_only:
+ deps = []
+ else:
+ deps = self.builddependencies()
if not build_only:
# use += rather than .extend to get a new list rather than updating list of build deps in place...
@@ -1222,6 +1299,32 @@ def all_dependencies(self):
return self._all_dependencies
+ @property
+ def is_parallel_set(self):
+ """Return if the desired parallelism has been determined yet"""
+ return self._parallel is not None
+
+ @property
+ def parallel(self):
+ """Degree of parallellism (number of cores) to be used for building, etc."""
+ if not self.is_parallel_set:
+ raise ValueError("Parallelism not set yet, 'set_parallel' method of easyblock must be called first")
+
+ # This gets set when an easyblock changes ec['parallel'].
+ # It also gets set/updated in set_parallel to mirror the old behavior during the deprecation phase
+ parallelLegacy = self._config['_parallelLegacy'][0]
+ if parallelLegacy is not None:
+ return max(1, parallelLegacy)
+ return self._parallel
+
+ @parallel.setter
+ def parallel(self, value):
+ # Update backstorage and template value
+ self._parallel = max(1, value) # Also handles False
+ self.template_values['parallel'] = self._parallel
+ # Backwards compatibility, only for easyblocks still reading self.cfg['parallel']
+ self._config['_parallelLegacy'][0] = self._parallel
+
def dump(self, fp, always_overwrite=True, backup=False, explicit_toolchains=False):
"""
Dump this easyconfig to file, with the given filename.
@@ -1232,11 +1335,11 @@ def dump(self, fp, always_overwrite=True, backup=False, explicit_toolchains=Fals
# templated values should be dumped unresolved
with self.disable_templating():
# build dict of default values
- default_values = {key: DEFAULT_CONFIG[key][0] for key in DEFAULT_CONFIG}
- default_values.update({key: self.extra_options[key][0] for key in self.extra_options})
+ default_values = {key: value[0] for key, value in DEFAULT_CONFIG.items()}
+ default_values.update({key: value[0] for key, value in self.extra_options.items()})
self.generate_template_values()
- templ_const = {quote_py_str(const[1]): const[0] for const in TEMPLATE_CONSTANTS}
+ templ_const = {quote_py_str(value): name for name, (value, _) in TEMPLATE_CONSTANTS.items()}
# create reverse map of templates, to inject template values where possible
# longer template values are considered first, shorter template keys get preference over longer ones
@@ -1289,7 +1392,10 @@ def _validate(self, attr, values): # private method
if values is None:
values = []
if self[attr] and self[attr] not in values:
- raise EasyBuildError("%s provided '%s' is not valid: %s", attr, self[attr], values)
+ raise EasyBuildError(
+ "%s provided '%s' is not valid: %s", attr, self[attr], values,
+ exit_code=EasyBuildExit.VALUE_ERROR
+ )
def probe_external_module_metadata(self, mod_name, existing_metadata=None):
"""
@@ -1485,13 +1591,13 @@ def get_parsed_multi_deps(self):
# all multi_deps entries should be listed in builddependencies (if not, something is very wrong)
if isinstance(builddeps, list) and all(isinstance(x, list) for x in builddeps):
- for iter_id in range(len(builddeps)):
+ for iter_id, current_builddeps in enumerate(builddeps):
# only build dependencies that correspond to multi_deps entries should be loaded as extra modules
# (other build dependencies should not be required to make sanity check pass for this iteration)
iter_deps = []
for key in self['multi_deps']:
- hits = [d for d in builddeps[iter_id] if d['name'] == key]
+ hits = [d for d in current_builddeps if d['name'] == key]
if len(hits) == 1:
iter_deps.append(hits[0])
else:
@@ -1644,8 +1750,9 @@ def _finalize_dependencies(self):
filter_deps_specs = self.parse_filter_deps()
for key in DEPENDENCY_PARAMETERS:
- # loop over a *copy* of dependency dicts (with resolved templates);
- deps = self[key]
+ # loop over a *copy* of dependency dicts with resolved templates,
+ # although some templates may not resolve yet (e.g. those relying on dependencies like %(pyver)s)
+ deps = resolve_template(self.get_ref(key), self.template_values, expect_resolved=False)
# to update the original dep dict, we need to get a reference with templating disabled...
deps_ref = self.get_ref(key)
@@ -1755,6 +1862,12 @@ def _generate_template_values(self, ignore=None):
if self.template_values[key] is None:
del self.template_values[key]
+ def resolve_template(self, value):
+ """Resolve all templates in the given value using this easyconfig"""
+ if not self.template_values:
+ self.generate_template_values()
+ return resolve_template(value, self.template_values, expect_resolved=self.expect_resolved_template_values)
+
@handle_deprecated_or_replaced_easyconfig_parameters
def __contains__(self, key):
"""Check whether easyconfig parameter is defined"""
@@ -1763,16 +1876,13 @@ def __contains__(self, key):
@handle_deprecated_or_replaced_easyconfig_parameters
def __getitem__(self, key):
"""Return value of specified easyconfig parameter (without help text, etc.)"""
- value = None
- if key in self._config:
+ try:
value = self._config[key][0]
- else:
+ except KeyError:
raise EasyBuildError("Use of unknown easyconfig parameter '%s' when getting parameter value", key)
- if self.enable_templating:
- if self.template_values is None or len(self.template_values) == 0:
- self.generate_template_values()
- value = resolve_template(value, self.template_values)
+ if self.templating_enabled:
+ value = self.resolve_template(value)
return value
@@ -1817,11 +1927,14 @@ def get(self, key, default=None, resolve=True):
# see also https://docs.python.org/2/reference/datamodel.html#object.__eq__
def __eq__(self, ec):
"""Is this EasyConfig instance equivalent to the provided one?"""
- return self.asdict() == ec.asdict()
+ # Compare raw values to check that templates used are the same
+ with self.disable_templating():
+ with ec.disable_templating():
+ return self.asdict() == ec.asdict()
def __ne__(self, ec):
"""Is this EasyConfig instance equivalent to the provided one?"""
- return self.asdict() != ec.asdict()
+ return not self == ec
def __hash__(self):
"""Return hash value for a hashable representation of this EasyConfig instance."""
@@ -1834,8 +1947,9 @@ def make_hashable(val):
return val
lst = []
- for (key, val) in sorted(self.asdict().items()):
- lst.append((key, make_hashable(val)))
+ with self.disable_templating():
+ for (key, val) in sorted(self.asdict().items()):
+ lst.append((key, make_hashable(val)))
# a list is not hashable, but a tuple is
return hash(tuple(lst))
@@ -1845,13 +1959,13 @@ def asdict(self):
Return dict representation of this EasyConfig instance.
"""
res = {}
- for key, tup in self._config.items():
- value = tup[0]
- if self.enable_templating:
- if not self.template_values:
- self.generate_template_values()
- value = resolve_template(value, self.template_values)
- res[key] = value
+ # Not all values can be resolved, e.g. %(installdir)s
+ with self.allow_unresolved_templates():
+ for key, tup in self._config.items():
+ value = tup[0]
+ if self.templating_enabled:
+ value = self.resolve_template(value)
+ res[key] = value
return res
def get_cuda_cc_template_value(self, key):
@@ -1861,7 +1975,7 @@ def get_cuda_cc_template_value(self, key):
Returns user-friendly error message in case neither are defined,
or if an unknown key is used.
"""
- if key.startswith('cuda_') and any(x[0] == key for x in TEMPLATE_NAMES_DYNAMIC):
+ if key.startswith('cuda_') and any(x == key for x in TEMPLATE_NAMES_DYNAMIC):
try:
return self.template_values[key]
except KeyError:
@@ -1880,19 +1994,10 @@ def det_installversion(version, toolchain_name, toolchain_version, prefix, suffi
_log.nosupport('Use det_full_ec_version from easybuild.tools.module_generator instead of %s' % old_fn, '2.0')
-def get_easyblock_class(easyblock, name=None, error_on_failed_import=True, error_on_missing_easyblock=None, **kwargs):
+def get_easyblock_class(easyblock, name=None, error_on_failed_import=True, error_on_missing_easyblock=True, **kwargs):
"""
Get class for a particular easyblock (or use default)
"""
- if 'default_fallback' in kwargs:
- msg = "Named argument 'default_fallback' for get_easyblock_class is deprecated, "
- msg += "use 'error_on_missing_easyblock' instead"
- _log.deprecated(msg, '4.0')
- if error_on_missing_easyblock is None:
- error_on_missing_easyblock = kwargs['default_fallback']
- elif error_on_missing_easyblock is None:
- error_on_missing_easyblock = True
-
cls = None
try:
if easyblock:
@@ -1948,12 +2053,20 @@ def get_easyblock_class(easyblock, name=None, error_on_failed_import=True, error
error_re = re.compile(r"No module named '?.*/?%s'?" % modname)
_log.debug("error regexp for ImportError on '%s' easyblock: %s", modname, error_re.pattern)
if error_re.match(str(err)):
+ # Missing easyblock type of error
if error_on_missing_easyblock:
- raise EasyBuildError("No software-specific easyblock '%s' found for %s", class_name, name)
- elif error_on_failed_import:
- raise EasyBuildError("Failed to import %s easyblock: %s", class_name, err)
+ raise EasyBuildError(
+ "No software-specific easyblock '%s' found for %s", class_name, name,
+ exit_code=EasyBuildExit.MISSING_EASYBLOCK
+ ) from err
else:
- _log.debug("Failed to import easyblock for %s, but ignoring it: %s" % (class_name, err))
+ # Broken import
+ if error_on_failed_import:
+ raise EasyBuildError(
+ "Failed to import %s easyblock: %s", class_name, err,
+ exit_code=EasyBuildExit.EASYBLOCK_ERROR
+ ) from err
+ _log.debug("Failed to import easyblock for %s, but ignoring it: %s" % (class_name, err))
if cls is not None:
_log.info("Successfully obtained class '%s' for easyblock '%s' (software name '%s')",
@@ -1967,13 +2080,10 @@ def get_easyblock_class(easyblock, name=None, error_on_failed_import=True, error
# simply reraise rather than wrapping it into another error
raise err
except Exception as err:
- raise EasyBuildError("Failed to obtain class for %s easyblock (not available?): %s", easyblock, err)
-
-
-def is_generic_easyblock(easyblock):
- """Return whether specified easyblock name is a generic easyblock or not."""
- _log.deprecated("is_generic_easyblock function was moved to easybuild.tools.filetools", '5.0')
- return filetools.is_generic_easyblock(easyblock)
+ raise EasyBuildError(
+ "Failed to obtain class for %s easyblock (not available?): %s", easyblock, err,
+ exit_code=EasyBuildExit.EASYBLOCK_ERROR
+ )
def get_module_path(name, generic=None, decode=True):
@@ -2002,12 +2112,13 @@ def get_module_path(name, generic=None, decode=True):
return '.'.join(modpath + [module_name])
-def resolve_template(value, tmpl_dict):
+def resolve_template(value, tmpl_dict, expect_resolved=True):
"""Given a value, try to susbstitute the templated strings with actual values.
- value: some python object (supported are string, tuple/list, dict or some mix thereof)
- tmpl_dict: template dictionary
+ - expect_resolved: Expects that all templates get resolved
"""
- if isinstance(value, string_type):
+ if isinstance(value, str):
# simple escaping, making all '%foo', '%%foo', '%%%foo' post-templates values available,
# but ignore a string like '%(name)s'
# behaviour of strings like '%(name)s',
@@ -2031,14 +2142,48 @@ def resolve_template(value, tmpl_dict):
# '%(name)s' -> '%(name)s'
# '%%(name)s' -> '%%(name)s'
if '%' in value:
- orig_value = value
+ raw_value = value
value = re.sub(re.compile(r'(%)(?!%*\(\w+\)s)'), r'\1\1', value)
try:
value = value % tmpl_dict
except KeyError:
- _log.warning("Unable to resolve template value %s with dict %s", value, tmpl_dict)
- value = orig_value # Undo "%"-escaping
+ # check if any alternative and/or deprecated templates resolve
+ try:
+ orig_value = value
+ # map old templates to new values for alternative and deprecated templates
+ alt_map = {old_tmpl: tmpl_dict[new_tmpl] for (old_tmpl, new_tmpl) in
+ ALTERNATIVE_EASYCONFIG_TEMPLATES.items() if new_tmpl in tmpl_dict.keys()}
+ alt_map2 = {new_tmpl: tmpl_dict[old_tmpl] for (old_tmpl, new_tmpl) in
+ ALTERNATIVE_EASYCONFIG_TEMPLATES.items() if old_tmpl in tmpl_dict.keys()}
+ depr_map = {old_tmpl: tmpl_dict[new_tmpl] for (old_tmpl, (new_tmpl, ver)) in
+ DEPRECATED_EASYCONFIG_TEMPLATES.items() if new_tmpl in tmpl_dict.keys()}
+
+ # try templating with alternative and deprecated templates included
+ value = value % {**tmpl_dict, **alt_map, **alt_map2, **depr_map}
+
+ for old_tmpl, val in depr_map.items():
+ # check which deprecated templates were replaced, and issue deprecation warnings
+ if old_tmpl in orig_value and val in value:
+ new_tmpl, ver = DEPRECATED_EASYCONFIG_TEMPLATES[old_tmpl]
+ _log.deprecated(f"Easyconfig template '{old_tmpl}' is deprecated, use '{new_tmpl}' instead",
+ ver)
+ except KeyError:
+ if expect_resolved:
+ msg = (f'Failed to resolve all templates in "{value}" using template dictionary: {tmpl_dict}. '
+ 'This might cause failures or unexpected behavior, '
+ 'check for correct escaping if this is intended!')
+ if build_option('allow_unresolved_templates'):
+ print_warning(msg)
+ else:
+ raise EasyBuildError(msg)
+ value = raw_value # Undo "%"-escaping
+
+ for key in tmpl_dict:
+ if key in DEPRECATED_EASYCONFIG_TEMPLATES:
+ new_key, ver = DEPRECATED_EASYCONFIG_TEMPLATES[key]
+ _log.deprecated(f"Easyconfig template '{key}' is deprecated, use '{new_key}' instead", ver)
+
else:
# this block deals with references to objects and returns other references
# for reading this is ok, but for self['x'] = {}
@@ -2052,11 +2197,12 @@ def resolve_template(value, tmpl_dict):
# self._config['x']['y'] = z
# it can not be intercepted with __setitem__ because the set is done at a deeper level
if isinstance(value, list):
- value = [resolve_template(val, tmpl_dict) for val in value]
+ value = [resolve_template(val, tmpl_dict, expect_resolved) for val in value]
elif isinstance(value, tuple):
- value = tuple(resolve_template(list(value), tmpl_dict))
+ value = tuple(resolve_template(list(value), tmpl_dict, expect_resolved))
elif isinstance(value, dict):
- value = {resolve_template(k, tmpl_dict): resolve_template(v, tmpl_dict) for k, v in value.items()}
+ value = {resolve_template(k, tmpl_dict, expect_resolved): resolve_template(v, tmpl_dict, expect_resolved)
+ for k, v in value.items()}
return value
@@ -2091,7 +2237,11 @@ def process_easyconfig(path, build_specs=None, validate=True, parse_only=False,
try:
ec = EasyConfig(spec, build_specs=build_specs, validate=validate, hidden=hidden)
except EasyBuildError as err:
- raise EasyBuildError("Failed to process easyconfig %s: %s", spec, err.msg)
+ try:
+ exit_code = err.exit_code
+ except AttributeError:
+ exit_code = EasyBuildExit.EASYCONFIG_ERROR
+ raise EasyBuildError("Failed to process easyconfig %s: %s", spec, err.msg, exit_code=exit_code)
name = ec['name']
@@ -2505,8 +2655,6 @@ def copy_patch_files(patch_specs, target_dir):
def fix_deprecated_easyconfigs(paths):
"""Fix use of deprecated functionality in easyconfigs at specified locations."""
- dummy_tc_regex = re.compile(r'^toolchain\s*=\s*{.*name.*dummy.*}', re.M)
-
easyconfig_paths = []
for path in paths:
easyconfig_paths.extend(find_easyconfigs(path))
@@ -2519,11 +2667,6 @@ def fix_deprecated_easyconfigs(paths):
fixed = False
- # fix use of 'dummy' toolchain, use SYSTEM constant instead
- if dummy_tc_regex.search(ectxt):
- ectxt = dummy_tc_regex.sub("toolchain = SYSTEM", ectxt)
- fixed = True
-
# fix use of local variables with a name other than a single letter or 'local_*'
ec = EasyConfig(path, local_var_naming_check=LOCAL_VAR_NAMING_CHECK_LOG)
for key in ec.unknown_keys:
diff --git a/easybuild/framework/easyconfig/format/__init__.py b/easybuild/framework/easyconfig/format/__init__.py
index c2a81766ad..7ca361b39a 100644
--- a/easybuild/framework/easyconfig/format/__init__.py
+++ b/easybuild/framework/easyconfig/format/__init__.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/framework/easyconfig/format/convert.py b/easybuild/framework/easyconfig/format/convert.py
index 82613d42e0..1fd3182621 100644
--- a/easybuild/framework/easyconfig/format/convert.py
+++ b/easybuild/framework/easyconfig/format/convert.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2014-2024 Ghent University
+# Copyright 2014-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/framework/easyconfig/format/format.py b/easybuild/framework/easyconfig/format/format.py
index 3aeae419a2..eec432967e 100644
--- a/easybuild/framework/easyconfig/format/format.py
+++ b/easybuild/framework/easyconfig/format/format.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -41,7 +41,6 @@
from easybuild.tools.build_log import EasyBuildError
from easybuild.tools.configobj import Section
from easybuild.tools.utilities import get_subclasses
-from easybuild.tools.py2vs3 import string_type
# format is mandatory major.minor
@@ -69,11 +68,11 @@
['preconfigopts', 'configopts'],
['prebuildopts', 'buildopts'],
['preinstallopts', 'installopts'],
- ['parallel', 'maxparallel'],
+ ['maxparallel'],
]
LAST_PARAMS = ['exts_default_options', 'exts_list',
'sanity_check_paths', 'sanity_check_commands',
- 'modextrapaths', 'modextrapaths_append', 'modextravars',
+ 'modextrapaths', 'modextravars',
'moduleclass']
SANITY_CHECK_PATHS_DIRS = 'dirs'
@@ -323,7 +322,7 @@ def parse_sections(self, toparse, current):
value_type = self.VERSION_OPERATOR_VALUE_TYPES[key]
# list of supported toolchains/versions
# first one is default
- if isinstance(value, string_type):
+ if isinstance(value, str):
# so the split should be unnecessary
# (if it's not a list already, it's just one value)
# TODO this is annoying. check if we can force this in configobj
@@ -434,7 +433,7 @@ def _squash(self, vt_tuple, processed, sanity):
# walk over dictionary of parsed sections, and check for marker conflicts (using .add())
for key, value in processed.items():
if isinstance(value, NestedDict):
- tmp = self._squash_netsed_dict(key, value, squashed, sanity, vt_tuple)
+ tmp = self._squash_nested_dict(key, value, squashed, sanity, vt_tuple)
res_sections.update(tmp)
elif key in self.VERSION_OPERATOR_VALUE_TYPES:
self.log.debug("Found VERSION_OPERATOR_VALUE_TYPES entry (%s)" % key)
@@ -454,7 +453,7 @@ def _squash(self, vt_tuple, processed, sanity):
(processed, squashed.versions, squashed.result))
return squashed
- def _squash_netsed_dict(self, key, nested_dict, squashed, sanity, vt_tuple):
+ def _squash_nested_dict(self, key, nested_dict, squashed, sanity, vt_tuple):
"""
Squash NestedDict instance, returns dict with already squashed data
from possible higher sections
diff --git a/easybuild/framework/easyconfig/format/one.py b/easybuild/framework/easyconfig/format/one.py
index 868e218925..a45c855436 100644
--- a/easybuild/framework/easyconfig/format/one.py
+++ b/easybuild/framework/easyconfig/format/one.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -49,7 +49,6 @@
from easybuild.tools.build_log import EasyBuildError, print_msg
from easybuild.tools.filetools import read_file, write_file
from easybuild.tools.toolchain.toolchain import SYSTEM_TOOLCHAIN_NAME
-from easybuild.tools.py2vs3 import string_type
from easybuild.tools.utilities import INDENT_4SPACES, quote_py_str
@@ -255,7 +254,7 @@ def _reformat_line(self, param_name, param_val, outer=False, addlen=0):
else:
# dependencies are already dumped as strings, so they do not need to be quoted again
- if isinstance(param_val, string_type) and param_name not in DEPENDENCY_PARAMETERS:
+ if isinstance(param_val, str) and param_name not in DEPENDENCY_PARAMETERS:
res = quote_py_str(param_val)
return res
diff --git a/easybuild/framework/easyconfig/format/pyheaderconfigobj.py b/easybuild/framework/easyconfig/format/pyheaderconfigobj.py
index 8227a40958..b7e7206536 100644
--- a/easybuild/framework/easyconfig/format/pyheaderconfigobj.py
+++ b/easybuild/framework/easyconfig/format/pyheaderconfigobj.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -39,7 +39,8 @@
from easybuild.framework.easyconfig.constants import EASYCONFIG_CONSTANTS
from easybuild.framework.easyconfig.format.format import get_format_version, EasyConfigFormat
from easybuild.framework.easyconfig.licenses import EASYCONFIG_LICENSES_DICT
-from easybuild.framework.easyconfig.templates import TEMPLATE_CONSTANTS
+from easybuild.framework.easyconfig.templates import ALTERNATIVE_EASYCONFIG_TEMPLATE_CONSTANTS
+from easybuild.framework.easyconfig.templates import DEPRECATED_EASYCONFIG_TEMPLATE_CONSTANTS, TEMPLATE_CONSTANTS
from easybuild.tools.build_log import EasyBuildError
from easybuild.tools.configobj import ConfigObj
from easybuild.tools.systemtools import get_shared_lib_ext
@@ -51,8 +52,8 @@
def build_easyconfig_constants_dict():
"""Make a dictionary with all constants that can be used"""
all_consts = [
- ('TEMPLATE_CONSTANTS', {x[0]: x[1] for x in TEMPLATE_CONSTANTS}),
- ('EASYCONFIG_CONSTANTS', {key: val[0] for key, val in EASYCONFIG_CONSTANTS.items()}),
+ ('TEMPLATE_CONSTANTS', {name: value for name, (value, _) in TEMPLATE_CONSTANTS.items()}),
+ ('EASYCONFIG_CONSTANTS', {name: value for name, (value, _) in EASYCONFIG_CONSTANTS.items()}),
('EASYCONFIG_LICENSES', {klass().name: name for name, klass in EASYCONFIG_LICENSES_DICT.items()}),
]
err = []
@@ -86,6 +87,58 @@ def build_easyconfig_variables_dict():
return vars_dict
+def handle_deprecated_constants(method):
+ """Decorator to handle deprecated easyconfig template constants"""
+ def wrapper(self, key, *args, **kwargs):
+ """Check whether any deprecated constants are used"""
+ alternative = ALTERNATIVE_EASYCONFIG_TEMPLATE_CONSTANTS
+ deprecated = DEPRECATED_EASYCONFIG_TEMPLATE_CONSTANTS
+ if key in alternative:
+ key = alternative[key]
+ elif key in deprecated:
+ depr_key = key
+ key, ver = deprecated[depr_key]
+ _log.deprecated(f"Easyconfig template constant '{depr_key}' is deprecated, use '{key}' instead", ver)
+ return method(self, key, *args, **kwargs)
+ return wrapper
+
+
+class DeprecatedDict(dict):
+ """Custom dictionary that handles deprecated easyconfig template constants gracefully"""
+
+ def __init__(self, *args, **kwargs):
+ self.clear()
+ self.update(*args, **kwargs)
+
+ @handle_deprecated_constants
+ def __contains__(self, key):
+ return super().__contains__(key)
+
+ @handle_deprecated_constants
+ def __delitem__(self, key):
+ return super().__delitem__(key)
+
+ @handle_deprecated_constants
+ def __getitem__(self, key):
+ return super().__getitem__(key)
+
+ @handle_deprecated_constants
+ def __setitem__(self, key, value):
+ return super().__setitem__(key, value)
+
+ def update(self, *args, **kwargs):
+ if args:
+ if isinstance(args[0], dict):
+ for key, value in args[0].items():
+ self.__setitem__(key, value)
+ else:
+ for key, value in args[0]:
+ self.__setitem__(key, value)
+
+ for key, value in kwargs.items():
+ self.__setitem__(key, value)
+
+
class EasyConfigFormatConfigObj(EasyConfigFormat):
"""
Extended EasyConfig format, with support for a header and sections that are actually parsed (as opposed to exec'ed).
@@ -176,7 +229,7 @@ def parse_header(self, header):
def parse_pyheader(self, pyheader):
"""Parse the python header, assign to docstring and cfg"""
- global_vars = self.pyheader_env()
+ global_vars = DeprecatedDict(self.pyheader_env())
self.log.debug("pyheader initial global_vars %s", global_vars)
self.log.debug("pyheader text being exec'ed: %s", pyheader)
diff --git a/easybuild/framework/easyconfig/format/two.py b/easybuild/framework/easyconfig/format/two.py
index 652b1c9b80..e84d807bd9 100644
--- a/easybuild/framework/easyconfig/format/two.py
+++ b/easybuild/framework/easyconfig/format/two.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/framework/easyconfig/format/version.py b/easybuild/framework/easyconfig/format/version.py
index b8f181e5aa..4b3cdb6098 100644
--- a/easybuild/framework/easyconfig/format/version.py
+++ b/easybuild/framework/easyconfig/format/version.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -37,7 +37,6 @@
from easybuild.base import fancylogger
from easybuild.tools.build_log import EasyBuildError
-from easybuild.tools.py2vs3 import string_type
from easybuild.tools.toolchain.utilities import search_toolchain
@@ -46,7 +45,16 @@
class EasyVersion(LooseVersion):
- """Exact LooseVersion. No modifications needed (yet)"""
+ """Represent a version"""
+
+ def __init__(self, vstring, is_default=False):
+ super().__init__(vstring)
+ self._is_default = is_default
+
+ @property
+ def is_default(self):
+ """Return whether this is the default version used when no explicit version is specified"""
+ return self._is_default
def __len__(self):
"""Determine length of this EasyVersion instance."""
@@ -75,7 +83,7 @@ class VersionOperator(object):
OPERATOR_FAMILIES = [['>', '>='], ['<', '<=']] # similar operators
# default version and operator when version is undefined
- DEFAULT_UNDEFINED_VERSION = EasyVersion('0.0.0')
+ DEFAULT_UNDEFINED_VERSION = EasyVersion('0.0', is_default=True)
DEFAULT_UNDEFINED_VERSION_OPERATOR = OPERATOR_MAP['>']
# default operator when operator is undefined (but version is)
DEFAULT_UNDEFINED_OPERATOR = OPERATOR_MAP['==']
@@ -144,7 +152,7 @@ def test(self, test_version):
if not self:
raise EasyBuildError('Not a valid %s. Not initialised yet?', self.__class__.__name__)
- if isinstance(test_version, string_type):
+ if isinstance(test_version, str):
test_version = self._convert(test_version)
elif not isinstance(test_version, EasyVersion):
raise EasyBuildError("test: argument should be a string or EasyVersion (type %s)", type(test_version))
@@ -257,7 +265,7 @@ def _convert_operator(self, operator_str, version=None):
"""Return the operator"""
operator = None
if operator_str is None:
- if version == self.DEFAULT_UNDEFINED_VERSION or version is None:
+ if version is None or version.is_default:
operator = self.DEFAULT_UNDEFINED_VERSION_OPERATOR
else:
operator = self.DEFAULT_UNDEFINED_OPERATOR
@@ -640,7 +648,7 @@ def add(self, versop_new, data=None, update=None):
:param update: if versop_new already exist and has data set, try to update the existing data with the new data;
instead of overriding the existing data with the new data (method used for updating is .update)
"""
- if isinstance(versop_new, string_type):
+ if isinstance(versop_new, str):
versop_new = VersionOperator(versop_new)
elif not isinstance(versop_new, VersionOperator):
raise EasyBuildError("add: argument must be a VersionOperator instance or string: %s; type %s",
diff --git a/easybuild/framework/easyconfig/format/yeb.py b/easybuild/framework/easyconfig/format/yeb.py
deleted file mode 100644
index 675d0f0a86..0000000000
--- a/easybuild/framework/easyconfig/format/yeb.py
+++ /dev/null
@@ -1,169 +0,0 @@
-# #
-# Copyright 2013-2024 Ghent University
-#
-# This file is part of EasyBuild,
-# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
-# with support of Ghent University (http://ugent.be/hpc),
-# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
-# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
-# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
-#
-# https://github.com/easybuilders/easybuild
-#
-# EasyBuild is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation v2.
-#
-# EasyBuild is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with EasyBuild. If not, see .
-# #
-"""
-YAML easyconfig format (.yeb)
-Useful: http://www.yaml.org/spec/1.2/spec.html
-
-Authors:
-
-* Caroline De Brouwer (Ghent University)
-* Kenneth Hoste (Ghent University)
-"""
-import copy
-import os
-import platform
-
-from easybuild.base import fancylogger
-from easybuild.framework.easyconfig.format.format import EasyConfigFormat
-from easybuild.framework.easyconfig.format.pyheaderconfigobj import build_easyconfig_constants_dict
-from easybuild.tools import LooseVersion
-from easybuild.tools.py2vs3 import string_type
-from easybuild.tools.utilities import INDENT_4SPACES, only_if_module_is_available, quote_str
-
-
-_log = fancylogger.getLogger('easyconfig.format.yeb', fname=False)
-
-
-YAML_DIR = r'%YAML'
-YAML_SEP = '---'
-YEB_FORMAT_EXTENSION = '.yeb'
-YAML_SPECIAL_CHARS = set(":{}[],&*#?|-<>=!%@\\")
-
-
-def yaml_join(loader, node):
- """
- defines custom YAML join function.
- see http://stackoverflow.com/questions/5484016/
- how-can-i-do-string-concatenation-or-string-replacement-in-yaml/23212524#23212524
- :param loader: the YAML Loader
- :param node: the YAML (sequence) node
- """
- seq = loader.construct_sequence(node)
- return ''.join([str(i) for i in seq])
-
-
-try:
- import yaml
- # register the tag handlers
- if LooseVersion(platform.python_version()) < LooseVersion(u'2.7'):
- yaml.add_constructor('!join', yaml_join)
- else:
- yaml.add_constructor(u'!join', yaml_join, Loader=yaml.SafeLoader)
-except ImportError:
- pass
-
-
-class FormatYeb(EasyConfigFormat):
- """Support for easyconfig YAML format"""
- USABLE = True
-
- def __init__(self):
- """FormatYeb constructor"""
- super(FormatYeb, self).__init__()
- self.log.experimental("Parsing .yeb easyconfigs")
-
- def validate(self):
- """Format validation"""
- _log.info(".yeb format validation isn't implemented (yet) - validation always passes")
- return True
-
- def get_config_dict(self):
- """
- Return parsed easyconfig as a dictionary, based on specified arguments.
- """
- # avoid passing anything by reference, so next time get_config_dict is called
- # we can be sure we return a dictionary that correctly reflects the contents of the easyconfig file
- return copy.deepcopy(self.parsed_yeb)
-
- @only_if_module_is_available('yaml')
- def parse(self, txt):
- """
- Process YAML file
- """
- txt = self._inject_constants_dict(txt)
- if LooseVersion(platform.python_version()) < LooseVersion(u'2.7'):
- self.parsed_yeb = yaml.load(txt)
- else:
- self.parsed_yeb = yaml.load(txt, Loader=yaml.SafeLoader)
-
- def _inject_constants_dict(self, txt):
- """Inject constants so they are resolved when actually parsing the YAML text."""
- constants_dict = build_easyconfig_constants_dict()
-
- lines = txt.splitlines()
-
- # extract possible YAML header, for example
- # %YAML 1.2
- # ---
- yaml_header = []
- for i, line in enumerate(lines):
- if line.startswith(YAML_DIR):
- if lines[i + 1].startswith(YAML_SEP):
- yaml_header.extend([lines.pop(i), lines.pop(i)])
-
- injected_constants = ['__CONSTANTS__: ']
- for key, value in constants_dict.items():
- injected_constants.append('%s- &%s %s' % (INDENT_4SPACES, key, quote_str(value)))
-
- full_txt = '\n'.join(yaml_header + injected_constants + lines)
-
- return full_txt
-
- def dump(self, ecfg, default_values, templ_const, templ_val, toolchain_hierarchy=None):
- """Dump parsed easyconfig in .yeb format"""
- raise NotImplementedError("Dumping of .yeb easyconfigs not supported yet")
-
- def extract_comments(self, txt):
- """Extract comments from easyconfig file"""
- self.log.debug("Not extracting comments from .yeb easyconfigs")
-
-
-def is_yeb_format(filename, rawcontent):
- """
- Determine whether easyconfig is in .yeb format.
- If filename is None, rawcontent will be used to check the format.
- """
- isyeb = False
- if filename:
- isyeb = os.path.splitext(filename)[-1] == YEB_FORMAT_EXTENSION
- else:
- # if one line like 'name: ' is found, this must be YAML format
- for line in rawcontent.splitlines():
- if line.startswith('name: '):
- isyeb = True
- return isyeb
-
-
-def quote_yaml_special_chars(val):
- """
- Single-quote values that contain special characters, specifically to be used in YAML context (.yeb files)
- Single quotes inside the string are escaped by doubling them.
- (see: http://symfony.com/doc/current/components/yaml/yaml_format.html#strings)
- """
- if isinstance(val, string_type):
- if "'" in val or YAML_SPECIAL_CHARS.intersection(val):
- val = "'%s'" % val.replace("'", "''")
-
- return val
diff --git a/easybuild/framework/easyconfig/licenses.py b/easybuild/framework/easyconfig/licenses.py
index e6286d54e3..d1f7ac787a 100644
--- a/easybuild/framework/easyconfig/licenses.py
+++ b/easybuild/framework/easyconfig/licenses.py
@@ -1,5 +1,5 @@
#
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/framework/easyconfig/parser.py b/easybuild/framework/easyconfig/parser.py
index d6521b17dc..f5914d65a4 100644
--- a/easybuild/framework/easyconfig/parser.py
+++ b/easybuild/framework/easyconfig/parser.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -37,16 +37,74 @@
from easybuild.base import fancylogger
from easybuild.framework.easyconfig.format.format import FORMAT_DEFAULT_VERSION
from easybuild.framework.easyconfig.format.format import get_format_version, get_format_version_classes
-from easybuild.framework.easyconfig.format.yeb import FormatYeb, is_yeb_format
from easybuild.framework.easyconfig.types import PARAMETER_TYPES, check_type_of_param_value
from easybuild.tools.build_log import EasyBuildError
from easybuild.tools.filetools import read_file, write_file
-from easybuild.tools.py2vs3 import string_type
+# alternative easyconfig parameters, and their non-deprecated equivalents
+ALTERNATIVE_EASYCONFIG_PARAMETERS = {
+ # : ,
+ 'build_deps': 'builddependencies',
+ 'build_in_install_dir': 'buildininstalldir',
+ 'build_opts': 'buildopts',
+ 'build_stats': 'buildstats',
+ 'clean_up_old_build': 'cleanupoldbuild',
+ 'clean_up_old_install': 'cleanupoldinstall',
+ 'configure_opts': 'configopts',
+ 'deps': 'dependencies',
+ 'doc_paths': 'docpaths',
+ 'doc_urls': 'docurls',
+ 'do_not_create_install_dir': 'dontcreateinstalldir',
+ 'exts_class_map': 'exts_classmap',
+ 'exts_default_class': 'exts_defaultclass',
+ 'exts_default_opts': 'exts_default_options',
+ 'hidden_deps': 'hiddendependencies',
+ 'include_modulepath_exts': 'include_modpath_extensions',
+ 'install_opts': 'installopts',
+ 'keep_previous_install': 'keeppreviousinstall',
+ 'keep_symlinks': 'keepsymlinks',
+ 'max_parallel': 'maxparallel',
+ 'env_mod_aliases': 'modaliases',
+ 'env_mod_alt_soft_name': 'modaltsoftname',
+ 'modulepath_prepend_paths': 'moddependpaths',
+ 'env_mod_extra_paths': 'modextrapaths',
+ 'env_mod_extra_vars': 'modextravars',
+ 'env_mod_load_msg': 'modloadmsg',
+ 'env_mod_lua_footer': 'modluafooter',
+ 'env_mod_tcl_footer': 'modtclfooter',
+ 'env_mod_category': 'moduleclass',
+ 'env_mod_depends_on': 'module_depends_on',
+ 'env_mod_force_unload': 'moduleforceunload',
+ 'env_mod_load_no_conflict': 'moduleloadnoconflict',
+ 'env_mod_unload_msg': 'modunloadmsg',
+ 'only_toolchain_env_mod': 'onlytcmod',
+ 'os_deps': 'osdependencies',
+ 'post_install_cmds': 'postinstallcmds',
+ 'post_install_msgs': 'postinstallmsgs',
+ 'post_install_patches': 'postinstallpatches',
+ 'pre_build_opts': 'prebuildopts',
+ 'pre_configure_opts': 'preconfigopts',
+ 'pre_install_opts': 'preinstallopts',
+ 'pre_test_opts': 'pretestopts',
+ 'recursive_env_mod_unload': 'recursive_module_unload',
+ 'run_test': 'runtest',
+ 'sanity_check_cmds': 'sanity_check_commands',
+ 'skip_fortran_mod_files_sanity_check': 'skip_mod_files_sanity_check',
+ 'skip_steps': 'skipsteps',
+ 'test_opts': 'testopts',
+ 'toolchain_opts': 'toolchainopts',
+ 'unpack_opts': 'unpack_options',
+ 'version_prefix': 'versionprefix',
+ 'version_suffix': 'versionsuffix',
+}
+
# deprecated easyconfig parameters, and their replacements
-DEPRECATED_PARAMETERS = {
+DEPRECATED_EASYCONFIG_PARAMETERS = {
# : (, ),
+ 'allow_append_abs_path': ('modextrapaths', '6.0'),
+ 'allow_prepend_abs_path': ('modextrapaths', '6.0'),
+ 'modextrapaths_append': ('modextrapaths', '6.0'),
}
# replaced easyconfig parameters, and their replacements
@@ -161,7 +219,7 @@ def _read(self, filename=None):
except IOError as err:
raise EasyBuildError('Failed to obtain content with %s: %s', self.get_fn, err)
- if not isinstance(self.rawcontent, string_type):
+ if not isinstance(self.rawcontent, str):
msg = 'rawcontent is not a string: type %s, content %s' % (type(self.rawcontent), self.rawcontent)
raise EasyBuildError("Unexpected result for raw content: %s", msg)
@@ -189,11 +247,8 @@ def _get_format_version_class(self):
def _set_formatter(self, filename):
"""Obtain instance of the formatter"""
if self._formatter is None:
- if is_yeb_format(filename, self.rawcontent):
- self._formatter = FormatYeb()
- else:
- klass = self._get_format_version_class()
- self._formatter = klass()
+ klass = self._get_format_version_class()
+ self._formatter = klass()
self._formatter.parse(self.rawcontent)
def set_format_text(self):
diff --git a/easybuild/framework/easyconfig/style.py b/easybuild/framework/easyconfig/style.py
index bad6ab5bda..87c6780e12 100644
--- a/easybuild/framework/easyconfig/style.py
+++ b/easybuild/framework/easyconfig/style.py
@@ -1,5 +1,5 @@
##
-# Copyright 2016-2024 Ghent University
+# Copyright 2016-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -31,24 +31,18 @@
* Ward Poelmans (Ghent University)
"""
import re
-import sys
+from importlib import reload
from easybuild.base import fancylogger
from easybuild.framework.easyconfig.easyconfig import EasyConfig
from easybuild.tools.build_log import EasyBuildError, print_msg
-from easybuild.tools.py2vs3 import reload, string_type
from easybuild.tools.utilities import only_if_module_is_available
try:
import pycodestyle
from pycodestyle import StyleGuide, register_check, trailing_whitespace
except ImportError:
- try:
- # fallback to importing from 'pep8', which was renamed to pycodestyle in 2016
- import pep8
- from pep8 import StyleGuide, register_check, trailing_whitespace
- except ImportError:
- pass
+ pass
_log = fancylogger.getLogger('easyconfig.style', fname=False)
@@ -100,13 +94,13 @@ def _eb_check_trailing_whitespace(physical_line, lines, line_number, checker_sta
# if the warning is about the multiline string of description
# we will not issue a warning
- if checker_state.get('eb_last_key') == 'description':
+ if checker_state.get('eb_last_key') in ['description', 'examples', 'citing']:
result = None
return result
-@only_if_module_is_available(('pycodestyle', 'pep8'))
+@only_if_module_is_available('pycodestyle')
def check_easyconfigs_style(easyconfigs, verbose=False):
"""
Check the given list of easyconfigs for style
@@ -114,12 +108,7 @@ def check_easyconfigs_style(easyconfigs, verbose=False):
:param verbose: print our statistics and be verbose about the errors and warning
:return: the number of warnings and errors
"""
- # importing autopep8 changes some pep8 functions.
- # We reload it to be sure to get the real pep8 functions.
- if 'pycodestyle' in sys.modules:
- reload(pycodestyle)
- else:
- reload(pep8)
+ reload(pycodestyle)
# register the extra checks before using pep8:
# any function in this module starting with `_eb_check_` will be used.
@@ -137,6 +126,7 @@ def check_easyconfigs_style(easyconfigs, verbose=False):
# note that W291 has been replaced by our custom W299
options.ignore = (
'W291', # replaced by W299
+ 'E741', # 'l' is considered an ambiguous name, but we use it often for 'lib'
)
options.verbose = int(verbose)
@@ -161,7 +151,7 @@ def cmdline_easyconfigs_style_check(ecs):
# if an EasyConfig instance is provided, just grab the corresponding file path
if isinstance(ec, EasyConfig):
path = ec.path
- elif isinstance(ec, string_type):
+ elif isinstance(ec, str):
path = ec
else:
raise EasyBuildError("Value of unknown type encountered in cmdline_easyconfigs_style_check: %s (type: %s)",
diff --git a/easybuild/framework/easyconfig/templates.py b/easybuild/framework/easyconfig/templates.py
index be9bb28bd3..46fe4c5970 100644
--- a/easybuild/framework/easyconfig/templates.py
+++ b/easybuild/framework/easyconfig/templates.py
@@ -1,5 +1,5 @@
#
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -38,7 +38,6 @@
from easybuild.base import fancylogger
from easybuild.tools.build_log import EasyBuildError
-from easybuild.tools.py2vs3 import string_type
from easybuild.tools.systemtools import get_shared_lib_ext, pick_dep_version
from easybuild.tools.config import build_option
@@ -46,21 +45,20 @@
_log = fancylogger.getLogger('easyconfig.templates', fname=False)
# derived from easyconfig, but not from ._config directly
-TEMPLATE_NAMES_EASYCONFIG = [
- ('module_name', "Module name"),
- ('nameletter', "First letter of software name"),
- ('toolchain_name', "Toolchain name"),
- ('toolchain_version', "Toolchain version"),
- ('version_major_minor', "Major.Minor version"),
- ('version_major', "Major version"),
- ('version_minor', "Minor version"),
-]
+TEMPLATE_NAMES_EASYCONFIG = {
+ 'module_name': 'Module name',
+ 'nameletter': 'First letter of software name',
+ 'toolchain_name': 'Toolchain name',
+ 'toolchain_version': 'Toolchain version',
+ 'version_major_minor': "Major.Minor version",
+ 'version_major': 'Major version',
+ 'version_minor': 'Minor version',
+}
# derived from EasyConfig._config
TEMPLATE_NAMES_CONFIG = [
'bitbucket_account',
'github_account',
'name',
- 'parallel',
'version',
'versionsuffix',
'versionprefix',
@@ -72,105 +70,202 @@
'nameletter',
]
# values taken from the EasyBlock before each step
-TEMPLATE_NAMES_EASYBLOCK_RUN_STEP = [
- ('builddir', "Build directory"),
- ('installdir', "Installation directory"),
- ('start_dir', "Directory in which the build process begins"),
-]
+TEMPLATE_NAMES_EASYBLOCK_RUN_STEP = {
+ 'builddir': 'Build directory',
+ 'installdir': 'Installation directory',
+ 'start_dir': 'Directory in which the build process begins',
+}
# software names for which to define ver, majver and shortver templates
-TEMPLATE_SOFTWARE_VERSIONS = [
- # software name, prefix for *ver, *majver and *shortver
- ('CUDA', 'cuda'),
- ('CUDAcore', 'cuda'),
- ('Java', 'java'),
- ('Perl', 'perl'),
- ('Python', 'py'),
- ('R', 'r'),
-]
+TEMPLATE_SOFTWARE_VERSIONS = {
+ # software name -> prefix for *ver, *majver and *shortver
+ 'CUDA': 'cuda',
+ 'CUDAcore': 'cuda',
+ 'Java': 'java',
+ 'Perl': 'perl',
+ 'Python': 'py',
+ 'R': 'r',
+}
# template values which are only generated dynamically
-TEMPLATE_NAMES_DYNAMIC = [
- ('arch', "System architecture (e.g. x86_64, aarch64, ppc64le, ...)"),
- ('cuda_compute_capabilities', "Comma-separated list of CUDA compute capabilities, as specified via "
- "--cuda-compute-capabilities configuration option or via cuda_compute_capabilities easyconfig parameter"),
- ('cuda_cc_cmake', "List of CUDA compute capabilities suitable for use with $CUDAARCHS in CMake 3.18+"),
- ('cuda_cc_space_sep', "Space-separated list of CUDA compute capabilities"),
- ('cuda_cc_space_sep_no_period',
- "Space-separated list of CUDA compute capabilities, without periods (e.g. '80 90')."),
- ('cuda_cc_semicolon_sep', "Semicolon-separated list of CUDA compute capabilities"),
- ('cuda_sm_comma_sep', "Comma-separated list of sm_* values that correspond with CUDA compute capabilities"),
- ('cuda_sm_space_sep', "Space-separated list of sm_* values that correspond with CUDA compute capabilities"),
- ('mpi_cmd_prefix', "Prefix command for running MPI programs (with default number of ranks)"),
- ('software_commit', "Git commit id to use for the software as specified by --software-commit command line option"),
- ('sysroot', "Location root directory of system, prefix for standard paths like /usr/lib and /usr/include"
- "as specified by the --sysroot configuration option"),
-]
+TEMPLATE_NAMES_DYNAMIC = {
+ 'arch': 'System architecture (e.g. x86_64, aarch64, ppc64le, ...)',
+ 'cuda_compute_capabilities': "Comma-separated list of CUDA compute capabilities, as specified via "
+ "--cuda-compute-capabilities configuration option or "
+ "via cuda_compute_capabilities easyconfig parameter",
+ 'cuda_cc_cmake': 'List of CUDA compute capabilities suitable for use with $CUDAARCHS in CMake 3.18+',
+ 'cuda_cc_space_sep': 'Space-separated list of CUDA compute capabilities',
+ 'cuda_cc_space_sep_no_period':
+ "Space-separated list of CUDA compute capabilities, without periods (e.g. '80 90').",
+ 'cuda_cc_semicolon_sep': 'Semicolon-separated list of CUDA compute capabilities',
+ 'cuda_int_comma_sep': 'Comma-separated list of integer CUDA compute capabilities',
+ 'cuda_int_space_sep': 'Space-separated list of integer CUDA compute capabilities',
+ 'cuda_int_semicolon_sep': 'Semicolon-separated list of integer CUDA compute capabilities',
+ 'cuda_sm_comma_sep': 'Comma-separated list of sm_* values that correspond with CUDA compute capabilities',
+ 'cuda_sm_space_sep': 'Space-separated list of sm_* values that correspond with CUDA compute capabilities',
+ 'mpi_cmd_prefix': 'Prefix command for running MPI programs (with default number of ranks)',
+ 'parallel': "Degree of parallelism for e.g. make",
+ # can't be a boolean (True/False), must be a string value since it's a string template
+ 'rpath_enabled': "String value indicating whether or not RPATH linking is used ('true' or 'false')",
+ 'software_commit': "Git commit id to use for the software as specified by --software-commit command line option",
+ 'sysroot': "Location root directory of system, prefix for standard paths like /usr/lib and /usr/include"
+ "as specify by the --sysroot configuration option",
+}
# constant templates that can be used in easyconfigs
-TEMPLATE_CONSTANTS = [
+# Entry: constant -> (value, doc)
+TEMPLATE_CONSTANTS = {
# source url constants
- ('APACHE_SOURCE', 'https://archive.apache.org/dist/%(namelower)s',
- 'apache.org source url'),
- ('BITBUCKET_SOURCE', 'https://bitbucket.org/%(bitbucket_account)s/%(namelower)s/get',
- 'bitbucket.org source url (namelower is used if bitbucket_account easyconfig parameter is not specified)'),
- ('BITBUCKET_DOWNLOADS', 'https://bitbucket.org/%(bitbucket_account)s/%(namelower)s/downloads',
- 'bitbucket.org downloads url (namelower is used if bitbucket_account easyconfig parameter is not specified)'),
- ('CRAN_SOURCE', 'https://cran.r-project.org/src/contrib',
- 'CRAN (contrib) source url'),
- ('FTPGNOME_SOURCE', 'https://ftp.gnome.org/pub/GNOME/sources/%(namelower)s/%(version_major_minor)s',
- 'http download for gnome ftp server'),
- ('GITHUB_SOURCE', 'https://github.com/%(github_account)s/%(name)s/archive',
- 'GitHub source URL (if github_account easyconfig parameter is not specified, namelower is used in its place)'),
- ('GITHUB_LOWER_SOURCE', 'https://github.com/%(github_account)s/%(namelower)s/archive',
- 'GitHub source URL with lowercase name (if github_account easyconfig '
- 'parameter is not specified, namelower is used in its place)'),
- ('GITHUB_RELEASE', 'https://github.com/%(github_account)s/%(name)s/releases/download/v%(version)s',
- 'GitHub release URL (if github_account easyconfig parameter is not specified, namelower is used in its place)'),
- ('GITHUB_LOWER_RELEASE', 'https://github.com/%(github_account)s/%(namelower)s/releases/download/v%(version)s',
- 'GitHub release URL with lowercase name (if github_account easyconfig '
- 'parameter is not specified, namelower is used in its place)'),
- ('GNU_SAVANNAH_SOURCE', 'https://download-mirror.savannah.gnu.org/releases/%(namelower)s',
- 'download.savannah.gnu.org source url'),
- ('GNU_SOURCE', 'https://ftpmirror.gnu.org/gnu/%(namelower)s',
- 'gnu.org source url (ftp mirror)'),
- ('GNU_FTP_SOURCE', 'https://ftp.gnu.org/gnu/%(namelower)s',
- 'gnu.org source url (main ftp)'),
- ('GOOGLECODE_SOURCE', 'http://%(namelower)s.googlecode.com/files',
- 'googlecode.com source url'),
- ('LAUNCHPAD_SOURCE', 'https://launchpad.net/%(namelower)s/%(version_major_minor)s.x/%(version)s/+download/',
- 'launchpad.net source url'),
- ('PYPI_SOURCE', 'https://pypi.python.org/packages/source/%(nameletter)s/%(name)s',
- 'pypi source url'), # e.g., Cython, Sphinx
- ('PYPI_LOWER_SOURCE', 'https://pypi.python.org/packages/source/%(nameletterlower)s/%(namelower)s',
- 'pypi source url (lowercase name)'), # e.g., Greenlet, PyZMQ
- ('R_SOURCE', 'https://cran.r-project.org/src/base/R-%(version_major)s',
- 'cran.r-project.org (base) source url'),
- ('SOURCEFORGE_SOURCE', 'https://download.sourceforge.net/%(namelower)s',
- 'sourceforge.net source url'),
- ('XORG_DATA_SOURCE', 'https://xorg.freedesktop.org/archive/individual/data/',
- 'xorg data source url'),
- ('XORG_LIB_SOURCE', 'https://xorg.freedesktop.org/archive/individual/lib/',
- 'xorg lib source url'),
- ('XORG_PROTO_SOURCE', 'https://xorg.freedesktop.org/archive/individual/proto/',
- 'xorg proto source url'),
- ('XORG_UTIL_SOURCE', 'https://xorg.freedesktop.org/archive/individual/util/',
- 'xorg util source url'),
- ('XORG_XCB_SOURCE', 'https://xorg.freedesktop.org/archive/individual/xcb/',
- 'xorg xcb source url'),
+ 'APACHE_SOURCE': ('https://archive.apache.org/dist/%(namelower)s',
+ 'apache.org source url'),
+ 'BITBUCKET_SOURCE': ('https://bitbucket.org/%(bitbucket_account)s/%(namelower)s/get',
+ 'bitbucket.org source url '
+ '(namelower is used if bitbucket_account easyconfig parameter is not specified)'),
+ 'BITBUCKET_DOWNLOADS': ('https://bitbucket.org/%(bitbucket_account)s/%(namelower)s/downloads',
+ 'bitbucket.org downloads url '
+ '(namelower is used if bitbucket_account easyconfig parameter is not specified)'),
+ 'CRAN_SOURCE': ('https://cran.r-project.org/src/contrib',
+ 'CRAN (contrib) source url'),
+ 'FTPGNOME_SOURCE': ('https://ftp.gnome.org/pub/GNOME/sources/%(namelower)s/%(version_major_minor)s',
+ 'http download for gnome ftp server'),
+ 'GITHUB_SOURCE': ('https://github.com/%(github_account)s/%(name)s/archive',
+ 'GitHub source URL '
+ '(namelower is used if github_account easyconfig parameter is not specified)'),
+ 'GITHUB_LOWER_SOURCE': ('https://github.com/%(github_account)s/%(namelower)s/archive',
+ 'GitHub source URL with lowercase name '
+ '(namelower is used if github_account easyconfig parameter is not specified)'),
+ 'GITHUB_RELEASE': ('https://github.com/%(github_account)s/%(name)s/releases/download/v%(version)s',
+ 'GitHub release URL '
+ '(namelower is use if github_account easyconfig parameter is not specified)'),
+ 'GITHUB_LOWER_RELEASE': ('https://github.com/%(github_account)s/%(namelower)s/releases/download/v%(version)s',
+ 'GitHub release URL with lowercase name (if github_account easyconfig '
+ 'parameter is not specified, namelower is used in its place)'),
+ 'GNU_SAVANNAH_SOURCE': ('https://download-mirror.savannah.gnu.org/releases/%(namelower)s',
+ 'download.savannah.gnu.org source url'),
+ 'GNU_SOURCE': ('https://ftpmirror.gnu.org/gnu/%(namelower)s',
+ 'gnu.org source url (ftp mirror)'),
+ 'GNU_FTP_SOURCE': ('https://ftp.gnu.org/gnu/%(namelower)s',
+ 'gnu.org source url (main ftp)'),
+ 'GOOGLECODE_SOURCE': ('http://%(namelower)s.googlecode.com/files',
+ 'googlecode.com source url'),
+ 'LAUNCHPAD_SOURCE': ('https://launchpad.net/%(namelower)s/%(version_major_minor)s.x/%(version)s/+download/',
+ 'launchpad.net source url'),
+ 'PYPI_SOURCE': ('https://pypi.python.org/packages/source/%(nameletter)s/%(name)s',
+ 'pypi source url'), # e.g., Cython, Sphinx
+ 'PYPI_LOWER_SOURCE': ('https://pypi.python.org/packages/source/%(nameletterlower)s/%(namelower)s',
+ 'pypi source url (lowercase name)'), # e.g., Greenlet, PyZMQ
+ 'R_SOURCE': ('https://cran.r-project.org/src/base/R-%(version_major)s',
+ 'cran.r-project.org (base) source url'),
+ 'SOURCEFORGE_SOURCE': ('https://download.sourceforge.net/%(namelower)s',
+ 'sourceforge.net source url'),
+ 'XORG_DATA_SOURCE': ('https://xorg.freedesktop.org/archive/individual/data/',
+ 'xorg data source url'),
+ 'XORG_LIB_SOURCE': ('https://xorg.freedesktop.org/archive/individual/lib/',
+ 'xorg lib source url'),
+ 'XORG_PROTO_SOURCE': ('https://xorg.freedesktop.org/archive/individual/proto/',
+ 'xorg proto source url'),
+ 'XORG_UTIL_SOURCE': ('https://xorg.freedesktop.org/archive/individual/util/',
+ 'xorg util source url'),
+ 'XORG_XCB_SOURCE': ('https://xorg.freedesktop.org/archive/individual/xcb/',
+ 'xorg xcb source url'),
# TODO, not urgent, yet nice to have:
# CPAN_SOURCE GNOME KDE_I18N XCONTRIB DEBIAN KDE GENTOO TEX_CTAN MOZILLA_ALL
# other constants
- ('SHLIB_EXT', get_shared_lib_ext(), 'extension for shared libraries'),
-]
-
-extensions = ['tar.gz', 'tar.xz', 'tar.bz2', 'tgz', 'txz', 'tbz2', 'tb2', 'gtgz', 'zip', 'tar', 'xz', 'tar.Z']
-for ext in extensions:
+ 'SHLIB_EXT': (get_shared_lib_ext(), 'extension for shared libraries'),
+}
+
+# alternative templates, and their equivalents
+ALTERNATIVE_EASYCONFIG_TEMPLATES = {
+ # : ,
+ 'build_dir': 'builddir',
+ 'cuda_cc_comma_sep': 'cuda_compute_capabilities',
+ 'cuda_maj_ver': 'cudamajver',
+ 'cuda_short_ver': 'cudashortver',
+ 'cuda_ver': 'cudaver',
+ 'install_dir': 'installdir',
+ 'java_maj_ver': 'javamajver',
+ 'java_short_ver': 'javashortver',
+ 'java_ver': 'javaver',
+ 'name_letter_lower': 'nameletterlower',
+ 'name_letter': 'nameletter',
+ 'name_lower': 'namelower',
+ 'perl_maj_ver': 'perlmajver',
+ 'perl_short_ver': 'perlshortver',
+ 'perl_ver': 'perlver',
+ 'py_maj_ver': 'pymajver',
+ 'py_short_ver': 'pyshortver',
+ 'py_ver': 'pyver',
+ 'r_maj_ver': 'rmajver',
+ 'r_short_ver': 'rshortver',
+ 'r_ver': 'rver',
+ 'toolchain_ver': 'toolchain_version',
+ 'ver_maj_min': 'version_major_minor',
+ 'ver_maj': 'version_major',
+ 'ver_min': 'version_minor',
+ 'version_prefix': 'versionprefix',
+ 'version_suffix': 'versionsuffix',
+}
+
+# deprecated templates, and their replacements
+DEPRECATED_EASYCONFIG_TEMPLATES = {
+ # : (, ),
+}
+
+# alternative template constants, and their equivalents
+ALTERNATIVE_EASYCONFIG_TEMPLATE_CONSTANTS = {
+ # : ,
+ 'APACHE_URL': 'APACHE_SOURCE',
+ 'BITBUCKET_GET_URL': 'BITBUCKET_SOURCE',
+ 'BITBUCKET_DOWNLOADS_URL': 'BITBUCKET_DOWNLOADS',
+ 'CRAN_URL': 'CRAN_SOURCE',
+ 'FTP_GNOME_URL': 'FTPGNOME_SOURCE',
+ 'GITHUB_URL': 'GITHUB_SOURCE',
+ 'GITHUB_URL_LOWER': 'GITHUB_LOWER_SOURCE',
+ 'GITHUB_RELEASE_URL': 'GITHUB_RELEASE',
+ 'GITHUB_RELEASE_URL_LOWER': 'GITHUB_LOWER_RELEASE',
+ 'GNU_SAVANNAH_URL': 'GNU_SAVANNAH_SOURCE',
+ 'GNU_FTP_URL': 'GNU_FTP_SOURCE',
+ 'GNU_URL': 'GNU_SOURCE',
+ 'GOOGLECODE_URL': 'GOOGLECODE_SOURCE',
+ 'LAUNCHPAD_URL': 'LAUNCHPAD_SOURCE',
+ 'PYPI_URL': 'PYPI_SOURCE',
+ 'PYPI_URL_LOWER': 'PYPI_LOWER_SOURCE',
+ 'R_URL': 'R_SOURCE',
+ 'SOURCEFORGE_URL': 'SOURCEFORGE_SOURCE',
+ 'XORG_DATA_URL': 'XORG_DATA_SOURCE',
+ 'XORG_LIB_URL': 'XORG_LIB_SOURCE',
+ 'XORG_PROTO_URL': 'XORG_PROTO_SOURCE',
+ 'XORG_UTIL_URL': 'XORG_UTIL_SOURCE',
+ 'XORG_XCB_URL': 'XORG_XCB_SOURCE',
+ 'SOURCE_LOWER_TAR_GZ': 'SOURCELOWER_TAR_GZ',
+ 'SOURCE_LOWER_TAR_XZ': 'SOURCELOWER_TAR_XZ',
+ 'SOURCE_LOWER_TAR_BZ2': 'SOURCELOWER_TAR_BZ2',
+ 'SOURCE_LOWER_TGZ': 'SOURCELOWER_TGZ',
+ 'SOURCE_LOWER_TXZ': 'SOURCELOWER_TXZ',
+ 'SOURCE_LOWER_TBZ2': 'SOURCELOWER_TBZ2',
+ 'SOURCE_LOWER_TB2': 'SOURCELOWER_TB2',
+ 'SOURCE_LOWER_GTGZ': 'SOURCELOWER_GTGZ',
+ 'SOURCE_LOWER_ZIP': 'SOURCELOWER_ZIP',
+ 'SOURCE_LOWER_TAR': 'SOURCELOWER_TAR',
+ 'SOURCE_LOWER_XZ': 'SOURCELOWER_XZ',
+ 'SOURCE_LOWER_TAR_Z': 'SOURCELOWER_TAR_Z',
+ 'SOURCE_LOWER_WHL': 'SOURCELOWER_WHL',
+ 'SOURCE_LOWER_PY2_WHL': 'SOURCELOWER_PY2_WHL',
+ 'SOURCE_LOWER_PY3_WHL': 'SOURCELOWER_PY3_WHL',
+}
+
+# deprecated template constants, and their replacements
+DEPRECATED_EASYCONFIG_TEMPLATE_CONSTANTS = {
+ # : (, ),
+}
+
+EXTENSIONS = ['tar.gz', 'tar.xz', 'tar.bz2', 'tgz', 'txz', 'tbz2', 'tb2', 'gtgz', 'zip', 'tar', 'xz', 'tar.Z']
+for ext in EXTENSIONS:
suffix = ext.replace('.', '_').upper()
- TEMPLATE_CONSTANTS += [
- ('SOURCE_%s' % suffix, '%(name)s-%(version)s.' + ext, "Source .%s bundle" % ext),
- ('SOURCELOWER_%s' % suffix, '%(namelower)s-%(version)s.' + ext, "Source .%s bundle with lowercase name" % ext),
- ]
+ TEMPLATE_CONSTANTS.update({
+ 'SOURCE_%s' % suffix: ('%(name)s-%(version)s.' + ext, "Source .%s bundle" % ext),
+ 'SOURCELOWER_%s' % suffix: ('%(namelower)s-%(version)s.' + ext, "Source .%s bundle with lowercase name" % ext),
+ })
for pyver in ('py2.py3', 'py2', 'py3'):
if pyver == 'py2.py3':
desc = 'Python 2 & Python 3'
@@ -178,26 +273,22 @@
else:
desc = 'Python ' + pyver[-1]
name_infix = pyver.upper() + '_'
- TEMPLATE_CONSTANTS += [
- ('SOURCE_%sWHL' % name_infix, '%%(name)s-%%(version)s-%s-none-any.whl' % pyver,
- 'Generic (non-compiled) %s wheel package' % desc),
- ('SOURCELOWER_%sWHL' % name_infix, '%%(namelower)s-%%(version)s-%s-none-any.whl' % pyver,
- 'Generic (non-compiled) %s wheel package with lowercase name' % desc),
- ]
+ TEMPLATE_CONSTANTS.update({
+ 'SOURCE_%sWHL' % name_infix: ('%%(name)s-%%(version)s-%s-none-any.whl' % pyver,
+ 'Generic (non-compiled) %s wheel package' % desc),
+ 'SOURCELOWER_%sWHL' % name_infix: ('%%(namelower)s-%%(version)s-%s-none-any.whl' % pyver,
+ 'Generic (non-compiled) %s wheel package with lowercase name' % desc),
+ })
# TODO derived config templates
# versionmajor, versionminor, versionmajorminor (eg '.'.join(version.split('.')[:2])) )
-def template_constant_dict(config, ignore=None, skip_lower=None, toolchain=None):
+def template_constant_dict(config, ignore=None, toolchain=None):
"""Create a dict for templating the values in the easyconfigs.
- - config is a dict with the structure of EasyConfig._config
+ - config -- Dict with the structure of EasyConfig._config
+ - ignore -- List of template names to ignore
"""
- if skip_lower is not None:
- _log.deprecated("Use of 'skip_lower' named argument for template_constant_dict has no effect anymore", '4.0')
-
- # TODO find better name
- # ignore
if ignore is None:
ignore = []
# make dict
@@ -208,6 +299,9 @@ def template_constant_dict(config, ignore=None, skip_lower=None, toolchain=None)
# set 'arch' for system architecture based on 'machine' (4th) element of platform.uname() return value
template_values['arch'] = platform.uname()[4]
+ # set 'rpath' template based on 'rpath' configuration option, using empty string as fallback
+ template_values['rpath_enabled'] = 'true' if build_option('rpath') else 'false'
+
# set 'sysroot' template based on 'sysroot' configuration option, using empty string as fallback
template_values['sysroot'] = build_option('sysroot') or ''
@@ -220,10 +314,10 @@ def template_constant_dict(config, ignore=None, skip_lower=None, toolchain=None)
continue
# check if this template name is already handled
- if template_values.get(name[0]) is not None:
+ if template_values.get(name) is not None:
continue
- if name[0].startswith('toolchain_'):
+ if name.startswith('toolchain_'):
tc = config.get('toolchain')
if tc is not None:
template_values['toolchain_name'] = tc.get('name', None)
@@ -231,7 +325,7 @@ def template_constant_dict(config, ignore=None, skip_lower=None, toolchain=None)
# only go through this once
ignore.extend(['toolchain_name', 'toolchain_version'])
- elif name[0].startswith('version_'):
+ elif name.startswith('version_'):
# parse major and minor version numbers
version = config['version']
if version is not None:
@@ -250,87 +344,85 @@ def template_constant_dict(config, ignore=None, skip_lower=None, toolchain=None)
# only go through this once
ignore.extend(['version_major', 'version_minor', 'version_major_minor'])
- elif name[0].endswith('letter'):
+ elif name.endswith('letter'):
# parse first letters
- if name[0].startswith('name'):
+ if name.startswith('name'):
softname = config['name']
if softname is not None:
template_values['nameletter'] = softname[0]
- elif name[0] == 'module_name':
+ elif name == 'module_name':
template_values['module_name'] = getattr(config, 'short_mod_name', None)
else:
raise EasyBuildError("Undefined name %s from TEMPLATE_NAMES_EASYCONFIG", name)
# step 2: define *ver and *shortver templates
- if TEMPLATE_SOFTWARE_VERSIONS:
-
- name_to_prefix = {name.lower(): pref for name, pref in TEMPLATE_SOFTWARE_VERSIONS}
- deps = config.get('dependencies', [])
-
- # also consider build dependencies for *ver and *shortver templates;
- # we need to be a bit careful here, because for iterative installations
- # (when multi_deps is used for example) the builddependencies value may be a list of lists
-
- # first, determine if we have an EasyConfig instance
- # (indirectly by checking for 'iterating' and 'iterate_options' attributes,
- # because we can't import the EasyConfig class here without introducing
- # a cyclic import...);
- # we need to know to determine whether we're iterating over a list of build dependencies
- is_easyconfig = hasattr(config, 'iterating') and hasattr(config, 'iterate_options')
- if is_easyconfig:
- # if we're iterating over different lists of build dependencies,
- # only consider build dependencies when we're actually in iterative mode!
- if 'builddependencies' in config.iterate_options:
- if config.iterating:
- build_deps = config.get('builddependencies')
- else:
- build_deps = None
- else:
+ name_to_prefix = {name.lower(): prefix for name, prefix in TEMPLATE_SOFTWARE_VERSIONS.items()}
+ deps = config.get('dependencies', [])
+
+ # also consider build dependencies for *ver and *shortver templates;
+ # we need to be a bit careful here, because for iterative installations
+ # (when multi_deps is used for example) the builddependencies value may be a list of lists
+
+ # first, determine if we have an EasyConfig instance
+ # (indirectly by checking for 'iterating' and 'iterate_options' attributes,
+ # because we can't import the EasyConfig class here without introducing
+ # a cyclic import...);
+ # we need to know to determine whether we're iterating over a list of build dependencies
+ is_easyconfig = hasattr(config, 'iterating') and hasattr(config, 'iterate_options')
+ if is_easyconfig:
+ # if we're iterating over different lists of build dependencies,
+ # only consider build dependencies when we're actually in iterative mode!
+ if 'builddependencies' in config.iterate_options:
+ if config.iterating:
build_deps = config.get('builddependencies')
+ else:
+ build_deps = None
+ else:
+ build_deps = config.get('builddependencies')
+ if build_deps:
+ # Don't use += to avoid changing original list
+ deps = deps + build_deps
+ # include all toolchain deps (e.g. CUDAcore component in fosscuda);
+ # access Toolchain instance via _toolchain to avoid triggering initialization of the toolchain!
+ if config._toolchain is not None and config._toolchain.tcdeps:
+ # If we didn't create a new list above do it here
if build_deps:
- # Don't use += to avoid changing original list
- deps = deps + build_deps
- # include all toolchain deps (e.g. CUDAcore component in fosscuda);
- # access Toolchain instance via _toolchain to avoid triggering initialization of the toolchain!
- if config._toolchain is not None and config._toolchain.tcdeps:
- # If we didn't create a new list above do it here
- if build_deps:
- deps.extend(config._toolchain.tcdeps)
- else:
- deps = deps + config._toolchain.tcdeps
-
- for dep in deps:
- if isinstance(dep, dict):
- dep_name, dep_version = dep['name'], dep['version']
-
- # take into account dependencies marked as external modules,
- # where name/version may have to be harvested from metadata available for that external module
- if dep.get('external_module', False):
- metadata = dep.get('external_module_metadata', {})
- if dep_name is None:
- # name is a list in metadata, just take first value (if any)
- dep_name = metadata.get('name', [None])[0]
- if dep_version is None:
- # version is a list in metadata, just take first value (if any)
- dep_version = metadata.get('version', [None])[0]
-
- elif isinstance(dep, (list, tuple)):
- dep_name, dep_version = dep[0], dep[1]
+ deps.extend(config._toolchain.tcdeps)
else:
- raise EasyBuildError("Unexpected type for dependency: %s", dep)
-
- if isinstance(dep_name, string_type) and dep_version:
- pref = name_to_prefix.get(dep_name.lower())
- if pref:
- dep_version = pick_dep_version(dep_version)
- template_values['%sver' % pref] = dep_version
- dep_version_parts = dep_version.split('.')
- template_values['%smajver' % pref] = dep_version_parts[0]
- if len(dep_version_parts) > 1:
- template_values['%sminver' % pref] = dep_version_parts[1]
- template_values['%sshortver' % pref] = '.'.join(dep_version_parts[:2])
+ deps = deps + config._toolchain.tcdeps
+
+ for dep in deps:
+ if isinstance(dep, dict):
+ dep_name, dep_version = dep['name'], dep['version']
+
+ # take into account dependencies marked as external modules,
+ # where name/version may have to be harvested from metadata available for that external module
+ if dep.get('external_module', False):
+ metadata = dep.get('external_module_metadata', {})
+ if dep_name is None:
+ # name is a list in metadata, just take first value (if any)
+ dep_name = metadata.get('name', [None])[0]
+ if dep_version is None:
+ # version is a list in metadata, just take first value (if any)
+ dep_version = metadata.get('version', [None])[0]
+
+ elif isinstance(dep, (list, tuple)):
+ dep_name, dep_version = dep[0], dep[1]
+ else:
+ raise EasyBuildError("Unexpected type for dependency: %s", dep)
+
+ if isinstance(dep_name, str) and dep_version:
+ pref = name_to_prefix.get(dep_name.lower())
+ if pref:
+ dep_version = pick_dep_version(dep_version)
+ template_values['%sver' % pref] = dep_version
+ dep_version_parts = dep_version.split('.')
+ template_values['%smajver' % pref] = dep_version_parts[0]
+ if len(dep_version_parts) > 1:
+ template_values['%sminver' % pref] = dep_version_parts[1]
+ template_values['%sshortver' % pref] = '.'.join(dep_version_parts[:2])
# step 3: add remaining from config
for name in TEMPLATE_NAMES_CONFIG:
@@ -378,14 +470,17 @@ def template_constant_dict(config, ignore=None, skip_lower=None, toolchain=None)
template_values['cuda_cc_space_sep_no_period'] = ' '.join(cc.replace('.', '') for cc in cuda_cc)
template_values['cuda_cc_semicolon_sep'] = ';'.join(cuda_cc)
template_values['cuda_cc_cmake'] = ';'.join(cc.replace('.', '') for cc in cuda_cc)
+ int_values = [cc.replace('.', '') for cc in cuda_cc]
+ template_values['cuda_int_comma_sep'] = ','.join(int_values)
+ template_values['cuda_int_space_sep'] = ' '.join(int_values)
+ template_values['cuda_int_semicolon_sep'] = ';'.join(int_values)
sm_values = ['sm_' + cc.replace('.', '') for cc in cuda_cc]
template_values['cuda_sm_comma_sep'] = ','.join(sm_values)
template_values['cuda_sm_space_sep'] = ' '.join(sm_values)
unknown_names = []
for key in template_values:
- dynamic_template_names = set(x for (x, _) in TEMPLATE_NAMES_DYNAMIC)
- if not (key in common_template_names or key in dynamic_template_names):
+ if not (key in common_template_names or key in TEMPLATE_NAMES_DYNAMIC):
unknown_names.append(key)
if unknown_names:
raise EasyBuildError("One or more template values found with unknown name: %s", ','.join(unknown_names))
@@ -435,15 +530,15 @@ def template_documentation():
# step 1: add TEMPLATE_NAMES_EASYCONFIG
doc.append('Template names/values derived from easyconfig instance')
- for name in TEMPLATE_NAMES_EASYCONFIG:
- doc.append("%s%%(%s)s: %s" % (indent_l1, name[0], name[1]))
+ for name, cur_doc in TEMPLATE_NAMES_EASYCONFIG.items():
+ doc.append("%s%%(%s)s: %s" % (indent_l1, name, cur_doc))
# step 2: add *ver/*shortver templates for software listed in TEMPLATE_SOFTWARE_VERSIONS
doc.append("Template names/values for (short) software versions")
- for name, pref in TEMPLATE_SOFTWARE_VERSIONS:
- doc.append("%s%%(%smajver)s: major version for %s" % (indent_l1, pref, name))
- doc.append("%s%%(%sshortver)s: short version for %s (.)" % (indent_l1, pref, name))
- doc.append("%s%%(%sver)s: full version for %s" % (indent_l1, pref, name))
+ for name, prefix in TEMPLATE_SOFTWARE_VERSIONS.items():
+ doc.append("%s%%(%smajver)s: major version for %s" % (indent_l1, prefix, name))
+ doc.append("%s%%(%sshortver)s: short version for %s (.)" % (indent_l1, prefix, name))
+ doc.append("%s%%(%sver)s: full version for %s" % (indent_l1, prefix, name))
# step 3: add remaining self._config
doc.append('Template names/values as set in easyconfig')
@@ -459,11 +554,16 @@ def template_documentation():
# step 5. self.template_values can/should be updated from outside easyconfig
# (eg the run_setp code in EasyBlock)
doc.append('Template values set outside EasyBlock runstep')
- for name in TEMPLATE_NAMES_EASYBLOCK_RUN_STEP:
- doc.append("%s%%(%s)s: %s" % (indent_l1, name[0], name[1]))
+ for name, cur_doc in TEMPLATE_NAMES_EASYBLOCK_RUN_STEP.items():
+ doc.append("%s%%(%s)s: %s" % (indent_l1, name, cur_doc))
doc.append('Template constants that can be used in easyconfigs')
- for cst in TEMPLATE_CONSTANTS:
- doc.append('%s%s: %s (%s)' % (indent_l1, cst[0], cst[2], cst[1]))
+ for name, (value, cur_doc) in TEMPLATE_CONSTANTS.items():
+ doc.append('%s%s: %s (%s)' % (indent_l1, name, cur_doc, value))
return "\n".join(doc)
+
+
+# Add template constants to export list
+globals().update({name: value for name, (value, _) in TEMPLATE_CONSTANTS.items()})
+__all__ = list(TEMPLATE_CONSTANTS.keys())
diff --git a/easybuild/framework/easyconfig/tools.py b/easybuild/framework/easyconfig/tools.py
index cb92b36b76..3d85b931ea 100644
--- a/easybuild/framework/easyconfig/tools.py
+++ b/easybuild/framework/easyconfig/tools.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -52,14 +52,13 @@
from easybuild.framework.easyconfig.easyconfig import EASYCONFIGS_ARCHIVE_DIR, ActiveMNS, EasyConfig
from easybuild.framework.easyconfig.easyconfig import create_paths, det_file_info, get_easyblock_class
from easybuild.framework.easyconfig.easyconfig import process_easyconfig
-from easybuild.framework.easyconfig.format.yeb import quote_yaml_special_chars
from easybuild.framework.easyconfig.style import cmdline_easyconfigs_style_check
from easybuild.tools import LooseVersion
-from easybuild.tools.build_log import EasyBuildError, print_error, print_msg, print_warning
+from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, print_error, print_msg, print_warning
from easybuild.tools.config import build_option
from easybuild.tools.environment import restore_env
-from easybuild.tools.filetools import find_easyconfigs, is_patch_file, locate_files
-from easybuild.tools.filetools import read_file, resolve_path, which, write_file
+from easybuild.tools.filetools import EASYBLOCK_CLASS_PREFIX, get_cwd, find_easyconfigs, is_patch_file
+from easybuild.tools.filetools import locate_files, read_file, resolve_path, which, write_file
from easybuild.tools.github import GITHUB_EASYCONFIGS_REPO
from easybuild.tools.github import det_pr_labels, det_pr_title, download_repo, fetch_easyconfigs_from_commit
from easybuild.tools.github import fetch_easyconfigs_from_pr, fetch_pr_data
@@ -404,13 +403,19 @@ def parse_easyconfigs(paths, validate=True):
"""
easyconfigs = []
generated_ecs = False
+ parsed_paths = []
for (path, generated) in paths:
+ # Avoid processing the same file multiple times
path = os.path.abspath(path)
+ if any(os.path.samefile(path, p) for p in parsed_paths):
+ continue
+ parsed_paths.append(path)
+
# keep track of whether any files were generated
generated_ecs |= generated
if not os.path.exists(path):
- raise EasyBuildError("Can't find path %s", path)
+ raise EasyBuildError("Can't find path %s", path, exit_code=EasyBuildExit.MISSING_EASYCONFIG)
try:
ec_files = find_easyconfigs(path, ignore_dirs=build_option('ignore_dirs'))
for ec_file in ec_files:
@@ -427,7 +432,7 @@ def parse_easyconfigs(paths, validate=True):
return easyconfigs, generated_ecs
-def stats_to_str(stats, isyeb=False):
+def stats_to_str(stats):
"""
Pretty print build statistics to string.
"""
@@ -437,13 +442,7 @@ def stats_to_str(stats, isyeb=False):
txt = "{\n"
pref = " "
for key in sorted(stats):
- if isyeb:
- val = stats[key]
- if isinstance(val, tuple):
- val = list(val)
- key, val = quote_yaml_special_chars(key), quote_yaml_special_chars(val)
- else:
- key, val = quote_str(key), quote_str(stats[key])
+ key, val = quote_str(key), quote_str(stats[key])
txt += "%s%s: %s,\n" % (pref, key, val)
txt += "}"
return txt
@@ -758,7 +757,7 @@ def avail_easyblocks():
"""Return a list of all available easyblocks."""
module_regexp = re.compile(r"^([^_].*)\.py$")
- class_regex = re.compile(r"^class ([^(]*)\(", re.M)
+ class_regex = re.compile(r"^class ([^(:]*)\(", re.M)
# finish initialisation of the toolchain module (ie set the TC_CONSTANT constants)
search_toolchain('')
@@ -768,33 +767,43 @@ def avail_easyblocks():
__import__(pkg)
# determine paths for this package
- paths = sys.modules[pkg].__path__
+ paths = [path for path in sys.modules[pkg].__path__ if os.path.exists(path)]
# import all modules in these paths
for path in paths:
- if os.path.exists(path):
- for fn in os.listdir(path):
- res = module_regexp.match(fn)
- if res:
- easyblock_mod_name = '%s.%s' % (pkg, res.group(1))
-
- if easyblock_mod_name not in easyblocks:
- __import__(easyblock_mod_name)
- easyblock_loc = os.path.join(path, fn)
-
- class_names = class_regex.findall(read_file(easyblock_loc))
- if len(class_names) == 1:
- easyblock_class = class_names[0]
- elif class_names:
- raise EasyBuildError("Found multiple class names for easyblock %s: %s",
- easyblock_loc, class_names)
- else:
- raise EasyBuildError("Failed to determine easyblock class name for %s", easyblock_loc)
-
- easyblocks[easyblock_mod_name] = {'class': easyblock_class, 'loc': easyblock_loc}
+ for fn in os.listdir(path):
+ res = module_regexp.match(fn)
+ if not res:
+ continue
+ easyblock_mod_name = res.group(1)
+ easyblock_full_mod_name = '%s.%s' % (pkg, easyblock_mod_name)
+
+ if easyblock_full_mod_name in easyblocks:
+ _log.debug("%s already imported from %s, ignoring %s",
+ easyblock_full_mod_name, easyblocks[easyblock_full_mod_name]['loc'], path)
+ else:
+ __import__(easyblock_full_mod_name)
+ easyblock_loc = os.path.join(path, fn)
+
+ class_names = class_regex.findall(read_file(easyblock_loc))
+ if len(class_names) > 1:
+ if pkg.endswith('.generic'):
+ # In generic easyblocks we have e.g. ConfigureMake in configuremake.py
+ sw_specific_class_names = [name for name in class_names
+ if name.lower() == easyblock_mod_name.lower()]
else:
- _log.debug("%s already imported from %s, ignoring %s",
- easyblock_mod_name, easyblocks[easyblock_mod_name]['loc'], path)
+ # If there is exactly one software specific easyblock we use that
+ sw_specific_class_names = [name for name in class_names
+ if name.startswith(EASYBLOCK_CLASS_PREFIX)]
+ if len(sw_specific_class_names) == 1:
+ class_names = sw_specific_class_names
+ if len(class_names) == 1:
+ easyblocks[easyblock_full_mod_name] = {'class': class_names[0], 'loc': easyblock_loc}
+ elif class_names:
+ raise EasyBuildError("Found multiple class names for easyblock %s: %s",
+ easyblock_loc, class_names)
+ else:
+ raise EasyBuildError("Failed to determine easyblock class name for %s", easyblock_loc)
return easyblocks
@@ -807,14 +816,13 @@ def det_copy_ec_specs(orig_paths, from_pr=None, from_commit=None):
target_path, paths = None, []
- # if only one argument is specified, use current directory as target directory
if len(orig_paths) == 1:
- target_path = os.getcwd()
+ # if only one argument is specified, use current directory as target directory
+ target_path = get_cwd()
paths = orig_paths[:]
-
- # if multiple arguments are specified, assume that last argument is target location,
- # and remove that from list of paths to copy
elif orig_paths:
+ # if multiple arguments are specified, assume that last argument is target location,
+ # and remove that from list of paths to copy
target_path = orig_paths[-1]
paths = orig_paths[:-1]
@@ -832,7 +840,7 @@ def det_copy_ec_specs(orig_paths, from_pr=None, from_commit=None):
pr_paths.extend(fetch_files_from_pr(pr=pr, path=tmpdir))
# assume that files need to be copied to current working directory for now
- target_path = os.getcwd()
+ target_path = get_cwd()
if orig_paths:
last_path = orig_paths[-1]
@@ -869,7 +877,7 @@ def det_copy_ec_specs(orig_paths, from_pr=None, from_commit=None):
commit_paths = fetch_files_from_commit(from_commit, path=tmpdir)
# assume that files need to be copied to current working directory for now
- target_path = os.getcwd()
+ target_path = get_cwd()
if orig_paths:
last_path = orig_paths[-1]
diff --git a/easybuild/framework/easyconfig/tweak.py b/easybuild/framework/easyconfig/tweak.py
index fb01f3a83e..2c34c83a74 100644
--- a/easybuild/framework/easyconfig/tweak.py
+++ b/easybuild/framework/easyconfig/tweak.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -36,6 +36,7 @@
* Fotis Georgatos (Uni.Lu, NTUA)
* Alan O'Cais (Juelich Supercomputing Centre)
* Maxime Boissonneault (Universite Laval, Calcul Quebec, Compute Canada)
+* Bart Oldeman (McGill University, Calcul Quebec, Digital Research Alliance of Canada)
"""
import copy
import functools
@@ -60,7 +61,6 @@
from easybuild.tools.config import build_option
from easybuild.tools.filetools import read_file, write_file
from easybuild.tools.module_naming_scheme.utilities import det_full_ec_version
-from easybuild.tools.py2vs3 import string_type
from easybuild.tools.robot import resolve_dependencies, robot_find_easyconfig, search_easyconfigs
from easybuild.tools.toolchain.toolchain import SYSTEM_TOOLCHAIN_NAME
from easybuild.tools.toolchain.toolchain import TOOLCHAIN_CAPABILITIES
@@ -83,8 +83,9 @@ def ec_filename_for(path):
return fn
-def tweak(easyconfigs, build_specs, modtool, targetdirs=None):
- """Tweak list of easyconfigs according to provided build specifications."""
+def tweak(easyconfigs, build_specs, modtool, targetdirs=None, return_map=False):
+ """Tweak list of easyconfigs according to provided build specifications.
+ If return_map=True, also returns tweaked to original file mapping"""
# keep track of originally listed easyconfigs (via their path)
listed_ec_paths = [ec['spec'] for ec in easyconfigs]
@@ -93,6 +94,7 @@ def tweak(easyconfigs, build_specs, modtool, targetdirs=None):
tweaked_ecs_path, tweaked_ecs_deps_path = targetdirs
modifying_toolchains_or_deps = False
src_to_dst_tc_mapping = {}
+ tweak_map = {}
revert_to_regex = False
if 'update_deps' in build_specs:
@@ -224,13 +226,18 @@ def tweak(easyconfigs, build_specs, modtool, targetdirs=None):
if modifying_toolchains_or_deps:
if tc_name in src_to_dst_tc_mapping:
# Note pruned_build_specs are not passed down for dependencies
- map_easyconfig_to_target_tc_hierarchy(orig_ec['spec'], src_to_dst_tc_mapping,
- targetdir=tweaked_ecs_deps_path,
- update_dep_versions=update_dependencies,
- ignore_versionsuffixes=ignore_versionsuffixes)
+ new_ec_file = map_easyconfig_to_target_tc_hierarchy(orig_ec['spec'], src_to_dst_tc_mapping,
+ targetdir=tweaked_ecs_deps_path,
+ update_dep_versions=update_dependencies,
+ ignore_versionsuffixes=ignore_versionsuffixes)
else:
- tweak_one(orig_ec['spec'], None, build_specs, targetdir=tweaked_ecs_deps_path)
+ new_ec_file = tweak_one(orig_ec['spec'], None, build_specs, targetdir=tweaked_ecs_deps_path)
+
+ if new_ec_file:
+ tweak_map[new_ec_file] = orig_ec['spec']
+ if return_map:
+ return tweaked_easyconfigs, tweak_map
return tweaked_easyconfigs
@@ -348,7 +355,7 @@ def __repr__(self):
'"None"': 'None',
}
for (key, val) in tweaks.items():
- if isinstance(val, string_type) and val in special_values:
+ if isinstance(val, str) and val in special_values:
str_val = val
val = special_values[val]
else:
@@ -994,10 +1001,10 @@ def map_easyconfig_to_target_tc_hierarchy(ec_spec, toolchain_mapping, targetdir=
if 'version' in update_build_specs:
# take into account that version in exts_list may have to be updated as well
- if 'exts_list' in parsed_ec and parsed_ec['exts_list']:
+ if 'exts_list' in parsed_ec and parsed_ec.get_ref('exts_list'):
_log.warning("Found 'exts_list' in %s, will only update extension version of %s (if applicable)",
ec_spec, parsed_ec['name'])
- for idx, extension in enumerate(parsed_ec['exts_list']):
+ for idx, extension in enumerate(parsed_ec.get_ref('exts_list')):
if isinstance(extension, tuple) and extension[0] == parsed_ec['name']:
ext_as_list = list(extension)
# in the extension tuple the version is the second element
@@ -1207,7 +1214,7 @@ def find_potential_version_mappings(dep, toolchain_mapping, versionsuffix_mappin
tc_candidate = fetch_parameters_from_easyconfig(read_file(path), ['toolchain'])[0]
if isinstance(tc_candidate, dict) and tc_candidate['name'] == SYSTEM_TOOLCHAIN_NAME:
cand_paths_filtered += [path]
- if isinstance(tc_candidate, string_type) and tc_candidate == TC_CONSTANT_SYSTEM:
+ if isinstance(tc_candidate, str) and tc_candidate == TC_CONSTANT_SYSTEM:
cand_paths_filtered += [path]
cand_paths = cand_paths_filtered
@@ -1237,7 +1244,7 @@ def find_potential_version_mappings(dep, toolchain_mapping, versionsuffix_mappin
(highest_version_ignoring_versionsuffix is not None and highest_version is not None and
LooseVersion(highest_version_ignoring_versionsuffix) > LooseVersion(highest_version))
- exclude_alternate_versionsuffixes = False
+ exclude_alternative_versionsuffixes = False
if ignored_versionsuffix_greater:
if ignore_versionsuffixes:
highest_version = highest_version_ignoring_versionsuffix
@@ -1248,11 +1255,11 @@ def find_potential_version_mappings(dep, toolchain_mapping, versionsuffix_mappin
dep['name'], versionsuffix, [d['path'] for d in potential_version_mappings if
d['version'] == highest_version_ignoring_versionsuffix])
# exclude candidates with a different versionsuffix
- exclude_alternate_versionsuffixes = True
+ exclude_alternative_versionsuffixes = True
else:
# If the other version suffixes are not greater, then just ignore them
- exclude_alternate_versionsuffixes = True
- if exclude_alternate_versionsuffixes:
+ exclude_alternative_versionsuffixes = True
+ if exclude_alternative_versionsuffixes:
potential_version_mappings = [d for d in potential_version_mappings if d['versionsuffix'] == versionsuffix]
if highest_versions_only and highest_version is not None:
diff --git a/easybuild/framework/easyconfig/types.py b/easybuild/framework/easyconfig/types.py
index 42be1b99e4..78f021768d 100644
--- a/easybuild/framework/easyconfig/types.py
+++ b/easybuild/framework/easyconfig/types.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2015-2024 Ghent University
+# Copyright 2015-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -31,13 +31,11 @@
* Caroline De Brouwer (Ghent University)
* Kenneth Hoste (Ghent University)
"""
-from distutils.util import strtobool
from easybuild.base import fancylogger
from easybuild.framework.easyconfig.format.format import DEPENDENCY_PARAMETERS
from easybuild.framework.easyconfig.format.format import SANITY_CHECK_PATHS_DIRS, SANITY_CHECK_PATHS_FILES
from easybuild.tools.build_log import EasyBuildError
-from easybuild.tools.py2vs3 import string_type
_log = fancylogger.getLogger('easyconfig.types', fname=False)
@@ -272,7 +270,7 @@ def to_toolchain_dict(spec):
:param spec: a comma-separated string with two or three values, or a 2/3-element list of strings, or a dict
"""
# check if spec is a string or a list of two values; else, it can not be converted
- if isinstance(spec, string_type):
+ if isinstance(spec, str):
spec = spec.split(',')
if isinstance(spec, (list, tuple)):
@@ -281,7 +279,14 @@ def to_toolchain_dict(spec):
res = {'name': spec[0].strip(), 'version': spec[1].strip()}
# 3-element list
elif len(spec) == 3:
- res = {'name': spec[0].strip(), 'version': spec[1].strip(), 'hidden': strtobool(spec[2].strip())}
+ hidden = spec[2].strip().lower()
+ if hidden in {'yes', 'true', 't', 'y', '1', 'on'}:
+ hidden = True
+ elif hidden in {'no', 'false', 'f', 'n', '0', 'off'}:
+ hidden = False
+ else:
+ raise EasyBuildError("Invalid truth value %s", hidden)
+ res = {'name': spec[0].strip(), 'version': spec[1].strip(), 'hidden': hidden}
else:
raise EasyBuildError("Can not convert list %s to toolchain dict. Expected 2 or 3 elements", spec)
@@ -314,11 +319,11 @@ def to_list_of_strings(value):
res = None
# if value is already of correct type, we don't need to change anything
- if isinstance(value, list) and all(isinstance(s, string_type) for s in value):
+ if isinstance(value, list) and all(isinstance(s, str) for s in value):
res = value
- elif isinstance(value, string_type):
+ elif isinstance(value, str):
res = [value]
- elif isinstance(value, tuple) and all(isinstance(s, string_type) for s in value):
+ elif isinstance(value, tuple) and all(isinstance(s, str) for s in value):
res = list(value)
else:
raise EasyBuildError("Don't know how to convert provided value to a list of strings: %s", value)
@@ -341,7 +346,7 @@ def to_list_of_strings_and_tuples(spec):
raise EasyBuildError("Expected value to be a list, found %s (%s)", spec, type(spec))
for elem in spec:
- if isinstance(elem, (string_type, tuple)):
+ if isinstance(elem, (str, tuple)):
str_tup_list.append(elem)
elif isinstance(elem, list):
str_tup_list.append(tuple(elem))
@@ -366,7 +371,7 @@ def to_list_of_strings_and_tuples_and_dicts(spec):
raise EasyBuildError("Expected value to be a list, found %s (%s)", spec, type(spec))
for elem in spec:
- if isinstance(elem, (string_type, tuple, dict)):
+ if isinstance(elem, (str, tuple, dict)):
str_tup_list.append(elem)
elif isinstance(elem, list):
str_tup_list.append(tuple(elem))
@@ -392,17 +397,17 @@ def to_sanity_check_paths_entry(spec):
raise EasyBuildError("Expected value to be a list, found %s (%s)", spec, type(spec))
for elem in spec:
- if isinstance(elem, (string_type, tuple)):
+ if isinstance(elem, (str, tuple)):
result.append(elem)
elif isinstance(elem, list):
result.append(tuple(elem))
elif isinstance(elem, dict):
for key, value in elem.items():
- if not isinstance(key, string_type):
+ if not isinstance(key, str):
raise EasyBuildError("Expected keys to be of type string, got %s (%s)", key, type(key))
elif isinstance(value, list):
elem[key] = tuple(value)
- elif not isinstance(value, (string_type, tuple)):
+ elif not isinstance(value, (str, tuple)):
raise EasyBuildError("Expected elements to be of type string, tuple or list, got %s (%s)",
value, type(value))
result.append(elem)
@@ -505,33 +510,51 @@ def to_dependencies(dep_list):
return [to_dependency(dep) for dep in dep_list]
-def to_checksums(checksums):
- """Ensure correct element types for list of checksums: convert list elements to tuples."""
- res = []
- for checksum in checksums:
- # each list entry can be:
- # * None (indicates no checksum)
- # * a string (MD5 or SHA256 checksum)
- # * a tuple with 2 elements: checksum type + checksum value
- # * a list of checksums (i.e. multiple checksums for a single file)
- # * a dict (filename to checksum mapping)
- if isinstance(checksum, string_type):
- res.append(checksum)
- elif isinstance(checksum, (list, tuple)):
- # 2 elements + only string/int values => a checksum tuple
- if len(checksum) == 2 and all(isinstance(x, (string_type, int)) for x in checksum):
- res.append(tuple(checksum))
+def _to_checksum(checksum, list_level=0, allow_dict=True):
+ """Ensure the correct element type for each checksum in the checksum list"""
+ # each entry can be:
+ # * None (indicates no checksum)
+ # * a string (SHA256 checksum)
+ # * a list or tuple with 2 elements: checksum type + checksum value
+ # * a list or tuple of checksums (i.e. multiple checksums for a single file)
+ # * a dict (filename to checksum mapping)
+ if checksum is None or isinstance(checksum, str):
+ return checksum
+ elif isinstance(checksum, (list, tuple)):
+ if len(checksum) == 2 and isinstance(checksum[0], str) and isinstance(checksum[1], (str, int)):
+ # 2 elements so either:
+ # - a checksum tuple (2nd element string or int)
+ # - 2 alternative checksums (tuple)
+ # - 2 checksums that must each match (list)
+ # --> Convert to tuple only if we can exclude the 3rd case
+ if not isinstance(checksum[1], str) or list_level > 0:
+ return tuple(checksum)
else:
- res.append(to_checksums(checksum))
- elif isinstance(checksum, dict):
- validated_dict = {}
- for key, value in checksum.items():
- validated_dict[key] = to_checksums(value)
- res.append(validated_dict)
- else:
- res.append(checksum)
+ return checksum
+ elif list_level < 2:
+ # Alternative checksums or multiple checksums for a single file
+ # Allowed to nest (at most) 2 times, e.g. [[[type, value]]] == [[(type, value)]]
+ # None is not allowed here
+ if any(x is None for x in checksum):
+ raise ValueError('Unexpected None in ' + str(checksum))
+ if isinstance(checksum, tuple) or list_level > 0:
+ # When we already are in a tuple no further recursion is allowed -> set list_level very high
+ return tuple(_to_checksum(x, list_level=99, allow_dict=allow_dict) for x in checksum)
+ else:
+ return list(_to_checksum(x, list_level=list_level+1, allow_dict=allow_dict) for x in checksum)
+ elif isinstance(checksum, dict) and allow_dict:
+ return {key: _to_checksum(value, allow_dict=False) for key, value in checksum.items()}
- return res
+ # Not returned -> Wrong type/format
+ raise ValueError('Unexpected type of "%s": %s' % (type(checksum), str(checksum)))
+
+
+def to_checksums(checksums):
+ """Ensure correct element types for list of checksums: convert list elements to tuples."""
+ try:
+ return [_to_checksum(checksum) for checksum in checksums]
+ except ValueError as e:
+ raise EasyBuildError('Invalid checksums: %s\n\tError: %s', checksums, e)
def ensure_iterable_license_specs(specs):
@@ -543,9 +566,9 @@ def ensure_iterable_license_specs(specs):
"""
if specs is None:
license_specs = [None]
- elif isinstance(specs, string_type):
+ elif isinstance(specs, str):
license_specs = [specs]
- elif isinstance(specs, (list, tuple)) and all(isinstance(x, string_type) for x in specs):
+ elif isinstance(specs, (list, tuple)) and all(isinstance(x, str) for x in specs):
license_specs = list(specs)
else:
msg = "Unsupported type %s for easyconfig parameter 'license_file'! " % type(specs)
@@ -608,37 +631,57 @@ def ensure_iterable_license_specs(specs):
}))
# checksums is a list of checksums, one entry per file (source/patch)
# each entry can be:
+# None
# a single checksum value (string)
# a single checksum value of a specified type (2-tuple, 1st element is checksum type, 2nd element is checksum)
# a list of checksums (of different types, perhaps different formats), which should *all* be valid
-# a dictionary with a mapping from filename to checksum value
-CHECKSUM_LIST = (list, as_hashable({'elem_types': [str, tuple, STRING_DICT]}))
-CHECKSUMS = (list, as_hashable({'elem_types': [str, tuple, STRING_DICT, CHECKSUM_LIST]}))
+# a tuple of checksums (of different types, perhaps different formats), where one should be valid
+# a dictionary with a mapping from filename to checksum (None, value, type&value, alternatives)
+
+# Type & value, value may be an int for type "size"
+# This is a bit too permissive as it allows the first element to be an int and doesn't restrict the number of elements
+CHECKSUM_AND_TYPE = (tuple, as_hashable({'elem_types': [str, int]}))
+CHECKSUM_LIST = (list, as_hashable({'elem_types': [str, CHECKSUM_AND_TYPE]}))
+CHECKSUM_TUPLE = (tuple, as_hashable({'elem_types': [str, CHECKSUM_AND_TYPE]}))
+CHECKSUM_DICT = (dict, as_hashable(
+ {
+ 'elem_types': [type(None), str, CHECKSUM_AND_TYPE, CHECKSUM_TUPLE, CHECKSUM_LIST],
+ 'key_types': [str],
+ }
+))
+# At the top-level we allow tuples/lists containing a dict
+CHECKSUM_LIST_W_DICT = (list, as_hashable({'elem_types': [str, CHECKSUM_AND_TYPE, CHECKSUM_DICT]}))
+CHECKSUM_TUPLE_W_DICT = (tuple, as_hashable({'elem_types': [str, CHECKSUM_AND_TYPE, CHECKSUM_DICT]}))
-CHECKABLE_TYPES = [CHECKSUM_LIST, CHECKSUMS, DEPENDENCIES, DEPENDENCY_DICT, LIST_OF_STRINGS,
+CHECKSUMS = (list, as_hashable({'elem_types': [type(None), str, CHECKSUM_AND_TYPE,
+ CHECKSUM_LIST_W_DICT, CHECKSUM_TUPLE_W_DICT, CHECKSUM_DICT]}))
+
+CHECKABLE_TYPES = [CHECKSUM_AND_TYPE, CHECKSUM_LIST, CHECKSUM_TUPLE,
+ CHECKSUM_LIST_W_DICT, CHECKSUM_TUPLE_W_DICT, CHECKSUM_DICT, CHECKSUMS,
+ DEPENDENCIES, DEPENDENCY_DICT, LIST_OF_STRINGS,
SANITY_CHECK_PATHS_DICT, SANITY_CHECK_PATHS_ENTRY, STRING_DICT, STRING_OR_TUPLE_LIST,
STRING_OR_TUPLE_DICT, STRING_OR_TUPLE_OR_DICT_LIST, TOOLCHAIN_DICT, TUPLE_OF_STRINGS]
# easy types, that can be verified with isinstance
-EASY_TYPES = [string_type, bool, dict, int, list, str, tuple]
+EASY_TYPES = [str, bool, dict, int, list, str, tuple, type(None)]
# type checking is skipped for easyconfig parameters names not listed in PARAMETER_TYPES
PARAMETER_TYPES = {
'checksums': CHECKSUMS,
'docurls': LIST_OF_STRINGS,
- 'name': string_type,
+ 'name': str,
'osdependencies': STRING_OR_TUPLE_LIST,
'patches': STRING_OR_TUPLE_OR_DICT_LIST,
'sanity_check_paths': SANITY_CHECK_PATHS_DICT,
'toolchain': TOOLCHAIN_DICT,
- 'version': string_type,
+ 'version': str,
}
# add all dependency types as dependencies
for dep in DEPENDENCY_PARAMETERS:
PARAMETER_TYPES[dep] = DEPENDENCIES
TYPE_CONVERSION_FUNCTIONS = {
- string_type: str,
+ str: str,
float: float,
int: int,
str: str,
diff --git a/easybuild/framework/easystack.py b/easybuild/framework/easystack.py
index 4a0ec15cdd..471b5ac5db 100644
--- a/easybuild/framework/easystack.py
+++ b/easybuild/framework/easystack.py
@@ -1,4 +1,4 @@
-# Copyright 2020-2024 Ghent University
+# Copyright 2020-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -37,7 +37,6 @@
from easybuild.base import fancylogger
from easybuild.tools.build_log import EasyBuildError
from easybuild.tools.filetools import read_file
-from easybuild.tools.py2vs3 import string_type
from easybuild.tools.utilities import only_if_module_is_available
try:
import yaml
@@ -53,7 +52,7 @@ def check_value(value, context):
Check whether specified value obtained from a YAML file in specified context represents is valid.
The value must be a string (not a float or an int).
"""
- if not isinstance(value, string_type):
+ if not isinstance(value, str):
error_msg = '\n'.join([
"Value %(value)s (of type %(type)s) obtained for %(context)s is not valid!",
"Make sure to wrap the value in single quotes (like '%(value)s') to avoid that it is interpreted "
@@ -189,9 +188,6 @@ def parse_by_easyconfigs(filepath, easyconfigs, easybuild_version=None, robot=Fa
@only_if_module_is_available('yaml', pkgname='PyYAML')
def parse_easystack(filepath):
"""Parses through easystack file, returns what EC are to be installed together with their options."""
- log_msg = "Support for easybuild-ing from multiple easyconfigs based on "
- log_msg += "information obtained from provided file (easystack) with build specifications."
- _log.experimental(log_msg)
_log.info("Building from easystack: '%s'" % filepath)
# class instance which contains all info about planned build
diff --git a/easybuild/framework/extension.py b/easybuild/framework/extension.py
index 56f242ffce..51a40bbe15 100644
--- a/easybuild/framework/extension.py
+++ b/easybuild/framework/extension.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -40,10 +40,9 @@
from easybuild.framework.easyconfig.easyconfig import resolve_template
from easybuild.framework.easyconfig.templates import TEMPLATE_NAMES_EASYBLOCK_RUN_STEP, template_constant_dict
-from easybuild.tools.build_log import EasyBuildError, raise_nosupport
+from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, raise_nosupport
from easybuild.tools.filetools import change_dir
-from easybuild.tools.run import check_async_cmd, run_cmd
-from easybuild.tools.py2vs3 import string_type
+from easybuild.tools.run import run_shell_cmd
def resolve_exts_filter_template(exts_filter, ext):
@@ -54,7 +53,7 @@ def resolve_exts_filter_template(exts_filter, ext):
:return: (cmd, input) as a tuple of strings
"""
- if isinstance(exts_filter, string_type) or len(exts_filter) != 2:
+ if isinstance(exts_filter, str) or len(exts_filter) != 2:
raise EasyBuildError('exts_filter should be a list or tuple of ("command","input")')
cmd, cmdinput = exts_filter
@@ -117,7 +116,7 @@ def __init__(self, mself, ext, extra_params=None):
# Add install/builddir templates with values from master.
for key in TEMPLATE_NAMES_EASYBLOCK_RUN_STEP:
- self.cfg.template_values[key[0]] = str(getattr(self.master, key[0], None))
+ self.cfg.template_values[key] = str(getattr(self.master, key, None))
# We can't inherit the 'start_dir' value from the parent (which will be set, and will most likely be wrong).
# It should be specified for the extension specifically, or be empty (so it is auto-derived).
@@ -129,7 +128,15 @@ def __init__(self, mself, ext, extra_params=None):
self.src = resolve_template(self.ext.get('src', []), self.cfg.template_values)
self.src_extract_cmd = self.ext.get('extract_cmd', None)
self.patches = resolve_template(self.ext.get('patches', []), self.cfg.template_values)
- self.options = resolve_template(copy.deepcopy(self.ext.get('options', {})), self.cfg.template_values)
+ # Some options may not be resolvable yet
+ self.options = resolve_template(copy.deepcopy(self.ext.get('options', {})),
+ self.cfg.template_values,
+ expect_resolved=False)
+ if 'parallel' in self.options:
+ # Replace value and issue better warning for easyconfig parameters,
+ # as opposed to warnings meant for easyblocks
+ self.log.deprecated("Easyconfig parameter 'parallel' is deprecated, use 'max_parallel' instead.", '6.0')
+ self.options['max_parallel'] = self.options.pop('parallel')
if extra_params:
self.cfg.extend_params(extra_params, overwrite=False)
@@ -147,16 +154,17 @@ def __init__(self, mself, ext, extra_params=None):
self.log.debug("Skipping unknown custom easyconfig parameter '%s' for extension %s/%s: %s",
key, name, version, value)
+ # If parallelism has been set already take potentially new limitation into account
+ if self.cfg.is_parallel_set:
+ max_par = self.cfg['max_parallel']
+ if max_par is not None and max_par < self.cfg.parallel:
+ self.cfg.parallel = max_par
+
self.sanity_check_fail_msgs = []
self.sanity_check_module_loaded = False
self.fake_mod_data = None
- self.async_cmd_info = None
- self.async_cmd_output = None
- self.async_cmd_check_cnt = None
- # initial read size should be relatively small,
- # to avoid hanging for a long time until desired output is available in async_cmd_check
- self.async_cmd_read_size = 1024
+ self.async_cmd_task = None
@property
def name(self):
@@ -173,18 +181,39 @@ def version(self):
return self.ext.get('version', None)
def prerun(self):
+ """
+ [DEPRECATED][6.0] Stuff to do before installing a extension.
+ """
+ # Deprecation warning triggered by Extension.install_extension_substep()
+ self.pre_install_extension()
+
+ def pre_install_extension(self):
"""
Stuff to do before installing a extension.
"""
pass
def run(self, *args, **kwargs):
+ """
+ [DEPRECATED][6.0] Actual installation of an extension.
+ """
+ # Deprecation warning triggered by Extension.install_extension_substep()
+ self.install_extension(*args, **kwargs)
+
+ def install_extension(self, *args, **kwargs):
"""
Actual installation of an extension.
"""
pass
def run_async(self, *args, **kwargs):
+ """
+ [DEPRECATED][6.0] Asynchronous installation of an extension.
+ """
+ # Deprecation warning triggered by Extension.install_extension_substep()
+ self.install_extension_async(*args, **kwargs)
+
+ def install_extension_async(self, *args, **kwargs):
"""
Asynchronous installation of an extension.
"""
@@ -192,47 +221,58 @@ def run_async(self, *args, **kwargs):
def postrun(self):
"""
- Stuff to do after installing a extension.
+ [DEPRECATED][6.0] Stuff to do after installing a extension.
"""
- self.master.run_post_install_commands(commands=self.cfg.get('postinstallcmds', []))
+ # Deprecation warning triggered by Extension.install_extension_substep()
+ self.post_install_extension()
- def async_cmd_start(self, cmd, inp=None):
+ def post_install_extension(self):
"""
- Start installation asynchronously using specified command.
+ Stuff to do after installing a extension.
"""
- self.async_cmd_output = ''
- self.async_cmd_check_cnt = 0
- self.async_cmd_info = run_cmd(cmd, log_all=True, simple=False, inp=inp, regexp=False, asynchronous=True)
+ self.master.run_post_install_commands(commands=self.cfg.get('postinstallcmds', []))
- def async_cmd_check(self):
+ def install_extension_substep(self, substep, *args, **kwargs):
"""
- Check progress of installation command that was started asynchronously.
-
- :return: True if command completed, False otherwise
+ Carry out extension installation substep allowing use of deprecated
+ methods on those extensions using an older EasyBlock
"""
- if self.async_cmd_info is None:
- raise EasyBuildError("No installation command running asynchronously for %s", self.name)
- elif self.async_cmd_info is False:
- self.log.info("No asynchronous command was started for extension %s", self.name)
- return True
+ substeps_mapping = {
+ 'pre_install_extension': 'prerun',
+ 'install_extension': 'run',
+ 'install_extension_async': 'run_async',
+ 'post_install_extension': 'postrun',
+ }
+
+ deprecated_substep = substeps_mapping.get(substep)
+ if deprecated_substep is None:
+ raise EasyBuildError("Unknown extension installation substep: %s", substep)
+
+ try:
+ substep_method = getattr(self, deprecated_substep)
+ except AttributeError:
+ log_msg = f"EasyBlock does not implement deprecated method '{deprecated_substep}' "
+ log_msg += f"for installation substep {substep}"
+ self.log.debug(log_msg)
+ substep_method = getattr(self, substep)
else:
- self.log.debug("Checking on installation of extension %s...", self.name)
- # use small read size, to avoid waiting for a long time until sufficient output is produced
- res = check_async_cmd(*self.async_cmd_info, output_read_size=self.async_cmd_read_size)
- self.async_cmd_output += res['output']
- if res['done']:
- self.log.info("Installation of extension %s completed!", self.name)
- self.async_cmd_info = None
+ # Qualified method name contains class defining the method (PEP 3155)
+ substep_method_name = substep_method.__qualname__
+ self.log.debug(f"Found deprecated method in EasyBlock: {substep_method_name}")
+
+ base_method_name = f"Extension.{deprecated_substep}"
+ if substep_method_name == base_method_name:
+ # No custom method in child Easyblock, deprecated method is defined by base Extension class
+ # Switch to non-deprecated substep method
+ substep_method = getattr(self, substep)
else:
- self.async_cmd_check_cnt += 1
- self.log.debug("Installation of extension %s still running (checked %d times)",
- self.name, self.async_cmd_check_cnt)
- # increase read size after sufficient checks,
- # to avoid that installation hangs due to output buffer filling up...
- if self.async_cmd_check_cnt % 10 == 0 and self.async_cmd_read_size < (1024 ** 2):
- self.async_cmd_read_size *= 2
+ # Custom deprecated method used by child Easyblock
+ self.log.deprecated(
+ f"{substep_method_name}() is deprecated, use {substep}() instead.",
+ "6.0",
+ )
- return res['done']
+ return substep_method(*args, **kwargs)
@property
def required_deps(self):
@@ -274,15 +314,14 @@ def sanity_check_step(self):
self.log.info("modulename set to False for '%s' extension, so skipping sanity check", self.name)
elif exts_filter:
cmd, stdin = resolve_exts_filter_template(exts_filter, self)
- # set log_ok to False so we can catch the error instead of run_cmd
- (output, ec) = run_cmd(cmd, log_ok=False, simple=False, regexp=False, inp=stdin)
+ cmd_res = run_shell_cmd(cmd, fail_on_error=False, stdin=stdin)
- if ec:
+ if cmd_res.exit_code != EasyBuildExit.SUCCESS:
if stdin:
fail_msg = 'command "%s" (stdin: "%s") failed' % (cmd, stdin)
else:
fail_msg = 'command "%s" failed' % cmd
- fail_msg += "; output:\n%s" % output.strip()
+ fail_msg += "; output:\n%s" % cmd_res.output.strip()
self.log.warning("Sanity check for '%s' extension failed: %s", self.name, fail_msg)
res = (False, fail_msg)
# keep track of all reasons of failure
diff --git a/easybuild/framework/extensioneasyblock.py b/easybuild/framework/extensioneasyblock.py
index efd7c349f7..52d787528d 100644
--- a/easybuild/framework/extensioneasyblock.py
+++ b/easybuild/framework/extensioneasyblock.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of the University of Ghent (http://ugent.be/hpc).
@@ -89,6 +89,7 @@ def __init__(self, *args, **kwargs):
self.installdir = self.master.installdir
self.modules_tool = self.master.modules_tool
self.module_generator = self.master.module_generator
+ self.module_load_environment = self.master.module_load_environment
self.robot_path = self.master.robot_path
self.is_extension = True
self.unpack_options = None
@@ -135,7 +136,7 @@ def _set_start_dir(self):
self.log.warning(warn_msg)
print_warning(warn_msg, silent=build_option('silent'))
- def run(self, unpack_src=False):
+ def install_extension(self, unpack_src=False):
"""Common operations for extensions: unpacking sources, patching, ..."""
# unpack file if desired
diff --git a/easybuild/main.py b/easybuild/main.py
index b6adc08559..fd25e27dc1 100644
--- a/easybuild/main.py
+++ b/easybuild/main.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python
# #
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -37,6 +37,7 @@
* Ward Poelmans (Ghent University)
* Fotis Georgatos (Uni.Lu, NTUA)
* Maxime Boissonneault (Compute Canada)
+* Bart Oldeman (McGill University, Calcul Quebec, Digital Research Alliance of Canada)
"""
import copy
import os
@@ -70,7 +71,7 @@
from easybuild.tools.github import add_pr_labels, install_github_token, list_prs, merge_pr, new_branch_github, new_pr
from easybuild.tools.github import new_pr_from_branch
from easybuild.tools.github import sync_branch_with_develop, sync_pr_with_develop, update_branch, update_pr
-from easybuild.tools.hooks import BUILD_AND_INSTALL_LOOP, PRE_PREF, POST_PREF, START, END, CANCEL, FAIL
+from easybuild.tools.hooks import BUILD_AND_INSTALL_LOOP, PRE_PREF, POST_PREF, START, END, CANCEL, CRASH, FAIL
from easybuild.tools.hooks import load_hooks, run_hook
from easybuild.tools.modules import modules_tool
from easybuild.tools.options import opts_dict_to_eb_opts, set_up_configuration, use_color
@@ -79,7 +80,6 @@
from easybuild.tools.robot import check_conflicts, dry_run, missing_deps, resolve_dependencies, search_easyconfigs
from easybuild.tools.package.utilities import check_pkg_support
from easybuild.tools.parallelbuild import submit_jobs
-from easybuild.tools.py2vs3 import python2_is_deprecated
from easybuild.tools.repository.repository import init_repository
from easybuild.tools.systemtools import check_easybuild_deps
from easybuild.tools.testing import create_test_report, overall_test_report, regtest, session_state
@@ -134,10 +134,10 @@ def build_and_install_software(ecs, init_session_state, exit_on_failure=True):
ec_res = {}
try:
- (ec_res['success'], app_log, err) = build_and_install_one(ec, init_env)
+ (ec_res['success'], app_log, err_msg, err_code) = build_and_install_one(ec, init_env)
ec_res['log_file'] = app_log
if not ec_res['success']:
- ec_res['err'] = EasyBuildError(err)
+ ec_res['err'] = EasyBuildError(err_msg, exit_code=err_code)
except Exception as err:
# purposely catch all exceptions
ec_res['success'] = False
@@ -152,11 +152,11 @@ def build_and_install_software(ecs, init_session_state, exit_on_failure=True):
# keep track of success/total count
if ec_res['success']:
- test_msg = "Successfully built %s" % ec['spec']
+ test_msg = "Successfully installed %s" % ec['spec']
else:
- test_msg = "Build of %s failed" % ec['spec']
+ test_msg = "Installation of %s failed" % os.path.basename(ec['spec'])
if 'err' in ec_res:
- test_msg += " (err: %s)" % ec_res['err']
+ test_msg += ": %s" % ec_res['err']
# dump test report next to log file
test_report_txt = create_test_report(test_msg, [(ec, ec_res)], init_session_state)
@@ -172,10 +172,10 @@ def build_and_install_software(ecs, init_session_state, exit_on_failure=True):
adjust_permissions(parent_dir, stat.S_IWUSR, add=False, recursive=False)
if not ec_res['success'] and exit_on_failure:
- if 'traceback' in ec_res:
- raise EasyBuildError(ec_res['traceback'])
+ if not isinstance(ec_res['err'], EasyBuildError):
+ raise ec_res['err']
else:
- raise EasyBuildError(test_msg)
+ raise EasyBuildError(test_msg, exit_code=err_code)
res.append((ec, ec_res))
@@ -431,7 +431,9 @@ def process_eb_args(eb_args, eb_go, cfg_settings, modtool, testing, init_session
# don't try and tweak anything if easyconfigs were generated, since building a full dep graph will fail
# if easyconfig files for the dependencies are not available
if try_to_generate and build_specs and not generated_ecs:
- easyconfigs = tweak(easyconfigs, build_specs, modtool, targetdirs=tweaked_ecs_paths)
+ easyconfigs, tweak_map = tweak(easyconfigs, build_specs, modtool, targetdirs=tweaked_ecs_paths, return_map=True)
+ else:
+ tweak_map = None
if options.containerize:
# if --containerize/-C create a container recipe (and optionally container image), and stop
@@ -553,7 +555,7 @@ def process_eb_args(eb_args, eb_go, cfg_settings, modtool, testing, init_session
# submit build as job(s), clean up and exit
if options.job:
- submit_jobs(ordered_ecs, eb_go.generate_cmd_line(), testing=testing)
+ submit_jobs(ordered_ecs, eb_go.generate_cmd_line(), testing=testing, tweak_map=tweak_map)
if not testing:
print_msg("Submitted parallel build jobs, exiting now")
return True
@@ -613,9 +615,6 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None, pr
options, orig_paths = eb_go.options, eb_go.args
- if 'python2' not in build_option('silence_deprecation_warnings'):
- python2_is_deprecated()
-
global _log
(build_specs, _log, logfile, robot_path, search_query, eb_tmpdir, try_to_generate,
from_pr_list, tweaked_ecs_paths) = cfg_settings
@@ -778,20 +777,28 @@ def prepare_main(args=None, logfile=None, testing=None):
return init_session_state, eb_go, cfg_settings
-if __name__ == "__main__":
+def main_with_hooks(args=None):
# take into account that EasyBuildError may be raised when parsing the EasyBuild configuration
try:
- init_session_state, eb_go, cfg_settings = prepare_main()
+ init_session_state, eb_go, cfg_settings = prepare_main(args=args)
except EasyBuildError as err:
- print_error(err.msg)
+ print_error(err.msg, exit_code=err.exit_code)
hooks = load_hooks(eb_go.options.hooks)
try:
- main(prepared_cfg_data=(init_session_state, eb_go, cfg_settings))
+ main(args=args, prepared_cfg_data=(init_session_state, eb_go, cfg_settings))
except EasyBuildError as err:
run_hook(FAIL, hooks, args=[err])
- print_error(err.msg)
+ print_error(err.msg, exit_on_error=True, exit_code=err.exit_code)
except KeyboardInterrupt as err:
run_hook(CANCEL, hooks, args=[err])
print_error("Cancelled by user: %s" % err)
+ except Exception as err:
+ run_hook(CRASH, hooks, args=[err])
+ sys.stderr.write("EasyBuild crashed! Please consider reporting a bug, this should not happen...\n\n")
+ raise
+
+
+if __name__ == "__main__":
+ main_with_hooks()
diff --git a/easybuild/scripts/bootstrap_eb.py b/easybuild/scripts/bootstrap_eb.py
deleted file mode 100755
index ef8c281162..0000000000
--- a/easybuild/scripts/bootstrap_eb.py
+++ /dev/null
@@ -1,1172 +0,0 @@
-#!/usr/bin/env python
-##
-# Copyright 2013-2024 Ghent University
-#
-# This file is part of EasyBuild,
-# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
-# with support of Ghent University (http://ugent.be/hpc),
-# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
-# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
-# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
-#
-# https://github.com/easybuilders/easybuild
-#
-# EasyBuild is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation v2.
-#
-# EasyBuild is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with EasyBuild. If not, see .
-##
-
-"""
-Bootstrap script for EasyBuild
-
-Installs distribute with included (patched) distribute_setup.py script to obtain easy_install,
-and then performs a staged install of EasyBuild:
- * stage 0: install setuptools (which provides easy_install), unless already available
- * stage 1: install EasyBuild with easy_install to a temporary directory
- * stage 2: install EasyBuild with EasyBuild from stage 1 to specified install directory
-
-Authors: Kenneth Hoste (UGent), Stijn Deweirdt (UGent), Ward Poelmans (UGent)
-License: GPLv2
-
-inspired by https://bitbucket.org/pdubroy/pip/raw/tip/getpip.py
-(via http://dubroy.com/blog/so-you-want-to-install-a-python-package/)
-"""
-
-import codecs
-import copy
-import glob
-import os
-import re
-import shutil
-import site
-import sys
-import tempfile
-import traceback
-from distutils.version import LooseVersion
-from hashlib import md5
-from platform import python_version
-
-IS_PY3 = sys.version_info[0] == 3
-
-if not IS_PY3:
- import urllib2 as std_urllib
-else:
- import urllib.request as std_urllib
-
-
-EB_BOOTSTRAP_VERSION = '20210715.01'
-
-# argparse preferrred, optparse deprecated >=2.7
-HAVE_ARGPARSE = False
-try:
- import argparse
- HAVE_ARGPARSE = True
-except ImportError:
- import optparse
-
-PYPI_SOURCE_URL = 'https://pypi.python.org/packages/source'
-
-VSC_BASE = 'vsc-base'
-VSC_INSTALL = 'vsc-install'
-# Python 3 is not supported by the vsc-* packages
-EASYBUILD_PACKAGES = (([] if IS_PY3 else [VSC_INSTALL, VSC_BASE]) +
- ['easybuild-framework', 'easybuild-easyblocks', 'easybuild-easyconfigs'])
-
-STAGE1_SUBDIR = 'eb_stage1'
-
-# the EasyBuild bootstrap script is deprecated, and will only run if $EASYBUILD_BOOTSTRAP_DEPRECATED is defined
-EASYBUILD_BOOTSTRAP_DEPRECATED = os.environ.pop('EASYBUILD_BOOTSTRAP_DEPRECATED', None)
-
-# set print_debug to True for detailed progress info
-print_debug = os.environ.pop('EASYBUILD_BOOTSTRAP_DEBUG', False)
-
-# install with --force in stage2?
-forced_install = os.environ.pop('EASYBUILD_BOOTSTRAP_FORCED', False)
-
-# don't add user site directory to sys.path (equivalent to python -s), see https://www.python.org/dev/peps/pep-0370/
-os.environ['PYTHONNOUSERSITE'] = '1'
-site.ENABLE_USER_SITE = False
-
-# clean PYTHONPATH to avoid finding readily installed stuff
-os.environ['PYTHONPATH'] = ''
-
-EASYBUILD_BOOTSTRAP_SOURCEPATH = os.environ.pop('EASYBUILD_BOOTSTRAP_SOURCEPATH', None)
-EASYBUILD_BOOTSTRAP_SKIP_STAGE0 = os.environ.pop('EASYBUILD_BOOTSTRAP_SKIP_STAGE0', False)
-EASYBUILD_BOOTSTRAP_FORCE_VERSION = os.environ.pop('EASYBUILD_BOOTSTRAP_FORCE_VERSION', None)
-
-# keep track of original environment (after clearing PYTHONPATH)
-orig_os_environ = copy.deepcopy(os.environ)
-
-# If the modules tool is specified, use it
-easybuild_modules_tool = os.environ.get('EASYBUILD_MODULES_TOOL', None)
-easybuild_module_syntax = os.environ.get('EASYBUILD_MODULE_SYNTAX', None)
-
-# If modules subdir specifications are defined, use them
-easybuild_installpath_modules = os.environ.get('EASYBUILD_INSTALLPATH_MODULES', None)
-easybuild_subdir_modules = os.environ.get('EASYBUILD_SUBDIR_MODULES', 'modules')
-easybuild_suffix_modules_path = os.environ.get('EASYBUILD_SUFFIX_MODULES_PATH', 'all')
-
-
-#
-# Utility functions
-#
-def debug(msg):
- """Print debug message."""
-
- if print_debug:
- print("[[DEBUG]] " + msg)
-
-
-def info(msg):
- """Print info message."""
-
- print("[[INFO]] " + msg)
-
-
-def error(msg, exit=True):
- """Print error message and exit."""
-
- print("[[ERROR]] " + msg)
- sys.exit(1)
-
-
-def mock_stdout_stderr():
- """Mock stdout/stderr channels"""
- try:
- from cStringIO import StringIO
- except ImportError:
- from io import StringIO
- orig_stdout, orig_stderr = sys.stdout, sys.stderr
- sys.stdout.flush()
- sys.stdout = StringIO()
- sys.stderr.flush()
- sys.stderr = StringIO()
-
- return orig_stdout, orig_stderr
-
-
-def restore_stdout_stderr(orig_stdout, orig_stderr):
- """Restore stdout/stderr channels after mocking"""
- # collect output
- sys.stdout.flush()
- stdout = sys.stdout.getvalue()
- sys.stderr.flush()
- stderr = sys.stderr.getvalue()
-
- # restore original stdout/stderr
- sys.stdout = orig_stdout
- sys.stderr = orig_stderr
-
- return stdout, stderr
-
-
-def det_lib_path(libdir):
- """Determine relative path of Python library dir."""
- if libdir is None:
- libdir = 'lib'
- pyver = '.'.join([str(x) for x in sys.version_info[:2]])
- return os.path.join(libdir, 'python%s' % pyver, 'site-packages')
-
-
-def det_modules_path(install_path):
- """Determine modules path."""
- if easybuild_installpath_modules is not None:
- modules_path = os.path.join(easybuild_installpath_modules, easybuild_suffix_modules_path)
- else:
- modules_path = os.path.join(install_path, easybuild_subdir_modules, easybuild_suffix_modules_path)
-
- return modules_path
-
-
-def find_egg_dir_for(path, pkg):
- """Find full path of egg dir for given package."""
-
- res = None
-
- for libdir in ['lib', 'lib64']:
- full_libpath = os.path.join(path, det_lib_path(libdir))
- eggdir_regex = re.compile('%s-[0-9a-z.]+-py[0-9.]+.egg' % pkg.replace('-', '_'))
- subdirs = (os.path.exists(full_libpath) and sorted(os.listdir(full_libpath))) or []
- for subdir in subdirs:
- if eggdir_regex.match(subdir):
- eggdir = os.path.join(full_libpath, subdir)
- if res is None:
- debug("Found egg dir for %s at %s" % (pkg, eggdir))
- res = eggdir
- else:
- debug("Found another egg dir for %s at %s (ignoring it)" % (pkg, eggdir))
-
- # no egg dir found
- if res is None:
- debug("Failed to determine egg dir path for %s in %s (subdirs: %s)" % (pkg, path, subdirs))
-
- return res
-
-
-def prep(path):
- """Prepare for installing a Python package in the specified path."""
-
- debug("Preparing for path %s" % path)
-
- # restore original environment first
- os.environ = copy.deepcopy(orig_os_environ)
- debug("os.environ['PYTHONPATH'] after reset: %s" % os.environ['PYTHONPATH'])
-
- # update PATH
- os.environ['PATH'] = os.pathsep.join([os.path.join(path, 'bin')] +
- [x for x in os.environ.get('PATH', '').split(os.pathsep) if len(x) > 0])
- debug("$PATH: %s" % os.environ['PATH'])
-
- # update actual Python search path
- sys.path.insert(0, path)
-
- # make sure directory exists (this is required by setuptools)
- # usually it's 'lib', but can be 'lib64' as well
- for libdir in ['lib', 'lib64']:
- full_libpath = os.path.join(path, det_lib_path(libdir))
- if not os.path.exists(full_libpath):
- os.makedirs(full_libpath)
- # PYTHONPATH needs to be set as well, otherwise setuptools will fail
- pythonpaths = [x for x in os.environ.get('PYTHONPATH', '').split(os.pathsep) if len(x) > 0]
- os.environ['PYTHONPATH'] = os.pathsep.join([full_libpath] + pythonpaths)
-
- debug("$PYTHONPATH: %s" % os.environ['PYTHONPATH'])
-
- os.environ['EASYBUILD_MODULES_TOOL'] = easybuild_modules_tool
- debug("$EASYBUILD_MODULES_TOOL set to %s" % os.environ['EASYBUILD_MODULES_TOOL'])
-
- if easybuild_module_syntax:
- # if module syntax is specified, use it
- os.environ['EASYBUILD_MODULE_SYNTAX'] = easybuild_module_syntax
- debug("Using specified module syntax: %s" % os.environ['EASYBUILD_MODULE_SYNTAX'])
- elif easybuild_modules_tool != 'Lmod':
- # Lua is the default module syntax, but that requires Lmod
- # if Lmod is not being used, use Tcl module syntax
- os.environ['EASYBUILD_MODULE_SYNTAX'] = 'Tcl'
- debug("$EASYBUILD_MODULE_SYNTAX set to %s" % os.environ['EASYBUILD_MODULE_SYNTAX'])
-
-
-def check_module_command(tmpdir):
- """Check which module command is available, and prepare for using it."""
- global easybuild_modules_tool
-
- if easybuild_modules_tool is not None:
- info("Using modules tool specified by $EASYBUILD_MODULES_TOOL: %s" % easybuild_modules_tool)
- return easybuild_modules_tool
-
- def check_cmd_help(modcmd):
- """Check 'help' output for specified command."""
- modcmd_re = re.compile(r'module\s.*command')
- cmd = "%s python help" % modcmd
- os.system("%s > %s 2>&1" % (cmd, out))
- txt = open(out, 'r').read()
- debug("Output from %s: %s" % (cmd, txt))
- return modcmd_re.search(txt)
-
- def is_modulecmd_tcl_modulestcl():
- """Determine if modulecmd.tcl is EnvironmentModulesTcl."""
- modcmd_re = re.compile('Modules Release Tcl')
- cmd = "modulecmd.tcl python --version"
- os.system("%s > %s 2>&1" % (cmd, out))
- txt = open(out, 'r').read()
- debug("Output from %s: %s" % (cmd, txt))
- return modcmd_re.search(txt)
-
- # order matters, which is why we don't use a dict
- known_module_commands = [
- ('lmod', 'Lmod'),
- ('modulecmd.tcl', 'EnvironmentModules'),
- ('modulecmd', 'EnvironmentModulesC'),
- ]
- out = os.path.join(tmpdir, 'module_command.out')
- modtool = None
- for modcmd, modtool in known_module_commands:
- if check_cmd_help(modcmd):
- # distinguish between EnvironmentModulesTcl and EnvironmentModules
- if modcmd == 'modulecmd.tcl' and is_modulecmd_tcl_modulestcl():
- modtool = 'EnvironmentModulesTcl'
- easybuild_modules_tool = modtool
- info("Found module command '%s' (%s), so using it." % (modcmd, modtool))
- break
- elif modcmd == 'lmod':
- # check value of $LMOD_CMD as fallback
- modcmd = os.environ.get('LMOD_CMD')
- if modcmd and check_cmd_help(modcmd):
- easybuild_modules_tool = modtool
- info("Found module command '%s' via $LMOD_CMD (%s), so using it." % (modcmd, modtool))
- break
- elif modtool == 'EnvironmentModules':
- # check value of $MODULESHOME as fallback
- moduleshome = os.environ.get('MODULESHOME', 'MODULESHOME_NOT_DEFINED')
- modcmd = os.path.join(moduleshome, 'libexec', 'modulecmd.tcl')
- if os.path.exists(modcmd) and check_cmd_help(modcmd):
- easybuild_modules_tool = modtool
- info("Found module command '%s' via $MODULESHOME (%s), so using it." % (modcmd, modtool))
- break
-
- if easybuild_modules_tool is None:
- mod_cmds = [m for (m, _) in known_module_commands]
- msg = [
- "Could not find any module command, make sure one available in your $PATH.",
- "Known module commands are checked in order, and include: %s" % ', '.join(mod_cmds),
- "Check the output of 'type module' to determine the location of the module command you are using.",
- ]
- error('\n'.join(msg))
-
- return modtool
-
-
-def check_setuptools():
- """Check whether a suitable setuptools installation is already available."""
-
- debug("Checking whether suitable setuptools installation is available...")
- res = None
-
- _, outfile = tempfile.mkstemp()
-
- # note: we need to be very careful here, because switching to a different setuptools installation (e.g. in stage0)
- # after the setuptools module was imported is very tricky...
- # So, we'll check things by running commands through os.system rather than importing setuptools directly.
- cmd_tmpl = "%s -c '%%s' > %s 2>&1" % (sys.executable, outfile)
-
- # check setuptools version
- try:
- os.system(cmd_tmpl % "import setuptools; print(setuptools.__version__)")
- setuptools_ver = LooseVersion(open(outfile).read().strip())
- debug("Found setuptools version %s" % setuptools_ver)
-
- min_setuptools_ver = '0.6c11'
- if setuptools_ver < LooseVersion(min_setuptools_ver):
- debug("Minimal setuptools version %s not satisfied, found '%s'" % (min_setuptools_ver, setuptools_ver))
- res = False
- except Exception as err:
- debug("Failed to check setuptools version: %s" % err)
- res = False
-
- os.system(cmd_tmpl % "from setuptools.command import easy_install; print(easy_install.__file__)")
- out = open(outfile).read().strip()
- debug("Location of setuptools' easy_install module: %s" % out)
- if 'setuptools/command/easy_install' not in out:
- debug("Module 'setuptools.command.easy_install not found")
- res = False
-
- if res is None:
- os.system(cmd_tmpl % "import setuptools; print(setuptools.__file__)")
- setuptools_loc = open(outfile).read().strip()
- res = os.path.dirname(os.path.dirname(setuptools_loc))
- debug("Location of setuptools installation: %s" % res)
-
- try:
- os.remove(outfile)
- except Exception:
- pass
-
- return res
-
-
-def run_easy_install(args):
- """Run easy_install with specified list of arguments"""
- import setuptools
- debug("Active setuptools installation: %s" % setuptools.__file__)
- from setuptools.command import easy_install
-
- orig_stdout, orig_stderr = mock_stdout_stderr()
- try:
- easy_install.main(args)
- easy_install_stdout, easy_install_stderr = restore_stdout_stderr(orig_stdout, orig_stderr)
- except (Exception, SystemExit) as err:
- easy_install_stdout, easy_install_stderr = restore_stdout_stderr(orig_stdout, orig_stderr)
- error("Running 'easy_install %s' failed: %s\n%s" % (' '.join(args), err, traceback.format_exc()))
-
- debug("stdout for 'easy_install %s':\n%s" % (' '.join(args), easy_install_stdout))
- debug("stderr for 'easy_install %s':\n%s" % (' '.join(args), easy_install_stderr))
-
-
-def check_easy_install_cmd():
- """Try to make sure available 'easy_install' command matches active 'setuptools' installation."""
-
- debug("Checking whether available 'easy_install' command matches active 'setuptools' installation...")
-
- _, outfile = tempfile.mkstemp()
-
- import setuptools
- debug("Location of active setuptools installation: %s" % setuptools.__file__)
-
- easy_install_regex = re.compile('^(setuptools|distribute) %s' % setuptools.__version__)
- debug("Pattern for 'easy_install --version': %s" % easy_install_regex.pattern)
-
- pythonpath = os.getenv('PYTHONPATH', '')
- cmd = "PYTHONPATH='%s' %s -m easy_install --version" % (pythonpath, sys.executable)
- os.system("%s > %s 2>&1" % (cmd, outfile))
- outtxt = open(outfile).read().strip()
- debug("Output of '%s':\n%s" % (cmd, outtxt))
- res = bool(easy_install_regex.match(outtxt))
- debug("Result: %s" % res)
- if res:
- debug("Found right 'easy_install' command")
- return
-
- error("Failed to find right 'easy_install' command!")
-
-
-#
-# Stage functions
-#
-def stage0(tmpdir):
- """STAGE 0: Prepare and install distribute via included (patched) distribute_setup.py script."""
-
- print('\n')
- info("+++ STAGE 0: installing distribute via included (patched) distribute_setup.py...\n")
-
- txt = DISTRIBUTE_SETUP_PY
- if not print_debug:
- # silence distribute_setup.py by redirecting output to /dev/null
- txt = re.sub(r'([^\n]*)(return subprocess.call\(args)(\) == 0)',
- r"\1f = open(os.devnull, 'w'); \2, stdout=f, stderr=f\3",
- txt)
- # silence distribute_setup.py completely by setting high log level threshold
- txt = re.sub(r'([^\n]*)(# extracting the tarball[^\n]*)', r'\1log.set_verbosity(1000)\n\1\2', txt)
-
- # write distribute_setup.py to file (with correct header)
- distribute_setup = os.path.join(tmpdir, 'distribute_setup.py')
- f = open(distribute_setup, "w")
- f.write(txt)
- f.close()
-
- # create expected directories, set Python search path
- debug("preparing environment...")
- prep(tmpdir)
-
- import distribute_setup
- debug("distribute_setup.__file__: %s" % distribute_setup.__file__)
-
- # install easy_install to temporary directory
- from distribute_setup import main as distribute_setup_main
- orig_sys_argv = sys.argv[:] # make a copy
- sys.argv.append('--prefix=%s' % tmpdir)
- # We download a custom version of distribute: it uses a newer version of markerlib to avoid a bug (#1099)
- # It's is the source of distribute 0.6.49 with the file _markerlib/markers.py replaced by the 0.6 version of
- # markerlib which can be found at https://pypi.python.org/pypi/markerlib/0.6.0
- sys.argv.append('--download-base=https://easybuilders.github.io/easybuild/files/')
- distribute_setup_main(version="0.6.49-patched1")
- sys.argv = orig_sys_argv
-
- # sanity check
- if os.path.exists(os.path.join(tmpdir, 'bin', 'easy_install')):
- debug("easy_install sanity check OK")
- else:
- error("Installing distribute which should deliver easy_install failed?")
-
- # prepend distribute egg dir to sys.path, so we know which setuptools we're using
- distribute_egg_dir = find_egg_dir_for(tmpdir, 'distribute')
- if distribute_egg_dir is None:
- error("Failed to determine egg dir path for distribute_egg_dir in %s" % tmpdir)
- else:
- sys.path.insert(0, distribute_egg_dir)
-
- # make sure we're getting the setuptools we expect
- import setuptools
- from setuptools.command import easy_install
-
- for mod, path in [('setuptools', setuptools.__file__), ('easy_install', easy_install.__file__)]:
- if tmpdir not in path:
- error("Found another %s module than expected: %s" % (mod, path))
- else:
- debug("Found %s in expected path, good!" % mod)
-
- info("Installed setuptools version %s (%s)" % (setuptools.__version__, setuptools.__file__))
-
- return distribute_egg_dir
-
-
-def stage1(tmpdir, sourcepath, distribute_egg_dir, forcedversion):
- """STAGE 1: temporary install EasyBuild using distribute's easy_install."""
-
- print('\n')
- info("+++ STAGE 1: installing EasyBuild in temporary dir with easy_install...\n")
-
- # determine locations of source tarballs, if sources path is specified
- source_tarballs = {}
- if sourcepath is not None:
- info("Fetching sources from %s..." % sourcepath)
- for pkg in EASYBUILD_PACKAGES:
- pkg_tarball_glob = os.path.join(sourcepath, '%s*.tar.gz' % pkg)
- pkg_tarball_paths = glob.glob(pkg_tarball_glob)
- if len(pkg_tarball_paths) > 1:
- error("Multiple tarballs found for %s: %s" % (pkg, pkg_tarball_paths))
- elif len(pkg_tarball_paths) == 0:
- if pkg not in [VSC_BASE, VSC_INSTALL]:
- # vsc-base package is not strictly required
- # it's only a dependency since EasyBuild v2.0;
- # with EasyBuild v2.0, it will be pulled in from PyPI when installing easybuild-framework;
- # vsc-install is an optional dependency, only required to run unit tests
- error("Missing source tarball: %s" % pkg_tarball_glob)
- else:
- info("Found %s for %s package" % (pkg_tarball_paths[0], pkg))
- source_tarballs.update({pkg: pkg_tarball_paths[0]})
-
- if print_debug:
- debug("$ easy_install --help")
- run_easy_install(['--help'])
-
- # prepare install dir
- targetdir_stage1 = os.path.join(tmpdir, STAGE1_SUBDIR)
- prep(targetdir_stage1) # set PATH, Python search path
-
- # install latest EasyBuild with easy_install from PyPi
- cmd = [
- '--upgrade', # make sure the latest version is pulled from PyPi
- '--prefix=%s' % targetdir_stage1,
- ]
-
- post_vsc_base = []
- if source_tarballs:
- # install provided source tarballs (order matters)
- cmd.extend([source_tarballs[pkg] for pkg in EASYBUILD_PACKAGES if pkg in source_tarballs])
- # add vsc-base again at the end, to avoid that the one available on the system is used instead
- if VSC_BASE in source_tarballs:
- cmd.append(source_tarballs[VSC_BASE])
- else:
- # install meta-package easybuild from PyPI
- if forcedversion:
- cmd.append('easybuild==%s' % forcedversion)
- elif IS_PY3:
- cmd.append('easybuild>=4.0') # Python 3 support added in EasyBuild 4
- else:
- cmd.append('easybuild')
-
- if not IS_PY3:
- # install vsc-base again at the end, to avoid that the one available on the system is used instead
- post_vsc_base = cmd[:]
- post_vsc_base[-1] = VSC_BASE + '<2.9.0'
-
- if not print_debug:
- cmd.insert(0, '--quiet')
-
- # There is no support for Python3 in the older vsc-* packages and EasyBuild 4 includes working versions of vsc-*
- if not IS_PY3:
- # install vsc-install version prior to 0.11.4, where mock was introduced as a dependency
- # workaround for problem reported in https://github.com/easybuilders/easybuild-framework/issues/2712
- # also stick to vsc-base < 2.9.0 to avoid requiring 'future' Python package as dependency
- for pkg in [VSC_INSTALL + '<0.11.4', VSC_BASE + '<2.9.0']:
- precmd = cmd[:-1] + [pkg]
- info("running pre-install command 'easy_install %s'" % (' '.join(precmd)))
- run_easy_install(precmd)
-
- info("installing EasyBuild with 'easy_install %s'\n" % (' '.join(cmd)))
- syntax_error_note = '\n'.join([
- "Note: a 'SyntaxError' may be reported for the easybuild/tools/py2vs3/py%s.py module." % ('3', '2')[IS_PY3],
- "You can safely ignore this message, it will not affect the functionality of the EasyBuild installation.",
- '',
- ])
- info(syntax_error_note)
- run_easy_install(cmd)
-
- if post_vsc_base:
- info("running post install command 'easy_install %s'" % (' '.join(post_vsc_base)))
- run_easy_install(post_vsc_base)
-
- pkg_egg_dir = find_egg_dir_for(targetdir_stage1, VSC_BASE)
- if pkg_egg_dir is None:
- # if vsc-base available on system is the same version as the one being installed,
- # the .egg directory may not get installed...
- # in that case, try to have it *copied* by also including --always-copy;
- # using --always-copy should be used as a last resort, since it can result in all kinds of problems
- info(".egg dir for vsc-base not found, trying again with --always-copy...")
- post_vsc_base.insert(0, '--always-copy')
- info("running post install command 'easy_install %s'" % (' '.join(post_vsc_base)))
- run_easy_install(post_vsc_base)
-
- # clear the Python search path, we only want the individual eggs dirs to be in the PYTHONPATH (see below)
- # this is needed to avoid easy-install.pth controlling what Python packages are actually used
- if distribute_egg_dir is not None:
- os.environ['PYTHONPATH'] = distribute_egg_dir
- else:
- del os.environ['PYTHONPATH']
-
- # template string to inject in template easyconfig
- templates = {}
-
- for pkg in EASYBUILD_PACKAGES:
- templates.update({pkg: ''})
-
- pkg_egg_dir = find_egg_dir_for(targetdir_stage1, pkg)
- if pkg_egg_dir is None:
- if pkg in [VSC_BASE, VSC_INSTALL]:
- # vsc-base is optional in older EasyBuild versions
- continue
-
- # prepend EasyBuild egg dirs to Python search path, so we know which EasyBuild we're using
- sys.path.insert(0, pkg_egg_dir)
- pythonpaths = [x for x in os.environ.get('PYTHONPATH', '').split(os.pathsep) if len(x) > 0]
- os.environ['PYTHONPATH'] = os.pathsep.join([pkg_egg_dir] + pythonpaths)
- debug("$PYTHONPATH: %s" % os.environ['PYTHONPATH'])
-
- if source_tarballs:
- if pkg in source_tarballs:
- templates.update({pkg: "'%s'," % os.path.basename(source_tarballs[pkg])})
- else:
- # determine per-package versions based on egg dirs, to use them in easyconfig template
- version_regex = re.compile('%s-([0-9a-z.-]*)-py[0-9.]*.egg' % pkg.replace('-', '_'))
- pkg_egg_dirname = os.path.basename(pkg_egg_dir)
- res = version_regex.search(pkg_egg_dirname)
- if res is not None:
- pkg_version = res.group(1)
- debug("Found version for easybuild-%s: %s" % (pkg, pkg_version))
- templates.update({pkg: "'%s-%s.tar.gz'," % (pkg, pkg_version)})
- else:
- tup = (pkg, pkg_egg_dirname, version_regex.pattern)
- error("Failed to determine version for easybuild-%s package from %s with %s" % tup)
-
- # figure out EasyBuild version via eb command line
- # note: EasyBuild uses some magic to determine the EasyBuild version based on the versions of the individual pkgs
- ver_regex = {'ver': '[0-9.]*[a-z0-9]*'}
- pattern = r"This is EasyBuild (?P%(ver)s) \(framework: %(ver)s, easyblocks: %(ver)s\)" % ver_regex
- version_re = re.compile(pattern)
- version_out_file = os.path.join(tmpdir, 'eb_version.out')
- eb_version_cmd = 'from easybuild.tools.version import this_is_easybuild; print(this_is_easybuild())'
- cmd = "%s -c '%s' > %s 2>&1" % (sys.executable, eb_version_cmd, version_out_file)
- debug("Determining EasyBuild version using command '%s'" % cmd)
- os.system(cmd)
- txt = open(version_out_file, "r").read()
- res = version_re.search(txt)
- if res:
- eb_version = res.group(1)
- debug("installing EasyBuild v%s" % eb_version)
- else:
- error("Stage 1 failed, could not determine EasyBuild version (txt: %s)." % txt)
-
- templates.update({'version': eb_version})
-
- # clear PYTHONPATH before we go to stage2
- # PYTHONPATH doesn't need to (and shouldn't) include the stage1 egg dirs
- os.environ['PYTHONPATH'] = ''
-
- # make sure we're getting the expected EasyBuild packages
- import easybuild.framework
- import easybuild.easyblocks
- pkgs_to_check = [easybuild.framework, easybuild.easyblocks]
- # vsc is part of EasyBuild 4
- if LooseVersion(eb_version) < LooseVersion('4'):
- import vsc.utils.fancylogger
- pkgs_to_check.append(vsc.utils.fancylogger)
-
- for pkg in pkgs_to_check:
- if tmpdir not in pkg.__file__:
- error("Found another %s than expected: %s" % (pkg.__name__, pkg.__file__))
- else:
- debug("Found %s in expected path, good!" % pkg.__name__)
-
- debug("templates: %s" % templates)
- return templates
-
-
-def stage2(tmpdir, templates, install_path, distribute_egg_dir, sourcepath):
- """STAGE 2: install EasyBuild to temporary dir with EasyBuild from stage 1."""
-
- print('\n')
- info("+++ STAGE 2: installing EasyBuild in %s with EasyBuild from stage 1...\n" % install_path)
-
- preinstallopts = ''
-
- eb_looseversion = LooseVersion(templates['version'])
-
- # setuptools is no longer required for EasyBuild v4.0 & newer, so skip the setuptools stuff in that case
- if eb_looseversion < LooseVersion('4.0') and distribute_egg_dir is not None:
- # inject path to distribute installed in stage 0 into $PYTHONPATH via preinstallopts
- # other approaches are not reliable, since EasyBuildMeta easyblock unsets $PYTHONPATH;
- # this is required for the easy_install from stage 0 to work
- preinstallopts += "export PYTHONPATH=%s:$PYTHONPATH && " % distribute_egg_dir
-
- # ensure that (latest) setuptools is installed as well alongside EasyBuild,
- # since it is a required runtime dependency for recent vsc-base and EasyBuild versions
- # this is necessary since we provide our own distribute installation during the bootstrap (cfr. stage0)
- preinstallopts += "%s -m easy_install -U --prefix %%(installdir)s setuptools && " % sys.executable
-
- # vsc-install is no longer required for EasyBuild v4.0, so skip pre-installed vsc-install in that case
- if eb_looseversion < LooseVersion('4.0'):
- # vsc-install is a runtime dependency for the EasyBuild unit test suite,
- # and is easily picked up from stage1 rather than being actually installed, so force it
- vsc_install = "'%s<0.11.4'" % VSC_INSTALL
- if sourcepath:
- vsc_install_tarball_paths = glob.glob(os.path.join(sourcepath, 'vsc-install*.tar.gz'))
- if len(vsc_install_tarball_paths) == 1:
- vsc_install = vsc_install_tarball_paths[0]
- preinstallopts += "%s -m easy_install -U --prefix %%(installdir)s %s && " % (sys.executable, vsc_install)
-
- templates.update({
- 'preinstallopts': preinstallopts,
- })
-
- # determine PyPI URLs for individual packages
- pkg_urls = []
- for pkg in EASYBUILD_PACKAGES:
-
- # vsc-base and vsc-install are not dependencies anymore for EasyBuild v4.0,
- # so skip them here for recent EasyBuild versions
- if eb_looseversion >= LooseVersion('4.0') and pkg in [VSC_INSTALL, VSC_BASE]:
- continue
-
- # format of pkg entries in templates: "'',"
- pkg_filename = templates[pkg][1:-2]
-
- # the lines below implement a simplified version of the 'pypi_source_urls' and 'derive_alt_pypi_url' functions,
- # which we can't leverage here, partially because of transitional changes in PyPI (#md5= -> #sha256=)
-
- # determine download URL via PyPI's 'simple' API
- pkg_simple = None
- try:
- pkg_simple = std_urllib.urlopen('https://pypi.python.org/simple/%s' % pkg, timeout=10).read()
- except (std_urllib.URLError, std_urllib.HTTPError) as err:
- # failing to figure out the package download URl may be OK when source tarballs are provided
- if sourcepath:
- info("Ignoring failed attempt to determine '%s' download URL since source tarballs are provided" % pkg)
- else:
- raise err
-
- if pkg_simple:
- if IS_PY3:
- pkg_simple = pkg_simple.decode('utf-8')
- pkg_url_part_regex = re.compile('/(packages/[^#]+)/%s#' % pkg_filename)
- res = pkg_url_part_regex.search(pkg_simple)
- if res:
- pkg_url = 'https://pypi.python.org/' + res.group(1)
- pkg_urls.append(pkg_url)
- elif sourcepath:
- info("Ignoring failure to determine source URL for '%s' (source tarballs are provided)" % pkg_filename)
- else:
- error_msg = "Failed to determine PyPI package URL for %s using pattern '%s': %s\n"
- error(error_msg % (pkg, pkg_url_part_regex.pattern, pkg_simple))
-
- # vsc-base and vsc-install are no longer required for EasyBuild v4.0.0,
- # so only include them in 'sources' for older versions
- sources_tmpl = "%(easybuild-framework)s%(easybuild-easyblocks)s%(easybuild-easyconfigs)s"
- if eb_looseversion < LooseVersion('4.0'):
- sources_tmpl = "%(vsc-install)s%(vsc-base)s" + sources_tmpl
-
- templates.update({
- 'source_urls': '\n'.join(["'%s'," % x for x in pkg_urls]),
- 'sources': sources_tmpl % templates,
- 'pythonpath': distribute_egg_dir,
- })
-
- # create easyconfig file
- ebfile = os.path.join(tmpdir, 'EasyBuild-%s.eb' % templates['version'])
- handle = open(ebfile, 'w')
- ebfile_txt = EASYBUILD_EASYCONFIG_TEMPLATE % templates
- handle.write(ebfile_txt)
- handle.close()
- debug("Contents of generated easyconfig file:\n%s" % ebfile_txt)
-
- # set command line arguments for eb
- eb_args = ['eb', ebfile, '--allow-modules-tool-mismatch']
- if print_debug:
- eb_args.extend(['--debug', '--logtostdout'])
- if forced_install:
- info("Performing FORCED installation, as requested...")
- eb_args.append('--force')
-
- # make sure we don't leave any stuff behind in default path $HOME/.local/easybuild
- # and set build and install path explicitely
- if LooseVersion(templates['version']) < LooseVersion('1.3.0'):
- os.environ['EASYBUILD_PREFIX'] = tmpdir
- os.environ['EASYBUILD_BUILDPATH'] = tmpdir
- if install_path is not None:
- os.environ['EASYBUILD_INSTALLPATH'] = install_path
- else:
- # only for v1.3 and up
- eb_args.append('--prefix=%s' % tmpdir)
- eb_args.append('--buildpath=%s' % tmpdir)
- if install_path is not None:
- eb_args.append('--installpath=%s' % install_path)
- if sourcepath is not None:
- eb_args.append('--sourcepath=%s' % sourcepath)
-
- # make sure EasyBuild can find EasyBuild-*.eb easyconfig file when it needs to;
- # (for example when HierarchicalMNS is used as module naming scheme,
- # see https://github.com/easybuilders/easybuild-framework/issues/2393)
- eb_args.append('--robot-paths=%s:' % tmpdir)
-
- # make sure parent modules path already exists (Lmod trips over a non-existing entry in $MODULEPATH)
- if install_path is not None:
- modules_path = det_modules_path(install_path)
- if not os.path.exists(modules_path):
- os.makedirs(modules_path)
- debug("Created path %s" % modules_path)
-
- debug("Running EasyBuild with arguments '%s'" % ' '.join(eb_args))
- sys.argv = eb_args
-
- # location to 'eb' command (from stage 1) may be expected to be included in $PATH
- # it usually is there after stage1, unless 'prep' is called again with another location
- # (only when stage 0 is not skipped)
- # cfr. https://github.com/easybuilders/easybuild-framework/issues/2279
- curr_path = [x for x in os.environ.get('PATH', '').split(os.pathsep) if len(x) > 0]
- os.environ['PATH'] = os.pathsep.join([os.path.join(tmpdir, STAGE1_SUBDIR, 'bin')] + curr_path)
- debug("$PATH: %s" % os.environ['PATH'])
-
- # install EasyBuild with EasyBuild
- from easybuild.main import main as easybuild_main
- easybuild_main()
-
- if print_debug:
- os.environ['EASYBUILD_DEBUG'] = '1'
-
- # make sure the EasyBuild module was actually installed
- # EasyBuild configuration options that are picked up from configuration files/environment may break the bootstrap,
- # for example by having $EASYBUILD_VERSION defined or via a configuration file specifies a value for 'stop'...
- from easybuild.tools.config import build_option, install_path, get_module_syntax
- from easybuild.framework.easyconfig.easyconfig import ActiveMNS
- eb_spec = {
- 'name': 'EasyBuild',
- 'hidden': False,
- 'toolchain': {'name': 'dummy', 'version': 'dummy'},
- 'version': templates['version'],
- 'versionprefix': '',
- 'versionsuffix': '',
- 'moduleclass': 'tools',
- }
-
- mod_path = os.path.join(install_path('mod'), build_option('suffix_modules_path'))
- debug("EasyBuild module should have been installed to %s" % mod_path)
-
- eb_mod_name = ActiveMNS().det_full_module_name(eb_spec)
- debug("EasyBuild module name: %s" % eb_mod_name)
-
- eb_mod_path = os.path.join(mod_path, eb_mod_name)
- if get_module_syntax() == 'Lua':
- eb_mod_path += '.lua'
-
- if os.path.exists(eb_mod_path):
- info("EasyBuild module installed: %s" % eb_mod_path)
- else:
- error("EasyBuild module not found at %s, define $EASYBUILD_BOOTSTRAP_DEBUG to debug" % eb_mod_path)
-
-
-def main():
- """Main script: bootstrap EasyBuild in stages."""
-
- self_txt = open(__file__).read()
- if IS_PY3:
- self_txt = self_txt.encode('utf-8')
-
- url = 'https://docs.easybuild.io/en/latest/Installation.html'
- info("Use of the EasyBuild boostrap script is DEPRECATED (since June 2021).")
- info("It is strongly recommended to use one of the installation methods outlined at %s instead!\n" % url)
- if not EASYBUILD_BOOTSTRAP_DEPRECATED:
- error("The EasyBuild bootstrap script will only run if $EASYBUILD_BOOTSTRAP_DEPRECATED is defined.")
- else:
- msg = "You have opted to continue with the EasyBuild bootstrap script by defining "
- msg += "$EASYBUILD_BOOTSTRAP_DEPRECATED. Good luck!\n"
- info(msg)
-
- info("EasyBuild bootstrap script (version %s, MD5: %s)" % (EB_BOOTSTRAP_VERSION, md5(self_txt).hexdigest()))
- info("Found Python %s\n" % '; '.join(sys.version.split('\n')))
-
- # disallow running as root, since stage 2 will fail
- if os.getuid() == 0:
- error("Don't run the EasyBuild bootstrap script as root, "
- "since stage 2 (installing EasyBuild with EasyBuild) will fail.")
-
- # general option/argument parser
- if HAVE_ARGPARSE:
- bs_argparser = argparse.ArgumentParser()
- bs_argparser.add_argument("prefix", help="Installation prefix directory",
- type=str)
- bs_args = bs_argparser.parse_args()
-
- # prefix specification
- install_path = os.path.abspath(bs_args.prefix)
- else:
- bs_argparser = optparse.OptionParser(usage="usage: %prog [options] prefix")
- (bs_opts, bs_args) = bs_argparser.parse_args()
-
- # poor method, but should prefer argparse module for better pos arg support.
- if len(bs_args) < 1:
- error("Too few arguments\n" + bs_argparser.get_usage())
- elif len(bs_args) > 1:
- error("Too many arguments\n" + bs_argparser.get_usage())
-
- # prefix specification
- install_path = os.path.abspath(str(bs_args[0]))
-
- info("Installation prefix %s" % install_path)
-
- sourcepath = EASYBUILD_BOOTSTRAP_SOURCEPATH
- if sourcepath is not None:
- info("Fetching sources from %s..." % sourcepath)
-
- forcedversion = EASYBUILD_BOOTSTRAP_FORCE_VERSION
- if forcedversion:
- info("Forcing specified version %s..." % forcedversion)
- if IS_PY3 and LooseVersion(forcedversion) < LooseVersion('4'):
- error('Python 3 support is only available with EasyBuild 4.x but you are trying to install EasyBuild %s'
- % forcedversion)
-
- # create temporary dir for temporary installations
- tmpdir = tempfile.mkdtemp()
- debug("Going to use %s as temporary directory" % tmpdir)
- os.chdir(tmpdir)
-
- # check whether a module command is available, we need that
- modtool = check_module_command(tmpdir)
-
- # clean sys.path, remove paths that may contain EasyBuild packages or stuff installed with easy_install
- orig_sys_path = sys.path[:]
- sys.path = []
- for path in orig_sys_path:
- include_path = True
- # exclude path if it's potentially an EasyBuild/VSC package, providing the 'easybuild'/'vsc' namespace, resp.
- if any(os.path.exists(os.path.join(path, pkg, '__init__.py')) for pkg in ['easyblocks', 'easybuild', 'vsc']):
- include_path = False
- # exclude any .egg paths
- if path.endswith('.egg'):
- include_path = False
- # exclude any path that contains an easy-install.pth file
- if os.path.exists(os.path.join(path, 'easy-install.pth')):
- include_path = False
-
- if include_path:
- sys.path.append(path)
- else:
- debug("Excluding %s from sys.path" % path)
-
- debug("sys.path after cleaning: %s" % sys.path)
-
- # install EasyBuild in stages
-
- # STAGE 0: install distribute, which delivers easy_install
- distribute_egg_dir = None
- if EASYBUILD_BOOTSTRAP_SKIP_STAGE0:
- info("Skipping stage 0, using local distribute/setuptools providing easy_install")
- else:
- setuptools_loc = check_setuptools()
- if setuptools_loc:
- info("Suitable setuptools installation already found, skipping stage 0...")
- sys.path.insert(0, setuptools_loc)
- else:
- info("No suitable setuptools installation found, proceeding with stage 0...")
- distribute_egg_dir = stage0(tmpdir)
-
- # STAGE 1: install EasyBuild using easy_install to tmp dir
- templates = stage1(tmpdir, sourcepath, distribute_egg_dir, forcedversion)
-
- # add location to easy_install provided through stage0 to $PATH
- # this must be done *after* stage1, since $PATH is reset during stage1
- if distribute_egg_dir:
- prep(tmpdir)
-
- # make sure the active 'easy_install' is the right one (i.e. it matches the active setuptools installation)
- check_easy_install_cmd()
-
- # STAGE 2: install EasyBuild using EasyBuild (to final target installation dir)
- stage2(tmpdir, templates, install_path, distribute_egg_dir, sourcepath)
-
- # clean up the mess
- debug("Cleaning up %s..." % tmpdir)
- shutil.rmtree(tmpdir)
-
- print('')
- info('Bootstrapping EasyBuild completed!\n')
-
- if install_path is not None:
- info('EasyBuild v%s was installed to %s, so make sure your $MODULEPATH includes %s' %
- (templates['version'], install_path, det_modules_path(install_path)))
- else:
- info('EasyBuild v%s was installed to configured install path, make sure your $MODULEPATH is set correctly.' %
- templates['version'])
- info('(default config => add "$HOME/.local/easybuild/modules/all" in $MODULEPATH)')
-
- print('')
- info("Run 'module load EasyBuild', and run 'eb --help' to get help on using EasyBuild.")
- info("Set $EASYBUILD_MODULES_TOOL to '%s' to use the same modules tool as was used now." % modtool)
- print('')
- info("By default, EasyBuild will install software to $HOME/.local/easybuild.")
- info("To install software with EasyBuild to %s, set $EASYBUILD_INSTALLPATH accordingly." % install_path)
- info("See http://easybuild.readthedocs.org/en/latest/Configuration.html for details on configuring EasyBuild.")
-
-
-# template easyconfig file for EasyBuild
-EASYBUILD_EASYCONFIG_TEMPLATE = """
-easyblock = 'EB_EasyBuildMeta'
-
-name = 'EasyBuild'
-version = '%(version)s'
-
-homepage = 'http://easybuilders.github.com/easybuild/'
-description = \"\"\"EasyBuild is a software build and installation framework
-written in Python that allows you to install software in a structured,
-repeatable and robust way.\"\"\"
-
-toolchain = {'name': 'dummy', 'version': 'dummy'}
-
-source_urls = [%(source_urls)s]
-sources = [%(sources)s]
-
-# EasyBuild is a (set of) Python packages, so it depends on Python
-# usually, we want to use the system Python, so no actual Python dependency is listed
-allow_system_deps = [('Python', SYS_PYTHON_VERSION)]
-
-preinstallopts = "%(preinstallopts)s"
-
-sanity_check_paths = {
- 'files': ['bin/eb'],
- 'dirs': ['lib'],
-}
-
-moduleclass = 'tools'
-"""
-
-# check Python version
-loose_pyver = LooseVersion(python_version())
-min_pyver2 = LooseVersion('2.6')
-min_pyver3 = LooseVersion('3.5')
-if loose_pyver < min_pyver2 or (loose_pyver >= LooseVersion('3') and loose_pyver < min_pyver3):
- sys.stderr.write("ERROR: Incompatible Python version: %s (should be Python 2 >= %s or Python 3 >= %s)\n"
- % (python_version(), min_pyver2, min_pyver3))
- sys.exit(1)
-
-# distribute_setup.py script (https://pypi.python.org/pypi/distribute)
-#
-# A compressed copy of a patched distribute_setup.py (version 0.6.49), generated like so:
-# >>> import base64
-# >>> import zlib
-# >>> base64.b64encode(zlib.compress(open("distribute_setup.py").read()))
-# compressed copy below is for setuptools 0.6c11, after applying patch:
-#
-# --- distribute_setup.py.orig 2013-07-05 03:50:13.000000000 +0200
-# +++ distribute_setup.py 2015-11-27 12:20:12.040032041 +0100
-# @@ -528,6 +528,8 @@
-# log.warn("--user requires Python 2.6 or later")
-# raise SystemExit(1)
-# install_args.append('--user')
-# + if options.prefix_install:
-# + install_args.append('--prefix=%s' % options.prefix_install)
-# return install_args
-#
-# def _parse_args():
-# @@ -539,6 +541,8 @@
-# '--user', dest='user_install', action='store_true', default=False,
-# help='install in user site package (requires Python 2.6 or later)')
-# parser.add_option(
-# + '--prefix', dest='prefix_install', metavar="PATH", help='install in prefix')
-# + parser.add_option(
-# '--download-base', dest='download_base', metavar="URL",
-# default=DEFAULT_URL,
-# help='alternative URL from where to download the distribute package')
-# @@ -549,7 +553,7 @@
-# def main(version=DEFAULT_VERSION):
-# """Install or upgrade setuptools and EasyInstall"""
-# options = _parse_args()
-# - tarball = download_setuptools(download_base=options.download_base)
-# + tarball = download_setuptools(version=version, download_base=options.download_base)
-# return _install(tarball, _build_install_args(options))
-#
-# if __name__ == '__main__':
-#
-DISTRIBUTE_SETUP_PY = """
-eJztPGtz2ziS3/UrcHK5SGVlxs7Mze6lTlOVmTizrs0mqdjZ/ZC4ZIiEJI75Gj6saH/9dTcAAiAh
-2bmZ/XBV592JJaLRaPS7G6BP/qPat9uymEyn05/Ksm3amlcsSeF3uupawdKiaXmW8TYFoMnVmu3L
-ju140bK2ZF0jWCParmrLMmsAFkdrVvH4nm9E0MjBqNrP2a9d0wJAnHWJYO02bSbrNEP08AWQ8FzA
-qrWI27Les13ablnazhkvEsaThCbgggjblhUr13Iljf/ly8mEwc+6LnOL+iWNszSvyrpFapeGWoJ3
-H4Wz0Q5r8VsHZDHOmkrE6TqN2YOoG2AG0mCmzvEzQCXlrshKnkzytK7Les7KmrjEC8azVtQFB55q
-ILPjOS0aA1RSsqZkqz1ruqrK9mmxmeCmeVXVZVWnOL2sUBjEj7u74Q7u7qLJ5AbZRfyNaWHEKFjd
-wecGtxLXaUXbU9IlKqtNzRNbnhEqxUQxr2z0p2bbtWnWf9v3A22aC/15XeS8jbf9kMgrpKf/zmv7
-K+yo4nUjJpNegLhGoyWXlZvJpK33L42QmxSVUw5/ur78uLy+urmciK+xgJ1d0fNLlICc0kOwBXtX
-FsLCpvfRrYDDsWgaqUmJWLOltI1lnCfhM15vmpmcgj/4FZCFsP9IfBVx1/JVJuYz9ica6uFqYGdd
-WOijGBgeEja2WLDzyUGiT8AOQDYgORBywtYgJEkQexF994cSecJ+68oWdA0fd7koWmD9GpYvQFUN
-GDxCTBV4AyAmR/IDgPnuRWCW1GQhQoHbnLljCk8A/wPbh/HxsMW2YHraTAN2ioAjOAUzHFKb/mwo
-INbBB7ViczuUTtlETcXBKEP49GH5z1dXN3M2YBp7Zsvs9eWbV5/e3iz/cfnx+ur9O1hveh79EH3/
-X9N+6NPHt/h427bVy+fPq32VRlJUUVlvnisf2TxvwI/F4nny3Lit59PJ9eXNpw8379+/vV6+efW3
-y9eDheKLi+nEBvrwt1+WV+/evMfx6fTL5O+i5Qlv+dk/pLd6yS6i88k7cLMvLQuf9KOnzeS6y3MO
-VsG+ws/kr2UuziqgkL5PXnVAeW1/PhM5TzP55G0ai6JRoK+F9C+EFx8AQSDBQzuaTCakxsoVheAa
-VvB7rn3TEtm+CGczbRTiK8SomLSVwoGEp8E2r8ClAg+0v4ny+wQ/g2fHcfAj0Y7XRRhcGiSgFKdN
-MFeTJWCZJctdAohAHzaijXeJwtC7DYICY97CnNCeSlCciJBOLiorUehtGZil2ofaszM5irOyERiO
-jIVuSkUsbroPHD0AOBi5dSCq4u02+hXgFWFzfJiBelm0fj6/nY03IrGYAcOwd+WO7cr63uaYhrao
-VELDsDXGcdUPste9sgcz278UZet4tEBHeFguUMjh4zNbOQYexqx3DToMmQMstwOXxnZ1CR+Trtaq
-Y+c2kUXHAIsQBC3QM7McXDhYBeOr8kEMJ6Fypm1cQhjVrvuFA6D8jXy4TgtY3adPUv0Mbhlyozpv
-ayF6bVOGs+pSABebTQj/zVlvP225RLD/N5v/A2ZDMqT4WzCQogfRTz2EZTsIawQh5W04dciMzn7D
-f1cYb1Bt6NEZfoN/agvRH6Kkcn9S77Xu1aLpstZVLSBEAisnoOUBBtW0DQ0bGmqeQk599Z6SpTD4
-ueyyhGYRH6W1bjZoncpGEtiSSrpDlb/P+zR8ueKN0Nu2Hici43u1KjJ6qCQKPjCB++y0Oav2p0kE
-/0fOelMb+DllhgpMM9QXiIHrEjTM8/DidvZ09iijAYL7vVglwoH9H6J18HOAS0brHnFHBEhZJJIP
-HljUbXg+Z0b+Kh93CzWrKlnpMlWnDEsqCBZkOFLeg6JI7XgxSNoGHFhYiZuXG3IHCzSArvYwYXHx
-n3MQznLN78Xipu5E73tzTg6g6aCS3FE6TzrKV02ZoRkjLyZmCUvTAAR/hzb3drxZSi4J9LVBdb9Z
-gklRHtlQWo38zcuky0SDpd2XfjeB4coQcOynnS+WaJz1Jg7ECbs27YDz6M8rquAhwoqapVR6diml
-0yzEgaoWa1Hj92EcBR/ZtGmLzKHkvdjbbk8JNHImjcjFH4fWSFXz4dSw4ccFkjmdjWZKl+U8VoVa
-6CLt6QKK3pXtm7IrEr81ufNU7v1zWayzNG49dVLFmwF/lfVvecPbtnYJAU+0TKy0ylt34Wylov7C
-i4bcnsiIM9L7mnK1hzhUybo8/V3u+LB2HhC1YcmPiykUvmq9mYew4T6OystdXIAhyoIxJocdzsBl
-D0VnG+6Y+zgdcg1IM6NdnULpOoKY3lDwpI0ldh/K6teFPy5OoUoFY0NR8weo0ah8/VJMx/jQBmNe
-BC1mqsqbAubdFltH1HNTrSL4VHcFBvCIfcgECMiLT7eTOMvLGkmNMe/WhK7Tumnn4P0AjXd6AIj3
-2quzs0/WvoLIA/+l+FKEP3d1Datke4mYndYzQO6EWBFhmYCp21iZpdDSNnzhjoms8VgI6CAbGJ3l
-Qj8PXPEtTsHkh5p95f2hDsa32MOTwvQhm/Hqtc9//W6zfZRMH4mjXPOYw1rGtYC4MvRXS9wcWWCf
-xlJKcCQT+r15Af14kgOVEyhHPJ1OX5u+b2+vlLv03WUwvqyMqSClyKk4n7aynVvwXMiAcKdov4O0
-m7LfFfaoH3iWOti16RVdvoIQ3G452XLvFggX9oSp7KBQ2zenQQ1oimB3Dj/uGHa2QnAS8VavLopE
-pibYuOcseB7MInYneXKHKzrVFzgYUQudpvelsl5FJDKw3xEH++lqD9R2j8sioQZ4xTGZWok1+hvs
-dcdtxzPThqf9tVjLtpEWw78lKXPCEQm1q7MsXVEgEnj2oQ4h6gwL24lljt5waaF44Zvbbv61RHXA
-hp9TgERYCG/+hR5QSZ/gYa5dD6AgIRhqLDLH5g8CWHqgzNGgKoOvY0QH+1INdctcB7WJRDt7iTx/
-9VCmqNUVmm7Sk2NazKOo3heIvfWQk2+mc9yS67MlUYpL4Wj4hH0UPHlOwZVhhIEEGEhnKzC5+zke
-u+xQCTEcSt8CmhyXdQ3ugqxvgAx2S1qt7SXFAzBIdBFeq7D+wVYsZgh1DPrAk0FSJdlIREtmzdl0
-N8xJAUjlBYjMjI2cppIErOXJMYAA3SQZwAN+T7yDRW140y8nCcNeMrIEJWNdboPDbsB9rcqv4bor
-YnRnygvSsD1OXfU5e/bsfjc7UnrIMx9TBKrp2jJea9dyLZ8fS5qHsJg3l1niS5iR2oe0lB3C0emK
-/UOp+vDhcKUI1wFJj5/3izwBRw8LmA7Pq/D4jepDrESfkNqYCW84ADw5px/OOyxDpTxaIxzRP6rO
-apkx3ccZ5BWBN63zQ9pab+ut1nQia4neIURDmIPDAB9QtCbo/5OiG4/vu4r8xVrm1qKQW4LYrkPS
-WjsBxDRTEQJcKDi8pUILEGvbg6wd6wQ2jScsNEmGdSfg/RDJXnPV02985ULYnW7FDqMmZtYHhMYq
-IdITINHEqLEkN2E25uwVLHAX+MKocniap/4kce3zUKT0E0s8sJ7tdqyR3mvhJQTNN680f7eAFHFH
-hKRpGXFNUlCInY76+BQieBC9f/s6Om3wkBQP4CP8Z9S3/4joZODErIn6xXKHGqNq4GPhjF9D36gi
-Xz80tOblAyTiYHRL+0glrDIeiy2YkdDHEIMMIW2wkTwGc4n/VNxDjHVOayCnU5uw5h5W0DXWNI5n
-w0xXX0Cxuvx+UoBqdZ8hUr9DnDu322nPsOF7hoXHMJDoxUd+eAV6cm+zZe2WXoYDpsFN1YF9ScNe
-d8CAyUAJ/l4+oAqITMgT/rJrMZtGp7Tje2OyuoIaZoIWc+ZMWo4ifiBPNd/igwkSyvCA6KUDK63J
-iUyjSdLNqhlz5jn+VocWip+jmGF4AXbAhx5uzlxqvKp0Aph3LBNt0DBU+96nD/nZq5niN2paaPdf
-54POrTqdwYst+NHinsZwTB4KxjlMHR4SKJBBHW27mhGWca7g00rwyfLUBdiqiVWcOHAGElh94sRq
-QASDnbju/JCnGTr3Q3C9q+drSNT7KwdIgOKKFQDX1LRWzaj+0KE3EsN8LIQAA554gkfPc6BiWa5+
-Dfvz6lmkTyoqKDahopOR8bEGhuPbFOHfMsdxuTa9oJSeOu0xN+wRt+aO7lmMXRCxav9ATAKjU4Eq
-fPKhlyw2zUZVKLfsSJ25NcYPwgJfHK0LD909mUvavsHtDUgZeUDNy7EL9PoeCX7E8wx9+M8ofxnI
-R7Pc3KnPVXp/2SdZKqW/LB7SuizQ2AZ5vV10Yzm8hfKOVaLO06bRVw9lGX2KHdD7tKqApumRbYzp
-U7md14kTwOE8T4qr7RO7w+Ky9CSCCcEwLxqws91aUjUcVI8fS1MdMqxG4FBnHt/fY2Y+8HdPcieT
-mVuz9BHYZJcnKq5BOIzvUatT/OarFQ7aiszmL3/55QwlifEMpCo/f4OtpKNs/GjQPxSgnhrkjZ5a
-gf5A4QLE5/c939xHR1kxe8TLPMq5Yxb9h5mWxQqnhFLa4q+i1KC5kUTt2D68jkLrT7JdezC2es4g
-FWbqSS/x2Dj9GuqoYwJdH3EpTKnzbV5vHl4O/JFJxtVtUQ34WX+AkJ2Ir1YMh1rr4uVtn9XR8Fzf
-KhVFl4uay/umdo8IQeV9bZn/nZ3VsFt55UZuY1guwDYARwSr1m2D/XTU4wUGNYnH04Bqy0oJh2ZW
-WdqGAa6zCGafzwbnkBYTNAPtxRSu4WGYpAoLVUmF93A5A+WUl2R/lPzx98occpvPBIncHdP5rbQa
-KoG9XSPqgE5RzO1vdSyKHWtvb8q3Sj/bYx1SK0fqOlT465jT4al1xqrv//oM2HvPguRwpBd3wnYi
-AKuKgQRP0mHl26CS1l2KpBSSKY0QOQZ2+yh43Pahrzs0Gbd4UZfckA3jDVnhCDNlmLxrIswi3TNu
-F+NHedCN6UlEbwcMqqdaUKjH8QW5bVdnnVTnZl+JEb+wGWudlb36cPXvJNhQZ4rU4TKgnq5q2hcJ
-7Tdt7JJJtg38gjphiWiFurCJ8RzvIPRBEfU2GWbY/fnjYkhepIeG2m2/AWQVZXJp0HXdrPGsonhB
-pynSN6J6km9MvxKxVV0+pIlI5MtA6VrB268dNVZrJFF3PfO5Oc3RjVa7HnJDiY8yvxQUNmDofEgw
-IJKHQL5bzmPfJl+AAFq2UB0drqCczAxsVMqxsW+Kqi15NhGJIlFBhC4kHthUcSYvkhrhOXSgtR/u
-8nmlqicr0gCHz996mj7WXePDJB1KojykHM+pFInhoZQUdc7tlnxDWnowLz3SWx/wxp037q7butGf
-WaJZ08G69JB0yG15/0IMResWBL9XnmN6ISblFRAprEOBjHdFvDWZnXkyDJ4f1YizcYx2siyll/Qk
-BPUk9FtcEqpKK5bz+h5vMpSM0/EzNxNW3UYVT3lygS8NBWex+6YBJBJ4rSgTZ6o1cSa+0nt8kECf
-5byAGJ4Etz2SF4eQ1CIuaw2pXqSihPPld7eYsEgKwJX4Bl5YoVQPn9/2fRBsF9rJ7Gf31aVbSK70
-tInGQXeevG+l9SKxbto3IlvLE4PFNII6Pxd4F6NZYLQyR13q7QA6W1cQ8uCW7lXUIEIpL/way2tb
-+nK83p+5I4LeHt+pLHcFVvJ5meDblzKy4BkHAdg9CXMIqpGkoN7U6QNFSpqI3eEGgv6uDd5HBaRr
-QYSM3g0AQhUH4CNMVjsK0MfLJJhnRESOr7bS/Ru8tSp061PjwbMFZR8QnVZ7thGtwhXO3EspKvGL
-y2pvf4eqD2oLdc9SMlS+I6EBFOPNZUybA6APt33GocUyyjT0AEb9bD3p6xxYiTwtxCoF4pTpalh1
-/gce70QT5tCj7gg1fC1QqmJwU8JARryCcjcJ1Rquk9R0LYhbEf5zFBDv6GGv6Pvv/yLTojjNQX5U
-nwGB538+P7eyv2wdKdlrpFL7+3Tlo0B3IFiD7Ldptu3bbmay/2bhizn73mIRGhnOF3UIGC7miOfF
-zFuXxHlFMBG2JCRgZM7lhnxDpKHE7AeoJfWh79RlhOhe7BdaAyO8QQH6izQHSEAww0ScsMkr75pB
-kBTS5RnA1Ztwb7aouyQPzBKHzBtonTXuEEnnn94mipa4y6DRdQSScrwF4oyMFd7ZGLCjc9UnAMZb
-2NkRQN18tUz2f3GVWKooTs6A+xkU3BfjYpsuag8Ked/VD0K2TFabEJRwqnwLvhOKl8jE4EUz+827
-UL2KbkIA/f6pfwvHvE4M3jRQby/rmMX6bk2pX8/qizFVHrve0VpZejXFDEVFhBW/ps/xUgeM8YdD
-GdpUdg/0XeuGfdDvXf+AIZreZxhcz5KX4q/3TSvyS4ytF1bSZBGunZpuUMyGm5CFhGcbfiQSfCHP
-Vfw4nL6FjabvB4P1SnkOBPkBR2S4ludaEMwKYTW1GgecENFFSfU+f/SeoAhNrbyNBIp4kiwlDlNS
-a57g3dmmXQS2POEhp2tDi6BpsbvYgrchyDXvMtUBMNdztyKrFjoDQzdC8qQ/GqBUi4XHpDsLnkKt
-6uBpel22B5gmtfyB14vph1c3f4W0aUSVbgE+YS19z/AMr272SzoXOu0VP318OzXs0FzyXmWWVOk/
-T4E5Gl7wpTxDXdQtzS1Hv52qHSilmOtEVO3IVjCdl5cgC5VC9T6CY1N4U4B0E1tltaqRtuYc/PyB
-i9tGe6+O/V0LCkGXvNkrKK2++u9qLFyTkO2sp7xSt/Bfil9os3SeOlY5fvv9mLcFj5zSNUqsRZfU
-7lwukTHLpfpLDH2GT+yCCf8D2cp1xw==
-"""
-if IS_PY3:
- DISTRIBUTE_SETUP_PY = DISTRIBUTE_SETUP_PY.encode('ascii')
-DISTRIBUTE_SETUP_PY = codecs.decode(codecs.decode(DISTRIBUTE_SETUP_PY, "base64"), "zlib")
-
-# run main function as body of script
-main()
diff --git a/easybuild/scripts/clean_gists.py b/easybuild/scripts/clean_gists.py
index ec1213e4b3..2063b8dfb1 100755
--- a/easybuild/scripts/clean_gists.py
+++ b/easybuild/scripts/clean_gists.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python
##
-# Copyright 2014-2024 Ward Poelmans
+# Copyright 2014-2025 Ward Poelmans
#
# https://github.com/easybuilders/easybuild
#
@@ -29,6 +29,7 @@
import re
+from urllib.request import HTTPError, URLError
from easybuild.base import fancylogger
from easybuild.base.generaloption import simple_option
@@ -37,7 +38,6 @@
from easybuild.tools.github import GITHUB_API_URL, HTTP_STATUS_OK, GITHUB_EASYCONFIGS_REPO, GITHUB_EASYBLOCKS_REPO
from easybuild.tools.github import GITHUB_EB_MAIN, fetch_github_token
from easybuild.tools.options import EasyBuildOptions
-from easybuild.tools.py2vs3 import HTTPError, URLError
HTTP_DELETE_OK = 204
diff --git a/easybuild/scripts/findPythonDeps.py b/easybuild/scripts/findPythonDeps.py
index c3607e47c7..6fc6141f00 100755
--- a/easybuild/scripts/findPythonDeps.py
+++ b/easybuild/scripts/findPythonDeps.py
@@ -1,4 +1,36 @@
#!/usr/bin/env python
+##
+# Copyright 2021-2025 Alexander Grund
+#
+# https://github.com/easybuilders/easybuild
+#
+# EasyBuild is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation v2.
+#
+# EasyBuild is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with EasyBuild. If not, see .
+##
+"""
+Find Python dependencies for a given Python package after loading dependencies specified in an EasyConfig.
+This is intended for writing or updating PythonBundle EasyConfigs:
+ 1. Create a EasyConfig with at least 'Python' as a dependency.
+ When updating to a new toolchain it is a good idea to reduce the dependencies to a minimum
+ as e.g. the new "Python" module might have different packages included.
+ 2. Run this script
+ 3. For each dependency found by this script search existing EasyConfigs for ones providing that Python package.
+ E.g many are contained in Python-bundle-PyPI. Some can be updated from an earlier toolchain.
+ 4. Add those EasyConfigs as dependencies to your new EasyConfig.
+ 5. Rerun this script so it takes the newly provided packages into account.
+ You can do steps 3-5 iteratively adding EasyConfig-dependencies one-by-one.
+ 6. Finally you copy the packages found by this script as "exts_list" into the new EasyConfig.
+ You usually want the list printed as "in install order", the format is already suitable to be copied as-is.
+"""
import argparse
import json
@@ -8,12 +40,13 @@
import subprocess
import sys
import tempfile
+import textwrap
from contextlib import contextmanager
from pprint import pprint
try:
import pkg_resources
except ImportError as e:
- print('pkg_resources could not be imported: %s\nYou might need to install setuptools!' % e)
+ print(f'pkg_resources could not be imported: {e}\nYou might need to install setuptools!')
sys.exit(1)
try:
@@ -22,6 +55,7 @@
_canonicalize_regex = re.compile(r"[-_.]+")
def canonicalize_name(name):
+ """Fallback if the import doesn't work with same behavior."""
return _canonicalize_regex.sub("-", name).lower()
@@ -36,19 +70,19 @@ def temporary_directory(*args, **kwargs):
def extract_pkg_name(package_spec):
- return re.split('<|>|=|~', args.package, 1)[0]
+ """Get the package name from a specification such as 'package>=3.42'"""
+ return re.split('<|>|=|~', package_spec, 1)[0]
-def can_run(cmd, argument):
+def can_run(cmd, *arguments):
"""Check if the given cmd and argument can be run successfully"""
- with open(os.devnull, 'w') as FNULL:
- try:
- return subprocess.call([cmd, argument], stdout=FNULL, stderr=subprocess.STDOUT) == 0
- except (subprocess.CalledProcessError, OSError):
- return False
+ try:
+ return subprocess.call([cmd, *arguments], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) == 0
+ except (subprocess.CalledProcessError, OSError):
+ return False
-def run_cmd(arguments, action_desc, capture_stderr=True, **kwargs):
+def run_shell_cmd(arguments, action_desc, capture_stderr=True, **kwargs):
"""Run the command and return the return code and output"""
extra_args = kwargs or {}
if sys.version_info[0] >= 3:
@@ -59,14 +93,14 @@ def run_cmd(arguments, action_desc, capture_stderr=True, **kwargs):
if p.returncode != 0:
if err:
err = "\nSTDERR:\n" + err
- raise RuntimeError('Failed to %s: %s%s' % (action_desc, out, err))
+ raise RuntimeError(f'Failed to {action_desc}: {out}{err}')
return out
def run_in_venv(cmd, venv_path, action_desc):
"""Run the given command in the virtualenv at the given path"""
- cmd = 'source %s/bin/activate && %s' % (venv_path, cmd)
- return run_cmd(cmd, action_desc, shell=True, executable='/bin/bash')
+ cmd = f'source {venv_path}/bin/activate && {cmd}'
+ return run_shell_cmd(cmd, action_desc, shell=True, executable='/bin/bash')
def get_dep_tree(package_spec, verbose):
@@ -78,43 +112,59 @@ def get_dep_tree(package_spec, verbose):
venv_dir = os.path.join(tmp_dir, 'venv')
if verbose:
print('Creating virtualenv at ' + venv_dir)
- run_cmd(['virtualenv', '--system-site-packages', venv_dir], action_desc='create virtualenv')
+ run_shell_cmd(
+ [sys.executable, '-m', 'venv', '--system-site-packages', venv_dir], action_desc='create virtualenv'
+ )
if verbose:
print('Updating pip in virtualenv')
run_in_venv('pip install --upgrade pip', venv_dir, action_desc='update pip')
if verbose:
- print('Installing %s into virtualenv' % package_spec)
- out = run_in_venv('pip install "%s"' % package_spec, venv_dir, action_desc='install ' + package_spec)
- print('%s installed: %s' % (package_spec, out))
+ print(f'Installing {package_spec} into virtualenv')
+ out = run_in_venv(f'pip install "{package_spec}"', venv_dir, action_desc='install ' + package_spec)
+ print(f'{package_spec} installed: {out}')
# install pipdeptree, figure out dependency tree for installed package
run_in_venv('pip install pipdeptree', venv_dir, action_desc='install pipdeptree')
- dep_tree = run_in_venv('pipdeptree -j -p "%s"' % package_name,
+ dep_tree = run_in_venv(f'pipdeptree -j -p "{package_name}"',
venv_dir, action_desc='collect dependencies')
return json.loads(dep_tree)
def find_deps(pkgs, dep_tree):
"""Recursively resolve dependencies of the given package(s) and return them"""
+ MAX_PACKAGES = 1000
res = []
- for orig_pkg in pkgs:
- pkg = canonicalize_name(orig_pkg)
- matching_entries = [entry for entry in dep_tree
- if pkg in (entry['package']['package_name'], entry['package']['key'])]
- if not matching_entries:
+ next_pkgs = set(pkgs)
+ # Don't check any package multiple times to avoid infinite recursion
+ seen_pkgs = set()
+ count = 0
+ while next_pkgs:
+ cur_pkgs = next_pkgs - seen_pkgs
+ seen_pkgs.update(cur_pkgs)
+ next_pkgs = set()
+ for orig_pkg in cur_pkgs:
+ count += 1
+ if count > MAX_PACKAGES:
+ raise RuntimeError(f"Aborting after checking {MAX_PACKAGES} packages. Possibly cycle detected!")
+ pkg = canonicalize_name(orig_pkg)
matching_entries = [entry for entry in dep_tree
- if orig_pkg in (entry['package']['package_name'], entry['package']['key'])]
- if not matching_entries:
- raise RuntimeError("Found no installed package for '%s' in %s" % (pkg, dep_tree))
- if len(matching_entries) > 1:
- raise RuntimeError("Found multiple installed packages for '%s' in %s" % (pkg, dep_tree))
- entry = matching_entries[0]
- res.append((entry['package']['package_name'], entry['package']['installed_version']))
- deps = (dep['package_name'] for dep in entry['dependencies'])
- res.extend(find_deps(deps, dep_tree))
+ if pkg in (entry['package']['package_name'], entry['package']['key'])]
+ if not matching_entries:
+ matching_entries = [entry for entry in dep_tree
+ if orig_pkg in (entry['package']['package_name'], entry['package']['key'])]
+ if not matching_entries:
+ raise RuntimeError(f"Found no installed package for '{pkg}' in {dep_tree}")
+ if len(matching_entries) > 1:
+ raise RuntimeError(f"Found multiple installed packages for '{pkg}' in {dep_tree}")
+ entry = matching_entries[0]
+ res.append(entry['package'])
+ # Add dependencies to list of packages to check next
+ # Could call this function recursively but that might exceed the max recursion depth
+ next_pkgs.update(dep['package_name'] for dep in entry['dependencies'])
return res
def print_deps(package, verbose):
+ """Print dependencies of the given package that are not installed yet in a format usable as 'exts_list'"""
if verbose:
print('Getting dep tree of ' + package)
dep_tree = get_dep_tree(package, verbose)
@@ -131,13 +181,18 @@ def print_deps(package, verbose):
res = []
handled = set()
for dep in reversed(deps):
- if dep not in handled:
- handled.add(dep)
- if dep[0] in installed_modules:
+ # Tuple as we need it for exts_list
+ dep_entry = (dep['package_name'], dep['installed_version'])
+ if dep_entry not in handled:
+ handled.add(dep_entry)
+ # Need to check for key and package_name as naming is not consistent. E.g.:
+ # "PyQt5-sip": 'key': 'pyqt5-sip', 'package_name': 'PyQt5-sip'
+ # "jupyter-core": 'key': 'jupyter-core', 'package_name': 'jupyter_core'
+ if dep['key'] in installed_modules or dep['package_name'] in installed_modules:
if verbose:
- print("Skipping installed module '%s'" % dep[0])
+ print(f"Skipping installed module '{dep['package_name']}'")
else:
- res.append(dep)
+ res.append(dep_entry)
print("List of dependencies in (likely) install order:")
pprint(res, indent=4)
@@ -145,69 +200,73 @@ def print_deps(package, verbose):
pprint(sorted(res), indent=4)
-examples = [
- 'Example usage with EasyBuild (after installing dependency modules):',
- '\t' + sys.argv[0] + ' --ec TensorFlow-2.3.4.eb tensorflow==2.3.4',
- 'Which is the same as:',
- '\t' + ' && '.join(['eb TensorFlow-2.3.4.eb --dump-env',
- 'source TensorFlow-2.3.4.env',
- sys.argv[0] + ' tensorflow==2.3.4',
- ]),
-]
-parser = argparse.ArgumentParser(
- description='Find dependencies of Python packages by installing it in a temporary virtualenv. ',
- epilog='\n'.join(examples),
- formatter_class=argparse.RawDescriptionHelpFormatter
-)
-parser.add_argument('package', metavar='python-pkg-spec',
- help='Python package spec, e.g. tensorflow==2.3.4')
-parser.add_argument('--ec', metavar='easyconfig', help='EasyConfig to use as the build environment. '
- 'You need to have dependency modules installed already!')
-parser.add_argument('--verbose', help='Verbose output', action='store_true')
-args = parser.parse_args()
-
-if args.ec:
- if not can_run('eb', '--version'):
- print('EasyBuild not found or executable. Make sure it is in your $PATH when using --ec!')
- sys.exit(1)
- if args.verbose:
- print('Checking with EasyBuild for missing dependencies')
- missing_dep_out = run_cmd(['eb', args.ec, '--missing'],
- capture_stderr=False,
- action_desc='Get missing dependencies'
- )
- excluded_dep = '(%s)' % os.path.basename(args.ec)
- missing_deps = [dep for dep in missing_dep_out.split('\n')
- if dep.startswith('*') and excluded_dep not in dep
- ]
- if missing_deps:
- print('You need to install all modules on which %s depends first!' % args.ec)
- print('\n\t'.join(['Missing:'] + missing_deps))
- sys.exit(1)
-
- # If the --ec argument is a (relative) existing path make it absolute so we can find it after the chdir
- ec_arg = os.path.abspath(args.ec) if os.path.exists(args.ec) else args.ec
- with temporary_directory() as tmp_dir:
- old_dir = os.getcwd()
- os.chdir(tmp_dir)
- if args.verbose:
- print('Running EasyBuild to get build environment')
- run_cmd(['eb', ec_arg, '--dump-env', '--force'], action_desc='Dump build environment')
- os.chdir(old_dir)
+def main():
+ """Entrypoint of the script"""
+ examples = textwrap.dedent(f"""
+ Example usage with EasyBuild (after installing dependency modules):
+ {sys.argv[0]} --ec TensorFlow-2.3.4.eb tensorflow==2.3.4
+ Which is the same as:
+ eb TensorFlow-2.3.4.eb --dump-env && source TensorFlow-2.3.4.env && {sys.argv[0]} tensorflow==2.3.4
+ Using the '--ec' parameter is recommended as the latter requires manually updating the .env file
+ after each change to the EasyConfig.
+ """)
+ parser = argparse.ArgumentParser(
+ description='Find dependencies of Python packages by installing it in a temporary virtualenv. ',
+ epilog='\n'.join(examples),
+ formatter_class=argparse.RawDescriptionHelpFormatter
+ )
+ parser.add_argument('package', metavar='python-pkg-spec',
+ help='Python package spec, e.g. tensorflow==2.3.4')
+ parser.add_argument('--ec', metavar='easyconfig', help='EasyConfig to use as the build environment. '
+ 'You need to have dependency modules installed already!')
+ parser.add_argument('--verbose', help='Verbose output', action='store_true')
+ args = parser.parse_args()
- cmd = "source %s/*.env && python %s '%s'" % (tmp_dir, sys.argv[0], args.package)
+ if args.ec:
+ if not can_run('eb', '--version'):
+ print('EasyBuild not found or executable. Make sure it is in your $PATH when using --ec!')
+ sys.exit(1)
if args.verbose:
- cmd += ' --verbose'
- print('Restarting script in new build environment')
-
- out = run_cmd(cmd, action_desc='Run in new environment', shell=True, executable='/bin/bash')
- print(out)
-else:
- if not can_run('virtualenv', '--version'):
- print('Virtualenv not found or executable. ' +
- 'Make sure it is installed (e.g. in the currently loaded Python module)!')
- sys.exit(1)
- if 'PIP_PREFIX' in os.environ:
- print("$PIP_PREFIX is set. Unsetting it as it doesn't work well with virtualenv.")
- del os.environ['PIP_PREFIX']
- print_deps(args.package, args.verbose)
+ print('Checking with EasyBuild for missing dependencies')
+ missing_dep_out = run_shell_cmd(['eb', args.ec, '--missing'],
+ capture_stderr=False,
+ action_desc='Get missing dependencies')
+ excluded_dep = f'({os.path.basename(args.ec)})'
+ missing_deps = [dep for dep in missing_dep_out.split('\n')
+ if dep.startswith('*') and excluded_dep not in dep
+ ]
+ if missing_deps:
+ print(f'You need to install all modules on which {args.ec} depends first!')
+ print('\n\t'.join(['Missing:'] + missing_deps))
+ sys.exit(1)
+
+ # If the --ec argument is a (relative) existing path make it absolute so we can find it after the chdir
+ ec_arg = os.path.abspath(args.ec) if os.path.exists(args.ec) else args.ec
+ with temporary_directory() as tmp_dir:
+ old_dir = os.getcwd()
+ os.chdir(tmp_dir)
+ if args.verbose:
+ print('Running EasyBuild to get build environment')
+ run_shell_cmd(['eb', ec_arg, '--dump-env', '--force'], action_desc='Dump build environment')
+ os.chdir(old_dir)
+
+ cmd = f"source {tmp_dir}/*.env && python {sys.argv[0]} '{args.package}'"
+ if args.verbose:
+ cmd += ' --verbose'
+ print('Restarting script in new build environment')
+
+ out = run_shell_cmd(cmd, action_desc='Run in new environment', shell=True, executable='/bin/bash')
+ print(out)
+ else:
+ if not can_run(sys.executable, '-m', 'venv', '-h'):
+ print("'venv' module not found. This should be available in Python 3.3+.")
+ sys.exit(1)
+ if 'PIP_PREFIX' in os.environ:
+ print("$PIP_PREFIX is set. Unsetting it as it doesn't work well with virtualenv.")
+ del os.environ['PIP_PREFIX']
+ os.environ['PYTHONNOUSERSITE'] = '1'
+ print_deps(args.package, args.verbose)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/easybuild/scripts/findUpdatedEcs.sh b/easybuild/scripts/findUpdatedEcs.sh
index 66b7e34bd8..918e9b2ca5 100755
--- a/easybuild/scripts/findUpdatedEcs.sh
+++ b/easybuild/scripts/findUpdatedEcs.sh
@@ -8,7 +8,7 @@ YELLOW='\033[0;33m'
NC='\033[0m'
function printError {
- echo -e "${RED}$@${NC}"
+ echo -e "${RED}$*${NC}"
}
verbose=0
@@ -23,11 +23,11 @@ function checkModule {
moduleStr="$moduleName/$moduleVersion"
printVerbose "Processing $moduleStr"
ec_glob=( "$moduleFolder/easybuild/"*.eb )
- if [[ ! -e "${ec_glob[@]}" ]]; then
+ if [[ ! -e "${ec_glob[0]}" ]]; then
printError "=== Did not find installed EC for $moduleStr"
return
fi
- ec_installed="$ec_glob"
+ ec_installed="${ec_glob[0]}"
ec_filename=$(basename "$ec_installed")
# Try with most likely location first for speed
first_letter=${ec_filename:0:1}
@@ -67,15 +67,15 @@ if path=$(which eb 2>/dev/null); then
fi
function usage {
- echo "Usage: $(basename "$0") [--verbose] [--diff] --loaded|--modules INSTALLPATH --easyconfigs EC-FOLDER"
+ echo "Usage: $(basename "$0") [--verbose] [--short] [--diff] --loaded|--modules INSTALLPATH --easyconfigs EC-FOLDER"
echo
echo "Check installed modules against the source EasyConfig (EC) files to determine which have changed."
echo "Can either check the currently loaded modules or all modules installed in a specific location"
echo
echo "--verbose Verbose status output while checking"
- echo "--loaded Check only currently loaded modules"
echo "--short Only show filename of changed ECs"
echo "--diff Show diff of changed module files"
+ echo "--loaded Check only currently loaded modules"
echo "--modules INSTALLPATH Check all modules in the specified (software) installpath, i.e. the root of module-binaries"
echo "--easyconfigs EC-FOLDER Path to the folder containg the current/updated EasyConfigs. ${ecDefaultFolder:+Defaults to $ecDefaultFolder}"
exit 0
@@ -115,6 +115,7 @@ done
if [ -z "$easyconfigFolder" ]; then
printError "Folder to easyconfigs not given!" && exit 1
fi
+
if [ -z "$modulesFolder" ]; then
if (( checkLoadedModules == 0 )); then
printError "Need either --modules or --loaded to specify what to check!" && exit 1
@@ -123,13 +124,18 @@ elif (( checkLoadedModules == 1 )); then
printError "Cannot specify --modules and --loaded!" && exit 1
fi
+if (( showDiff == 1 && short == 1 )); then
+ printError "Cannot specify --diff and --short" && exit 1
+fi
+
if [ -d "$easyconfigFolder/easybuild/easyconfigs" ]; then
easyconfigFolder="$easyconfigFolder/easybuild/easyconfigs"
fi
if (( checkLoadedModules == 1 )); then
- for varname in $(compgen -A variable | grep '^EBROOT'); do
- checkModule "${!varname}"
+ IFS=$'\n' read -r -d '' -a unique_module_paths <<< "$(for varname in $(compgen -A variable | grep '^EBROOT'); do echo "${!varname}"; done | sort -u )" || true
+ for module in "${unique_module_paths[@]}"; do
+ checkModule "$module"
done
else
for module in "$modulesFolder"/*/*/easybuild; do
diff --git a/easybuild/scripts/fix_docs.py b/easybuild/scripts/fix_docs.py
index a14f18caf8..bd56b88981 100755
--- a/easybuild/scripts/fix_docs.py
+++ b/easybuild/scripts/fix_docs.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python
##
-# Copyright 2016-2024 Ghent University
+# Copyright 2016-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/scripts/mk_tmpl_easyblock_for.py b/easybuild/scripts/mk_tmpl_easyblock_for.py
index f6958572fa..bd5906e110 100755
--- a/easybuild/scripts/mk_tmpl_easyblock_for.py
+++ b/easybuild/scripts/mk_tmpl_easyblock_for.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -36,7 +36,7 @@
import sys
from optparse import OptionParser
-from easybuild.tools.filetools import encode_class_name
+from easybuild.tools.filetools import encode_class_name, EASYBLOCK_CLASS_PREFIX
# parse options
parser = OptionParser()
@@ -83,8 +83,9 @@
# determine parent easyblock class
parent_import = "from easybuild.framework.easyblock import EasyBlock"
if not options.parent == "EasyBlock":
- if options.parent.startswith('EB_'):
- ebmod = options.parent[3:].lower() # FIXME: here we should actually decode the encoded class name
+ if options.parent.startswith(EASYBLOCK_CLASS_PREFIX):
+ # FIXME: here we should actually decode the encoded class name
+ ebmod = options.parent[len(EASYBLOCK_CLASS_PREFIX):].lower()
else:
ebmod = "generic.%s" % options.parent.lower()
parent_import = "from easybuild.easyblocks.%s import %s" % (ebmod, options.parent)
@@ -122,7 +123,7 @@
import easybuild.tools.toolchain as toolchain
%(parent_import)s
from easybuild.framework.easyconfig import CUSTOM, MANDATORY
-from easybuild.tools.run import run_cmd
+from easybuild.tools.run import run_shell_cmd
class %(class_name)s(%(parent)s):
@@ -150,7 +151,7 @@ def configure_step(self):
env.setvar('CUSTOM_ENV_VAR', 'foo')
cmd = "configure command"
- run_cmd(cmd, log_all=True, simple=True, log_ok=True)
+ run_shell_cmd(cmd)
# complete configuration with configure_method of parent
super(%(class_name)s, self).configure_step()
@@ -165,22 +166,22 @@ def build_step(self):
comp_fam = comp_map[self.toolchain.comp_family()]
# enable parallel build
- par = self.cfg['parallel']
+ par = self.cfg.parallel
cmd = "build command --parallel %%d --compiler-family %%s" %% (par, comp_fam)
- run_cmd(cmd, log_all=True, simple=True, log_ok=True)
+ run_shell_cmd(cmd)
def test_step(self):
\"\"\"Custom built-in test procedure for %(name)s.\"\"\"
if self.cfg['runtest']:
cmd = "test-command"
- run_cmd(cmd, simple=True, log_all=True, log_ok=True)
+ run_shell_cmd(cmd)
def install_step(self):
\"\"\"Custom install procedure for %(name)s.\"\"\"
cmd = "install command"
- run_cmd(cmd, log_all=True, simple=True, log_ok=True)
+ run_shell_cmd(cmd)
def sanity_check_step(self):
\"\"\"Custom sanity check for %(name)s.\"\"\"
diff --git a/easybuild/scripts/rpath_args.py b/easybuild/scripts/rpath_args.py
index b477aa63d8..611c6a414c 100755
--- a/easybuild/scripts/rpath_args.py
+++ b/easybuild/scripts/rpath_args.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python
##
-# Copyright 2016-2024 Ghent University
+# Copyright 2016-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/scripts/rpath_wrapper_template.sh.in b/easybuild/scripts/rpath_wrapper_template.sh.in
index 44a5aac43a..558a494581 100644
--- a/easybuild/scripts/rpath_wrapper_template.sh.in
+++ b/easybuild/scripts/rpath_wrapper_template.sh.in
@@ -1,6 +1,6 @@
-#!/bin/bash
+#!/usr/bin/env bash
##
-# Copyright 2016-2024 Ghent University
+# Copyright 2016-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/__init__.py b/easybuild/toolchains/__init__.py
index 2f468f2812..da63a3c7c2 100644
--- a/easybuild/toolchains/__init__.py
+++ b/easybuild/toolchains/__init__.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/cgmpich.py b/easybuild/toolchains/cgmpich.py
index 9e25c6ec57..9a31fa2a61 100644
--- a/easybuild/toolchains/cgmpich.py
+++ b/easybuild/toolchains/cgmpich.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is triple-licensed under GPLv2 (see below), MIT, and
# BSD three-clause licenses.
diff --git a/easybuild/toolchains/cgmpolf.py b/easybuild/toolchains/cgmpolf.py
index 5629ee5be3..e6fa8e31e9 100644
--- a/easybuild/toolchains/cgmpolf.py
+++ b/easybuild/toolchains/cgmpolf.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is triple-licensed under GPLv2 (see below), MIT, and
# BSD three-clause licenses.
diff --git a/easybuild/toolchains/cgmvapich2.py b/easybuild/toolchains/cgmvapich2.py
index 76e306b12d..1c108b4709 100644
--- a/easybuild/toolchains/cgmvapich2.py
+++ b/easybuild/toolchains/cgmvapich2.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is triple-licensed under GPLv2 (see below), MIT, and
# BSD three-clause licenses.
diff --git a/easybuild/toolchains/cgmvolf.py b/easybuild/toolchains/cgmvolf.py
index e68aa90fe1..a61507caff 100644
--- a/easybuild/toolchains/cgmvolf.py
+++ b/easybuild/toolchains/cgmvolf.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is triple-licensed under GPLv2 (see below), MIT, and
# BSD three-clause licenses.
diff --git a/easybuild/toolchains/cgompi.py b/easybuild/toolchains/cgompi.py
index 9b9267bf78..f7c4500831 100644
--- a/easybuild/toolchains/cgompi.py
+++ b/easybuild/toolchains/cgompi.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is triple-licensed under GPLv2 (see below), MIT, and
# BSD three-clause licenses.
diff --git a/easybuild/toolchains/cgoolf.py b/easybuild/toolchains/cgoolf.py
index 8e2ea8503e..a2a9b23e7e 100644
--- a/easybuild/toolchains/cgoolf.py
+++ b/easybuild/toolchains/cgoolf.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is triple-licensed under GPLv2 (see below), MIT, and
# BSD three-clause licenses.
diff --git a/easybuild/toolchains/clanggcc.py b/easybuild/toolchains/clanggcc.py
index c1e09e68ef..b22a78d883 100644
--- a/easybuild/toolchains/clanggcc.py
+++ b/easybuild/toolchains/clanggcc.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is triple-licensed under GPLv2 (see below), MIT, and
# BSD three-clause licenses.
diff --git a/easybuild/toolchains/compiler/__init__.py b/easybuild/toolchains/compiler/__init__.py
index 9c1ba469c5..37b7514bba 100644
--- a/easybuild/toolchains/compiler/__init__.py
+++ b/easybuild/toolchains/compiler/__init__.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/compiler/clang.py b/easybuild/toolchains/compiler/clang.py
index 8b7b16e8e3..4477454913 100644
--- a/easybuild/toolchains/compiler/clang.py
+++ b/easybuild/toolchains/compiler/clang.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is triple-licensed under GPLv2 (see below), MIT, and
# BSD three-clause licenses.
@@ -34,7 +34,6 @@
"""
import easybuild.tools.systemtools as systemtools
-from easybuild.tools.build_log import EasyBuildError
from easybuild.tools.toolchain.compiler import Compiler
@@ -56,10 +55,10 @@ class Clang(Compiler):
'basic-block-vectorize': (False, "Basic block vectorization"),
}
COMPILER_UNIQUE_OPTION_MAP = {
- 'unroll': 'funroll-loops',
- 'loop-vectorize': ['fvectorize'],
- 'basic-block-vectorize': ['fslp-vectorize'],
- 'optarch': 'march=native',
+ 'unroll': '-funroll-loops',
+ 'loop-vectorize': ['-fvectorize'],
+ 'basic-block-vectorize': ['-fslp-vectorize'],
+ 'optarch': '-march=native',
# Clang's options do not map well onto these precision modes. The flags enable and disable certain classes of
# optimizations.
#
@@ -81,38 +80,31 @@ class Clang(Compiler):
#
# 'strict', 'precise' and 'defaultprec' are all ISO C++ and IEEE complaint, but we explicitly specify details
# flags for strict and precise for robustness against future changes.
- 'strict': ['fno-fast-math'],
- 'precise': ['fno-unsafe-math-optimizations'],
+ 'strict': ['-fno-fast-math'],
+ 'precise': ['-fno-unsafe-math-optimizations'],
'defaultprec': [],
- 'loose': ['ffast-math', 'fno-unsafe-math-optimizations'],
- 'veryloose': ['ffast-math'],
- 'vectorize': {False: 'fno-vectorize', True: 'fvectorize'},
+ 'loose': ['-ffast-math', '-fno-unsafe-math-optimizations'],
+ 'veryloose': ['-ffast-math'],
+ 'vectorize': {False: '-fno-vectorize', True: '-fvectorize'},
}
# used when 'optarch' toolchain option is enabled (and --optarch is not specified)
COMPILER_OPTIMAL_ARCHITECTURE_OPTION = {
- (systemtools.POWER, systemtools.POWER): 'mcpu=native', # no support for march=native on POWER
- (systemtools.POWER, systemtools.POWER_LE): 'mcpu=native', # no support for march=native on POWER
- (systemtools.X86_64, systemtools.AMD): 'march=native',
- (systemtools.X86_64, systemtools.INTEL): 'march=native',
+ (systemtools.POWER, systemtools.POWER): '-mcpu=native', # no support for march=native on POWER
+ (systemtools.POWER, systemtools.POWER_LE): '-mcpu=native', # no support for march=native on POWER
+ (systemtools.X86_64, systemtools.AMD): '-march=native',
+ (systemtools.X86_64, systemtools.INTEL): '-march=native',
}
# used with --optarch=GENERIC
COMPILER_GENERIC_OPTION = {
- (systemtools.RISCV64, systemtools.RISCV): 'march=rv64gc -mabi=lp64d', # default for -mabi is system-dependent
- (systemtools.X86_64, systemtools.AMD): 'march=x86-64 -mtune=generic',
- (systemtools.X86_64, systemtools.INTEL): 'march=x86-64 -mtune=generic',
+ (systemtools.RISCV64, systemtools.RISCV): '-march=rv64gc -mabi=lp64d', # default for -mabi is system-dependent
+ (systemtools.X86_64, systemtools.AMD): '-march=x86-64 -mtune=generic',
+ (systemtools.X86_64, systemtools.INTEL): '-march=x86-64 -mtune=generic',
}
COMPILER_CC = 'clang'
COMPILER_CXX = 'clang++'
- COMPILER_C_UNIQUE_FLAGS = []
+ COMPILER_C_UNIQUE_OPTIONS = []
LIB_MULTITHREAD = ['pthread']
LIB_MATH = ['m']
-
- def _set_compiler_vars(self):
- """Set compiler variables."""
- super(Clang, self)._set_compiler_vars()
-
- if self.options.get('32bit', None):
- raise EasyBuildError("_set_compiler_vars: 32bit set, but no support yet for 32bit Clang in EasyBuild")
diff --git a/easybuild/toolchains/compiler/craype.py b/easybuild/toolchains/compiler/craype.py
index 73b6a103e2..bcb0197b93 100644
--- a/easybuild/toolchains/compiler/craype.py
+++ b/easybuild/toolchains/compiler/craype.py
@@ -1,5 +1,5 @@
##
-# Copyright 2014-2024 Ghent University
+# Copyright 2014-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -65,7 +65,7 @@ class CrayPECompiler(Compiler):
COMPILER_UNIQUE_OPTS = {
'dynamic': (True, "Generate dynamically linked executable"),
- 'mpich-mt': (False, "Directs the driver to link in an alternate version of the Cray-MPICH library which \
+ 'mpich-mt': (False, "Directs the driver to link in an alternative version of the Cray-MPICH library which \
provides fine-grained multi-threading support to applications that perform \
MPI operations within threaded regions."),
'optarch': (False, "Enable architecture optimizations"),
@@ -76,8 +76,8 @@ class CrayPECompiler(Compiler):
# handle shared and dynamic always via $CRAYPE_LINK_TYPE environment variable, don't pass flags to wrapper
'shared': '',
'dynamic': '',
- 'verbose': 'craype-verbose',
- 'mpich-mt': 'craympich-mt',
+ 'verbose': '-craype-verbose',
+ 'mpich-mt': '-craympich-mt',
}
COMPILER_CC = 'cc'
@@ -98,7 +98,7 @@ def __init__(self, *args, **kwargs):
"""Constructor."""
super(CrayPECompiler, self).__init__(*args, **kwargs)
# 'register' additional toolchain options that correspond to a compiler flag
- self.COMPILER_FLAGS.extend(['dynamic', 'mpich-mt'])
+ self.COMPILER_OPTIONS.extend(['dynamic', 'mpich-mt'])
# use name of PrgEnv module as name of module that provides compiler
self.COMPILER_MODULE_NAME = ['PrgEnv-%s' % self.PRGENV_MODULE_NAME_SUFFIX]
@@ -139,7 +139,7 @@ class CrayPEGCC(CrayPECompiler):
def __init__(self, *args, **kwargs):
"""CrayPEGCC constructor."""
super(CrayPEGCC, self).__init__(*args, **kwargs)
- for precflag in self.COMPILER_PREC_FLAGS:
+ for precflag in self.COMPILER_PREC_OPTIONS:
self.COMPILER_UNIQUE_OPTION_MAP[precflag] = Gcc.COMPILER_UNIQUE_OPTION_MAP[precflag]
@@ -151,7 +151,7 @@ class CrayPEIntel(CrayPECompiler):
def __init__(self, *args, **kwargs):
"""CrayPEIntel constructor."""
super(CrayPEIntel, self).__init__(*args, **kwargs)
- for precflag in self.COMPILER_PREC_FLAGS:
+ for precflag in self.COMPILER_PREC_OPTIONS:
self.COMPILER_UNIQUE_OPTION_MAP[precflag] = IntelIccIfort.COMPILER_UNIQUE_OPTION_MAP[precflag]
@@ -163,8 +163,8 @@ class CrayPEPGI(CrayPECompiler):
def __init__(self, *args, **kwargs):
"""CrayPEPGI constructor."""
super(CrayPEPGI, self).__init__(*args, **kwargs)
- self.COMPILER_UNIQUE_OPTION_MAP['openmp'] = 'mp'
- for precflag in self.COMPILER_PREC_FLAGS:
+ self.COMPILER_UNIQUE_OPTION_MAP['openmp'] = '-mp'
+ for precflag in self.COMPILER_PREC_OPTIONS:
self.COMPILER_UNIQUE_OPTION_MAP[precflag] = Pgi.COMPILER_UNIQUE_OPTION_MAP[precflag]
@@ -176,6 +176,6 @@ class CrayPECray(CrayPECompiler):
def __init__(self, *args, **kwargs):
"""CrayPEIntel constructor."""
super(CrayPECray, self).__init__(*args, **kwargs)
- self.COMPILER_UNIQUE_OPTION_MAP['openmp'] = 'homp'
- for precflag in self.COMPILER_PREC_FLAGS:
+ self.COMPILER_UNIQUE_OPTION_MAP['openmp'] = '-homp'
+ for precflag in self.COMPILER_PREC_OPTIONS:
self.COMPILER_UNIQUE_OPTION_MAP[precflag] = []
diff --git a/easybuild/toolchains/compiler/cuda.py b/easybuild/toolchains/compiler/cuda.py
index 3fd3d705f1..24e8c038bb 100644
--- a/easybuild/toolchains/compiler/cuda.py
+++ b/easybuild/toolchains/compiler/cuda.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -60,8 +60,8 @@ class Cuda(Compiler):
# always C++ compiler command, even for C!
COMPILER_CUDA_UNIQUE_OPTION_MAP = {
- '_opt_CUDA_CC': 'ccbin="%(CXX_base)s"',
- '_opt_CUDA_CXX': 'ccbin="%(CXX_base)s"',
+ '_opt_CUDA_CC': '-ccbin="%(CXX_base)s"',
+ '_opt_CUDA_CXX': '-ccbin="%(CXX_base)s"',
}
COMPILER_CUDA_CC = 'nvcc'
@@ -90,14 +90,14 @@ def _set_compiler_flags(self):
# note: using $LIBS will yield the use of -lcudart in Xlinker, which is silly, but fine
cuda_flags = [
- 'Xcompiler="%s"' % str(self.variables['CXXFLAGS']),
- 'Xlinker="%s %s"' % (str(self.variables['LDFLAGS']), str(self.variables['LIBS'])),
+ '-Xcompiler="%s"' % str(self.variables['CXXFLAGS']),
+ '-Xlinker="%s %s"' % (str(self.variables['LDFLAGS']), str(self.variables['LIBS'])),
]
self.variables.nextend('CUDA_CFLAGS', cuda_flags)
self.variables.nextend('CUDA_CXXFLAGS', cuda_flags)
# add gencode compiler flags to list of flags for compiler variables
for gencode_val in self.options.get('cuda_gencode', []):
- gencode_option = 'gencode %s' % gencode_val
+ gencode_option = '-gencode %s' % gencode_val
self.variables.nappend('CUDA_CFLAGS', gencode_option)
self.variables.nappend('CUDA_CXXFLAGS', gencode_option)
diff --git a/easybuild/toolchains/compiler/fujitsu.py b/easybuild/toolchains/compiler/fujitsu.py
index 437ec34048..c49d36fc33 100644
--- a/easybuild/toolchains/compiler/fujitsu.py
+++ b/easybuild/toolchains/compiler/fujitsu.py
@@ -1,5 +1,5 @@
##
-# Copyright 2014-2024 Ghent University
+# Copyright 2014-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -58,23 +58,23 @@ class FujitsuCompiler(Compiler):
COMPILER_FC = 'frt'
COMPILER_UNIQUE_OPTION_MAP = {
- DEFAULT_OPT_LEVEL: 'O2',
- 'lowopt': 'O1',
- 'noopt': 'O0',
- 'opt': 'Kfast', # -O3 -Keval,fast_matmul,fp_contract,fp_relaxed,fz,ilfunc,mfunc,omitfp,simd_packed_promotion
+ DEFAULT_OPT_LEVEL: '-O2',
+ 'lowopt': '-O1',
+ 'noopt': '-O0',
+ 'opt': '-Kfast', # -O3 -Keval,fast_matmul,fp_contract,fp_relaxed,fz,ilfunc,mfunc,omitfp,simd_packed_promotion
'optarch': '', # Fujitsu compiler by default generates code for the arch it is running on
- 'openmp': 'Kopenmp',
- 'unroll': 'funroll-loops',
+ 'openmp': '-Kopenmp',
+ 'unroll': '-funroll-loops',
# apparently the -Kfp_precision flag doesn't work in clang mode, will need to look into these later
# also at strict vs precise and loose vs veryloose
- 'strict': ['Knoeval,nofast_matmul,nofp_contract,nofp_relaxed,noilfunc'], # ['Kfp_precision'],
- 'precise': ['Knoeval,nofast_matmul,nofp_contract,nofp_relaxed,noilfunc'], # ['Kfp_precision'],
+ 'strict': ['-Knoeval,nofast_matmul,nofp_contract,nofp_relaxed,noilfunc'], # ['-Kfp_precision'],
+ 'precise': ['-Knoeval,nofast_matmul,nofp_contract,nofp_relaxed,noilfunc'], # ['-Kfp_precision'],
'defaultprec': [],
- 'loose': ['Kfp_relaxed'],
- 'veryloose': ['Kfp_relaxed'],
+ 'loose': ['-Kfp_relaxed'],
+ 'veryloose': ['-Kfp_relaxed'],
# apparently the -K[NO]SVE flags don't work in clang mode
# SVE is enabled by default, -Knosimd seems to disable it
- 'vectorize': {False: 'Knosimd', True: ''},
+ 'vectorize': {False: '-Knosimd', True: ''},
}
# used when 'optarch' toolchain option is enabled (and --optarch is not specified)
@@ -109,8 +109,8 @@ def _set_compiler_vars(self):
super(FujitsuCompiler, self)._set_compiler_vars()
# enable clang compatibility mode
- self.variables.nappend('CFLAGS', ['Nclang'])
- self.variables.nappend('CXXFLAGS', ['Nclang'])
+ self.variables.nappend('CFLAGS', ['-Nclang'])
+ self.variables.nappend('CXXFLAGS', ['-Nclang'])
# also add fujitsu module library path to LDFLAGS
libdir = os.path.join(os.getenv(TC_CONSTANT_MODULE_VAR), 'lib64')
diff --git a/easybuild/toolchains/compiler/gcc.py b/easybuild/toolchains/compiler/gcc.py
index e9748f3bda..20a969e102 100644
--- a/easybuild/toolchains/compiler/gcc.py
+++ b/easybuild/toolchains/compiler/gcc.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -55,26 +55,26 @@ class Gcc(Compiler):
'lto': (False, "Enable Link Time Optimization"),
}
COMPILER_UNIQUE_OPTION_MAP = {
- 'i8': 'fdefault-integer-8',
- 'r8': ['fdefault-real-8', 'fdefault-double-8'],
- 'unroll': 'funroll-loops',
- 'f2c': 'ff2c',
- 'loop': ['ftree-switch-conversion', 'floop-interchange', 'floop-strip-mine', 'floop-block'],
- 'lto': 'flto',
- 'ieee': ['mieee-fp', 'fno-trapping-math'],
- 'strict': ['mieee-fp', 'mno-recip'],
- 'precise': ['mno-recip'],
- 'defaultprec': ['fno-math-errno'],
- 'loose': ['fno-math-errno', 'mrecip', 'mno-ieee-fp'],
- 'veryloose': ['fno-math-errno', 'mrecip=all', 'mno-ieee-fp'],
- 'vectorize': {False: 'fno-tree-vectorize', True: 'ftree-vectorize'},
- DEFAULT_OPT_LEVEL: ['O2', 'ftree-vectorize'],
+ 'i8': '-fdefault-integer-8',
+ 'r8': ['-fdefault-real-8', '-fdefault-double-8'],
+ 'unroll': '-funroll-loops',
+ 'f2c': '-ff2c',
+ 'loop': ['-ftree-switch-conversion', '-floop-interchange', '-floop-strip-mine', '-floop-block'],
+ 'lto': '-flto',
+ 'ieee': ['-mieee-fp', '-fno-trapping-math'],
+ 'strict': ['-mieee-fp', '-mno-recip'],
+ 'precise': ['-mno-recip'],
+ 'defaultprec': ['-fno-math-errno'],
+ 'loose': ['-fno-math-errno', '-mrecip', '-mno-ieee-fp'],
+ 'veryloose': ['-fno-math-errno', '-mrecip=all', '-mno-ieee-fp'],
+ 'vectorize': {False: '-fno-tree-vectorize', True: '-ftree-vectorize'},
+ DEFAULT_OPT_LEVEL: ['-O2', '-ftree-vectorize'],
}
# gcc on aarch64 does not support -mno-recip, -mieee-fp, -mfno-math-errno...
# https://gcc.gnu.org/onlinedocs/gcc/AArch64-Options.html
if systemtools.get_cpu_architecture() == systemtools.AARCH64:
- no_recip_alternative = ['mno-low-precision-recip-sqrt', 'mno-low-precision-sqrt', 'mno-low-precision-div']
+ no_recip_alternative = ['-mno-low-precision-recip-sqrt', '-mno-low-precision-sqrt', '-mno-low-precision-div']
COMPILER_UNIQUE_OPTION_MAP['strict'] = no_recip_alternative
COMPILER_UNIQUE_OPTION_MAP['precise'] = no_recip_alternative
@@ -84,38 +84,38 @@ class Gcc(Compiler):
if systemtools.get_cpu_family() == systemtools.RISCV:
COMPILER_UNIQUE_OPTION_MAP['strict'] = []
COMPILER_UNIQUE_OPTION_MAP['precise'] = []
- COMPILER_UNIQUE_OPTION_MAP['loose'] = ['fno-math-errno']
- COMPILER_UNIQUE_OPTION_MAP['verloose'] = ['fno-math-errno']
+ COMPILER_UNIQUE_OPTION_MAP['loose'] = ['-fno-math-errno']
+ COMPILER_UNIQUE_OPTION_MAP['veryloose'] = ['-fno-math-errno']
# used when 'optarch' toolchain option is enabled (and --optarch is not specified)
COMPILER_OPTIMAL_ARCHITECTURE_OPTION = {
- (systemtools.AARCH32, systemtools.ARM): 'mcpu=native', # implies -march=native and -mtune=native
- (systemtools.AARCH64, systemtools.ARM): 'mcpu=native', # since GCC 6; implies -march=native and -mtune=native
+ (systemtools.AARCH32, systemtools.ARM): '-mcpu=native', # implies -march=native and -mtune=native
+ (systemtools.AARCH64, systemtools.ARM): '-mcpu=native', # since GCC 6; implies -march=native and -mtune=native
# no support for -march on POWER; implies -mtune=native
- (systemtools.POWER, systemtools.POWER): 'mcpu=native',
- (systemtools.POWER, systemtools.POWER_LE): 'mcpu=native',
- (systemtools.X86_64, systemtools.AMD): 'march=native', # implies -mtune=native
- (systemtools.X86_64, systemtools.INTEL): 'march=native', # implies -mtune=native
+ (systemtools.POWER, systemtools.POWER): '-mcpu=native',
+ (systemtools.POWER, systemtools.POWER_LE): '-mcpu=native',
+ (systemtools.X86_64, systemtools.AMD): '-march=native', # implies -mtune=native
+ (systemtools.X86_64, systemtools.INTEL): '-march=native', # implies -mtune=native
}
# used with --optarch=GENERIC
COMPILER_GENERIC_OPTION = {
- (systemtools.AARCH32, systemtools.ARM): 'mcpu=generic-armv7', # implies -march=armv7 and -mtune=generic-armv7
- (systemtools.AARCH64, systemtools.ARM): 'mcpu=generic', # implies -march=armv8-a and -mtune=generic
- (systemtools.POWER, systemtools.POWER): 'mcpu=powerpc64', # no support for -march on POWER
- (systemtools.POWER, systemtools.POWER_LE): 'mcpu=powerpc64le', # no support for -march on POWER
- (systemtools.RISCV64, systemtools.RISCV): 'march=rv64gc -mabi=lp64d', # default for -mabi is system-dependent
- (systemtools.X86_64, systemtools.AMD): 'march=x86-64 -mtune=generic',
- (systemtools.X86_64, systemtools.INTEL): 'march=x86-64 -mtune=generic',
+ (systemtools.AARCH32, systemtools.ARM): '-mcpu=generic-armv7', # implies -march=armv7 and -mtune=generic-armv7
+ (systemtools.AARCH64, systemtools.ARM): '-mcpu=generic', # implies -march=armv8-a and -mtune=generic
+ (systemtools.POWER, systemtools.POWER): '-mcpu=powerpc64', # no support for -march on POWER
+ (systemtools.POWER, systemtools.POWER_LE): '-mcpu=powerpc64le', # no support for -march on POWER
+ (systemtools.RISCV64, systemtools.RISCV): '-march=rv64gc -mabi=lp64d', # default for -mabi is system-dependent
+ (systemtools.X86_64, systemtools.AMD): '-march=x86-64 -mtune=generic',
+ (systemtools.X86_64, systemtools.INTEL): '-march=x86-64 -mtune=generic',
}
COMPILER_CC = 'gcc'
COMPILER_CXX = 'g++'
- COMPILER_C_UNIQUE_FLAGS = []
+ COMPILER_C_UNIQUE_OPTIONS = []
COMPILER_F77 = 'gfortran'
COMPILER_F90 = 'gfortran'
COMPILER_FC = 'gfortran'
- COMPILER_F_UNIQUE_FLAGS = ['f2c']
+ COMPILER_F_UNIQUE_OPTIONS = ['f2c']
LIB_MULTITHREAD = ['pthread']
LIB_MATH = ['m']
@@ -123,9 +123,6 @@ class Gcc(Compiler):
def _set_compiler_vars(self):
super(Gcc, self)._set_compiler_vars()
- if self.options.get('32bit', None):
- raise EasyBuildError("_set_compiler_vars: 32bit set, but no support yet for 32bit GCC in EasyBuild")
-
# to get rid of lots of problems with libgfortranbegin
# or remove the system gcc-gfortran
# also used in eg LIBBLAS variable
@@ -189,8 +186,8 @@ def _guess_aarch64_default_optarch(self):
break
if core_types:
# On big.LITTLE setups, sort core types to have big core (higher model number) first.
- # Example: 'mcpu=cortex-a72.cortex-a53' for "ARM Cortex-A53 + Cortex-A72"
- default_optarch = 'mcpu=%s' % '.'.join([ct[1] for ct in sorted(core_types, reverse=True)])
+ # Example: '-mcpu=cortex-a72.cortex-a53' for "ARM Cortex-A53 + Cortex-A72"
+ default_optarch = '-mcpu=%s' % '.'.join([ct[1] for ct in sorted(core_types, reverse=True)])
self.log.debug("Using architecture-specific compiler optimization flag '%s'", default_optarch)
return default_optarch
diff --git a/easybuild/toolchains/compiler/intel_compilers.py b/easybuild/toolchains/compiler/intel_compilers.py
index c33b5ee1a2..637b056a3b 100644
--- a/easybuild/toolchains/compiler/intel_compilers.py
+++ b/easybuild/toolchains/compiler/intel_compilers.py
@@ -1,5 +1,5 @@
##
-# Copyright 2021-2024 Ghent University
+# Copyright 2021-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -106,22 +106,22 @@ def set_variables(self):
if oneapi:
# fp-model source is not supported by icx but is equivalent to precise
- self.options.options_map['defaultprec'] = ['fp-speculation=safe', 'fp-model precise']
+ self.options.options_map['defaultprec'] = ['-fp-speculation=safe', '-fp-model precise']
if LooseVersion(comp_ver) >= LooseVersion('2022'):
- self.options.options_map['defaultprec'].insert(0, 'ftz')
+ self.options.options_map['defaultprec'].insert(0, '-ftz')
# icx doesn't like -fp-model fast=1; fp-model fast is equivalent
- self.options.options_map['loose'] = ['fp-model fast']
+ self.options.options_map['loose'] = ['-fp-model fast']
# fp-model fast=2 gives "warning: overriding '-ffp-model=fast=2' option with '-ffp-model=fast'"
- self.options.options_map['veryloose'] = ['fp-model fast']
+ self.options.options_map['veryloose'] = ['-fp-model fast']
# recommended in porting guide: qopenmp, unlike fiopenmp, works for both classic and oneapi compilers
# https://www.intel.com/content/www/us/en/developer/articles/guide/porting-guide-for-ifort-to-ifx.html
- self.options.options_map['openmp'] = ['qopenmp']
+ self.options.options_map['openmp'] = ['-qopenmp']
# -xSSE2 is not supported by Intel oneAPI compilers,
# so use -march=x86-64 -mtune=generic when using optarch=GENERIC
self.COMPILER_GENERIC_OPTION = {
- (systemtools.X86_64, systemtools.AMD): 'march=x86-64 -mtune=generic',
- (systemtools.X86_64, systemtools.INTEL): 'march=x86-64 -mtune=generic',
+ (systemtools.X86_64, systemtools.AMD): '-march=x86-64 -mtune=generic',
+ (systemtools.X86_64, systemtools.INTEL): '-march=x86-64 -mtune=generic',
}
# skip IntelIccIfort.set_variables (no longer relevant for recent versions)
diff --git a/easybuild/toolchains/compiler/inteliccifort.py b/easybuild/toolchains/compiler/inteliccifort.py
index 01015a0d83..434250fce0 100644
--- a/easybuild/toolchains/compiler/inteliccifort.py
+++ b/easybuild/toolchains/compiler/inteliccifort.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -57,40 +57,40 @@ class IntelIccIfort(Compiler):
}
COMPILER_UNIQUE_OPTION_MAP = {
- 'i8': 'i8',
- 'r8': 'r8',
- 'optarch': 'xHost',
- 'ieee': 'fltconsistency',
- 'strict': ['fp-speculation=strict', 'fp-model strict'],
- 'precise': ['fp-model precise'],
- 'defaultprec': ['ftz', 'fp-speculation=safe', 'fp-model source'],
- 'loose': ['fp-model fast=1'],
- 'veryloose': ['fp-model fast=2'],
- 'vectorize': {False: 'no-vec', True: 'vec'},
- 'intel-static': 'static-intel',
- 'no-icc': 'no-icc',
- 'error-unknown-option': 'we10006', # error at warning #10006: ignoring unknown option
+ 'i8': '-i8',
+ 'r8': '-r8',
+ 'optarch': '-xHost',
+ 'ieee': '-fltconsistency',
+ 'strict': ['-fp-speculation=strict', '-fp-model strict'],
+ 'precise': ['-fp-model precise'],
+ 'defaultprec': ['-ftz', '-fp-speculation=safe', '-fp-model source'],
+ 'loose': ['-fp-model fast=1'],
+ 'veryloose': ['-fp-model fast=2'],
+ 'vectorize': {False: '-no-vec', True: '-vec'},
+ 'intel-static': '-static-intel',
+ 'no-icc': '-no-icc',
+ 'error-unknown-option': '-we10006', # error at warning #10006: ignoring unknown option
}
# used when 'optarch' toolchain option is enabled (and --optarch is not specified)
COMPILER_OPTIMAL_ARCHITECTURE_OPTION = {
- (systemtools.X86_64, systemtools.AMD): 'xHost',
- (systemtools.X86_64, systemtools.INTEL): 'xHost',
+ (systemtools.X86_64, systemtools.AMD): '-xHost',
+ (systemtools.X86_64, systemtools.INTEL): '-xHost',
}
# used with --optarch=GENERIC
COMPILER_GENERIC_OPTION = {
- (systemtools.X86_64, systemtools.AMD): 'xSSE2',
- (systemtools.X86_64, systemtools.INTEL): 'xSSE2',
+ (systemtools.X86_64, systemtools.AMD): '-xSSE2',
+ (systemtools.X86_64, systemtools.INTEL): '-xSSE2',
}
COMPILER_CC = 'icc'
COMPILER_CXX = 'icpc'
- COMPILER_C_UNIQUE_FLAGS = ['intel-static', 'no-icc']
+ COMPILER_C_UNIQUE_OPTIONS = ['intel-static', 'no-icc']
COMPILER_F77 = 'ifort'
COMPILER_F90 = 'ifort'
COMPILER_FC = 'ifort'
- COMPILER_F_UNIQUE_FLAGS = ['intel-static']
+ COMPILER_F_UNIQUE_OPTIONS = ['intel-static']
LINKER_TOGGLE_STATIC_DYNAMIC = {
'static': '-Bstatic',
@@ -124,20 +124,17 @@ def _set_compiler_vars(self):
if LooseVersion(icc_version) < LooseVersion('2011'):
self.LIB_MULTITHREAD.insert(1, "guide")
- libpaths = ['intel64']
- if self.options.get('32bit', None):
- libpaths.append('ia32')
- libpaths = ['lib/%s' % x for x in libpaths]
+ libpath = 'lib/intel64'
if LooseVersion(icc_version) > LooseVersion('2011.4') and LooseVersion(icc_version) < LooseVersion('2013_sp1'):
- libpaths = ['compiler/%s' % x for x in libpaths]
+ libpath = 'compiler/%s' % libpath
- self.variables.append_subdirs("LDFLAGS", icc_root, subdirs=libpaths)
+ self.variables.append_subdirs("LDFLAGS", icc_root, subdirs=[libpath])
def set_variables(self):
"""Set the variables."""
# -fopenmp is not supported in old versions (11.x)
icc_version, _ = self.get_software_version(self.COMPILER_MODULE_NAME)[0:2]
if LooseVersion(icc_version) < LooseVersion('12'):
- self.options.options_map['openmp'] = 'openmp'
+ self.options.options_map['openmp'] = '-openmp'
super(IntelIccIfort, self).set_variables()
diff --git a/easybuild/toolchains/compiler/nvhpc.py b/easybuild/toolchains/compiler/nvhpc.py
index 2011137716..e32d70de8b 100644
--- a/easybuild/toolchains/compiler/nvhpc.py
+++ b/easybuild/toolchains/compiler/nvhpc.py
@@ -1,5 +1,5 @@
##
-# Copyright 2015-2024 Bart Oldeman
+# Copyright 2015-2025 Bart Oldeman
#
# This file is triple-licensed under GPLv2 (see below), MIT, and
# BSD three-clause licenses.
@@ -60,28 +60,28 @@ class NVHPC(Compiler):
# http://www.pgroup.com/products/freepgi/freepgi_ref/ch02.html#Mfprelaxed
# http://www.pgroup.com/products/freepgi/freepgi_ref/ch02.html#Mfpapprox
COMPILER_UNIQUE_OPTION_MAP = {
- 'i8': 'i8',
- 'r8': 'r8',
+ 'i8': '-i8',
+ 'r8': '-r8',
'optarch': '', # PGI by default generates code for the arch it is running on!
- 'openmp': 'mp',
- 'ieee': 'Kieee',
- 'strict': ['Mnoflushz', 'Kieee'],
- 'precise': ['Mnoflushz'],
- 'defaultprec': ['Mflushz'],
- 'loose': ['Mfprelaxed'],
- 'veryloose': ['Mfprelaxed=div,order,intrinsic,recip,sqrt,rsqrt', 'Mfpapprox'],
- 'vectorize': {False: 'Mnovect', True: 'Mvect'},
+ 'openmp': '-mp',
+ 'ieee': '-Kieee',
+ 'strict': ['-Mnoflushz', '-Kieee'],
+ 'precise': ['-Mnoflushz'],
+ 'defaultprec': ['-Mflushz'],
+ 'loose': ['-Mfprelaxed'],
+ 'veryloose': ['-Mfprelaxed=div,order,intrinsic,recip,sqrt,rsqrt', 'Mfpapprox'],
+ 'vectorize': {False: '-Mnovect', True: '-Mvect'},
}
# used when 'optarch' toolchain option is enabled (and --optarch is not specified)
COMPILER_OPTIMAL_ARCHITECTURE_OPTION = {
- (systemtools.X86_64, systemtools.AMD): 'tp=host',
- (systemtools.X86_64, systemtools.INTEL): 'tp=host',
+ (systemtools.X86_64, systemtools.AMD): '-tp=host',
+ (systemtools.X86_64, systemtools.INTEL): '-tp=host',
}
# used with --optarch=GENERIC
COMPILER_GENERIC_OPTION = {
- (systemtools.X86_64, systemtools.AMD): 'tp=px',
- (systemtools.X86_64, systemtools.INTEL): 'tp=px',
+ (systemtools.X86_64, systemtools.AMD): '-tp=px',
+ (systemtools.X86_64, systemtools.INTEL): '-tp=px',
}
COMPILER_CC = 'nvc'
diff --git a/easybuild/toolchains/compiler/pgi.py b/easybuild/toolchains/compiler/pgi.py
index 0f55614926..416004b1e2 100644
--- a/easybuild/toolchains/compiler/pgi.py
+++ b/easybuild/toolchains/compiler/pgi.py
@@ -1,5 +1,5 @@
##
-# Copyright 2015-2024 Bart Oldeman
+# Copyright 2015-2025 Bart Oldeman
#
# This file is triple-licensed under GPLv2 (see below), MIT, and
# BSD three-clause licenses.
@@ -56,17 +56,17 @@ class Pgi(Compiler):
# http://www.pgroup.com/products/freepgi/freepgi_ref/ch02.html#Mfprelaxed
# http://www.pgroup.com/products/freepgi/freepgi_ref/ch02.html#Mfpapprox
COMPILER_UNIQUE_OPTION_MAP = {
- 'i8': 'i8',
- 'r8': 'r8',
+ 'i8': '-i8',
+ 'r8': '-r8',
'optarch': '', # PGI by default generates code for the arch it is running on!
- 'openmp': 'mp',
- 'ieee': 'Kieee',
- 'strict': ['Mnoflushz', 'Kieee'],
- 'precise': ['Mnoflushz'],
- 'defaultprec': ['Mflushz'],
- 'loose': ['Mfprelaxed'],
- 'veryloose': ['Mfprelaxed=div,order,intrinsic,recip,sqrt,rsqrt', 'Mfpapprox'],
- 'vectorize': {False: 'Mnovect', True: 'Mvect'},
+ 'openmp': '-mp',
+ 'ieee': '-Kieee',
+ 'strict': ['-Mnoflushz', '-Kieee'],
+ 'precise': ['-Mnoflushz'],
+ 'defaultprec': ['-Mflushz'],
+ 'loose': ['-Mfprelaxed'],
+ 'veryloose': ['-Mfprelaxed=div,order,intrinsic,recip,sqrt,rsqrt', '-Mfpapprox'],
+ 'vectorize': {False: '-Mnovect', True: '-Mvect'},
}
# used when 'optarch' toolchain option is enabled (and --optarch is not specified)
@@ -76,8 +76,8 @@ class Pgi(Compiler):
}
# used with --optarch=GENERIC
COMPILER_GENERIC_OPTION = {
- (systemtools.X86_64, systemtools.AMD): 'tp=x64',
- (systemtools.X86_64, systemtools.INTEL): 'tp=x64',
+ (systemtools.X86_64, systemtools.AMD): '-tp=x64',
+ (systemtools.X86_64, systemtools.INTEL): '-tp=x64',
}
COMPILER_CC = 'pgcc'
diff --git a/easybuild/toolchains/compiler/systemcompiler.py b/easybuild/toolchains/compiler/systemcompiler.py
index d1fd06d4d9..2a7f4164f5 100644
--- a/easybuild/toolchains/compiler/systemcompiler.py
+++ b/easybuild/toolchains/compiler/systemcompiler.py
@@ -1,5 +1,5 @@
##
-# Copyright 2019-2024 Ghent University
+# Copyright 2019-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -40,3 +40,12 @@ class SystemCompiler(Compiler):
"""System compiler"""
COMPILER_MODULE_NAME = []
COMPILER_FAMILY = TC_CONSTANT_SYSTEM
+
+ # The system compiler does not currently support even the shared options
+ # (changing this would require updating set_minimal_build_env() of the toolchain class)
+ COMPILER_UNIQUE_OPTS = None
+ # only keep the rpath toolchainopt since we want to be able to disable it for
+ # sanity checks in binary-only installations
+ COMPILER_SHARED_OPTS = {k: Compiler.COMPILER_SHARED_OPTS[k] for k in ('rpath',)}
+ COMPILER_UNIQUE_OPTION_MAP = None
+ COMPILER_SHARED_OPTION_MAP = None
diff --git a/easybuild/toolchains/craycce.py b/easybuild/toolchains/craycce.py
index 13b92e55d2..7c93c10c33 100644
--- a/easybuild/toolchains/craycce.py
+++ b/easybuild/toolchains/craycce.py
@@ -1,5 +1,5 @@
##
-# Copyright 2014-2024 Ghent University
+# Copyright 2014-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/craygnu.py b/easybuild/toolchains/craygnu.py
index 8d198362bd..5ea033c113 100644
--- a/easybuild/toolchains/craygnu.py
+++ b/easybuild/toolchains/craygnu.py
@@ -1,5 +1,5 @@
##
-# Copyright 2014-2024 Ghent University
+# Copyright 2014-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/crayintel.py b/easybuild/toolchains/crayintel.py
index 5997eb63d6..c99aeaa8b0 100644
--- a/easybuild/toolchains/crayintel.py
+++ b/easybuild/toolchains/crayintel.py
@@ -1,5 +1,5 @@
##
-# Copyright 2014-2024 Ghent University
+# Copyright 2014-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/craypgi.py b/easybuild/toolchains/craypgi.py
index 49004775a4..c11f474c24 100644
--- a/easybuild/toolchains/craypgi.py
+++ b/easybuild/toolchains/craypgi.py
@@ -1,5 +1,5 @@
##
-# Copyright 2014-2024 Ghent University
+# Copyright 2014-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/fcc.py b/easybuild/toolchains/fcc.py
index bc99335c48..c0d4e056ce 100644
--- a/easybuild/toolchains/fcc.py
+++ b/easybuild/toolchains/fcc.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -40,41 +40,10 @@ class FCC(FujitsuCompiler):
OPTIONAL = False
# override in order to add an exception for the Fujitsu lang/tcsds module
- def _add_dependency_variables(self, names=None, cpp=None, ld=None):
- """ Add LDFLAGS and CPPFLAGS to the self.variables based on the dependencies
- names should be a list of strings containing the name of the dependency
+ def _add_dependency_cpp_headers(self, dep_root, extra_dirs=None):
"""
- cpp_paths = ['include']
- ld_paths = ['lib']
- if not self.options.get('32bit', None):
- ld_paths.insert(0, 'lib64')
-
- if cpp is not None:
- for p in cpp:
- if p not in cpp_paths:
- cpp_paths.append(p)
- if ld is not None:
- for p in ld:
- if p not in ld_paths:
- ld_paths.append(p)
-
- if not names:
- deps = self.dependencies
- else:
- deps = [{'name': name} for name in names if name is not None]
-
- # collect software install prefixes for dependencies
- roots = []
- for dep in deps:
- if dep.get('external_module', False):
- # for software names provided via external modules, install prefix may be unknown
- names = dep['external_module_metadata'].get('name', [])
- roots.extend([root for root in self.get_software_root(names) if root is not None])
- else:
- roots.extend(self.get_software_root(dep['name']))
-
- for root in roots:
- # skip Fujitsu's 'lang/tcsds' module, including the top level include breaks vectorization in clang mode
- if 'tcsds' not in root:
- self.variables.append_subdirs("CPPFLAGS", root, subdirs=cpp_paths)
- self.variables.append_subdirs("LDFLAGS", root, subdirs=ld_paths)
+ Append prepocessor paths for given dependency root directory
+ """
+ # skip Fujitsu's 'lang/tcsds' module, including the top level include breaks vectorization in clang mode
+ if "tcsds" not in dep_root:
+ super()._add_dependency_cpp_headers(dep_root, extra_dirs=extra_dirs)
diff --git a/easybuild/toolchains/ffmpi.py b/easybuild/toolchains/ffmpi.py
index 71a49f7913..d0a2293b9e 100644
--- a/easybuild/toolchains/ffmpi.py
+++ b/easybuild/toolchains/ffmpi.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/fft/__init__.py b/easybuild/toolchains/fft/__init__.py
index afcf0211dd..c566907d41 100644
--- a/easybuild/toolchains/fft/__init__.py
+++ b/easybuild/toolchains/fft/__init__.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/fft/fftw.py b/easybuild/toolchains/fft/fftw.py
index 9cb4c4e851..cb0d3f2cef 100644
--- a/easybuild/toolchains/fft/fftw.py
+++ b/easybuild/toolchains/fft/fftw.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/fft/fujitsufftw.py b/easybuild/toolchains/fft/fujitsufftw.py
index 4de1769528..d9e74aae5f 100644
--- a/easybuild/toolchains/fft/fujitsufftw.py
+++ b/easybuild/toolchains/fft/fujitsufftw.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/fft/intelfftw.py b/easybuild/toolchains/fft/intelfftw.py
index 7882278a91..5541d2b939 100644
--- a/easybuild/toolchains/fft/intelfftw.py
+++ b/easybuild/toolchains/fft/intelfftw.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/foss.py b/easybuild/toolchains/foss.py
index 031daa4c9c..942aa8a2f5 100644
--- a/easybuild/toolchains/foss.py
+++ b/easybuild/toolchains/foss.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/fosscuda.py b/easybuild/toolchains/fosscuda.py
index 9465515d36..45dbe6cce2 100644
--- a/easybuild/toolchains/fosscuda.py
+++ b/easybuild/toolchains/fosscuda.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/fujitsu.py b/easybuild/toolchains/fujitsu.py
index 707255e03c..3a65c6ae50 100644
--- a/easybuild/toolchains/fujitsu.py
+++ b/easybuild/toolchains/fujitsu.py
@@ -1,5 +1,5 @@
##
-# Copyright 2014-2024 Ghent University
+# Copyright 2014-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/gcc.py b/easybuild/toolchains/gcc.py
index 71091e795c..0b6b2b96f9 100644
--- a/easybuild/toolchains/gcc.py
+++ b/easybuild/toolchains/gcc.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/gcccore.py b/easybuild/toolchains/gcccore.py
index ce4703e89c..f3ff61dae7 100644
--- a/easybuild/toolchains/gcccore.py
+++ b/easybuild/toolchains/gcccore.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/gcccuda.py b/easybuild/toolchains/gcccuda.py
index 47ab250824..cc2194c853 100644
--- a/easybuild/toolchains/gcccuda.py
+++ b/easybuild/toolchains/gcccuda.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/gfbf.py b/easybuild/toolchains/gfbf.py
index d41f5b7d06..72274b641f 100644
--- a/easybuild/toolchains/gfbf.py
+++ b/easybuild/toolchains/gfbf.py
@@ -1,5 +1,5 @@
##
-# Copyright 2021-2024 Ghent University
+# Copyright 2021-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/gimkl.py b/easybuild/toolchains/gimkl.py
index 1d7b9704f6..7e69b3e788 100644
--- a/easybuild/toolchains/gimkl.py
+++ b/easybuild/toolchains/gimkl.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/gimpi.py b/easybuild/toolchains/gimpi.py
index 76e6bd638a..1f5ed9ba0e 100644
--- a/easybuild/toolchains/gimpi.py
+++ b/easybuild/toolchains/gimpi.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/gimpic.py b/easybuild/toolchains/gimpic.py
index dde1d6c8f8..3005b97919 100644
--- a/easybuild/toolchains/gimpic.py
+++ b/easybuild/toolchains/gimpic.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/giolf.py b/easybuild/toolchains/giolf.py
index 121c81121b..7cbaacb130 100644
--- a/easybuild/toolchains/giolf.py
+++ b/easybuild/toolchains/giolf.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/giolfc.py b/easybuild/toolchains/giolfc.py
index df798626aa..588d4922bb 100644
--- a/easybuild/toolchains/giolfc.py
+++ b/easybuild/toolchains/giolfc.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/gmacml.py b/easybuild/toolchains/gmacml.py
index ee95c3f7f3..9eda2321f8 100644
--- a/easybuild/toolchains/gmacml.py
+++ b/easybuild/toolchains/gmacml.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/gmkl.py b/easybuild/toolchains/gmkl.py
index 67e33e1e62..521edab090 100644
--- a/easybuild/toolchains/gmkl.py
+++ b/easybuild/toolchains/gmkl.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/gmklc.py b/easybuild/toolchains/gmklc.py
index bead896d5e..182d7f4220 100644
--- a/easybuild/toolchains/gmklc.py
+++ b/easybuild/toolchains/gmklc.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/gmpflf.py b/easybuild/toolchains/gmpflf.py
index 30d10a90f7..cd584ae4e1 100644
--- a/easybuild/toolchains/gmpflf.py
+++ b/easybuild/toolchains/gmpflf.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is triple-licensed under GPLv2 (see below), MIT, and
# BSD three-clause licenses.
diff --git a/easybuild/toolchains/gmpich.py b/easybuild/toolchains/gmpich.py
index 66d2b29123..1b16206eba 100644
--- a/easybuild/toolchains/gmpich.py
+++ b/easybuild/toolchains/gmpich.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/gmpich2.py b/easybuild/toolchains/gmpich2.py
index f734d3cb56..e54cc84260 100644
--- a/easybuild/toolchains/gmpich2.py
+++ b/easybuild/toolchains/gmpich2.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/gmpit.py b/easybuild/toolchains/gmpit.py
index a39baf934d..c5a658d888 100644
--- a/easybuild/toolchains/gmpit.py
+++ b/easybuild/toolchains/gmpit.py
@@ -1,5 +1,5 @@
##
-# Copyright 2022-2024 Ghent University
+# Copyright 2022-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/gmpolf.py b/easybuild/toolchains/gmpolf.py
index c5a8be97d9..e64d1c4e05 100644
--- a/easybuild/toolchains/gmpolf.py
+++ b/easybuild/toolchains/gmpolf.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is triple-licensed under GPLv2 (see below), MIT, and
# BSD three-clause licenses.
diff --git a/easybuild/toolchains/gmvapich2.py b/easybuild/toolchains/gmvapich2.py
index 26aa6ad73e..98fef55752 100644
--- a/easybuild/toolchains/gmvapich2.py
+++ b/easybuild/toolchains/gmvapich2.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/gmvolf.py b/easybuild/toolchains/gmvolf.py
index 613d8a6ed3..23f859fd86 100644
--- a/easybuild/toolchains/gmvolf.py
+++ b/easybuild/toolchains/gmvolf.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is triple-licensed under GPLv2 (see below), MIT, and
# BSD three-clause licenses.
diff --git a/easybuild/toolchains/gnu.py b/easybuild/toolchains/gnu.py
index 84b06af535..0968a5237d 100644
--- a/easybuild/toolchains/gnu.py
+++ b/easybuild/toolchains/gnu.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/goalf.py b/easybuild/toolchains/goalf.py
index db1280907b..97bb0b07fd 100644
--- a/easybuild/toolchains/goalf.py
+++ b/easybuild/toolchains/goalf.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/gobff.py b/easybuild/toolchains/gobff.py
index 9df26ff706..36801ddba7 100644
--- a/easybuild/toolchains/gobff.py
+++ b/easybuild/toolchains/gobff.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/goblf.py b/easybuild/toolchains/goblf.py
index 63e7c0c0a4..be7aa4c5b4 100644
--- a/easybuild/toolchains/goblf.py
+++ b/easybuild/toolchains/goblf.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/gofbf.py b/easybuild/toolchains/gofbf.py
index 3128c87091..94f2d609d7 100644
--- a/easybuild/toolchains/gofbf.py
+++ b/easybuild/toolchains/gofbf.py
@@ -1,5 +1,5 @@
##
-# Copyright 2021-2024 Ghent University
+# Copyright 2021-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/golf.py b/easybuild/toolchains/golf.py
index 6d046413a8..a9574c08b5 100644
--- a/easybuild/toolchains/golf.py
+++ b/easybuild/toolchains/golf.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/golfc.py b/easybuild/toolchains/golfc.py
index 3b99dce5c0..f6e17f6d2a 100644
--- a/easybuild/toolchains/golfc.py
+++ b/easybuild/toolchains/golfc.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/gomkl.py b/easybuild/toolchains/gomkl.py
index ba93882a1b..419ab5b178 100644
--- a/easybuild/toolchains/gomkl.py
+++ b/easybuild/toolchains/gomkl.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/gomklc.py b/easybuild/toolchains/gomklc.py
index 95620d8360..5507b23a84 100644
--- a/easybuild/toolchains/gomklc.py
+++ b/easybuild/toolchains/gomklc.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/gompi.py b/easybuild/toolchains/gompi.py
index 7af87123c9..3473e117cd 100644
--- a/easybuild/toolchains/gompi.py
+++ b/easybuild/toolchains/gompi.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/gompic.py b/easybuild/toolchains/gompic.py
index 0beb2b4d10..5c62fbb876 100644
--- a/easybuild/toolchains/gompic.py
+++ b/easybuild/toolchains/gompic.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/goolf.py b/easybuild/toolchains/goolf.py
index 099f7bc11a..ed8fb837f5 100644
--- a/easybuild/toolchains/goolf.py
+++ b/easybuild/toolchains/goolf.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/goolfc.py b/easybuild/toolchains/goolfc.py
index 76cf4db890..bf5e4f6b24 100644
--- a/easybuild/toolchains/goolfc.py
+++ b/easybuild/toolchains/goolfc.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/gpsmpi.py b/easybuild/toolchains/gpsmpi.py
index 529d17dc5e..9452d5a08f 100644
--- a/easybuild/toolchains/gpsmpi.py
+++ b/easybuild/toolchains/gpsmpi.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/gpsolf.py b/easybuild/toolchains/gpsolf.py
index e87f3a5de7..6666906f12 100644
--- a/easybuild/toolchains/gpsolf.py
+++ b/easybuild/toolchains/gpsolf.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is triple-licensed under GPLv2 (see below), MIT, and
# BSD three-clause licenses.
diff --git a/easybuild/toolchains/gqacml.py b/easybuild/toolchains/gqacml.py
index 7456b4011e..aa56351070 100644
--- a/easybuild/toolchains/gqacml.py
+++ b/easybuild/toolchains/gqacml.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/gsmpi.py b/easybuild/toolchains/gsmpi.py
index 68f1a6d0f0..2e89020c43 100644
--- a/easybuild/toolchains/gsmpi.py
+++ b/easybuild/toolchains/gsmpi.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/gsolf.py b/easybuild/toolchains/gsolf.py
index cb917ca7d3..c58266d8e0 100644
--- a/easybuild/toolchains/gsolf.py
+++ b/easybuild/toolchains/gsolf.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/iccifort.py b/easybuild/toolchains/iccifort.py
index 38ec224a5b..80841b9aad 100644
--- a/easybuild/toolchains/iccifort.py
+++ b/easybuild/toolchains/iccifort.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/iccifortcuda.py b/easybuild/toolchains/iccifortcuda.py
index 149cfc8e39..b0370e0b59 100644
--- a/easybuild/toolchains/iccifortcuda.py
+++ b/easybuild/toolchains/iccifortcuda.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/ictce.py b/easybuild/toolchains/ictce.py
index 2055346b37..3d8e574701 100644
--- a/easybuild/toolchains/ictce.py
+++ b/easybuild/toolchains/ictce.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/ifbf.py b/easybuild/toolchains/ifbf.py
index 9f689279cb..ae493a20ed 100644
--- a/easybuild/toolchains/ifbf.py
+++ b/easybuild/toolchains/ifbf.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/iibff.py b/easybuild/toolchains/iibff.py
index 07a6b1e567..292a6ea902 100644
--- a/easybuild/toolchains/iibff.py
+++ b/easybuild/toolchains/iibff.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/iimkl.py b/easybuild/toolchains/iimkl.py
index 605445cc5c..7e55d71dbb 100644
--- a/easybuild/toolchains/iimkl.py
+++ b/easybuild/toolchains/iimkl.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/iimklc.py b/easybuild/toolchains/iimklc.py
index 89325c2e67..e74005a6be 100644
--- a/easybuild/toolchains/iimklc.py
+++ b/easybuild/toolchains/iimklc.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/iimpi.py b/easybuild/toolchains/iimpi.py
index 9337365ec6..a8dca0f953 100644
--- a/easybuild/toolchains/iimpi.py
+++ b/easybuild/toolchains/iimpi.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/iimpic.py b/easybuild/toolchains/iimpic.py
index 45a8df3804..a54524cf38 100644
--- a/easybuild/toolchains/iimpic.py
+++ b/easybuild/toolchains/iimpic.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/iiqmpi.py b/easybuild/toolchains/iiqmpi.py
index 5ff1466ff3..a1bc6809fb 100644
--- a/easybuild/toolchains/iiqmpi.py
+++ b/easybuild/toolchains/iiqmpi.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/impich.py b/easybuild/toolchains/impich.py
index 7feda33805..c06815d98b 100644
--- a/easybuild/toolchains/impich.py
+++ b/easybuild/toolchains/impich.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/impmkl.py b/easybuild/toolchains/impmkl.py
index 952bff34ce..4963682668 100644
--- a/easybuild/toolchains/impmkl.py
+++ b/easybuild/toolchains/impmkl.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/intel-para.py b/easybuild/toolchains/intel-para.py
index ddfee3aaae..22322e88dd 100644
--- a/easybuild/toolchains/intel-para.py
+++ b/easybuild/toolchains/intel-para.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/intel.py b/easybuild/toolchains/intel.py
index 659b05faf2..3842933b9a 100644
--- a/easybuild/toolchains/intel.py
+++ b/easybuild/toolchains/intel.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/intel_compilers.py b/easybuild/toolchains/intel_compilers.py
index 27fdfbebfd..4ffb3b81ee 100644
--- a/easybuild/toolchains/intel_compilers.py
+++ b/easybuild/toolchains/intel_compilers.py
@@ -1,5 +1,5 @@
##
-# Copyright 2021-2024 Ghent University
+# Copyright 2021-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/intelcuda.py b/easybuild/toolchains/intelcuda.py
index bdd59abaab..fa84a3a759 100644
--- a/easybuild/toolchains/intelcuda.py
+++ b/easybuild/toolchains/intelcuda.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/iofbf.py b/easybuild/toolchains/iofbf.py
index cd3313d600..2db44455f7 100644
--- a/easybuild/toolchains/iofbf.py
+++ b/easybuild/toolchains/iofbf.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/iomkl.py b/easybuild/toolchains/iomkl.py
index 033277919c..9f556b6970 100644
--- a/easybuild/toolchains/iomkl.py
+++ b/easybuild/toolchains/iomkl.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/iomklc.py b/easybuild/toolchains/iomklc.py
index f40cd78e16..afd182955a 100644
--- a/easybuild/toolchains/iomklc.py
+++ b/easybuild/toolchains/iomklc.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/iompi.py b/easybuild/toolchains/iompi.py
index b578c45892..2ec287cb77 100644
--- a/easybuild/toolchains/iompi.py
+++ b/easybuild/toolchains/iompi.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/iompic.py b/easybuild/toolchains/iompic.py
index 5b675f5621..8220be6f7c 100644
--- a/easybuild/toolchains/iompic.py
+++ b/easybuild/toolchains/iompic.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/ipsmpi.py b/easybuild/toolchains/ipsmpi.py
index a8a772adcb..2960b5ed22 100644
--- a/easybuild/toolchains/ipsmpi.py
+++ b/easybuild/toolchains/ipsmpi.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/iqacml.py b/easybuild/toolchains/iqacml.py
index e4c5476cd4..82500946c3 100644
--- a/easybuild/toolchains/iqacml.py
+++ b/easybuild/toolchains/iqacml.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/ismkl.py b/easybuild/toolchains/ismkl.py
index 4ccdc87054..527af51051 100644
--- a/easybuild/toolchains/ismkl.py
+++ b/easybuild/toolchains/ismkl.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/linalg/__init__.py b/easybuild/toolchains/linalg/__init__.py
index 90cdb068a8..364c4eca88 100644
--- a/easybuild/toolchains/linalg/__init__.py
+++ b/easybuild/toolchains/linalg/__init__.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/linalg/acml.py b/easybuild/toolchains/linalg/acml.py
index 268e3b0cc2..2a30636243 100644
--- a/easybuild/toolchains/linalg/acml.py
+++ b/easybuild/toolchains/linalg/acml.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -73,15 +73,13 @@ def __init__(self, *args, **kwargs):
def _set_blas_variables(self):
"""Fix the map a bit"""
- if self.options.get('32bit', None):
- raise EasyBuildError("_set_blas_variables: 32bit ACML not (yet) supported")
try:
for root in self.get_software_root(self.BLAS_MODULE_NAME):
subdirs = self.ACML_SUBDIRS_MAP[self.COMPILER_FAMILY]
self.BLAS_LIB_DIR = [os.path.join(x, 'lib') for x in subdirs]
- self.variables.append_exists('LDFLAGS', root, self.BLAS_LIB_DIR, append_all=True)
+ self._add_dependency_linker_paths(root, extra_dirs=self.BLAS_LIB_DIR)
incdirs = [os.path.join(x, 'include') for x in subdirs]
- self.variables.append_exists('CPPFLAGS', root, incdirs, append_all=True)
+ self._add_dependency_cpp_headers(root, extra_dirs=incdirs)
except Exception:
raise EasyBuildError("_set_blas_variables: ACML set LDFLAGS/CPPFLAGS unknown entry in ACML_SUBDIRS_MAP "
"with compiler family %s", self.COMPILER_FAMILY)
diff --git a/easybuild/toolchains/linalg/atlas.py b/easybuild/toolchains/linalg/atlas.py
index bc894bb108..5adbcda66c 100644
--- a/easybuild/toolchains/linalg/atlas.py
+++ b/easybuild/toolchains/linalg/atlas.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/linalg/blacs.py b/easybuild/toolchains/linalg/blacs.py
index 201681c51f..db18ec85d3 100644
--- a/easybuild/toolchains/linalg/blacs.py
+++ b/easybuild/toolchains/linalg/blacs.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/linalg/blis.py b/easybuild/toolchains/linalg/blis.py
index d89f3b8d83..f4ba16574f 100644
--- a/easybuild/toolchains/linalg/blis.py
+++ b/easybuild/toolchains/linalg/blis.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/linalg/flame.py b/easybuild/toolchains/linalg/flame.py
index 5f6f5cfe87..99f963f84c 100644
--- a/easybuild/toolchains/linalg/flame.py
+++ b/easybuild/toolchains/linalg/flame.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/linalg/flexiblas.py b/easybuild/toolchains/linalg/flexiblas.py
index 0e88c1a9b2..8b874cc767 100644
--- a/easybuild/toolchains/linalg/flexiblas.py
+++ b/easybuild/toolchains/linalg/flexiblas.py
@@ -1,5 +1,5 @@
##
-# Copyright 2021-2024 Ghent University
+# Copyright 2021-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -34,7 +34,7 @@
from easybuild.tools.toolchain.linalg import LinAlg
-from easybuild.tools.run import run_cmd
+from easybuild.tools.run import run_shell_cmd
from easybuild.tools.systemtools import get_shared_lib_ext
@@ -48,11 +48,11 @@ def det_flexiblas_backend_libs():
# System-wide (config directory):
# OPENBLAS
# library = libflexiblas_openblas.so
- out, _ = run_cmd("flexiblas list", simple=False, trace=False)
+ res = run_shell_cmd("flexiblas list", hidden=True)
shlib_ext = get_shared_lib_ext()
flexiblas_lib_regex = re.compile(r'library = (?Plib.*\.%s)' % shlib_ext, re.M)
- flexiblas_libs = flexiblas_lib_regex.findall(out)
+ flexiblas_libs = flexiblas_lib_regex.findall(res.output)
backend_libs = []
for flexiblas_lib in flexiblas_libs:
diff --git a/easybuild/toolchains/linalg/fujitsussl.py b/easybuild/toolchains/linalg/fujitsussl.py
index dde792455b..9e32da3a2c 100644
--- a/easybuild/toolchains/linalg/fujitsussl.py
+++ b/easybuild/toolchains/linalg/fujitsussl.py
@@ -1,5 +1,5 @@
##
-# Copyright 2014-2024 Ghent University
+# Copyright 2014-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/linalg/gotoblas.py b/easybuild/toolchains/linalg/gotoblas.py
index e8a99a651a..253d32d4cc 100644
--- a/easybuild/toolchains/linalg/gotoblas.py
+++ b/easybuild/toolchains/linalg/gotoblas.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/linalg/intelmkl.py b/easybuild/toolchains/linalg/intelmkl.py
index 9dc89da827..19f81963b9 100644
--- a/easybuild/toolchains/linalg/intelmkl.py
+++ b/easybuild/toolchains/linalg/intelmkl.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -132,9 +132,6 @@ def _set_blas_variables(self):
raise EasyBuildError("_set_blas_variables: interface_mt unsupported combination with compiler family %s",
self.COMPILER_FAMILY)
- if self.options.get('32bit', None):
- # 32bit
- self.BLAS_LIB_MAP.update({"lp64": ''})
if self.options.get('i8', None):
# ilp64/i8
self.BLAS_LIB_MAP.update({"lp64": '_ilp64'})
@@ -146,28 +143,21 @@ def _set_blas_variables(self):
found_version = self.get_software_version(self.BLAS_MODULE_NAME)[0]
ver = LooseVersion(found_version)
if ver < LooseVersion('10.3'):
- if self.options.get('32bit', None):
- self.BLAS_LIB_DIR = ['lib/32']
- else:
- self.BLAS_LIB_DIR = ['lib/em64t']
+ self.BLAS_LIB_DIR = ['lib/em64t']
self.BLAS_INCLUDE_DIR = ['include']
else:
- if self.options.get('32bit', None):
- raise EasyBuildError("_set_blas_variables: 32-bit libraries not supported yet for IMKL v%s (> v10.3)",
- found_version)
+ if ver >= LooseVersion('2021'):
+ if os.path.islink(os.path.join(root, 'mkl', 'latest')):
+ found_version = os.readlink(os.path.join(root, 'mkl', 'latest'))
+ basedir = os.path.join('mkl', found_version)
else:
- if ver >= LooseVersion('2021'):
- if os.path.islink(os.path.join(root, 'mkl', 'latest')):
- found_version = os.readlink(os.path.join(root, 'mkl', 'latest'))
- basedir = os.path.join('mkl', found_version)
- else:
- basedir = 'mkl'
-
- self.BLAS_LIB_DIR = [os.path.join(basedir, 'lib', 'intel64')]
- if ver >= LooseVersion('10.3.4') and ver < LooseVersion('11.1'):
- self.BLAS_LIB_DIR.append(os.path.join('compiler', 'lib', 'intel64'))
- elif ver < LooseVersion('2021'):
- self.BLAS_LIB_DIR.append(os.path.join('lib', 'intel64'))
+ basedir = 'mkl'
+
+ self.BLAS_LIB_DIR = [os.path.join(basedir, 'lib', 'intel64')]
+ if ver >= LooseVersion('10.3.4') and ver < LooseVersion('11.1'):
+ self.BLAS_LIB_DIR.append(os.path.join('compiler', 'lib', 'intel64'))
+ elif ver < LooseVersion('2021'):
+ self.BLAS_LIB_DIR.append(os.path.join('lib', 'intel64'))
self.BLAS_INCLUDE_DIR = [os.path.join(basedir, 'include')]
@@ -201,11 +191,7 @@ def _set_scalapack_variables(self):
self.SCALAPACK_LIB.append("mkl_solver%(lp64)s_sequential")
self.SCALAPACK_LIB_MT.append("mkl_solver%(lp64)s")
- if self.options.get('32bit', None):
- # 32 bit
- self.SCALAPACK_LIB_MAP.update({"lp64_sc": '_core'})
-
- elif self.options.get('i8', None):
+ if self.options.get('i8', None):
# ilp64/i8
self.SCALAPACK_LIB_MAP.update({"lp64_sc": '_ilp64'})
diff --git a/easybuild/toolchains/linalg/lapack.py b/easybuild/toolchains/linalg/lapack.py
index e26c9e2961..9f173e889f 100644
--- a/easybuild/toolchains/linalg/lapack.py
+++ b/easybuild/toolchains/linalg/lapack.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/linalg/libsci.py b/easybuild/toolchains/linalg/libsci.py
index 6d7e93c16d..773952497c 100644
--- a/easybuild/toolchains/linalg/libsci.py
+++ b/easybuild/toolchains/linalg/libsci.py
@@ -1,5 +1,5 @@
##
-# Copyright 2014-2024 Ghent University
+# Copyright 2014-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/linalg/openblas.py b/easybuild/toolchains/linalg/openblas.py
index 18118d1f01..607235181d 100644
--- a/easybuild/toolchains/linalg/openblas.py
+++ b/easybuild/toolchains/linalg/openblas.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/linalg/scalapack.py b/easybuild/toolchains/linalg/scalapack.py
index 980e3bfdab..b4c12e6963 100644
--- a/easybuild/toolchains/linalg/scalapack.py
+++ b/easybuild/toolchains/linalg/scalapack.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/mpi/__init__.py b/easybuild/toolchains/mpi/__init__.py
index c5abd0f595..1445f255b7 100644
--- a/easybuild/toolchains/mpi/__init__.py
+++ b/easybuild/toolchains/mpi/__init__.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/mpi/craympich.py b/easybuild/toolchains/mpi/craympich.py
index 8eab736d82..92cca9d4dc 100644
--- a/easybuild/toolchains/mpi/craympich.py
+++ b/easybuild/toolchains/mpi/craympich.py
@@ -1,5 +1,5 @@
##
-# Copyright 2014-2024 Ghent University
+# Copyright 2014-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/mpi/fujitsumpi.py b/easybuild/toolchains/mpi/fujitsumpi.py
index ad65c463cf..65c411f0d1 100644
--- a/easybuild/toolchains/mpi/fujitsumpi.py
+++ b/easybuild/toolchains/mpi/fujitsumpi.py
@@ -1,5 +1,5 @@
##
-# Copyright 2014-2024 Ghent University
+# Copyright 2014-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/mpi/intelmpi.py b/easybuild/toolchains/mpi/intelmpi.py
index 8bc67fe788..74dfb7fb57 100644
--- a/easybuild/toolchains/mpi/intelmpi.py
+++ b/easybuild/toolchains/mpi/intelmpi.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -97,6 +97,6 @@ def set_variables(self):
super(IntelMPI, self).set_variables()
# add -mt_mpi flag to ensure linking against thread-safe MPI library when OpenMP is enabled
if self.options.get('openmp', None) and self.options.get('usempi', None):
- mt_mpi_option = ['mt_mpi']
+ mt_mpi_option = ['-mt_mpi']
for flags_var, _ in COMPILER_FLAGS:
self.variables.nappend(flags_var, mt_mpi_option)
diff --git a/easybuild/toolchains/mpi/mpich.py b/easybuild/toolchains/mpi/mpich.py
index 7a067a0d37..6584a88d5d 100644
--- a/easybuild/toolchains/mpi/mpich.py
+++ b/easybuild/toolchains/mpi/mpich.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/mpi/mpich2.py b/easybuild/toolchains/mpi/mpich2.py
index b6f1f6f082..24653a662d 100644
--- a/easybuild/toolchains/mpi/mpich2.py
+++ b/easybuild/toolchains/mpi/mpich2.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/mpi/mpitrampoline.py b/easybuild/toolchains/mpi/mpitrampoline.py
index 43202b59d3..f3001a8897 100644
--- a/easybuild/toolchains/mpi/mpitrampoline.py
+++ b/easybuild/toolchains/mpi/mpitrampoline.py
@@ -1,5 +1,5 @@
##
-# Copyright 2022-2024 Ghent University
+# Copyright 2022-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/mpi/mvapich2.py b/easybuild/toolchains/mpi/mvapich2.py
index 4f69c970f5..d75616753f 100644
--- a/easybuild/toolchains/mpi/mvapich2.py
+++ b/easybuild/toolchains/mpi/mvapich2.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/mpi/openmpi.py b/easybuild/toolchains/mpi/openmpi.py
index 91d10923e7..44e9b3d380 100644
--- a/easybuild/toolchains/mpi/openmpi.py
+++ b/easybuild/toolchains/mpi/openmpi.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/mpi/psmpi.py b/easybuild/toolchains/mpi/psmpi.py
index 07e8c9af78..03daf3db82 100644
--- a/easybuild/toolchains/mpi/psmpi.py
+++ b/easybuild/toolchains/mpi/psmpi.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/mpi/qlogicmpi.py b/easybuild/toolchains/mpi/qlogicmpi.py
index b7355cd3e1..7af5ed98b9 100644
--- a/easybuild/toolchains/mpi/qlogicmpi.py
+++ b/easybuild/toolchains/mpi/qlogicmpi.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/mpi/spectrummpi.py b/easybuild/toolchains/mpi/spectrummpi.py
index 1fee1d51b7..8907031955 100644
--- a/easybuild/toolchains/mpi/spectrummpi.py
+++ b/easybuild/toolchains/mpi/spectrummpi.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/nvhpc.py b/easybuild/toolchains/nvhpc.py
index b4b871f84f..9a0db2bbd7 100644
--- a/easybuild/toolchains/nvhpc.py
+++ b/easybuild/toolchains/nvhpc.py
@@ -1,5 +1,5 @@
##
-# Copyright 2015-2024 Bart Oldeman
+# Copyright 2015-2025 Bart Oldeman
#
# This file is triple-licensed under GPLv2 (see below), MIT, and
# BSD three-clause licenses.
diff --git a/easybuild/toolchains/nvofbf.py b/easybuild/toolchains/nvofbf.py
index 62d3cb4d37..06bc8fda64 100644
--- a/easybuild/toolchains/nvofbf.py
+++ b/easybuild/toolchains/nvofbf.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/nvompi.py b/easybuild/toolchains/nvompi.py
index 3feedaae35..578d3f9262 100644
--- a/easybuild/toolchains/nvompi.py
+++ b/easybuild/toolchains/nvompi.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/nvompic.py b/easybuild/toolchains/nvompic.py
index b9c5abc298..648cefc874 100644
--- a/easybuild/toolchains/nvompic.py
+++ b/easybuild/toolchains/nvompic.py
@@ -1,6 +1,6 @@
##
-# Copyright 2016-2024 Ghent University
-# Copyright 2016-2024 Forschungszentrum Juelich
+# Copyright 2016-2025 Ghent University
+# Copyright 2016-2025 Forschungszentrum Juelich
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/nvpsmpi.py b/easybuild/toolchains/nvpsmpi.py
index cbaee42bc0..8eb03cca89 100644
--- a/easybuild/toolchains/nvpsmpi.py
+++ b/easybuild/toolchains/nvpsmpi.py
@@ -1,6 +1,6 @@
##
-# Copyright 2016-2024 Ghent University
-# Copyright 2016-2024 Forschungszentrum Juelich
+# Copyright 2016-2025 Ghent University
+# Copyright 2016-2025 Forschungszentrum Juelich
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/nvpsmpic.py b/easybuild/toolchains/nvpsmpic.py
index 0b916c30a0..90a7d63257 100644
--- a/easybuild/toolchains/nvpsmpic.py
+++ b/easybuild/toolchains/nvpsmpic.py
@@ -1,6 +1,6 @@
##
-# Copyright 2016-2024 Ghent University
-# Copyright 2016-2024 Forschungszentrum Juelich
+# Copyright 2016-2025 Ghent University
+# Copyright 2016-2025 Forschungszentrum Juelich
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/pgi.py b/easybuild/toolchains/pgi.py
index df2258bda4..1dc6153992 100644
--- a/easybuild/toolchains/pgi.py
+++ b/easybuild/toolchains/pgi.py
@@ -1,5 +1,5 @@
##
-# Copyright 2015-2024 Bart Oldeman
+# Copyright 2015-2025 Bart Oldeman
#
# This file is triple-licensed under GPLv2 (see below), MIT, and
# BSD three-clause licenses.
diff --git a/easybuild/toolchains/pmkl.py b/easybuild/toolchains/pmkl.py
index 8b22d1d35f..58624761d0 100644
--- a/easybuild/toolchains/pmkl.py
+++ b/easybuild/toolchains/pmkl.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/pomkl.py b/easybuild/toolchains/pomkl.py
index f28f445296..cac22b48d5 100644
--- a/easybuild/toolchains/pomkl.py
+++ b/easybuild/toolchains/pomkl.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/pompi.py b/easybuild/toolchains/pompi.py
index 9de2e52050..e6dc33ecba 100644
--- a/easybuild/toolchains/pompi.py
+++ b/easybuild/toolchains/pompi.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/toolchains/system.py b/easybuild/toolchains/system.py
index 8a8b17cfaa..7d9d25e6b1 100644
--- a/easybuild/toolchains/system.py
+++ b/easybuild/toolchains/system.py
@@ -1,5 +1,5 @@
##
-# Copyright 2019-2024 Ghent University
+# Copyright 2019-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/tools/__init__.py b/easybuild/tools/__init__.py
index 8d34fc25c5..60a2671221 100644
--- a/easybuild/tools/__init__.py
+++ b/easybuild/tools/__init__.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -37,14 +37,4 @@
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
-import distutils.version
-import warnings
from easybuild.tools.loose_version import LooseVersion # noqa(F401)
-
-
-class StrictVersion(distutils.version.StrictVersion):
- """Temporary wrapper over distuitls StrictVersion that silences the deprecation warning"""
- def __init__(self, *args, **kwargs):
- with warnings.catch_warnings():
- warnings.simplefilter("ignore", category=DeprecationWarning)
- distutils.version.StrictVersion.__init__(self, *args, **kwargs)
diff --git a/easybuild/tools/asyncprocess.py b/easybuild/tools/asyncprocess.py
index 0a3c133fc4..2d9f64f22c 100644
--- a/easybuild/tools/asyncprocess.py
+++ b/easybuild/tools/asyncprocess.py
@@ -1,6 +1,6 @@
##
# Copyright 2005 Josiah Carlson
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# The Asynchronous Python Subprocess recipe was originally created by Josiah Carlson.
# and released under the GPL v2 on March 14, 2012
diff --git a/easybuild/tools/build_details.py b/easybuild/tools/build_details.py
index f5ce1ebc01..d2a0127389 100644
--- a/easybuild/tools/build_details.py
+++ b/easybuild/tools/build_details.py
@@ -1,4 +1,4 @@
-# Copyright 2014-2024 Ghent University
+# Copyright 2014-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/tools/build_log.py b/easybuild/tools/build_log.py
index 0bbff702a1..044f53faac 100644
--- a/easybuild/tools/build_log.py
+++ b/easybuild/tools/build_log.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -40,12 +40,12 @@
import tempfile
from copy import copy
from datetime import datetime
+from enum import IntEnum
from easybuild.base import fancylogger
from easybuild.base.exceptions import LoggedException
from easybuild.tools.version import VERSION, this_is_easybuild
-
# EasyBuild message prefix
EB_MSG_PREFIX = "=="
@@ -55,17 +55,71 @@
# allow some experimental experimental code
EXPERIMENTAL = False
-DEPRECATED_DOC_URL = 'http://easybuild.readthedocs.org/en/latest/Deprecated-functionality.html'
+DEPRECATED_DOC_URL = 'https://docs.easybuild.io/deprecated-functionality/'
DRY_RUN_BUILD_DIR = None
DRY_RUN_SOFTWARE_INSTALL_DIR = None
DRY_RUN_MODULES_INSTALL_DIR = None
+CWD_NOTFOUND_ERROR = (
+ "Current working directory does not exist! It was either unexpectedly removed "
+ "by an external process to EasyBuild or the filesystem is misbehaving."
+)
+
DEVEL_LOG_LEVEL = logging.DEBUG - 1
logging.addLevelName(DEVEL_LOG_LEVEL, 'DEVEL')
+class EasyBuildExit(IntEnum):
+ """
+ Table of exit codes
+ """
+ SUCCESS = 0
+ ERROR = 1
+ # core errors
+ OPTION_ERROR = 2
+ VALUE_ERROR = 3
+ MISSING_EASYCONFIG = 4
+ EASYCONFIG_ERROR = 5
+ MISSING_EASYBLOCK = 6
+ EASYBLOCK_ERROR = 7
+ MODULE_ERROR = 8
+ # step errors in order of execution
+ FAIL_FETCH_STEP = 10
+ FAIL_READY_STEP = 11
+ FAIL_SOURCE_STEP = 12
+ FAIL_PATCH_STEP = 13
+ FAIL_PREPARE_STEP = 14
+ FAIL_CONFIGURE_STEP = 15
+ FAIL_BUILD_STEP = 16
+ FAIL_TEST_STEP = 17
+ FAIL_INSTALL_STEP = 18
+ FAIL_EXTENSIONS_STEP = 19
+ FAIL_POST_ITER_STEP = 20
+ FAIL_POST_PROC_STEP = 21
+ FAIL_SANITY_CHECK_STEP = 22
+ FAIL_CLEANUP_STEP = 23
+ FAIL_MODULE_STEP = 24
+ FAIL_PERMISSIONS_STEP = 25
+ FAIL_PACKAGE_STEP = 26
+ FAIL_TEST_CASES_STEP = 27
+ # errors on missing things
+ MISSING_SOURCES = 30
+ MISSING_DEPENDENCY = 31
+ MISSING_SYSTEM_DEPENDENCY = 32
+ MISSING_EB_DEPENDENCY = 33
+ # errors on specific task failures
+ FAIL_SYSTEM_CHECK = 40
+ FAIL_DOWNLOAD = 41
+ FAIL_CHECKSUM = 42
+ FAIL_EXTRACT = 43
+ FAIL_PATCH_APPLY = 44
+ FAIL_SANITY_CHECK = 45
+ FAIL_MODULE_WRITE = 46
+ FAIL_GITHUB = 47
+
+
class EasyBuildError(LoggedException):
"""
EasyBuildError is thrown when EasyBuild runs into something horribly wrong.
@@ -75,12 +129,13 @@ class EasyBuildError(LoggedException):
# always include location where error was raised from, even under 'python -O'
INCLUDE_LOCATION = True
- def __init__(self, msg, *args):
+ def __init__(self, msg, *args, exit_code=EasyBuildExit.ERROR, **kwargs):
"""Constructor: initialise EasyBuildError instance."""
if args:
msg = msg % args
- LoggedException.__init__(self, msg)
+ LoggedException.__init__(self, msg, exit_code=exit_code, **kwargs)
self.msg = msg
+ self.exit_code = exit_code
def __str__(self):
"""Return string representation of this EasyBuildError instance."""
@@ -167,7 +222,7 @@ def nosupport(self, msg, ver):
def error(self, msg, *args, **kwargs):
"""Print error message and raise an EasyBuildError."""
- ebmsg = "EasyBuild crashed with an error %s: " % self.caller_info()
+ ebmsg = "EasyBuild encountered an error %s: " % self.caller_info()
fancylogger.FancyLogger.error(self, ebmsg + msg, *args, **kwargs)
def devel(self, msg, *args, **kwargs):
@@ -335,8 +390,18 @@ def print_error(msg, *args, **kwargs):
if args:
msg = msg % args
+ # grab exit code, if specified;
+ # also consider deprecated 'exitCode' option
+ exitCode = kwargs.pop('exitCode', None)
+ exit_code = kwargs.pop('exit_code', exitCode)
+ if exitCode is not None:
+ _init_easybuildlog.deprecated("'exitCode' option in print_error function is replaced with 'exit_code'", '6.0')
+
+ # use 1 as defaut exit code
+ if exit_code is None:
+ exit_code = 1
+
log = kwargs.pop('log', None)
- exitCode = kwargs.pop('exitCode', 1)
opt_parser = kwargs.pop('opt_parser', None)
exit_on_error = kwargs.pop('exit_on_error', True)
silent = kwargs.pop('silent', False)
@@ -348,7 +413,7 @@ def print_error(msg, *args, **kwargs):
if opt_parser:
opt_parser.print_shorthelp()
sys.stderr.write("ERROR: %s\n" % msg)
- sys.exit(exitCode)
+ sys.exit(exit_code)
elif log is not None:
raise EasyBuildError(msg)
diff --git a/easybuild/tools/config.py b/easybuild/tools/config.py
index 2dd71388cd..664080d8f2 100644
--- a/easybuild/tools/config.py
+++ b/easybuild/tools/config.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -45,11 +45,12 @@
import tempfile
import time
from abc import ABCMeta
+from string import ascii_letters
from easybuild.base import fancylogger
from easybuild.base.frozendict import FrozenDictKnownKeys
-from easybuild.tools.build_log import EasyBuildError
-from easybuild.tools.py2vs3 import ascii_letters, create_base_metaclass, string_type
+from easybuild.base.wrapper import create_base_metaclass
+from easybuild.tools.build_log import EasyBuildError, EasyBuildExit
try:
import rich # noqa
@@ -95,10 +96,11 @@
DEFAULT_ENV_FOR_SHEBANG = '/usr/bin/env'
DEFAULT_ENVVAR_USERS_MODULES = 'HOME'
DEFAULT_INDEX_MAX_AGE = 7 * 24 * 60 * 60 # 1 week (in seconds)
-DEFAULT_JOB_BACKEND = 'GC3Pie'
+DEFAULT_JOB_BACKEND = 'Slurm'
DEFAULT_JOB_EB_CMD = 'eb'
DEFAULT_LOGFILE_FORMAT = ("easybuild", "easybuild-%(name)s-%(version)s-%(date)s.%(time)s.log")
DEFAULT_MAX_FAIL_RATIO_PERMS = 0.5
+DEFAULT_MAX_PARALLEL = 16
DEFAULT_MINIMAL_BUILD_ENV = 'CC:gcc,CXX:g++'
DEFAULT_MNS = 'EasyBuildMNS'
DEFAULT_MODULE_SYNTAX = 'Lua'
@@ -167,13 +169,29 @@
LOCAL_VAR_NAMING_CHECK_WARN = WARN
LOCAL_VAR_NAMING_CHECKS = [LOCAL_VAR_NAMING_CHECK_ERROR, LOCAL_VAR_NAMING_CHECK_LOG, LOCAL_VAR_NAMING_CHECK_WARN]
-
OUTPUT_STYLE_AUTO = 'auto'
OUTPUT_STYLE_BASIC = 'basic'
OUTPUT_STYLE_NO_COLOR = 'no_color'
OUTPUT_STYLE_RICH = 'rich'
OUTPUT_STYLES = (OUTPUT_STYLE_AUTO, OUTPUT_STYLE_BASIC, OUTPUT_STYLE_NO_COLOR, OUTPUT_STYLE_RICH)
+SEARCH_PATH_BIN_DIRS = ['bin']
+SEARCH_PATH_HEADER_DIRS = ['include']
+SEARCH_PATH_LIB_DIRS = ['lib', 'lib64']
+
+PYTHONPATH = 'PYTHONPATH'
+EBPYTHONPREFIXES = 'EBPYTHONPREFIXES'
+PYTHON_SEARCH_PATH_TYPES = [PYTHONPATH, EBPYTHONPREFIXES]
+
+# options to handle header search paths in environment of modules
+MOD_SEARCH_PATH_HEADERS_CPATH = 'cpath'
+MOD_SEARCH_PATH_HEADERS_INCLUDE_PATHS = 'include_paths'
+MOD_SEARCH_PATH_HEADERS = {
+ MOD_SEARCH_PATH_HEADERS_CPATH: ['CPATH'],
+ MOD_SEARCH_PATH_HEADERS_INCLUDE_PATHS: ['C_INCLUDE_PATH', 'CPLUS_INCLUDE_PATH', 'OBJC_INCLUDE_PATH'],
+}
+DEFAULT_MOD_SEARCH_PATH_HEADERS = MOD_SEARCH_PATH_HEADERS_CPATH
+
class Singleton(ABCMeta):
"""Serves as metaclass for classes that should implement the Singleton pattern.
@@ -260,6 +278,8 @@ def mk_full_default_path(name, prefix=DEFAULT_PREFIX):
'rpath_filter',
'rpath_override_dirs',
'required_linked_shared_libs',
+ 'search_path_cpp_headers',
+ 'search_path_linker',
'skip',
'software_commit',
'stop',
@@ -268,13 +288,12 @@ def mk_full_default_path(name, prefix=DEFAULT_PREFIX):
'test_report_env_filter',
'testoutput',
'umask',
- 'wait_on_lock',
'zip_logs',
],
False: [
- 'add_dummy_to_minimal_toolchains',
'add_system_to_minimal_toolchains',
'allow_modules_tool_mismatch',
+ 'allow_unresolved_templates',
'backup_patched_files',
'consider_archived_easyconfigs',
'container_build_image',
@@ -285,6 +304,7 @@ def mk_full_default_path(name, prefix=DEFAULT_PREFIX):
'enforce_checksums',
'experimental',
'extended_dry_run',
+ 'fail_on_mod_files_gcccore',
'force',
'generate_devel_module',
'group_writable_installdir',
@@ -294,9 +314,9 @@ def mk_full_default_path(name, prefix=DEFAULT_PREFIX):
'ignore_locks',
'ignore_test_failure',
'install_latest_eb_release',
+ 'keep_debug_symbols',
'logtostdout',
'minimal_toolchains',
- 'module_extensions',
'module_only',
'package',
'parallel_extensions_install',
@@ -315,7 +335,6 @@ def mk_full_default_path(name, prefix=DEFAULT_PREFIX):
'skip_test_step',
'sticky_bit',
'terse',
- 'trace',
'unit_testing_mode',
'upload_test_report',
'update_modules_tool_cache',
@@ -334,10 +353,13 @@ def mk_full_default_path(name, prefix=DEFAULT_PREFIX):
'lib64_fallback_sanity_check',
'lib64_lib_symlink',
'map_toolchains',
+ 'module_extensions',
'modules_tool_version_check',
'mpi_tests',
'pre_create_installdir',
'show_progress_bar',
+ 'strict_rpath_sanity_check',
+ 'trace',
],
EMPTY_LIST: [
'accept_eula_for',
@@ -374,9 +396,15 @@ def mk_full_default_path(name, prefix=DEFAULT_PREFIX):
DEFAULT_MAX_FAIL_RATIO_PERMS: [
'max_fail_ratio_adjust_permissions',
],
+ DEFAULT_MAX_PARALLEL: [
+ 'max_parallel',
+ ],
DEFAULT_MINIMAL_BUILD_ENV: [
'minimal_build_env',
],
+ DEFAULT_MOD_SEARCH_PATH_HEADERS: [
+ 'module_search_path_headers',
+ ],
DEFAULT_PKG_RELEASE: [
'package_release',
],
@@ -407,6 +435,9 @@ def mk_full_default_path(name, prefix=DEFAULT_PREFIX):
OUTPUT_STYLE_AUTO: [
'output_style',
],
+ PYTHONPATH: [
+ 'prefer_python_search_path',
+ ]
}
# build option that do not have a perfectly matching command line option
BUILD_OPTIONS_OTHER = {
@@ -415,13 +446,13 @@ def mk_full_default_path(name, prefix=DEFAULT_PREFIX):
'command_line',
'external_modules_metadata',
'extra_ec_paths',
+ 'mod_depends_on', # deprecated
'robot_path',
'valid_module_classes',
'valid_stops',
],
False: [
'dry_run',
- 'mod_depends_on',
'recursive_mod_unload',
'retain_all_deps',
'silent',
@@ -478,6 +509,8 @@ class ConfigurationVariables(BaseConfigurationVariables):
'buildpath',
'config',
'containerpath',
+ 'failed_install_build_dirs_path',
+ 'failed_install_logs_path',
'installpath',
'installpath_modules',
'installpath_software',
@@ -506,7 +539,10 @@ def get_items_check_required(self):
"""
missing = [x for x in self.KNOWN_KEYS if x not in self]
if len(missing) > 0:
- raise EasyBuildError("Cannot determine value for configuration variables %s. Please specify it.", missing)
+ raise EasyBuildError(
+ "Cannot determine value for configuration variables %s. Please specify it.", ', '.join(missing),
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
return self.items()
@@ -535,11 +571,14 @@ def init(options, config_options_dict):
# make sure source path is a list
sourcepath = tmpdict['sourcepath']
- if isinstance(sourcepath, string_type):
+ if isinstance(sourcepath, str):
tmpdict['sourcepath'] = sourcepath.split(':')
_log.debug("Converted source path ('%s') to a list of paths: %s" % (sourcepath, tmpdict['sourcepath']))
elif not isinstance(sourcepath, (tuple, list)):
- raise EasyBuildError("Value for sourcepath has invalid type (%s): %s", type(sourcepath), sourcepath)
+ raise EasyBuildError(
+ "Value for sourcepath has invalid type (%s): %s", type(sourcepath), sourcepath,
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
# initialize configuration variables (any future calls to ConfigurationVariables() will yield the same instance
variables = ConfigurationVariables(tmpdict, ignore_unknown_keys=True)
@@ -580,10 +619,6 @@ def init_build_options(build_options=None, cmdline_options=None):
_log.info("Auto-enabling ignoring of OS dependencies")
cmdline_options.ignore_osdeps = True
- if not cmdline_options.accept_eula_for and cmdline_options.accept_eula:
- _log.deprecated("Use accept-eula-for configuration setting rather than accept-eula.", '5.0')
- cmdline_options.accept_eula_for = cmdline_options.accept_eula
-
cmdline_build_option_names = [k for ks in BUILD_OPTIONS_CMDLINE.values() for k in ks]
active_build_options.update({key: getattr(cmdline_options, key) for key in cmdline_build_option_names})
# other options which can be derived but have no perfectly matching cmdline option
@@ -603,12 +638,12 @@ def init_build_options(build_options=None, cmdline_options=None):
# seed in defaults to make sure all build options are defined, and that build_option() doesn't fail on valid keys
bo = {}
for build_options_by_default in [BUILD_OPTIONS_CMDLINE, BUILD_OPTIONS_OTHER]:
- for default in build_options_by_default:
+ for default, options in build_options_by_default.items():
if default == EMPTY_LIST:
- for opt in build_options_by_default[default]:
+ for opt in options:
bo[opt] = []
else:
- bo.update({opt: default for opt in build_options_by_default[default]})
+ bo.update({opt: default for opt in options})
bo.update(active_build_options)
# BuildOptions is a singleton, so any future calls to BuildOptions will yield the same instance
@@ -621,16 +656,13 @@ def build_option(key, **kwargs):
build_options = BuildOptions()
if key in build_options:
return build_options[key]
- elif key == 'accept_eula':
- _log.deprecated("Use accept_eula_for build option rather than accept_eula.", '5.0')
- return build_options['accept_eula_for']
elif 'default' in kwargs:
return kwargs['default']
else:
error_msg = "Undefined build option: '%s'. " % key
error_msg += "Make sure you have set up the EasyBuild configuration using set_up_configuration() "
error_msg += "(from easybuild.tools.options) in case you're not using EasyBuild via the 'eb' CLI."
- raise EasyBuildError(error_msg)
+ raise EasyBuildError(error_msg, exit_code=EasyBuildExit.OPTION_ERROR)
def update_build_option(key, value):
@@ -695,7 +727,10 @@ def install_path(typ=None):
known_types = ['modules', 'software']
if typ not in known_types:
- raise EasyBuildError("Unknown type specified in install_path(): %s (known: %s)", typ, ', '.join(known_types))
+ raise EasyBuildError(
+ "Unknown type specified in install_path(): %s (known: %s)", typ, ', '.join(known_types),
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
variables = ConfigurationVariables()
@@ -748,7 +783,7 @@ def container_path():
def get_modules_tool():
"""
- Return modules tool (EnvironmentModulesC, Lmod, ...)
+ Return modules tool (EnvironmentModules, Lmod, ...)
"""
# 'modules_tool' key will only be present if EasyBuild config is initialized
return ConfigurationVariables().get('modules_tool', None)
@@ -787,7 +822,10 @@ def get_output_style():
output_style = OUTPUT_STYLE_BASIC
if output_style == OUTPUT_STYLE_RICH and not HAVE_RICH:
- raise EasyBuildError("Can't use '%s' output style, Rich Python package is not available!", OUTPUT_STYLE_RICH)
+ raise EasyBuildError(
+ "Can't use '%s' output style, Rich Python package is not available!", OUTPUT_STYLE_RICH,
+ exit_code=EasyBuildExit.MISSING_EB_DEPENDENCY
+ )
return output_style
@@ -812,8 +850,10 @@ def log_file_format(return_directory=False, ec=None, date=None, timestamp=None):
logfile_format = ConfigurationVariables()['logfile_format']
if not isinstance(logfile_format, tuple) or len(logfile_format) != 2:
- raise EasyBuildError("Incorrect log file format specification, should be 2-tuple (, ): %s",
- logfile_format)
+ raise EasyBuildError(
+ "Incorrect log file format specification, should be 2-tuple (, ): %s", logfile_format,
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
idx = int(not return_directory)
res = ConfigurationVariables()['logfile_format'][idx] % {
@@ -843,6 +883,42 @@ def log_path(ec=None):
return log_file_format(return_directory=True, ec=ec, date=date, timestamp=timestamp)
+def get_failed_install_build_dirs_path(ec):
+ """
+ Return the location where the build directory is copied to if installation failed
+
+ :param ec: dict-like value with 'name' and 'version' keys defined
+ """
+ base_path = ConfigurationVariables()['failed_install_build_dirs_path']
+ if not base_path:
+ return None
+
+ try:
+ name, version = ec['name'], ec['version']
+ except KeyError:
+ raise EasyBuildError("The 'name' and 'version' keys are required.")
+
+ return os.path.join(base_path, f'{name}-{version}')
+
+
+def get_failed_install_logs_path(ec):
+ """
+ Return the location where log files are copied to if installation failed
+
+ :param ec: dict-like value with 'name' and 'version' keys defined
+ """
+ base_path = ConfigurationVariables()['failed_install_logs_path']
+ if not base_path:
+ return None
+
+ try:
+ name, version = ec['name'], ec['version']
+ except KeyError:
+ raise EasyBuildError("The 'name' and 'version' keys are required.")
+
+ return os.path.join(base_path, f'{name}-{version}')
+
+
def get_build_log_path():
"""
Return (temporary) directory for build log
@@ -920,7 +996,10 @@ def find_last_log(curlog):
sorted_paths = [p for (_, p) in sorted(paths)]
except OSError as err:
- raise EasyBuildError("Failed to locate/select/order log files matching '%s': %s", glob_pattern, err)
+ raise EasyBuildError(
+ "Failed to locate/select/order log files matching '%s': %s", glob_pattern, err,
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
try:
# log of current session is typically listed last, should be taken into account
diff --git a/easybuild/tools/configobj.py b/easybuild/tools/configobj.py
index 6ac667b5bd..d2ff5d150e 100644
--- a/easybuild/tools/configobj.py
+++ b/easybuild/tools/configobj.py
@@ -27,8 +27,6 @@
from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF16_BE, BOM_UTF16_LE
-from easybuild.tools.py2vs3 import string_type
-
# imported lazily to avoid startup performance hit if it isn't used
compiler = None
@@ -562,11 +560,11 @@ def __getitem__(self, key):
"""Fetch the item and do string interpolation."""
val = dict.__getitem__(self, key)
if self.main.interpolation:
- if isinstance(val, string_type):
+ if isinstance(val, str):
return self._interpolate(key, val)
if isinstance(val, list):
def _check(entry):
- if isinstance(entry, string_type):
+ if isinstance(entry, str):
return self._interpolate(key, entry)
return entry
new = [_check(entry) for entry in val]
@@ -588,7 +586,7 @@ def __setitem__(self, key, value, unrepr=False):
``unrepr`` must be set when setting a value to a dictionary, without
creating a new sub-section.
"""
- if not isinstance(key, (bytes, string_type)):
+ if not isinstance(key, (bytes, str)):
raise ValueError('The key "%s" is not a string.' % key)
# add the comment
@@ -622,11 +620,11 @@ def __setitem__(self, key, value, unrepr=False):
if key not in self:
self.scalars.append(key)
if not self.main.stringify:
- if isinstance(value, string_type):
+ if isinstance(value, str):
pass
elif isinstance(value, (list, tuple)):
for entry in value:
- if not isinstance(entry, string_type):
+ if not isinstance(entry, str):
raise TypeError('Value is not a string "%s".' % entry)
else:
raise TypeError('Value is not a string "%s".' % value)
@@ -878,31 +876,24 @@ def walk(self, function, raise_errors=True,
"""
out = {}
# scalars first
- for i in range(len(self.scalars)):
- entry = self.scalars[i]
+ for i, entry in enumerate(self.scalars):
try:
val = function(self, entry, **keywargs)
- # bound again in case name has changed
- entry = self.scalars[i]
- out[entry] = val
except Exception:
if raise_errors:
raise
- else:
- entry = self.scalars[i]
- out[entry] = False
+ val = False
+ # bound again in case name has changed
+ entry = self.scalars[i]
+ out[entry] = val
# then sections
- for i in range(len(self.sections)):
- entry = self.sections[i]
+ for i, entry in enumerate(self.sections):
if call_on_sections:
try:
function(self, entry, **keywargs)
except Exception:
if raise_errors:
raise
- else:
- entry = self.sections[i]
- out[entry] = False
# bound again in case name has changed
entry = self.sections[i]
# previous result is discarded
@@ -946,7 +937,7 @@ def as_bool(self, key):
return val
else:
try:
- if not isinstance(val, string_type):
+ if not isinstance(val, str):
# TODO: Why do we raise a KeyError here?
raise KeyError()
else:
@@ -1210,7 +1201,7 @@ def __init__(self, infile=None, options=None, configspec=None, encoding=None,
self._load(infile, configspec)
def _load(self, infile, configspec):
- if isinstance(infile, string_type):
+ if isinstance(infile, str):
self.filename = infile
if os.path.isfile(infile):
with open(infile, 'r') as fh:
@@ -1431,7 +1422,7 @@ def _handle_bom(self, infile):
else:
infile = newline
# UTF8 - don't decode
- if isinstance(infile, string_type):
+ if isinstance(infile, str):
return infile.splitlines(True)
else:
return infile
@@ -1439,7 +1430,7 @@ def _handle_bom(self, infile):
return self._decode(infile, encoding)
# No BOM discovered and no encoding specified, just return
- if isinstance(infile, (bytes, string_type)):
+ if isinstance(infile, (bytes, str)):
# infile read from a file will be a single string
return infile.splitlines(True)
return infile
@@ -1457,7 +1448,7 @@ def _decode(self, infile, encoding):
if is a string, it also needs converting to a list.
"""
- if isinstance(infile, string_type):
+ if isinstance(infile, str):
# can't be unicode
# NOTE: Could raise a ``UnicodeDecodeError``
return infile.decode(encoding).splitlines(True)
@@ -1482,7 +1473,7 @@ def _str(self, value):
Used by ``stringify`` within validate, to turn non-string values
into strings.
"""
- if not isinstance(value, string_type):
+ if not isinstance(value, str):
return str(value)
else:
return value
@@ -1730,7 +1721,7 @@ def _quote(self, value, multiline=True):
return self._quote(value[0], multiline=False) + ','
return ', '.join([self._quote(val, multiline=False)
for val in value])
- if not isinstance(value, string_type):
+ if not isinstance(value, str):
if self.stringify:
value = str(value)
else:
@@ -2275,7 +2266,7 @@ def reload(self):
This method raises a ``ReloadError`` if the ConfigObj doesn't have
a filename attribute pointing to a file.
"""
- if not isinstance(self.filename, string_type):
+ if not isinstance(self.filename, str):
raise ReloadError()
filename = self.filename
diff --git a/easybuild/tools/containers/__init__.py b/easybuild/tools/containers/__init__.py
index 215ba61c0c..2a310c4acc 100644
--- a/easybuild/tools/containers/__init__.py
+++ b/easybuild/tools/containers/__init__.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/tools/containers/apptainer.py b/easybuild/tools/containers/apptainer.py
index 4cb252dac4..4bbdaff914 100644
--- a/easybuild/tools/containers/apptainer.py
+++ b/easybuild/tools/containers/apptainer.py
@@ -1,4 +1,4 @@
-# Copyright 2022-2024 Ghent University
+# Copyright 2022-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -29,13 +29,13 @@
import os
import re
-from easybuild.tools.build_log import EasyBuildError, print_msg
+from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, print_msg
from easybuild.tools.containers.singularity import SingularityContainer
from easybuild.tools.config import CONT_IMAGE_FORMAT_EXT3, CONT_IMAGE_FORMAT_SANDBOX
from easybuild.tools.config import CONT_IMAGE_FORMAT_SIF, CONT_IMAGE_FORMAT_SQUASHFS
from easybuild.tools.config import build_option, container_path
from easybuild.tools.filetools import remove_file, which
-from easybuild.tools.run import run_cmd
+from easybuild.tools.run import run_shell_cmd
class ApptainerContainer(SingularityContainer):
@@ -48,15 +48,15 @@ class ApptainerContainer(SingularityContainer):
def apptainer_version():
"""Get Apptainer version."""
version_cmd = "apptainer --version"
- out, ec = run_cmd(version_cmd, simple=False, trace=False, force_in_dry_run=True)
- if ec:
- raise EasyBuildError("Error running '%s': %s for tool {1} with output: {2}" % (version_cmd, out))
+ res = run_shell_cmd(version_cmd, hidden=True, in_dry_run=True)
+ if res.exit_code != EasyBuildExit.SUCCESS:
+ raise EasyBuildError(f"Error running '{version_cmd}': {res.output}")
- res = re.search(r"\d+\.\d+(\.\d+)?", out.strip())
- if not res:
- raise EasyBuildError("Error parsing Apptainer version: %s" % out)
+ regex_res = re.search(r"\d+\.\d+(\.\d+)?", res.output.strip())
+ if not regex_res:
+ raise EasyBuildError(f"Error parsing Apptainer version: {res.output}")
- return res.group(0)
+ return regex_res.group(0)
def build_image(self, recipe_path):
"""Build container image by calling out to 'sudo apptainer build'."""
@@ -108,5 +108,5 @@ def build_image(self, recipe_path):
cmd = ' '.join(['sudo', cmd_env, apptainer, 'build', cmd_opts, img_path, recipe_path])
print_msg("Running '%s', you may need to enter your 'sudo' password..." % cmd)
- run_cmd(cmd, stream_output=True)
+ run_shell_cmd(cmd, stream_output=True)
print_msg("Apptainer image created at %s" % img_path, log=self.log)
diff --git a/easybuild/tools/containers/base.py b/easybuild/tools/containers/base.py
index faf3b0cc7a..3371719c3d 100644
--- a/easybuild/tools/containers/base.py
+++ b/easybuild/tools/containers/base.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/tools/containers/common.py b/easybuild/tools/containers/common.py
index 6824c16864..3a90c98e43 100644
--- a/easybuild/tools/containers/common.py
+++ b/easybuild/tools/containers/common.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/tools/containers/docker.py b/easybuild/tools/containers/docker.py
index 9061bba762..76f5713e09 100644
--- a/easybuild/tools/containers/docker.py
+++ b/easybuild/tools/containers/docker.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -36,7 +36,7 @@
from easybuild.tools.containers.utils import det_os_deps
from easybuild.tools.filetools import remove_dir
from easybuild.tools.module_naming_scheme.easybuild_mns import EasyBuildMNS
-from easybuild.tools.run import run_cmd
+from easybuild.tools.run import run_shell_cmd
DOCKER_TMPL_HEADER = """\
@@ -164,7 +164,7 @@ def build_image(self, dockerfile):
docker_cmd = ' '.join(['sudo', 'docker', 'build', '-f', dockerfile, '-t', container_name, '.'])
print_msg("Running '%s', you may need to enter your 'sudo' password..." % docker_cmd)
- run_cmd(docker_cmd, path=tempdir, stream_output=True)
+ run_shell_cmd(docker_cmd, work_dir=tempdir, stream_output=True)
print_msg("Docker image created at %s" % container_name, log=self.log)
remove_dir(tempdir)
diff --git a/easybuild/tools/containers/singularity.py b/easybuild/tools/containers/singularity.py
index 809fdc35d1..3fdac7acd8 100644
--- a/easybuild/tools/containers/singularity.py
+++ b/easybuild/tools/containers/singularity.py
@@ -1,4 +1,4 @@
-# Copyright 2017-2024 Ghent University
+# Copyright 2017-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -34,14 +34,13 @@
import re
from easybuild.tools import LooseVersion
-from easybuild.tools.build_log import EasyBuildError, print_msg
+from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, print_msg
from easybuild.tools.config import CONT_IMAGE_FORMAT_EXT3, CONT_IMAGE_FORMAT_SANDBOX
from easybuild.tools.config import CONT_IMAGE_FORMAT_SIF, CONT_IMAGE_FORMAT_SQUASHFS
from easybuild.tools.config import build_option, container_path
from easybuild.tools.containers.base import ContainerGenerator
from easybuild.tools.filetools import read_file, remove_file, which
-from easybuild.tools.run import run_cmd
-from easybuild.tools.py2vs3 import string_type
+from easybuild.tools.run import run_shell_cmd
ARCH = 'arch' # Arch Linux
@@ -163,15 +162,15 @@ class SingularityContainer(ContainerGenerator):
def singularity_version():
"""Get Singularity version."""
version_cmd = "singularity --version"
- out, ec = run_cmd(version_cmd, simple=False, trace=False, force_in_dry_run=True)
- if ec:
- raise EasyBuildError("Error running '%s': %s for tool {1} with output: {2}" % (version_cmd, out))
+ res = run_shell_cmd(version_cmd, hidden=True, in_dry_run=True)
+ if res.exit_code != EasyBuildExit.SUCCESS:
+ raise EasyBuildError(f"Error running '{version_cmd}': {res.output}")
- res = re.search(r"\d+\.\d+(\.\d+)?", out.strip())
- if not res:
- raise EasyBuildError("Error parsing Singularity version: %s" % out)
+ regex_res = re.search(r"\d+\.\d+(\.\d+)?", res.output.strip())
+ if not regex_res:
+ raise EasyBuildError(f"Error parsing Singularity version: {res.output}")
- return res.group(0)
+ return regex_res.group(0)
def resolve_template(self):
"""Return template container recipe."""
@@ -299,7 +298,7 @@ def resolve_template_data(self):
install_os_deps = []
for osdep in osdeps:
- if isinstance(osdep, string_type):
+ if isinstance(osdep, str):
install_os_deps.append("yum install --quiet --assumeyes %s" % osdep)
# tuple entry indicates multiple options
elif isinstance(osdep, tuple):
@@ -404,5 +403,5 @@ def build_image(self, recipe_path):
cmd = ' '.join(['sudo', cmd_env, singularity, 'build', cmd_opts, img_path, recipe_path])
print_msg("Running '%s', you may need to enter your 'sudo' password..." % cmd)
- run_cmd(cmd, stream_output=True)
+ run_shell_cmd(cmd, stream_output=True)
print_msg("Singularity image created at %s" % img_path, log=self.log)
diff --git a/easybuild/tools/containers/utils.py b/easybuild/tools/containers/utils.py
index a575254335..956bffad63 100644
--- a/easybuild/tools/containers/utils.py
+++ b/easybuild/tools/containers/utils.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -34,10 +34,9 @@
from functools import reduce
from easybuild.tools import LooseVersion
-from easybuild.tools.build_log import EasyBuildError, print_msg
+from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, print_msg
from easybuild.tools.filetools import which
-from easybuild.tools.py2vs3 import string_type
-from easybuild.tools.run import run_cmd
+from easybuild.tools.run import run_shell_cmd
def det_os_deps(easyconfigs):
@@ -49,7 +48,7 @@ def det_os_deps(easyconfigs):
res = set()
os_deps = reduce(operator.add, [obj['ec']['osdependencies'] for obj in easyconfigs], [])
for os_dep in os_deps:
- if isinstance(os_dep, string_type):
+ if isinstance(os_dep, str):
res.add(os_dep)
elif isinstance(os_dep, tuple):
res.update(os_dep)
@@ -71,20 +70,23 @@ def check_tool(tool_name, min_tool_version=None):
if not tool_path:
return False
- print_msg("{0} tool found at {1}".format(tool_name, tool_path))
+ print_msg(f"{tool_name} tool found at {tool_path}")
if not min_tool_version:
return True
- version_cmd = "{0} --version".format(tool_name)
- out, ec = run_cmd(version_cmd, simple=False, trace=False, force_in_dry_run=True)
- if ec:
- raise EasyBuildError("Error running '{0}' for tool {1} with output: {2}".format(version_cmd, tool_name, out))
- res = re.search(r"\d+\.\d+(\.\d+)?", out.strip())
- if not res:
- raise EasyBuildError("Error parsing version for tool {0}".format(tool_name))
- tool_version = res.group(0)
+ version_cmd = f"{tool_name} --version"
+ res = run_shell_cmd(version_cmd, hidden=True, in_dry_run=True)
+ if res.exit_code != EasyBuildExit.SUCCESS:
+ raise EasyBuildError(f"Error running '{version_cmd}' for tool {tool_name} with output: {res.output}")
+
+ regex_res = re.search(r"\d+\.\d+(\.\d+)?", res.output.strip())
+ if not regex_res:
+ raise EasyBuildError(f"Error parsing version for tool {tool_name}")
+
+ tool_version = regex_res.group(0)
version_ok = LooseVersion(str(min_tool_version)) <= LooseVersion(tool_version)
if version_ok:
- print_msg("{0} version '{1}' is {2} or higher ... OK".format(tool_name, tool_version, min_tool_version))
+ print_msg(f"{tool_name} version '{tool_version}' is {min_tool_version} or higher ... OK")
+
return version_ok
diff --git a/easybuild/tools/convert.py b/easybuild/tools/convert.py
index 134566812d..3b55a392f6 100644
--- a/easybuild/tools/convert.py
+++ b/easybuild/tools/convert.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2014-2024 Ghent University
+# Copyright 2014-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -35,7 +35,6 @@
from easybuild.base import fancylogger
from easybuild.base.wrapper import Wrapper
from easybuild.tools.build_log import EasyBuildError
-from easybuild.tools.py2vs3 import string_type
_log = fancylogger.getLogger('tools.convert', fname=False)
@@ -52,7 +51,7 @@ def __init__(self, obj):
"""Support the conversion of obj to something"""
self.__dict__['log'] = fancylogger.getLogger(self.__class__.__name__, fname=False)
self.__dict__['data'] = None
- if isinstance(obj, string_type):
+ if isinstance(obj, str):
self.data = self._from_string(obj)
else:
raise EasyBuildError("unsupported type %s for %s: %s", type(obj), self.__class__.__name__, obj)
diff --git a/easybuild/tools/docs.py b/easybuild/tools/docs.py
index d5b9cd4628..d7fba3885d 100644
--- a/easybuild/tools/docs.py
+++ b/easybuild/tools/docs.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -42,6 +42,7 @@
import os
from collections import OrderedDict
from easybuild.tools import LooseVersion
+from string import ascii_lowercase
from easybuild.base import fancylogger
from easybuild.framework.easyconfig.default import DEFAULT_CONFIG, HIDDEN, sorted_categories
@@ -49,7 +50,7 @@
from easybuild.framework.easyconfig.constants import EASYCONFIG_CONSTANTS
from easybuild.framework.easyconfig.easyconfig import get_easyblock_class, process_easyconfig
from easybuild.framework.easyconfig.licenses import EASYCONFIG_LICENSES_DICT
-from easybuild.framework.easyconfig.parser import EasyConfigParser
+from easybuild.framework.easyconfig.parser import ALTERNATIVE_EASYCONFIG_PARAMETERS, EasyConfigParser
from easybuild.framework.easyconfig.templates import TEMPLATE_CONSTANTS, TEMPLATE_NAMES_CONFIG, TEMPLATE_NAMES_DYNAMIC
from easybuild.framework.easyconfig.templates import TEMPLATE_NAMES_EASYBLOCK_RUN_STEP, TEMPLATE_NAMES_EASYCONFIG
from easybuild.framework.easyconfig.templates import TEMPLATE_NAMES_LOWER, TEMPLATE_NAMES_LOWER_TEMPLATE
@@ -61,8 +62,7 @@
from easybuild.tools.config import build_option
from easybuild.tools.filetools import read_file
from easybuild.tools.modules import modules_tool
-from easybuild.tools.py2vs3 import ascii_lowercase
-from easybuild.tools.toolchain.toolchain import DUMMY_TOOLCHAIN_NAME, SYSTEM_TOOLCHAIN_NAME, is_system_toolchain
+from easybuild.tools.toolchain.toolchain import SYSTEM_TOOLCHAIN_NAME, is_system_toolchain
from easybuild.tools.toolchain.utilities import search_toolchain
from easybuild.tools.utilities import INDENT_2SPACES, INDENT_4SPACES
from easybuild.tools.utilities import import_available_modules, mk_md_table, mk_rst_table, nub, quote_str
@@ -315,7 +315,7 @@ def avail_easyconfig_licenses_md():
return '\n'.join(doc)
-def avail_easyconfig_params_md(title, grouped_params):
+def avail_easyconfig_params_md(title, grouped_params, alternative_params):
"""
Compose overview of available easyconfig parameters, in MarkDown format.
"""
@@ -328,13 +328,14 @@ def avail_easyconfig_params_md(title, grouped_params):
for grpname in grouped_params:
# group section title
title = "%s%s parameters" % (grpname[0].upper(), grpname[1:])
- table_titles = ["**Parameter name**", "**Description**", "**Default value**"]
+ table_titles = ["**Parameter name**", "**Description**", "**Default value**", "**Alternative name**"]
keys = sorted(grouped_params[grpname].keys())
values = [grouped_params[grpname][key] for key in keys]
table_values = [
['`%s`' % name for name in keys], # parameter name
[x[0].replace('<', '<').replace('>', '>') for x in values], # description
- ['`' + str(quote_str(x[1])) + '`' for x in values] # default value
+ ['`' + str(quote_str(x[1])) + '`' for x in values], # default value
+ ['`%s`' % alternative_params[name] if name in alternative_params else '' for name in keys],
]
doc.extend(md_title_and_table(title, table_titles, table_values, title_level=2))
@@ -343,7 +344,7 @@ def avail_easyconfig_params_md(title, grouped_params):
return '\n'.join(doc)
-def avail_easyconfig_params_rst(title, grouped_params):
+def avail_easyconfig_params_rst(title, grouped_params, alternative_params):
"""
Compose overview of available easyconfig parameters, in RST format.
"""
@@ -357,13 +358,14 @@ def avail_easyconfig_params_rst(title, grouped_params):
for grpname in grouped_params:
# group section title
title = "%s parameters" % grpname
- table_titles = ["**Parameter name**", "**Description**", "**Default value**"]
+ table_titles = ["**Parameter name**", "**Description**", "**Default value**", "**Alternative name**"]
keys = sorted(grouped_params[grpname].keys())
values = [grouped_params[grpname][key] for key in keys]
table_values = [
['``%s``' % name for name in keys], # parameter name
[x[0] for x in values], # description
- [str(quote_str(x[1])) for x in values] # default value
+ [str(quote_str(x[1])) for x in values], # default value
+ ['``%s``' % alternative_params[name] if name in alternative_params else '' for name in keys],
]
doc.extend(rst_title_and_table(title, table_titles, table_values))
@@ -372,14 +374,14 @@ def avail_easyconfig_params_rst(title, grouped_params):
return '\n'.join(doc)
-def avail_easyconfig_params_json():
+def avail_easyconfig_params_json(*args):
"""
Compose overview of available easyconfig parameters, in json format.
"""
raise NotImplementedError("JSON output format not supported for avail_easyconfig_params_json")
-def avail_easyconfig_params_txt(title, grouped_params):
+def avail_easyconfig_params_txt(title, grouped_params, alternative_params):
"""
Compose overview of available easyconfig parameters, in plain text format.
"""
@@ -399,7 +401,17 @@ def avail_easyconfig_params_txt(title, grouped_params):
# line by parameter
for name, (descr, dflt) in sorted(grouped_params[grpname].items()):
- doc.append("{0:<{nw}} {1:} [default: {2:}]".format(name, descr, str(quote_str(dflt)), nw=nw))
+ line = ' '.join([
+ '{0:<{nw}} ',
+ '{1:}',
+ '[default: {2:}]',
+ ]).format(name, descr, str(quote_str(dflt)), nw=nw)
+
+ alternative = alternative_params.get(name)
+ if alternative:
+ line += ' {alternative: %s}' % alternative
+
+ doc.append(line)
doc.append('')
return '\n'.join(doc)
@@ -418,6 +430,9 @@ def avail_easyconfig_params(easyblock, output_format=FORMAT_TXT):
extra_params = app.extra_options()
params.update(extra_params)
+ # reverse mapping of alternative easyconfig parameter names
+ alternative_params = {v: k for k, v in ALTERNATIVE_EASYCONFIG_PARAMETERS.items()}
+
# compose title
title = "Available easyconfig parameters"
if extra_params:
@@ -443,7 +458,7 @@ def avail_easyconfig_params(easyblock, output_format=FORMAT_TXT):
del grouped_params[grpname]
# compose output, according to specified format (txt, rst, ...)
- return generate_doc('avail_easyconfig_params_%s' % output_format, [title, grouped_params])
+ return generate_doc('avail_easyconfig_params_%s' % output_format, [title, grouped_params, alternative_params])
def avail_easyconfig_templates(output_format=FORMAT_TXT):
@@ -463,16 +478,16 @@ def avail_easyconfig_templates_txt():
# step 1: add TEMPLATE_NAMES_EASYCONFIG
doc.append('Template names/values derived from easyconfig instance')
- for name in TEMPLATE_NAMES_EASYCONFIG:
- doc.append("%s%%(%s)s: %s" % (INDENT_4SPACES, name[0], name[1]))
+ for name, curDoc in TEMPLATE_NAMES_EASYCONFIG.items():
+ doc.append("%s%%(%s)s: %s" % (INDENT_4SPACES, name, curDoc))
doc.append('')
# step 2: add SOFTWARE_VERSIONS
doc.append('Template names/values for (short) software versions')
- for name, pref in TEMPLATE_SOFTWARE_VERSIONS:
- doc.append("%s%%(%smajver)s: major version for %s" % (INDENT_4SPACES, pref, name))
- doc.append("%s%%(%sshortver)s: short version for %s (.)" % (INDENT_4SPACES, pref, name))
- doc.append("%s%%(%sver)s: full version for %s" % (INDENT_4SPACES, pref, name))
+ for name, prefix in TEMPLATE_SOFTWARE_VERSIONS.items():
+ doc.append("%s%%(%smajver)s: major version for %s" % (INDENT_4SPACES, prefix, name))
+ doc.append("%s%%(%sshortver)s: short version for %s (.)" % (INDENT_4SPACES, prefix, name))
+ doc.append("%s%%(%sver)s: full version for %s" % (INDENT_4SPACES, prefix, name))
doc.append('')
# step 3: add remaining config
@@ -491,20 +506,20 @@ def avail_easyconfig_templates_txt():
# step 5: template_values can/should be updated from outside easyconfig
# (eg the run_step code in EasyBlock)
doc.append('Template values set outside EasyBlock runstep')
- for name in TEMPLATE_NAMES_EASYBLOCK_RUN_STEP:
- doc.append("%s%%(%s)s: %s" % (INDENT_4SPACES, name[0], name[1]))
+ for name, cur_doc in TEMPLATE_NAMES_EASYBLOCK_RUN_STEP.items():
+ doc.append("%s%%(%s)s: %s" % (INDENT_4SPACES, name, cur_doc))
doc.append('')
# some template values are only defined dynamically,
# see template_constant_dict function in easybuild.framework.easyconfigs.templates
doc.append('Template values which are defined dynamically')
- for name in TEMPLATE_NAMES_DYNAMIC:
- doc.append("%s%%(%s)s: %s" % (INDENT_4SPACES, name[0], name[1]))
+ for name, cur_doc in TEMPLATE_NAMES_DYNAMIC.items():
+ doc.append("%s%%(%s)s: %s" % (INDENT_4SPACES, name, cur_doc))
doc.append('')
doc.append('Template constants that can be used in easyconfigs')
- for cst in TEMPLATE_CONSTANTS:
- doc.append('%s%s: %s (%s)' % (INDENT_4SPACES, cst[0], cst[2], cst[1]))
+ for name, (value, cur_doc) in TEMPLATE_CONSTANTS.items():
+ doc.append('%s%s: %s (%s)' % (INDENT_4SPACES, name, cur_doc, value))
return '\n'.join(doc)
@@ -515,8 +530,8 @@ def avail_easyconfig_templates_rst():
title = 'Template names/values derived from easyconfig instance'
table_values = [
- ['``%%(%s)s``' % name[0] for name in TEMPLATE_NAMES_EASYCONFIG],
- [name[1] for name in TEMPLATE_NAMES_EASYCONFIG],
+ ['``%%(%s)s``' % name for name in TEMPLATE_NAMES_EASYCONFIG],
+ list(TEMPLATE_NAMES_EASYCONFIG.values()),
]
doc = rst_title_and_table(title, table_titles, table_values)
doc.append('')
@@ -524,10 +539,10 @@ def avail_easyconfig_templates_rst():
title = 'Template names/values for (short) software versions'
ver = []
ver_desc = []
- for name, pref in TEMPLATE_SOFTWARE_VERSIONS:
- ver.append('``%%(%smajver)s``' % pref)
- ver.append('``%%(%sshortver)s``' % pref)
- ver.append('``%%(%sver)s``' % pref)
+ for name, prefix in TEMPLATE_SOFTWARE_VERSIONS.items():
+ ver.append('``%%(%smajver)s``' % prefix)
+ ver.append('``%%(%sshortver)s``' % prefix)
+ ver.append('``%%(%sver)s``' % prefix)
ver_desc.append('major version for %s' % name)
ver_desc.append('short version for %s (.)' % name)
ver_desc.append('full version for %s' % name)
@@ -550,24 +565,24 @@ def avail_easyconfig_templates_rst():
title = 'Template values set outside EasyBlock runstep'
table_values = [
- ['``%%(%s)s``' % name[0] for name in TEMPLATE_NAMES_EASYBLOCK_RUN_STEP],
- [name[1] for name in TEMPLATE_NAMES_EASYBLOCK_RUN_STEP],
+ ['``%%(%s)s``' % name for name in TEMPLATE_NAMES_EASYBLOCK_RUN_STEP],
+ list(TEMPLATE_NAMES_EASYBLOCK_RUN_STEP.values()),
]
doc.extend(rst_title_and_table(title, table_titles, table_values))
title = 'Template values which are defined dynamically'
table_values = [
- ['``%%(%s)s``' % name[0] for name in TEMPLATE_NAMES_DYNAMIC],
- [name[1] for name in TEMPLATE_NAMES_DYNAMIC],
+ ['``%%(%s)s``' % name for name in TEMPLATE_NAMES_DYNAMIC],
+ list(TEMPLATE_NAMES_DYNAMIC.values()),
]
doc.extend(rst_title_and_table(title, table_titles, table_values))
title = 'Template constants that can be used in easyconfigs'
- titles = ['Constant', 'Template value', 'Template name']
+ titles = ['Constant', 'Template description', 'Template value']
table_values = [
- ['``%s``' % cst[0] for cst in TEMPLATE_CONSTANTS],
- [cst[2] for cst in TEMPLATE_CONSTANTS],
- ['``%s``' % cst[1] for cst in TEMPLATE_CONSTANTS],
+ ['``%s``' % name for name in TEMPLATE_CONSTANTS],
+ [doc for _, doc in TEMPLATE_CONSTANTS.values()],
+ ['``%s``' % value for value, _ in TEMPLATE_CONSTANTS.values()],
]
doc.extend(rst_title_and_table(title, titles, table_values))
@@ -580,8 +595,8 @@ def avail_easyconfig_templates_md():
title = 'Template names/values derived from easyconfig instance'
table_values = [
- ['``%%(%s)s``' % name[0] for name in TEMPLATE_NAMES_EASYCONFIG],
- [name[1] for name in TEMPLATE_NAMES_EASYCONFIG],
+ ['``%%(%s)s``' % name for name in TEMPLATE_NAMES_EASYCONFIG],
+ list(TEMPLATE_NAMES_EASYCONFIG.values()),
]
doc = md_title_and_table(title, table_titles, table_values, title_level=2)
doc.append('')
@@ -589,10 +604,10 @@ def avail_easyconfig_templates_md():
title = 'Template names/values for (short) software versions'
ver = []
ver_desc = []
- for name, pref in TEMPLATE_SOFTWARE_VERSIONS:
- ver.append('``%%(%smajver)s``' % pref)
- ver.append('``%%(%sshortver)s``' % pref)
- ver.append('``%%(%sver)s``' % pref)
+ for name, prefix in TEMPLATE_SOFTWARE_VERSIONS.items():
+ ver.append('``%%(%smajver)s``' % prefix)
+ ver.append('``%%(%sshortver)s``' % prefix)
+ ver.append('``%%(%sver)s``' % prefix)
ver_desc.append('major version for %s' % name)
ver_desc.append('short version for %s (``.``)' % name)
ver_desc.append('full version for %s' % name)
@@ -616,27 +631,28 @@ def avail_easyconfig_templates_md():
title = 'Template values set outside EasyBlock runstep'
table_values = [
- ['``%%(%s)s``' % name[0] for name in TEMPLATE_NAMES_EASYBLOCK_RUN_STEP],
- [name[1] for name in TEMPLATE_NAMES_EASYBLOCK_RUN_STEP],
+ ['``%%(%s)s``' % name for name in TEMPLATE_NAMES_EASYBLOCK_RUN_STEP],
+ list(TEMPLATE_NAMES_EASYBLOCK_RUN_STEP.values()),
]
doc.extend(md_title_and_table(title, table_titles, table_values, title_level=2))
doc.append('')
title = 'Template values which are defined dynamically'
table_values = [
- ['``%%(%s)s``' % name[0] for name in TEMPLATE_NAMES_DYNAMIC],
- [name[1] for name in TEMPLATE_NAMES_DYNAMIC],
+ ['``%%(%s)s``' % name for name in TEMPLATE_NAMES_DYNAMIC],
+ list(TEMPLATE_NAMES_DYNAMIC.values()),
]
doc.extend(md_title_and_table(title, table_titles, table_values, title_level=2))
doc.append('')
title = 'Template constants that can be used in easyconfigs'
- titles = ['Constant', 'Template value', 'Template name']
+ titles = ['Constant', 'Template description', 'Template value']
table_values = [
- ['``%s``' % cst[0] for cst in TEMPLATE_CONSTANTS],
- [cst[2] for cst in TEMPLATE_CONSTANTS],
- ['``%s``' % cst[1] for cst in TEMPLATE_CONSTANTS],
+ ['``%s``' % name for name in TEMPLATE_CONSTANTS],
+ [doc for _, doc in TEMPLATE_CONSTANTS.values()],
+ ['``%s``' % value for value, _ in TEMPLATE_CONSTANTS.values()],
]
+
doc.extend(md_title_and_table(title, titles, table_values, title_level=2))
return '\n'.join(doc)
@@ -807,8 +823,8 @@ def list_software(output_format=FORMAT_TXT, detailed=False, only_installed=False
# rebuild software, only retain entries with a corresponding available module
software, all_software = {}, software
- for key in all_software:
- for entry in all_software[key]:
+ for key, entries in all_software.items():
+ for entry in entries:
if entry['mod_name'] in avail_mod_names:
software.setdefault(key, []).append(entry)
@@ -1094,7 +1110,7 @@ def list_toolchains(output_format=FORMAT_TXT):
# start with dict that maps toolchain name to corresponding subclass of Toolchain
# filter deprecated 'dummy' toolchain
- tcs = {tc.NAME: tc for tc in all_tcs if tc.NAME != DUMMY_TOOLCHAIN_NAME}
+ tcs = {tc.NAME: tc for tc in all_tcs}
for tcname in sorted(tcs):
tcc = tcs[tcname]
@@ -1125,7 +1141,7 @@ def list_toolchains_md(tcs):
none_txt = '*(none)*'
# Initialize an empty list of lists for the table data
- table_values = [[] for i in range(len(table_titles))]
+ table_values = [[] for _ in range(len(table_titles))]
for col_id, col_name in enumerate(table_titles):
if col_name == 'NAME':
@@ -1141,6 +1157,8 @@ def list_toolchains_md(tcs):
entry = 'cray-mpich'
elif col_name == 'LINALG':
entry = 'cray-libsci'
+ else:
+ entry = none_txt
# Combine the linear algebra libraries into a single column
elif col_name == 'LINALG':
linalg = []
@@ -1185,7 +1203,7 @@ def list_toolchains_rst(tcs):
none_txt = '*(none)*'
# Initialize an empty list of lists for the table data
- table_values = [[] for i in range(len(table_titles))]
+ table_values = [[] for _ in range(len(table_titles))]
for col_id, col_name in enumerate(table_titles):
if col_name == 'NAME':
@@ -1201,6 +1219,8 @@ def list_toolchains_rst(tcs):
entry = 'cray-mpich'
elif col_name == 'LINALG':
entry = 'cray-libsci'
+ else:
+ entry = none_txt
# Combine the linear algebra libraries into a single column
elif col_name == 'LINALG':
linalg = []
@@ -1309,15 +1329,19 @@ def get_easyblock_classes(package_name):
"""
Get list of all easyblock classes in specified easyblocks.* package
"""
- easyblocks = []
+ easyblocks = set()
modules = import_available_modules(package_name)
for mod in modules:
+ easyblock_found = False
for name, _ in inspect.getmembers(mod, inspect.isclass):
eb_class = getattr(mod, name)
# skip imported classes that are not easyblocks
- if eb_class.__module__.startswith(package_name) and eb_class not in easyblocks:
- easyblocks.append(eb_class)
+ if eb_class.__module__.startswith(package_name) and EasyBlock in inspect.getmro(eb_class):
+ easyblocks.add(eb_class)
+ easyblock_found = True
+ if not easyblock_found:
+ raise RuntimeError("No easyblocks found in module: %s", mod.__name__)
return easyblocks
diff --git a/easybuild/tools/environment.py b/easybuild/tools/environment.py
index a5cc708ce2..cf6bb2e09c 100644
--- a/easybuild/tools/environment.py
+++ b/easybuild/tools/environment.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -54,8 +54,8 @@ def write_changes(filename):
"""
try:
with open(filename, 'w') as script:
- for key in _changes:
- script.write('export %s=%s\n' % (key, shell_quote(_changes[key])))
+ for key, changed_value in _changes.items():
+ script.write('export %s=%s\n' % (key, shell_quote(changed_value)))
except IOError as err:
raise EasyBuildError("Failed to write to %s: %s", filename, err)
reset_changes()
diff --git a/easybuild/tools/filetools.py b/easybuild/tools/filetools.py
index 973f83e1f5..00494e5bd9 100644
--- a/easybuild/tools/filetools.py
+++ b/easybuild/tools/filetools.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -48,6 +48,7 @@
import inspect
import itertools
import os
+import pathlib
import platform
import re
import shutil
@@ -55,19 +56,23 @@
import stat
import ssl
import sys
+import tarfile
import tempfile
import time
import zlib
from functools import partial
+from html.parser import HTMLParser
+import urllib.request as std_urllib
from easybuild.base import fancylogger
-from easybuild.tools import LooseVersion, run
+from easybuild.tools import LooseVersion
# import build_log must stay, to use of EasyBuildLog
-from easybuild.tools.build_log import EasyBuildError, dry_run_msg, print_msg, print_warning
-from easybuild.tools.config import DEFAULT_WAIT_ON_LOCK_INTERVAL, ERROR, GENERIC_EASYBLOCK_PKG, IGNORE, WARN
-from easybuild.tools.config import build_option, install_path
+from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, CWD_NOTFOUND_ERROR
+from easybuild.tools.build_log import dry_run_msg, print_msg, print_warning
+from easybuild.tools.config import ERROR, GENERIC_EASYBLOCK_PKG, IGNORE, WARN, build_option, install_path
from easybuild.tools.output import PROGRESS_BAR_DOWNLOAD_ONE, start_progress_bar, stop_progress_bar, update_progress_bar
-from easybuild.tools.py2vs3 import HTMLParser, load_source, makedirs, std_urllib, string_type
+from easybuild.tools.hooks import load_source
+from easybuild.tools.run import run_shell_cmd
from easybuild.tools.utilities import natural_keys, nub, remove_unwanted_chars, trace_msg
try:
@@ -122,13 +127,25 @@
CHECKSUM_TYPE_MD5 = 'md5'
CHECKSUM_TYPE_SHA256 = 'sha256'
-DEFAULT_CHECKSUM = CHECKSUM_TYPE_MD5
+DEFAULT_CHECKSUM = CHECKSUM_TYPE_SHA256
+
+
+def _hashlib_md5():
+ """
+ Wrapper function for hashlib.md5,
+ to set usedforsecurity to False when supported (Python >= 3.9)
+ """
+ kwargs = {}
+ if sys.version_info[0] >= 3 and sys.version_info[1] >= 9:
+ kwargs = {'usedforsecurity': False}
+ return hashlib.md5(**kwargs)
+
# map of checksum types to checksum functions
CHECKSUM_FUNCTIONS = {
'adler32': lambda p: calc_block_checksum(p, ZlibChecksum(zlib.adler32)),
'crc32': lambda p: calc_block_checksum(p, ZlibChecksum(zlib.crc32)),
- CHECKSUM_TYPE_MD5: lambda p: calc_block_checksum(p, hashlib.md5()),
+ CHECKSUM_TYPE_MD5: lambda p: calc_block_checksum(p, _hashlib_md5()),
'sha1': lambda p: calc_block_checksum(p, hashlib.sha1()),
CHECKSUM_TYPE_SHA256: lambda p: calc_block_checksum(p, hashlib.sha256()),
'sha512': lambda p: calc_block_checksum(p, hashlib.sha512()),
@@ -394,7 +411,7 @@ def remove(paths):
:param paths: path(s) to remove
"""
- if isinstance(paths, string_type):
+ if isinstance(paths, str):
paths = [paths]
_log.info("Removing %d files & directories", len(paths))
@@ -408,6 +425,22 @@ def remove(paths):
raise EasyBuildError("Specified path to remove is not an existing file or directory: %s", path)
+def get_cwd(must_exist=True):
+ """
+ Retrieve current working directory
+ """
+ try:
+ cwd = os.getcwd()
+ except FileNotFoundError as err:
+ if must_exist is True:
+ raise EasyBuildError(CWD_NOTFOUND_ERROR)
+
+ _log.debug("Failed to determine current working directory, but proceeding anyway: %s", err)
+ cwd = None
+
+ return cwd
+
+
def change_dir(path):
"""
Change to directory at specified location.
@@ -415,22 +448,23 @@ def change_dir(path):
:param path: location to change to
:return: previous location we were in
"""
- # determining the current working directory can fail if we're in a non-existing directory
- try:
- cwd = os.getcwd()
- except OSError as err:
- _log.debug("Failed to determine current working directory (but proceeding anyway: %s", err)
- cwd = None
+ # determine origin working directory: can fail if non-existent
+ prev_dir = get_cwd(must_exist=False)
try:
os.chdir(path)
except OSError as err:
- raise EasyBuildError("Failed to change from %s to %s: %s", cwd, path, err)
+ raise EasyBuildError("Failed to change from %s to %s: %s", prev_dir, path, err)
- return cwd
+ # determine final working directory: must exist
+ # stoplight meant to catch filesystems in a faulty state
+ get_cwd()
+
+ return prev_dir
-def extract_file(fn, dest, cmd=None, extra_options=None, overwrite=False, forced=False, change_into_dir=None):
+def extract_file(fn, dest, cmd=None, extra_options=None, overwrite=False, forced=False, change_into_dir=False,
+ trace=True):
"""
Extract file at given path to specified directory
:param fn: path to file to extract
@@ -439,18 +473,13 @@ def extract_file(fn, dest, cmd=None, extra_options=None, overwrite=False, forced
:param extra_options: extra options to pass to extract command
:param overwrite: overwrite existing unpacked file
:param forced: force extraction in (extended) dry run mode
- :param change_into_dir: change into resulting directory;
- None (current default) implies True, but this is deprecated,
- this named argument should be set to False or True explicitely
- (in a future major release, default will be changed to False)
+ :param change_into_dir: change into resulting directorys
+ :param trace: produce trace output for extract command being run
:return: path to directory (in case of success)
"""
- if change_into_dir is None:
- _log.deprecated("extract_file function was called without specifying value for change_into_dir", '5.0')
- change_into_dir = True
if not os.path.isfile(fn) and not build_option('extended_dry_run'):
- raise EasyBuildError("Can't extract file %s: no such file", fn)
+ raise EasyBuildError(f"Can't extract file {fn}: no such file")
mkdir(dest, parents=True)
@@ -458,24 +487,24 @@ def extract_file(fn, dest, cmd=None, extra_options=None, overwrite=False, forced
abs_dest = os.path.abspath(dest)
# change working directory
- _log.debug("Unpacking %s in directory %s", fn, abs_dest)
+ _log.debug(f"Unpacking {fn} in directory {abs_dest}")
cwd = change_dir(abs_dest)
if cmd:
# complete command template with filename
cmd = cmd % fn
- _log.debug("Using specified command to unpack %s: %s", fn, cmd)
+ _log.debug(f"Using specified command to unpack {fn}: {cmd}")
else:
cmd = extract_cmd(fn, overwrite=overwrite)
- _log.debug("Using command derived from file extension to unpack %s: %s", fn, cmd)
+ _log.debug(f"Using command derived from file extension to unpack {fn}: {cmd}")
if not cmd:
- raise EasyBuildError("Can't extract file %s with unknown filetype", fn)
+ raise EasyBuildError(f"Can't extract file {fn} with unknown filetype")
if extra_options:
- cmd = "%s %s" % (cmd, extra_options)
+ cmd = f"{cmd} {extra_options}"
- run.run_cmd(cmd, simple=True, force_in_dry_run=forced)
+ run_shell_cmd(cmd, in_dry_run=forced, hidden=not trace)
# note: find_base_dir also changes into the base dir!
base_dir = find_base_dir()
@@ -484,14 +513,14 @@ def extract_file(fn, dest, cmd=None, extra_options=None, overwrite=False, forced
# change back to where we came from (unless that was a non-existing directory)
if not change_into_dir:
if cwd is None:
- raise EasyBuildError("Can't change back to non-existing directory after extracting %s in %s", fn, dest)
+ raise EasyBuildError(f"Can't change back to non-existing directory after extracting {fn} in {dest}")
else:
change_dir(cwd)
return base_dir
-def which(cmd, retain_all=False, check_perms=True, log_ok=True, log_error=None, on_error=None):
+def which(cmd, retain_all=False, check_perms=True, log_ok=True, on_error=WARN):
"""
Return (first) path in $PATH for specified command, or None if command is not found
@@ -500,17 +529,6 @@ def which(cmd, retain_all=False, check_perms=True, log_ok=True, log_error=None,
:param log_ok: Log an info message where the command has been found (if any)
:param on_error: What to do if the command was not found, default: WARN. Possible values: IGNORE, WARN, ERROR
"""
- if log_error is not None:
- _log.deprecated("'log_error' named argument in which function has been replaced by 'on_error'", '5.0')
- # If set, make sure on_error is at least WARN
- if log_error and on_error == IGNORE:
- on_error = WARN
- elif not log_error and on_error is None: # If set to False, use IGNORE unless on_error is also set
- on_error = IGNORE
- # Set default
- # TODO: After removal of log_error from the parameters, on_error=WARN can be used instead of this
- if on_error is None:
- on_error = WARN
if on_error not in (IGNORE, WARN, ERROR):
raise EasyBuildError("Invalid value for 'on_error': %s", on_error)
@@ -585,12 +603,25 @@ def normalize_path(path):
return start_slashes + os.path.sep.join(filtered_comps)
+def is_parent_path(path1, path2):
+ """
+ Return True if path1 is a prefix of path2
+
+ :param path1: absolute or relative path
+ :param path2: absolute or relative path
+ """
+ path1 = os.path.realpath(path1)
+ path2 = os.path.realpath(path2)
+ common_path = os.path.commonprefix([path1, path2])
+ return common_path == path1
+
+
def is_alt_pypi_url(url):
- """Determine whether specified URL is already an alternate PyPI URL, i.e. whether it contains a hash."""
+ """Determine whether specified URL is already an alternative PyPI URL, i.e. whether it contains a hash."""
# example: .../packages/5b/03/e135b19fadeb9b1ccb45eac9f60ca2dc3afe72d099f6bd84e03cb131f9bf/easybuild-2.7.0.tar.gz
alt_url_regex = re.compile('/packages/[a-f0-9]{2}/[a-f0-9]{2}/[a-f0-9]{60}/[^/]+$')
res = bool(alt_url_regex.search(url))
- _log.debug("Checking whether '%s' is an alternate PyPI URL using pattern '%s'...: %s",
+ _log.debug("Checking whether '%s' is an alternative PyPI URL using pattern '%s'...: %s",
url, alt_url_regex.pattern, res)
return res
@@ -638,7 +669,7 @@ def handle_starttag(self, tag, attrs):
def derive_alt_pypi_url(url):
- """Derive alternate PyPI URL for given URL."""
+ """Derive alternative PyPI URL for given URL."""
alt_pypi_url = None
# example input URL: https://pypi.python.org/packages/source/e/easybuild/easybuild-2.7.0.tar.gz
@@ -687,9 +718,9 @@ def parse_http_header_fields_urlpat(arg, urlpat=None, header=None, urlpat_header
if argline == '' or '#' in argline[0]:
continue # permit comment lines: ignore them
- if os.path.isfile(os.path.join(os.getcwd(), argline)):
+ if os.path.isfile(os.path.join(get_cwd(), argline)):
# expand existing relative path to absolute
- argline = os.path.join(os.path.join(os.getcwd(), argline))
+ argline = os.path.join(os.path.join(get_cwd(), argline))
if os.path.isfile(argline):
# argline is a file path, so read that instead
_log.debug('File included in parse_http_header_fields_urlpat: %s' % argline)
@@ -746,7 +777,7 @@ def det_file_size(http_header):
return res
-def download_file(filename, url, path, forced=False):
+def download_file(filename, url, path, forced=False, trace=True):
"""Download a file from the given URL, to the specified path."""
insecure = build_option('insecure_download')
@@ -842,7 +873,10 @@ def download_file(filename, url, path, forced=False):
if error_re.match(str(err)):
switch_to_requests = True
except Exception as err:
- raise EasyBuildError("Unexpected error occurred when trying to download %s to %s: %s", url, path, err)
+ raise EasyBuildError(
+ "Unexpected error occurred when trying to download %s to %s: %s", url, path, err,
+ exit_code=EasyBuildExit.FAIL_DOWNLOAD
+ )
if not downloaded and attempt_cnt < max_attempts:
_log.info("Attempt %d of downloading %s to %s failed, trying again..." % (attempt_cnt, url, path))
@@ -855,11 +889,13 @@ def download_file(filename, url, path, forced=False):
if downloaded:
_log.info("Successful download of file %s from url %s to path %s" % (filename, url, path))
- trace_msg("download succeeded: %s" % url)
+ if trace:
+ trace_msg("download succeeded: %s" % url)
return path
else:
_log.warning("Download of %s to %s failed, done trying" % (url, path))
- trace_msg("download failed: %s" % url)
+ if trace:
+ trace_msg("download failed: %s" % url)
return None
@@ -1055,7 +1091,10 @@ def locate_files(files, paths, ignore_subdirs=None):
if files_to_find:
filenames = ', '.join([f for (_, f) in files_to_find])
paths = ', '.join(paths)
- raise EasyBuildError("One or more files not found: %s (search paths: %s)", filenames, paths)
+ raise EasyBuildError(
+ "One or more files not found: %s (search paths: %s)", filenames, paths,
+ exit_code=EasyBuildExit.MISSING_EASYCONFIG
+ )
return [os.path.abspath(f) for f in files]
@@ -1206,12 +1245,16 @@ def compute_checksum(path, checksum_type=DEFAULT_CHECKSUM):
Compute checksum of specified file.
:param path: Path of file to compute checksum for
- :param checksum_type: type(s) of checksum ('adler32', 'crc32', 'md5' (default), 'sha1', 'sha256', 'sha512', 'size')
+ :param checksum_type: type(s) of checksum ('adler32', 'crc32', 'md5', 'sha1', 'sha256', 'sha512', 'size')
"""
if checksum_type not in CHECKSUM_FUNCTIONS:
raise EasyBuildError("Unknown checksum type (%s), supported types are: %s",
checksum_type, CHECKSUM_FUNCTIONS.keys())
+ if checksum_type in ['adler32', 'crc32', 'md5', 'sha1', 'size']:
+ _log.deprecated("Checksum type %s is deprecated. Use sha256 (default) or sha512 instead" % checksum_type,
+ '6.0')
+
try:
checksum = CHECKSUM_FUNCTIONS[checksum_type](path)
except IOError as err:
@@ -1249,8 +1292,7 @@ def verify_checksum(path, checksums, computed_checksums=None):
Verify checksum of specified file.
:param path: path of file to verify checksum of
- :param checksums: checksum values to compare to
- (and type, optionally, default is MD5), e.g., 'af314', ('sha', '5ec1b')
+ :param checksums: checksum values (and type, optionally, default is sha256), e.g., 'af314', ('sha', '5ec1b')
:param computed_checksums: Optional dictionary of (current) checksum(s) for this file
indexed by the checksum type (e.g. 'sha256').
Each existing entry will be used, missing ones will be computed.
@@ -1276,8 +1318,11 @@ def verify_checksum(path, checksums, computed_checksums=None):
checksum = checksum[filename]
except KeyError:
raise EasyBuildError("Missing checksum for %s in %s", filename, checksum)
+ if not verify_checksum(path, checksum, computed_checksums):
+ return False
+ continue
- if isinstance(checksum, string_type):
+ if isinstance(checksum, str):
# if no checksum type is specified, it is assumed to be MD5 (32 characters) or SHA256 (64 characters)
if len(checksum) == 64:
typ = CHECKSUM_TYPE_SHA256
@@ -1305,7 +1350,7 @@ def verify_checksum(path, checksums, computed_checksums=None):
# no matching checksums
return False
else:
- raise EasyBuildError("Invalid checksum spec '%s': should be a string (MD5 or SHA256), "
+ raise EasyBuildError("Invalid checksum spec '%s': should be a string (SHA256), "
"2-tuple (type, value), or tuple of alternative checksum specs.",
checksum)
@@ -1328,7 +1373,7 @@ def verify_checksum(path, checksums, computed_checksums=None):
def is_sha256_checksum(value):
"""Check whether provided string is a SHA256 checksum."""
res = False
- if isinstance(value, string_type):
+ if isinstance(value, str):
if re.match('^[0-9a-f]{64}$', value):
res = True
_log.debug("String value '%s' has the correct format to be a SHA256 checksum", value)
@@ -1352,14 +1397,14 @@ def get_local_dirs_purged():
# and hidden directories
ignoredirs = ["easybuild"]
- lst = os.listdir(os.getcwd())
+ lst = os.listdir(get_cwd())
lst = [d for d in lst if not d.startswith('.') and d not in ignoredirs]
return lst
lst = get_local_dirs_purged()
- new_dir = os.getcwd()
+ new_dir = get_cwd()
while len(lst) == 1:
- new_dir = os.path.join(os.getcwd(), lst[0])
+ new_dir = os.path.join(get_cwd(), lst[0])
if not os.path.isdir(new_dir):
break
@@ -1381,13 +1426,12 @@ def find_extension(filename):
suffixes = sorted(EXTRACT_CMDS.keys(), key=len, reverse=True)
pat = r'(?P%s)$' % '|'.join([s.replace('.', '\\.') for s in suffixes])
res = re.search(pat, filename, flags=re.IGNORECASE)
+
if res:
- ext = res.group('ext')
+ return res.group('ext')
else:
raise EasyBuildError("%s has unknown file extension", filename)
- return ext
-
def extract_cmd(filepath, overwrite=False):
"""
@@ -1500,17 +1544,19 @@ def create_patch_info(patch_spec):
# string value as patch argument can be either path where patch should be applied,
# or path to where a non-patch file should be copied
- elif isinstance(patch_arg, string_type):
+ elif isinstance(patch_arg, str):
if patch_spec[0].endswith('.patch'):
patch_info['sourcepath'] = patch_arg
# non-patch files are assumed to be files to copy
else:
patch_info['copy'] = patch_arg
else:
- raise EasyBuildError("Wrong patch spec '%s', only int/string are supported as 2nd element",
- str(patch_spec))
+ raise EasyBuildError(
+ "Wrong patch spec '%s', only int/string are supported as 2nd element", str(patch_spec),
+ exit_code=EasyBuildExit.EASYCONFIG_ERROR
+ )
- elif isinstance(patch_spec, string_type):
+ elif isinstance(patch_spec, str):
validate_patch_spec(patch_spec)
patch_info = {'name': patch_spec}
elif isinstance(patch_spec, dict):
@@ -1519,13 +1565,17 @@ def create_patch_info(patch_spec):
if key in valid_keys:
patch_info[key] = patch_spec[key]
else:
- raise EasyBuildError("Wrong patch spec '%s', use of unknown key %s in dict (valid keys are %s)",
- str(patch_spec), key, valid_keys)
+ raise EasyBuildError(
+ "Wrong patch spec '%s', use of unknown key %s in dict (valid keys are %s)",
+ str(patch_spec), key, valid_keys, exit_code=EasyBuildExit.EASYCONFIG_ERROR
+ )
# Dict must contain at least the patchfile name
if 'name' not in patch_info.keys():
- raise EasyBuildError("Wrong patch spec '%s', when using a dict 'name' entry must be supplied",
- str(patch_spec))
+ raise EasyBuildError(
+ "Wrong patch spec '%s', when using a dict 'name' entry must be supplied", str(patch_spec),
+ exit_code=EasyBuildExit.EASYCONFIG_ERROR
+ )
if 'copy' not in patch_info.keys():
validate_patch_spec(patch_info['name'])
else:
@@ -1534,9 +1584,11 @@ def create_patch_info(patch_spec):
"this implies you want to copy a file to the 'copy' location)",
str(patch_spec))
else:
- error_msg = "Wrong patch spec, should be string, 2-tuple with patch name + argument, or a dict " \
- "(with possible keys %s): %s" % (valid_keys, patch_spec)
- raise EasyBuildError(error_msg)
+ error_msg = (
+ "Wrong patch spec, should be string, 2-tuple with patch name + argument, or a dict "
+ f"(with possible keys {valid_keys}): {patch_spec}"
+ )
+ raise EasyBuildError(error_msg, exit_code=EasyBuildExit.EASYCONFIG_ERROR)
return patch_info
@@ -1544,12 +1596,13 @@ def create_patch_info(patch_spec):
def validate_patch_spec(patch_spec):
allowed_patch_exts = ['.patch' + x for x in ('',) + ZIPPED_PATCH_EXTS]
if not any(patch_spec.endswith(x) for x in allowed_patch_exts):
- msg = "Use of patch file with filename that doesn't end with correct extension: %s " % patch_spec
- msg += "(should be any of: %s)" % (', '.join(allowed_patch_exts))
- _log.deprecated(msg, '5.0')
+ raise EasyBuildError(
+ "Wrong patch spec (%s), extension type should be any of %s.", patch_spec, ', '.join(allowed_patch_exts),
+ exit_code=EasyBuildExit.EASYCONFIG_ERROR
+ )
-def apply_patch(patch_file, dest, fn=None, copy=False, level=None, use_git_am=False, use_git=False):
+def apply_patch(patch_file, dest, fn=None, copy=False, level=None, use_git=False):
"""
Apply a patch to source code in directory dest
- assume unified diff created with "diff -ru old new"
@@ -1557,34 +1610,30 @@ def apply_patch(patch_file, dest, fn=None, copy=False, level=None, use_git_am=Fa
Raises EasyBuildError on any error and returns True on success
"""
- if use_git_am:
- _log.deprecated("'use_git_am' named argument in apply_patch function has been renamed to 'use_git'", '5.0')
- use_git = True
-
if build_option('extended_dry_run'):
# skip checking of files in dry run mode
patch_filename = os.path.basename(patch_file)
- dry_run_msg("* applying patch file %s" % patch_filename, silent=build_option('silent'))
+ dry_run_msg(f"* applying patch file {patch_filename}", silent=build_option('silent'))
elif not os.path.isfile(patch_file):
- raise EasyBuildError("Can't find patch %s: no such file", patch_file)
+ raise EasyBuildError(f"Can't find patch {patch_file}: no such file")
elif fn and not os.path.isfile(fn):
- raise EasyBuildError("Can't patch file %s: no such file", fn)
+ raise EasyBuildError(f"Can't patch file {fn}: no such file")
# copy missing files
if copy:
if build_option('extended_dry_run'):
- dry_run_msg(" %s copied to %s" % (patch_file, dest), silent=build_option('silent'))
+ dry_run_msg(f" {patch_file} copied to {dest}", silent=build_option('silent'))
else:
copy_file(patch_file, dest)
- _log.debug("Copied patch %s to dir %s" % (patch_file, dest))
+ _log.debug(f"Copied patch {patch_file} to dir {dest}")
# early exit, work is done after copying
return True
elif not os.path.isdir(dest):
- raise EasyBuildError("Can't patch directory %s: no such directory", dest)
+ raise EasyBuildError(f"Can't patch directory {dest}: no such directory")
# use absolute paths
abs_patch_file = os.path.abspath(patch_file)
@@ -1599,14 +1648,14 @@ def apply_patch(patch_file, dest, fn=None, copy=False, level=None, use_git_am=Fa
patch_subextension = os.path.splitext(patch_stem)[1]
if patch_subextension == ".patch":
workdir = tempfile.mkdtemp(prefix='eb-patch-')
- _log.debug("Extracting the patch to: %s", workdir)
+ _log.debug(f"Extracting the patch to: {workdir}")
# extracting the patch
extracted_dir = extract_file(abs_patch_file, workdir, change_into_dir=False)
abs_patch_file = os.path.join(extracted_dir, patch_stem)
if use_git:
verbose = '--verbose ' if build_option('debug') else ''
- patch_cmd = "git apply %s%s" % (verbose, abs_patch_file)
+ patch_cmd = f"git apply {verbose}{abs_patch_file}"
else:
if level is None and build_option('extended_dry_run'):
level = ''
@@ -1619,63 +1668,79 @@ def apply_patch(patch_file, dest, fn=None, copy=False, level=None, use_git_am=Fa
patched_files = det_patched_files(path=abs_patch_file)
if not patched_files:
- raise EasyBuildError("Can't guess patchlevel from patch %s: no testfile line found in patch",
- abs_patch_file)
+ msg = f"Can't guess patchlevel from patch {abs_patch_file}: no testfile line found in patch"
+ raise EasyBuildError(msg)
level = guess_patch_level(patched_files, abs_dest)
if level is None: # level can also be 0 (zero), so don't use "not level"
# no match
- raise EasyBuildError("Can't determine patch level for patch %s from directory %s", patch_file, abs_dest)
+ raise EasyBuildError(f"Can't determine patch level for patch {patch_file} from directory {abs_dest}")
else:
- _log.debug("Guessed patch level %d for patch %s" % (level, patch_file))
+ _log.debug(f"Guessed patch level {level} for patch {patch_file}")
else:
- _log.debug("Using specified patch level %d for patch %s" % (level, patch_file))
+ _log.debug(f"Using specified patch level {level} for patch {patch_file}")
backup_option = '-b ' if build_option('backup_patched_files') else ''
- patch_cmd = "patch " + backup_option + "-p%s -i %s" % (level, abs_patch_file)
+ patch_cmd = f"patch {backup_option} -p{level} -i {abs_patch_file}"
- out, ec = run.run_cmd(patch_cmd, simple=False, path=abs_dest, log_ok=False, trace=False)
+ res = run_shell_cmd(patch_cmd, fail_on_error=False, hidden=True, work_dir=abs_dest)
+
+ if res.exit_code:
+ msg = f"Couldn't apply patch file {patch_file}. "
+ msg += f"Process exited with code {res.exit_code}: {res.output}"
+ raise EasyBuildError(msg, exit_code=EasyBuildExit.FAIL_PATCH_APPLY)
- if ec:
- raise EasyBuildError("Couldn't apply patch file %s. Process exited with code %s: %s", patch_file, ec, out)
return True
-def apply_regex_substitutions(paths, regex_subs, backup='.orig.eb', on_missing_match=None):
+def apply_regex_substitutions(paths, regex_subs, backup='.orig.eb',
+ on_missing_match=None, match_all=False, single_line=True):
"""
Apply specified list of regex substitutions.
:param paths: list of paths to files to patch (or just a single filepath)
- :param regex_subs: list of substitutions to apply, specified as (, )
+ :param regex_subs: list of substitutions to apply,
+ specified as (, )
:param backup: create backup of original file with specified suffix (no backup if value evaluates to False)
:param on_missing_match: Define what to do when no match was found in the file.
Can be 'error' to raise an error, 'warn' to print a warning or 'ignore' to do nothing
Defaults to the value of --strict
+ :param match_all: Expect to match all patterns in all files
+ instead of at least one per file for error/warning reporting
+ :param single_line: Replace first match of each pattern for each line in the order of the patterns.
+ If False the patterns are applied in order to the full text and may match line breaks.
"""
if on_missing_match is None:
on_missing_match = build_option('strict')
allowed_values = (ERROR, IGNORE, WARN)
if on_missing_match not in allowed_values:
- raise EasyBuildError('Invalid value passed to on_missing_match: %s (allowed: %s)',
- on_missing_match, ', '.join(allowed_values))
+ raise ValueError('Invalid value passed to on_missing_match: %s (allowed: %s)',
+ on_missing_match, ', '.join(allowed_values))
- if isinstance(paths, string_type):
+ if isinstance(paths, str):
paths = [paths]
+ if (not isinstance(regex_subs, (list, tuple)) or
+ not all(isinstance(sub, (list, tuple)) and len(sub) == 2 for sub in regex_subs)):
+ raise ValueError('Parameter regex_subs must be a list of 2-element tuples. Got:', regex_subs)
+
+ flags = 0 if single_line else re.M
+ compiled_regex_subs = [(re.compile(regex, flags) if isinstance(regex, str) else regex, subtxt)
+ for (regex, subtxt) in regex_subs]
# only report when in 'dry run' mode
if build_option('extended_dry_run'):
paths_str = ', '.join(paths)
dry_run_msg("applying regex substitutions to file(s): %s" % paths_str, silent=build_option('silent'))
- for regex, subtxt in regex_subs:
- dry_run_msg(" * regex pattern '%s', replacement string '%s'" % (regex, subtxt))
+ for regex, subtxt in compiled_regex_subs:
+ dry_run_msg(" * regex pattern '%s', replacement string '%s'" % (regex.pattern, subtxt))
else:
- _log.info("Applying following regex substitutions to %s: %s", paths, regex_subs)
-
- compiled_regex_subs = [(re.compile(regex), subtxt) for (regex, subtxt) in regex_subs]
+ _log.info("Applying following regex substitutions to %s: %s",
+ paths, [(regex.pattern, subtxt) for regex, subtxt in compiled_regex_subs])
+ replacement_failed_msgs = []
for path in paths:
try:
# make sure that file can be opened in text mode;
@@ -1695,32 +1760,49 @@ def apply_regex_substitutions(paths, regex_subs, backup='.orig.eb', on_missing_m
if backup:
copy_file(path, path + backup)
replacement_msgs = []
+ replaced = [False] * len(compiled_regex_subs)
with open_file(path, 'w') as out_file:
- lines = txt_utf8.split('\n')
- del txt_utf8
- for line_id, line in enumerate(lines):
- for regex, subtxt in compiled_regex_subs:
- match = regex.search(line)
- if match:
+ if single_line:
+ lines = txt_utf8.split('\n')
+ del txt_utf8
+ for line_id, line in enumerate(lines):
+ for i, (regex, subtxt) in enumerate(compiled_regex_subs):
+ match = regex.search(line)
+ if match:
+ origtxt = match.group(0)
+ replacement_msgs.append("Replaced in line %d: '%s' -> '%s'" %
+ (line_id + 1, origtxt, subtxt))
+ replaced[i] = True
+ line = regex.sub(subtxt, line)
+ lines[line_id] = line
+ out_file.write('\n'.join(lines))
+ else:
+ for i, (regex, subtxt) in enumerate(compiled_regex_subs):
+ def do_replace(match):
origtxt = match.group(0)
- replacement_msgs.append("Replaced in line %d: '%s' -> '%s'" %
- (line_id + 1, origtxt, subtxt))
- line = regex.sub(subtxt, line)
- lines[line_id] = line
- out_file.write('\n'.join(lines))
+ # pylint: disable=cell-var-from-loop
+ cur_subtxt = match.expand(subtxt)
+ # pylint: disable=cell-var-from-loop
+ replacement_msgs.append("Replaced: '%s' -> '%s'" % (origtxt, cur_subtxt))
+ return cur_subtxt
+ txt_utf8, replaced[i] = regex.subn(do_replace, txt_utf8)
+ out_file.write(txt_utf8)
if replacement_msgs:
_log.info('Applied the following substitutions to %s:\n%s', path, '\n'.join(replacement_msgs))
- else:
- msg = 'Nothing found to replace in %s' % path
- if on_missing_match == ERROR:
- raise EasyBuildError(msg)
- elif on_missing_match == WARN:
- _log.warning(msg)
- else:
- _log.info(msg)
-
+ if (match_all and not all(replaced)) or (not match_all and not any(replaced)):
+ errors = ["Nothing found to replace '%s'" % regex.pattern
+ for cur_replaced, (regex, _) in zip(replaced, compiled_regex_subs) if not cur_replaced]
+ replacement_failed_msgs.append(', '.join(errors) + ' in ' + path)
except (IOError, OSError) as err:
raise EasyBuildError("Failed to patch %s: %s", path, err)
+ if replacement_failed_msgs:
+ msg = '\n'.join(replacement_failed_msgs)
+ if on_missing_match == ERROR:
+ raise EasyBuildError(msg)
+ elif on_missing_match == WARN:
+ _log.warning(msg)
+ else:
+ _log.info(msg)
def modify_env(old, new):
@@ -1748,7 +1830,7 @@ def convert_name(name, upper=False):
def adjust_permissions(provided_path, permission_bits, add=True, onlyfiles=False, onlydirs=False, recursive=True,
- group_id=None, relative=True, ignore_errors=False, skip_symlinks=None):
+ group_id=None, relative=True, ignore_errors=False):
"""
Change permissions for specified path, using specified permission bits
@@ -1765,11 +1847,6 @@ def adjust_permissions(provided_path, permission_bits, add=True, onlyfiles=False
and directories (if onlyfiles is False) in path
"""
- if skip_symlinks is not None:
- depr_msg = "Use of 'skip_symlinks' argument for 'adjust_permissions' is deprecated "
- depr_msg += "(symlinks are never followed anymore)"
- _log.deprecated(depr_msg, '4.0')
-
provided_path = os.path.abspath(provided_path)
if recursive:
@@ -1929,9 +2006,15 @@ def mkdir(path, parents=False, set_gid=None, sticky=None):
# climb up until we hit an existing path or the empty string (for relative paths)
while existing_parent_path and not os.path.exists(existing_parent_path):
existing_parent_path = os.path.dirname(existing_parent_path)
- makedirs(path, exist_ok=True)
+ os.makedirs(path, exist_ok=True)
else:
os.mkdir(path)
+ except FileExistsError as err:
+ if os.path.exists(path):
+ # This may happen if a parallel build creates the directory after we checked for its existence
+ _log.debug("Directory creation aborted as it seems it was already created: %s", err)
+ else:
+ raise EasyBuildError("Failed to create directory %s: %s", path, err)
except OSError as err:
raise EasyBuildError("Failed to create directory %s: %s", path, err)
@@ -1972,7 +2055,7 @@ def check_lock(lock_name):
Check whether a lock with specified name already exists.
If it exists, either wait until it's released, or raise an error
- (depending on --wait-on-lock configuration option).
+ (depending on --wait-on-lock-* configuration option).
"""
lock_path = det_lock_path(lock_name)
if os.path.exists(lock_path):
@@ -1981,22 +2064,6 @@ def check_lock(lock_name):
wait_interval = build_option('wait_on_lock_interval')
wait_limit = build_option('wait_on_lock_limit')
- # --wait-on-lock is deprecated, should use --wait-on-lock-limit and --wait-on-lock-interval instead
- wait_on_lock = build_option('wait_on_lock')
- if wait_on_lock is not None:
- depr_msg = "Use of --wait-on-lock is deprecated, use --wait-on-lock-limit and --wait-on-lock-interval"
- _log.deprecated(depr_msg, '5.0')
-
- # if --wait-on-lock-interval has default value and --wait-on-lock is specified too, the latter wins
- # (required for backwards compatibility)
- if wait_interval == DEFAULT_WAIT_ON_LOCK_INTERVAL and wait_on_lock > 0:
- wait_interval = wait_on_lock
-
- # if --wait-on-lock-limit is not specified we need to wait indefinitely if --wait-on-lock is specified,
- # since the original semantics of --wait-on-lock was that it specified the waiting time interval (no limit)
- if not wait_limit:
- wait_limit = -1
-
# wait limit could be zero (no waiting), -1 (no waiting limit) or non-zero value (waiting limit in seconds)
if wait_limit != 0:
wait_time = 0
@@ -2115,13 +2182,6 @@ def path_matches(path, paths):
return False
-def rmtree2(path, n=3):
- """Wrapper around shutil.rmtree to make it more robust when used on NFS mounted file systems."""
-
- _log.deprecated("Use 'remove_dir' rather than 'rmtree2'", '5.0')
- remove_dir(path)
-
-
def find_backup_name_candidate(src_file):
"""Returns a non-existing file to be used as destination for backup files"""
@@ -2175,7 +2235,7 @@ def move_logs(src_logfile, target_logfile):
try:
# there may be multiple log files, due to log rotation
- app_logs = glob.glob('%s*' % src_logfile)
+ app_logs = glob.glob(f'{src_logfile}*')
for app_log in app_logs:
# retain possible suffix
new_log_path = target_logfile + app_log[src_logfile_len:]
@@ -2186,11 +2246,11 @@ def move_logs(src_logfile, target_logfile):
# move log to target path
move_file(app_log, new_log_path)
- _log.info("Moved log file %s to %s" % (src_logfile, new_log_path))
+ _log.info(f"Moved log file {src_logfile} to {new_log_path}")
if zip_log_cmd:
- run.run_cmd("%s %s" % (zip_log_cmd, new_log_path))
- _log.info("Zipped log %s using '%s'", new_log_path, zip_log_cmd)
+ run_shell_cmd(f"{zip_log_cmd} {new_log_path}")
+ _log.info(f"Zipped log {new_log_path} using '{zip_log_cmd}'")
except (IOError, OSError) as err:
raise EasyBuildError("Failed to move log file(s) %s* to new log file %s*: %s",
@@ -2228,11 +2288,6 @@ def cleanup(logfile, tempdir, testing, silent=False):
print_msg(msg, log=None, silent=testing or silent)
-def copytree(src, dst, symlinks=False, ignore=None):
- """DEPRECATED and removed. Use copy_dir"""
- _log.deprecated("Use 'copy_dir' rather than 'copytree'", '4.0')
-
-
def encode_string(name):
"""
This encoding function handles funky software names ad infinitum, like:
@@ -2280,21 +2335,6 @@ def decode_class_name(name):
return decode_string(name)
-def run_cmd(cmd, log_ok=True, log_all=False, simple=False, inp=None, regexp=True, log_output=False, path=None):
- """NO LONGER SUPPORTED: use run_cmd from easybuild.tools.run instead"""
- _log.nosupport("run_cmd was moved from easybuild.tools.filetools to easybuild.tools.run", '2.0')
-
-
-def run_cmd_qa(cmd, qa, no_qa=None, log_ok=True, log_all=False, simple=False, regexp=True, std_qa=None, path=None):
- """NO LONGER SUPPORTED: use run_cmd_qa from easybuild.tools.run instead"""
- _log.nosupport("run_cmd_qa was moved from easybuild.tools.filetools to easybuild.tools.run", '2.0')
-
-
-def parse_log_for_error(txt, regExp=None, stdout=True, msg=None):
- """NO LONGER SUPPORTED: use parse_log_for_error from easybuild.tools.run instead"""
- _log.nosupport("parse_log_for_error was moved from easybuild.tools.filetools to easybuild.tools.run", '2.0')
-
-
def det_size(path):
"""
Determine total size of given filepath (in bytes).
@@ -2309,7 +2349,7 @@ def det_size(path):
if os.path.exists(fullpath):
installsize += os.path.getsize(fullpath)
except OSError as err:
- _log.warn("Could not determine install size: %s" % err)
+ _log.warning("Could not determine install size: %s" % err)
return installsize
@@ -2336,7 +2376,7 @@ def find_flexlm_license(custom_env_vars=None, lic_specs=None):
# always consider $LM_LICENSE_FILE
lic_env_vars = ['LM_LICENSE_FILE']
- if isinstance(custom_env_vars, string_type):
+ if isinstance(custom_env_vars, str):
lic_env_vars.insert(0, custom_env_vars)
elif custom_env_vars is not None:
lic_env_vars = custom_env_vars + lic_env_vars
@@ -2629,7 +2669,7 @@ def copy(paths, target_path, force_in_dry_run=False, **kwargs):
:param force_in_dry_run: force running the command during dry run
:param kwargs: additional named arguments to pass down to copy_dir
"""
- if isinstance(paths, string_type):
+ if isinstance(paths, str):
paths = [paths]
_log.info("Copying %d files & directories to %s", len(paths), target_path)
@@ -2647,17 +2687,17 @@ def copy(paths, target_path, force_in_dry_run=False, **kwargs):
raise EasyBuildError("Specified path to copy is not an existing file or directory: %s", path)
-def get_source_tarball_from_git(filename, targetdir, git_config):
+def get_source_tarball_from_git(filename, target_dir, git_config):
"""
Downloads a git repository, at a specific tag or commit, recursively or not, and make an archive with it
- :param filename: name of the archive to save the code to (must be .tar.gz)
- :param targetdir: target directory where to save the archive to
+ :param filename: name of the archive file to save the code to (including extension)
+ :param target_dir: target directory where to save the archive to
:param git_config: dictionary containing url, repo_name, recursive, and one of tag or commit
"""
# sanity check on git_config value being passed
if not isinstance(git_config, dict):
- raise EasyBuildError("Found unexpected type of value for 'git_config' argument: %s" % type(git_config))
+ raise EasyBuildError("Found unexpected type of value for 'git_config' argument: {type(git_config)}")
# Making a copy to avoid modifying the object with pops
git_config = git_config.copy()
@@ -2673,7 +2713,7 @@ def get_source_tarball_from_git(filename, targetdir, git_config):
# input validation of git_config dict
if git_config:
- raise EasyBuildError("Found one or more unexpected keys in 'git_config' specification: %s", git_config)
+ raise EasyBuildError("Found one or more unexpected keys in 'git_config' specification: {git_config}")
if not repo_name:
raise EasyBuildError("repo_name not specified in git_config parameter")
@@ -2687,102 +2727,185 @@ def get_source_tarball_from_git(filename, targetdir, git_config):
if not url:
raise EasyBuildError("url not specified in git_config parameter")
- if not filename.endswith('.tar.gz'):
- raise EasyBuildError("git_config currently only supports filename ending in .tar.gz")
-
# prepare target directory and clone repository
- mkdir(targetdir, parents=True)
- targetpath = os.path.join(targetdir, filename)
+ mkdir(target_dir, parents=True)
+
+ # compose base git command
+ git_cmd = 'git'
+ if extra_config_params is not None:
+ git_cmd_params = [f"-c {param}" for param in extra_config_params]
+ git_cmd += f" {' '.join(git_cmd_params)}"
# compose 'git clone' command, and run it
- if extra_config_params:
- git_cmd = 'git ' + ' '.join(['-c %s' % param for param in extra_config_params])
- else:
- git_cmd = 'git'
clone_cmd = [git_cmd, 'clone']
+ # checkout is done separately below for specific commits
+ clone_cmd.append('--no-checkout')
- if not keep_git_dir and not commit:
- # Speed up cloning by only fetching the most recent commit, not the whole history
- # When we don't want to keep the .git folder there won't be a difference in the result
- clone_cmd.extend(['--depth', '1'])
-
- if tag:
- clone_cmd.extend(['--branch', tag])
- if recursive:
- clone_cmd.append('--recursive')
- if recurse_submodules:
- clone_cmd.extend(["--recurse-submodules='%s'" % pat for pat in recurse_submodules])
- else:
- # checkout is done separately below for specific commits
- clone_cmd.append('--no-checkout')
-
- clone_cmd.append('%s/%s.git' % (url, repo_name))
+ clone_cmd.append(f'{url}/{repo_name}.git')
if clone_into:
clone_cmd.append(clone_into)
tmpdir = tempfile.mkdtemp()
- cwd = change_dir(tmpdir)
- run.run_cmd(' '.join(clone_cmd), log_all=True, simple=True, regexp=False)
+
+ run_shell_cmd(' '.join(clone_cmd), hidden=True, verbose_dry_run=True, work_dir=tmpdir)
# If the clone is done into a specified name, change repo_name
if clone_into:
repo_name = clone_into
+ repo_dir = os.path.join(tmpdir, repo_name)
+
+ # compose checkout command
+ checkout_cmd = [git_cmd, 'checkout']
# if a specific commit is asked for, check it out
if commit:
- checkout_cmd = [git_cmd, 'checkout', commit]
-
- if recursive or recurse_submodules:
- checkout_cmd.extend(['&&', git_cmd, 'submodule', 'update', '--init'])
- if recursive:
- checkout_cmd.append('--recursive')
- if recurse_submodules:
- checkout_cmd.extend(["--recurse-submodules='%s'" % pat for pat in recurse_submodules])
-
- run.run_cmd(' '.join(checkout_cmd), log_all=True, simple=True, regexp=False, path=repo_name)
-
- elif not build_option('extended_dry_run'):
- # If we wanted to get a tag make sure we actually got a tag and not a branch with the same name
- # This doesn't make sense in dry-run mode as we don't have anything to check
- cmd = '%s describe --exact-match --tags HEAD' % git_cmd
- # Note: Disable logging to also disable the error handling in run_cmd
- (out, ec) = run.run_cmd(cmd, log_ok=False, log_all=False, regexp=False, path=repo_name)
- if ec != 0 or tag not in out.splitlines():
- print_warning('Tag %s was not downloaded in the first try due to %s/%s containing a branch'
- ' with the same name. You might want to alert the maintainers of %s about that issue.',
- tag, url, repo_name, repo_name)
- cmds = []
-
- if not keep_git_dir:
- # make the repo unshallow first;
- # this is equivalent with 'git fetch -unshallow' in Git 1.8.3+
- # (first fetch seems to do nothing, unclear why)
- cmds.append('%s fetch --depth=2147483647 && git fetch --depth=2147483647' % git_cmd)
-
- cmds.append('%s checkout refs/tags/' % git_cmd + tag)
- # Clean all untracked files, e.g. from left-over submodules
- cmds.append('%s clean --force -d -x' % git_cmd)
- if recursive:
- cmds.append('%s submodule update --init --recursive' % git_cmd)
- elif recurse_submodules:
- cmds.append('%s submodule update --init ' % git_cmd)
- cmds[-1] += ' '.join(["--recurse-submodules='%s'" % pat for pat in recurse_submodules])
- for cmd in cmds:
- run.run_cmd(cmd, log_all=True, simple=True, regexp=False, path=repo_name)
-
- # create an archive and delete the git repo directory
- if keep_git_dir:
- tar_cmd = ['tar', 'cfvz', targetpath, repo_name]
- else:
- tar_cmd = ['tar', 'cfvz', targetpath, '--exclude', '.git', repo_name]
- run.run_cmd(' '.join(tar_cmd), log_all=True, simple=True, regexp=False)
+ checkout_cmd.append(f"{commit}")
+ elif tag:
+ checkout_cmd.append(f"refs/tags/{tag}")
+
+ run_shell_cmd(' '.join(checkout_cmd), work_dir=repo_dir, hidden=True, verbose_dry_run=True)
+
+ if recursive or recurse_submodules:
+ submodule_cmd = [git_cmd, 'submodule', 'update', '--init']
+ if recursive:
+ submodule_cmd.append('--recursive')
+ if recurse_submodules:
+ submodule_pathspec = [f"':{submod_path}'" for submod_path in recurse_submodules]
+ submodule_cmd.extend(['--'] + submodule_pathspec)
+
+ run_shell_cmd(' '.join(submodule_cmd), work_dir=repo_dir, hidden=True, verbose_dry_run=True)
+
+ # Create archive
+ reproducible = not keep_git_dir # presence of .git directory renders repo unreproducible
+ archive_path = make_archive(repo_dir, archive_file=filename, archive_dir=target_dir, reproducible=reproducible)
# cleanup (repo_name dir does not exist in dry run mode)
- change_dir(cwd)
remove(tmpdir)
- return targetpath
+ return archive_path
+
+
+def make_archive(source_dir, archive_file=None, archive_dir=None, reproducible=True):
+ """
+ Create an archive file of the given directory
+ The format of the tarball is defined by the extension of the archive file name
+
+ :source_dir: string with path to directory to be archived
+ :archive_file: string with filename of archive
+ :archive_dir: string with path to directory to place the archive
+ :reproducible: make a tarball that is reproducible accross systems
+ - see https://reproducible-builds.org/docs/archives/
+ - requires uncompressed or LZMA compressed archive images
+ - gzip is currently not supported due to undeterministic data injected in its headers
+ see https://github.com/python/cpython/issues/112346
+
+ Default behaviour: reproducible tarball in .tar.xz
+ """
+ def reproducible_filter(tarinfo):
+ "Filter out system-dependent data from tarball"
+ # contents of '.git' subdir are inherently system dependent
+ if "/.git/" in tarinfo.name or tarinfo.name.endswith("/.git"):
+ return None
+ # set timestamp to epoch 0
+ tarinfo.mtime = 0
+ # reset file permissions by applying go+u,go-w
+ user_mode = tarinfo.mode & stat.S_IRWXU
+ group_mode = (user_mode >> 3) & ~stat.S_IWGRP # user mode without write
+ other_mode = group_mode >> 3 # same as group mode
+ tarinfo.mode = (tarinfo.mode & ~0o77) | group_mode | other_mode
+ # reset ownership to numeric UID/GID 0
+ # equivalent in GNU tar to 'tar --owner=0 --group=0 --numeric-owner'
+ tarinfo.uid = tarinfo.gid = 0
+ tarinfo.uname = tarinfo.gname = ""
+ return tarinfo
+
+ ext_compression_map = {
+ # taken from EXTRACT_CMDS
+ '.gtgz': 'gz',
+ '.tar.gz': 'gz',
+ '.tgz': 'gz',
+ '.tar.bz2': 'bz2',
+ '.tb2': 'bz2',
+ '.tbz': 'bz2',
+ '.tbz2': 'bz2',
+ '.tar.xz': 'xz',
+ '.txz': 'xz',
+ '.tar': '',
+ }
+ reproducible_compression = ['', 'xz']
+ default_ext = '.tar.xz'
+
+ if archive_file is None:
+ archive_file = os.path.basename(source_dir) + default_ext
+
+ try:
+ archive_ext = find_extension(archive_file)
+ except EasyBuildError:
+ if '.' in archive_file:
+ # archive filename has unknown extension (set for raise)
+ archive_ext = ''
+ else:
+ # archive filename has no extension, use default one
+ archive_ext = default_ext
+ archive_file += archive_ext
+
+ if archive_ext not in ext_compression_map:
+ # archive filename has unsupported extension
+ supported_exts = ', '.join(ext_compression_map)
+ raise EasyBuildError(
+ f"Unsupported archive format: {archive_file}. Supported tarball extensions: {supported_exts}"
+ )
+ compression = ext_compression_map[archive_ext]
+ _log.debug(f"Archive extension and compression: {archive_ext} in {compression}")
+
+ archive_path = archive_file if archive_dir is None else os.path.join(archive_dir, archive_file)
+
+ archive_specs = {
+ 'name': archive_path,
+ 'mode': f"w:{compression}",
+ 'format': tarfile.GNU_FORMAT,
+ 'encoding': "utf-8",
+ }
+
+ if reproducible:
+ if compression == 'xz':
+ # ensure a consistent compression level in reproducible tarballs with XZ
+ archive_specs['preset'] = 6
+ elif compression not in reproducible_compression:
+ # requested archive compression cannot be made reproducible
+ print_warning(
+ f"Can not create reproducible archive due to unsupported file compression ({compression}). "
+ "Please use XZ instead."
+ )
+ reproducible = False
+
+ archive_filter = reproducible_filter if reproducible else None
+
+ if build_option('extended_dry_run'):
+ # early return in dry run mode
+ dry_run_msg("Archiving '%s' into '%s'...", source_dir, archive_path)
+ return archive_path
+ _log.info("Archiving '%s' into '%s'...", source_dir, archive_path)
+
+ # TODO: replace with TarFile.add(recursive=True) when support for Python 3.6 drops
+ # since Python v3.7 tarfile automatically orders the list of files added to the archive
+ # see Tarfile.add documentation: https://docs.python.org/3/library/tarfile.html#tarfile.TarFile.add
+ source_files = [source_dir]
+ # pathlib's glob includes hidden files
+ source_files.extend([str(filepath) for filepath in pathlib.Path(source_dir).glob("**/*")])
+ source_files.sort() # independent of locale
+
+ with tarfile.open(**archive_specs) as tar_archive:
+ for filepath in source_files:
+ # archive with target directory in its top level, remove any prefix in path
+ file_name = os.path.relpath(filepath, start=os.path.dirname(source_dir))
+ tar_archive.add(filepath, arcname=file_name, recursive=False, filter=archive_filter)
+ _log.debug("File/folder added to archive '%s': %s", archive_file, filepath)
+
+ _log.info("Archive '%s' created successfully", archive_file)
+
+ return archive_path
def move_file(path, target_path, force_in_dry_run=False):
@@ -2980,3 +3103,69 @@ def create_unused_dir(parent_folder, name):
set_gid_sticky_bits(path, recursive=True)
return path
+
+
+def get_first_non_existing_parent_path(path):
+ """
+ Get first directory that does not exist, starting at path and going up.
+ """
+ path = os.path.abspath(path)
+
+ non_existing_parent = None
+ while not os.path.exists(path):
+ non_existing_parent = path
+ path = os.path.dirname(path)
+
+ return non_existing_parent
+
+
+def create_non_existing_paths(paths, max_tries=10000):
+ """
+ Create directories with given paths (including the parent directories).
+ When a directory in the same location for any of the specified paths already exists,
+ then the suffix '_' is appended , with i iteratively picked between 0 and (max_tries-1),
+ until an index is found so that all required paths are non-existing.
+ All created directories have the same suffix.
+
+ :param paths: list of directory paths to be created
+ :param max_tries: maximum number of tries before failing
+ """
+ paths = [os.path.abspath(p) for p in paths]
+ for idx_path, path in enumerate(paths):
+ for idx_parent, parent in enumerate(paths):
+ if idx_parent != idx_path and is_parent_path(parent, path):
+ raise EasyBuildError(f"Path '{parent}' is a parent path of '{path}'.")
+
+ first_non_existing_parent_paths = [get_first_non_existing_parent_path(p) for p in paths]
+
+ non_existing_paths = paths
+ all_paths_created = False
+ suffix = -1
+ while suffix < max_tries and not all_paths_created:
+ tried_paths = []
+ if suffix >= 0:
+ non_existing_paths = [f'{p}_{suffix}' for p in paths]
+ try:
+ for path in non_existing_paths:
+ tried_paths.append(path)
+ # os.makedirs will raise OSError if directory already exists
+ os.makedirs(path)
+ all_paths_created = True
+ except OSError as err:
+ # Distinguish between error due to existing folder and anything else
+ if not os.path.exists(tried_paths[-1]):
+ raise EasyBuildError("Failed to create directory %s: %s", tried_paths[-1], err)
+ remove(tried_paths[:-1])
+ except BaseException as err:
+ remove(tried_paths)
+ raise err
+ suffix += 1
+
+ if not all_paths_created:
+ raise EasyBuildError(f"Exceeded maximum number of attempts ({max_tries}) to generate non-existing paths")
+
+ # set group ID and sticky bits, if desired
+ for path in first_non_existing_parent_paths:
+ set_gid_sticky_bits(path, recursive=True)
+
+ return non_existing_paths
diff --git a/easybuild/tools/github.py b/easybuild/tools/github.py
index 6d4a1300fc..c03d92ad24 100644
--- a/easybuild/tools/github.py
+++ b/easybuild/tools/github.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -45,6 +45,8 @@
import tempfile
import time
from datetime import datetime, timedelta
+from string import ascii_letters
+from urllib.request import HTTPError, URLError, urlopen
from easybuild.base import fancylogger
from easybuild.framework.easyconfig.easyconfig import EASYCONFIGS_ARCHIVE_DIR
@@ -52,12 +54,11 @@
from easybuild.framework.easyconfig.easyconfig import process_easyconfig
from easybuild.framework.easyconfig.parser import EasyConfigParser
from easybuild.tools import LooseVersion
-from easybuild.tools.build_log import EasyBuildError, print_msg, print_warning
+from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, print_msg, print_warning
from easybuild.tools.config import build_option
from easybuild.tools.filetools import apply_patch, copy_dir, copy_easyblocks, copy_file, copy_framework_files
from easybuild.tools.filetools import det_patched_files, download_file, extract_file
from easybuild.tools.filetools import get_easyblock_class_name, mkdir, read_file, symlink, which, write_file
-from easybuild.tools.py2vs3 import HTTPError, URLError, ascii_letters, urlopen
from easybuild.tools.systemtools import UNKNOWN, get_tool_version
from easybuild.tools.utilities import nub, only_if_module_is_available
from easybuild.tools.version import FRAMEWORK_VERSION, different_major_versions
@@ -207,7 +208,7 @@ def listdir(self, path):
return listing[1]
else:
self.log.warning("error: %s" % str(listing))
- raise EasyBuildError("Invalid response from github (I/O error)")
+ raise EasyBuildError("Invalid response from github (I/O error)", exit_code=EasyBuildExit.FAIL_GITHUB)
def walk(self, top=None, topdown=True):
"""
@@ -243,7 +244,7 @@ def read(self, path, api=True):
if not api:
outfile = tempfile.mkstemp()[1]
url = '/'.join([GITHUB_RAW, self.githubuser, self.reponame, self.branchname, path])
- download_file(os.path.basename(path), url, outfile)
+ download_file(os.path.basename(path), url, outfile, trace=False)
return outfile
else:
obj = self.get_path(path).get(ref=self.branchname)[1]
@@ -314,9 +315,12 @@ def github_api_put_request(request_f, github_user=None, token=None, **kwargs):
if status == 200:
_log.info("Put request successful: %s", data['message'])
elif status in [405, 409]:
- raise EasyBuildError("FAILED: %s", data['message'])
+ raise EasyBuildError("FAILED: %s", data['message'], exit_code=EasyBuildExit.FAIL_GITHUB)
else:
- raise EasyBuildError("FAILED: %s", data.get('message', "(unknown reason)"))
+ raise EasyBuildError(
+ "FAILED: %s", data.get('message', "(unknown reason)"),
+ exit_code=EasyBuildExit.FAIL_GITHUB
+ )
_log.debug("get request result for %s: status: %d, data: %s", url.url, status, data)
return (status, data)
@@ -338,20 +342,23 @@ def fetch_latest_commit_sha(repo, account, branch=None, github_user=None, token=
status, data = github_api_get_request(lambda x: x.repos[account][repo].branches,
github_user=github_user, token=token, per_page=GITHUB_MAX_PER_PAGE)
if status != HTTP_STATUS_OK:
- raise EasyBuildError("Failed to get latest commit sha for branch %s from %s/%s (status: %d %s)",
- branch, account, repo, status, data)
+ raise EasyBuildError(
+ "Failed to get latest commit sha for branch %s from %s/%s (status: %d %s)",
+ branch, account, repo, status, data, exit_code=EasyBuildExit.FAIL_GITHUB
+ )
res = None
for entry in data:
- if entry[u'name'] == branch:
+ if entry['name'] == branch:
res = entry['commit']['sha']
break
if res is None:
- error_msg = "No branch with name %s found in repo %s/%s" % (branch, account, repo)
+ error_msg = f"No branch with name {branch} found in repo {account}/{repo}"
if len(data) >= GITHUB_MAX_PER_PAGE:
- error_msg += "; only %d branches were checked (too many branches in %s/%s?)" % (len(data), account, repo)
- raise EasyBuildError(error_msg + ': ' + ', '.join([x[u'name'] for x in data]))
+ error_msg += f"; only {len(data)} branches were checked (too many branches in {account}/{repo}?)"
+ error_msg += ": " + ", ".join([x['name'] for x in data])
+ raise EasyBuildError(error_msg, exit_code=EasyBuildExit.FAIL_GITHUB)
return res
@@ -386,7 +393,7 @@ def download_repo(repo=GITHUB_EASYCONFIGS_REPO, branch=None, commit=None, accoun
else:
error_msg = r"Specified commit SHA %s for downloading %s/%s is not valid, "
error_msg += "must be full SHA-1 (40 chars)"
- raise EasyBuildError(error_msg, commit, account, repo)
+ raise EasyBuildError(error_msg, commit, account, repo, exit_code=EasyBuildExit.VALUE_ERROR)
extracted_dir_name = '%s-%s' % (repo, commit)
base_name = '%s.tar.gz' % commit
@@ -396,7 +403,9 @@ def download_repo(repo=GITHUB_EASYCONFIGS_REPO, branch=None, commit=None, accoun
base_name = '%s.tar.gz' % branch
latest_commit_sha = fetch_latest_commit_sha(repo, account, branch, github_user=github_user)
else:
- raise EasyBuildError("Either branch or commit should be specified in download_repo")
+ raise EasyBuildError(
+ "Either branch or commit should be specified in download_repo", exit_code=EasyBuildExit.VALUE_ERROR
+ )
expected_path = os.path.join(path, extracted_dir_name)
latest_sha_path = os.path.join(expected_path, 'latest-sha')
@@ -412,13 +421,16 @@ def download_repo(repo=GITHUB_EASYCONFIGS_REPO, branch=None, commit=None, accoun
target_path = os.path.join(path, base_name)
_log.debug("downloading repo %s/%s as archive from %s to %s" % (account, repo, url, target_path))
- downloaded_path = download_file(base_name, url, target_path, forced=True)
+ downloaded_path = download_file(base_name, url, target_path, forced=True, trace=False)
if downloaded_path is None:
- raise EasyBuildError("Failed to download tarball for %s/%s commit %s", account, repo, commit)
+ raise EasyBuildError(
+ "Failed to download tarball for %s/%s commit %s", account, repo, commit,
+ exit_code=EasyBuildExit.FAIL_DOWNLOAD
+ )
else:
- _log.debug("%s downloaded to %s, extracting now" % (base_name, path))
+ _log.debug("%s downloaded to %s, extracting now", base_name, path)
- base_dir = extract_file(target_path, path, forced=True, change_into_dir=False)
+ base_dir = extract_file(target_path, path, forced=True, change_into_dir=False, trace=False)
extracted_path = os.path.join(base_dir, extracted_dir_name)
# check if extracted_path exists
@@ -428,7 +440,7 @@ def download_repo(repo=GITHUB_EASYCONFIGS_REPO, branch=None, commit=None, accoun
error_msg += "at branch " + branch
elif commit:
error_msg += "at commit " + commit
- raise EasyBuildError(error_msg)
+ raise EasyBuildError(error_msg, exit_code=EasyBuildExit.FAIL_EXTRACT)
write_file(latest_sha_path, latest_commit_sha, forced=True)
@@ -490,13 +502,15 @@ def fetch_files_from_pr(pr, path=None, github_user=None, github_account=None, gi
if len(cands) == 1:
path = cands[0]
else:
- raise EasyBuildError("Failed to isolate path for PR #%s from list of PR paths: %s",
- pr, extra_ec_paths)
+ raise EasyBuildError(
+ "Failed to isolate path for PR #%s from list of PR paths: %s", pr, extra_ec_paths,
+ exit_code=EasyBuildExit.FAIL_GITHUB
+ )
elif github_repo == GITHUB_EASYBLOCKS_REPO:
path = os.path.join(tempfile.gettempdir(), 'ebs_pr%s' % pr)
else:
- raise EasyBuildError("Unknown repo: %s" % github_repo)
+ raise EasyBuildError("Unknown repo: %s", github_repo, exit_code=EasyBuildExit.OPTION_ERROR)
if path is None:
path = tempfile.mkdtemp()
@@ -512,7 +526,9 @@ def fetch_files_from_pr(pr, path=None, github_user=None, github_account=None, gi
elif github_repo == GITHUB_EASYBLOCKS_REPO:
easyfiles = 'easyblocks'
else:
- raise EasyBuildError("Don't know how to fetch files from repo %s", github_repo)
+ raise EasyBuildError(
+ "Don't know how to fetch files from repo %s", github_repo, exit_code=EasyBuildExit.OPTION_ERROR
+ )
subdir = os.path.join('easybuild', easyfiles)
@@ -532,7 +548,7 @@ def fetch_files_from_pr(pr, path=None, github_user=None, github_account=None, gi
# determine list of changed files via diff
diff_fn = os.path.basename(pr_data['diff_url'])
diff_filepath = os.path.join(path, diff_fn)
- download_file(diff_fn, pr_data['diff_url'], diff_filepath, forced=True)
+ download_file(diff_fn, pr_data['diff_url'], diff_filepath, forced=True, trace=False)
diff_txt = read_file(diff_filepath)
_log.debug("Diff for PR #%s:\n%s", pr, diff_txt)
@@ -568,7 +584,7 @@ def fetch_files_from_pr(pr, path=None, github_user=None, github_account=None, gi
sha = pr_data['head']['sha']
full_url = URL_SEPARATOR.join([GITHUB_RAW, github_account, github_repo, sha, patched_file])
_log.info("Downloading %s from %s", fn, full_url)
- download_file(fn, full_url, path=os.path.join(path, fn), forced=True)
+ download_file(fn, full_url, path=os.path.join(path, fn), forced=True, trace=False)
final_path = path
@@ -586,7 +602,9 @@ def fetch_files_from_pr(pr, path=None, github_user=None, github_account=None, gi
if os.path.exists(full_path):
files.append(full_path)
else:
- raise EasyBuildError("Couldn't find path to patched file %s", full_path)
+ raise EasyBuildError(
+ "Couldn't find path to patched file %s", full_path, exit_code=EasyBuildExit.OPTION_ERROR
+ )
if github_repo == GITHUB_EASYCONFIGS_REPO:
ver_file = os.path.join(final_path, 'setup.py')
@@ -665,29 +683,34 @@ def fetch_files_from_commit(commit, files=None, path=None, github_account=None,
if len(cands) == 1:
path = cands[0]
else:
- raise EasyBuildError("Failed to isolate path for commit %s from list of commit paths: %s",
- commit, extra_ec_paths)
+ raise EasyBuildError(
+ "Failed to isolate path for commit %s from list of commit paths: %s",
+ commit, extra_ec_paths, exit_code=EasyBuildExit.FAIL_GITHUB
+ )
else:
path = os.path.join(tempfile.gettempdir(), 'ecs_commit_' + commit)
elif github_repo == GITHUB_EASYBLOCKS_REPO:
path = os.path.join(tempfile.gettempdir(), 'ebs_commit_' + commit)
else:
- raise EasyBuildError("Unknown repo: %s" % github_repo)
+ raise EasyBuildError("Unknown repo: %s", github_repo, exit_code=EasyBuildExit.OPTION_ERROR)
# if no files are specified, determine which files are touched in commit
if not files:
diff_url = os.path.join(GITHUB_URL, github_account, github_repo, 'commit', commit + '.diff')
diff_fn = os.path.basename(diff_url)
diff_filepath = os.path.join(path, diff_fn)
- if download_file(diff_fn, diff_url, diff_filepath, forced=True):
+ if download_file(diff_fn, diff_url, diff_filepath, forced=True, trace=False):
diff_txt = read_file(diff_filepath)
_log.debug("Diff for commit %s:\n%s", commit, diff_txt)
files = det_patched_files(txt=diff_txt, omit_ab_prefix=True, github=True, filter_deleted=True)
_log.debug("List of patched files for commit %s: %s", commit, files)
else:
- raise EasyBuildError("Failed to download diff for commit %s of %s/%s", commit, github_account, github_repo)
+ raise EasyBuildError(
+ "Failed to download diff for commit %s of %s/%s", commit, github_account, github_repo,
+ exit_code=EasyBuildExit.FAIL_GITHUB
+ )
# download tarball for specific commit
repo_commit = download_repo(repo=github_repo, commit=commit, account=github_account)
@@ -697,7 +720,7 @@ def fetch_files_from_commit(commit, files=None, path=None, github_account=None,
elif github_repo == GITHUB_EASYBLOCKS_REPO:
files_subdir = 'easybuild/easyblocks/'
else:
- raise EasyBuildError("Unknown repo: %s" % github_repo)
+ raise EasyBuildError("Unknown repo: %s", github_repo, exit_code=EasyBuildExit.OPTION_ERROR)
# symlink subdirectories of 'easybuild/easy{blocks,configs}' into path that gets added to robot search path
mkdir(path, parents=True)
@@ -776,7 +799,9 @@ def create_gist(txt, fn, descr=None, github_user=None, github_token=None):
status, data = g.gists.post(body=body)
if status != HTTP_STATUS_CREATED:
- raise EasyBuildError("Failed to create gist; status %s, data: %s", status, data)
+ raise EasyBuildError(
+ "Failed to create gist; status %s, data: %s", status, data, exit_code=EasyBuildExit.FAIL_GITHUB
+ )
return data['html_url']
@@ -791,7 +816,9 @@ def delete_gist(gist_id, github_user=None, github_token=None):
status, data = gh.gists[gist_id].delete()
if status != HTTP_STATUS_NO_CONTENT:
- raise EasyBuildError("Failed to delete gist with ID %s: status %s, data: %s", status, data)
+ raise EasyBuildError(
+ "Failed to delete gist with ID %s: status %s, data: %s", status, data, exit_code=EasyBuildExit.FAIL_GITHUB
+ )
def post_comment_in_issue(issue, txt, account=GITHUB_EB_MAIN, repo=GITHUB_EASYCONFIGS_REPO, github_user=None):
@@ -800,7 +827,10 @@ def post_comment_in_issue(issue, txt, account=GITHUB_EB_MAIN, repo=GITHUB_EASYCO
try:
issue = int(issue)
except ValueError as err:
- raise EasyBuildError("Failed to parse specified pull request number '%s' as an int: %s; ", issue, err)
+ raise EasyBuildError(
+ "Failed to parse specified pull request number '%s' as an int: %s; ", issue, err,
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
dry_run = build_option('dry_run') or build_option('extended_dry_run')
@@ -817,7 +847,10 @@ def post_comment_in_issue(issue, txt, account=GITHUB_EB_MAIN, repo=GITHUB_EASYCO
status, data = pr_url.comments.post(body={'body': txt})
if not status == HTTP_STATUS_CREATED:
- raise EasyBuildError("Failed to create comment in PR %s#%d; status %s, data: %s", repo, issue, status, data)
+ raise EasyBuildError(
+ "Failed to create comment in PR %s#%d; status %s, data: %s", repo, issue, status, data,
+ exit_code=EasyBuildExit.FAIL_GITHUB
+ )
def init_repo(path, repo_name, silent=False):
@@ -843,13 +876,15 @@ def init_repo(path, repo_name, silent=False):
workrepo = git.Repo(workdir)
workrepo.clone(repo_path)
except GitCommandError as err:
- raise EasyBuildError("Failed to clone git repo at %s: %s", workdir, err)
+ raise EasyBuildError(
+ "Failed to clone git repo at %s: %s", workdir, err, exit_code=EasyBuildExit.FAIL_GITHUB
+ )
# initalize repo in repo_path
try:
repo = git.Repo.init(repo_path)
except GitCommandError as err:
- raise EasyBuildError("Failed to init git repo at %s: %s", repo_path, err)
+ raise EasyBuildError("Failed to init git repo at %s: %s", repo_path, err, exit_code=EasyBuildExit.FAIL_GITHUB)
_log.debug("temporary git working directory ready at %s", repo_path)
@@ -869,7 +904,7 @@ def setup_repo_from(git_repo, github_url, target_account, branch_name, silent=Fa
_log.debug("Cloning from %s", github_url)
if target_account is None:
- raise EasyBuildError("target_account not specified in setup_repo_from!")
+ raise EasyBuildError("target_account not specified in setup_repo_from!", exit_code=EasyBuildExit.OPTION_ERROR)
# salt to use for names of remotes/branches that are created
salt = ''.join(random.choice(ascii_letters) for _ in range(5))
@@ -878,7 +913,7 @@ def setup_repo_from(git_repo, github_url, target_account, branch_name, silent=Fa
origin = git_repo.create_remote(remote_name, github_url)
if not origin.exists():
- raise EasyBuildError("%s does not exist?", github_url)
+ raise EasyBuildError("%s does not exist?", github_url, exit_code=EasyBuildExit.FAIL_GITHUB)
# git fetch
# can't use --depth to only fetch a shallow copy, since pushing to another repo from a shallow copy doesn't work
@@ -887,21 +922,32 @@ def setup_repo_from(git_repo, github_url, target_account, branch_name, silent=Fa
try:
res = origin.fetch()
except GitCommandError as err:
- raise EasyBuildError("Failed to fetch branch '%s' from %s: %s", branch_name, github_url, err)
+ raise EasyBuildError(
+ "Failed to fetch branch '%s' from %s: %s", branch_name, github_url, err,
+ exit_code=EasyBuildExit.FAIL_GITHUB
+ )
if res:
if res[0].flags & res[0].ERROR:
- raise EasyBuildError("Fetching branch '%s' from remote %s failed: %s", branch_name, origin, res[0].note)
+ raise EasyBuildError(
+ "Fetching branch '%s' from remote %s failed: %s", branch_name, origin, res[0].note,
+ exit_code=EasyBuildExit.FAIL_GITHUB
+ )
else:
_log.debug("Fetched branch '%s' from remote %s (note: %s)", branch_name, origin, res[0].note)
else:
- raise EasyBuildError("Fetching branch '%s' from remote %s failed: empty result", branch_name, origin)
+ raise EasyBuildError(
+ "Fetching branch '%s' from remote %s failed: empty result", branch_name, origin,
+ exit_code=EasyBuildExit.FAIL_GITHUB
+ )
# git checkout -b ; git pull
try:
origin_branch = getattr(origin.refs, branch_name)
except AttributeError:
- raise EasyBuildError("Branch '%s' not found at %s", branch_name, github_url)
+ raise EasyBuildError(
+ "Branch '%s' not found at %s", branch_name, github_url, exit_code=EasyBuildExit.FAIL_GITHUB
+ )
_log.debug("Checking out branch '%s' from remote %s", branch_name, github_url)
try:
@@ -912,7 +958,10 @@ def setup_repo_from(git_repo, github_url, target_account, branch_name, silent=Fa
try:
origin_branch.checkout(b=alt_branch, force=True)
except GitCommandError as err:
- raise EasyBuildError("Failed to check out branch '%s' from repo at %s: %s", alt_branch, github_url, err)
+ raise EasyBuildError(
+ "Failed to check out branch '%s' from repo at %s: %s", alt_branch, github_url, err,
+ exit_code=EasyBuildExit.FAIL_GITHUB
+ )
return remote_name
@@ -948,7 +997,7 @@ def setup_repo(git_repo, target_account, target_repo, branch_name, silent=False,
if res:
return res
else:
- raise EasyBuildError('\n'.join(errors))
+ raise EasyBuildError('\n'.join(errors), exit_code=EasyBuildExit.FAIL_GITHUB)
@only_if_module_is_available('git', pkgname='GitPython')
@@ -980,14 +1029,20 @@ def _easyconfigs_pr_common(paths, ecs, start_branch=None, pr_branch=None, start_
ec_paths.append(path)
if non_existing_paths:
- raise EasyBuildError("One or more non-existing paths specified: %s", ', '.join(non_existing_paths))
+ raise EasyBuildError(
+ "One or more non-existing paths specified: %s", ', '.join(non_existing_paths),
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
if not any(paths.values()):
- raise EasyBuildError("No paths specified")
+ raise EasyBuildError("No paths specified", exit_code=EasyBuildExit.OPTION_ERROR)
pr_target_repo = det_pr_target_repo(paths)
if pr_target_repo is None:
- raise EasyBuildError("Failed to determine target repository, please specify it via --pr-target-repo!")
+ raise EasyBuildError(
+ "Failed to determine target repository, please specify it via --pr-target-repo!",
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
# initialize repository
git_working_dir = tempfile.mkdtemp(prefix='git-working-dir')
@@ -995,7 +1050,10 @@ def _easyconfigs_pr_common(paths, ecs, start_branch=None, pr_branch=None, start_
repo_path = os.path.join(git_working_dir, pr_target_repo)
if pr_target_repo not in [GITHUB_EASYCONFIGS_REPO, GITHUB_EASYBLOCKS_REPO, GITHUB_FRAMEWORK_REPO]:
- raise EasyBuildError("Don't know how to create/update a pull request to the %s repository", pr_target_repo)
+ raise EasyBuildError(
+ "Don't know how to create/update a pull request to the %s repository", pr_target_repo,
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
if start_account is None:
start_account = build_option('pr_target_account')
@@ -1006,7 +1064,9 @@ def _easyconfigs_pr_common(paths, ecs, start_branch=None, pr_branch=None, start_
target_account = build_option('github_org') or build_option('github_user')
if target_account is None:
- raise EasyBuildError("--github-org or --github-user must be specified!")
+ raise EasyBuildError(
+ "--github-org or --github-user must be specified!", exit_code=EasyBuildExit.OPTION_ERROR
+ )
# if branch to start from is specified, we're updating an existing PR
start_branch = build_option('pr_target_branch')
@@ -1037,8 +1097,16 @@ def _easyconfigs_pr_common(paths, ecs, start_branch=None, pr_branch=None, start_
elif pr_target_repo == GITHUB_EASYBLOCKS_REPO and all(file_info['new']):
commit_msg = "adding easyblocks: %s" % ', '.join(os.path.basename(p) for p in file_info['paths_in_repo'])
else:
+ msg = ''
+ modified_files = [os.path.basename(p) for new, p in zip(file_info['new'], file_info['paths_in_repo'])
+ if not new]
+ if modified_files:
+ msg += '\nModified: ' + ', '.join(modified_files)
+ if paths['files_to_delete']:
+ msg += '\nDeleted: ' + ', '.join(paths['files_to_delete'])
raise EasyBuildError("A meaningful commit message must be specified via --pr-commit-msg when "
- "modifying/deleting files or targeting the framework repo.")
+ "modifying/deleting files or targeting the framework repo." + msg,
+ exit_code=EasyBuildExit.OPTION_ERROR)
# figure out to which software name patches relate, and copy them to the right place
if paths['patch_files']:
@@ -1059,7 +1127,10 @@ def _easyconfigs_pr_common(paths, ecs, start_branch=None, pr_branch=None, start_
if len(hits) == 1:
deleted_paths.append(hits[0])
else:
- raise EasyBuildError("Path doesn't exist or file to delete isn't found in target branch: %s", fn)
+ raise EasyBuildError(
+ "Path doesn't exist or file to delete isn't found in target branch: %s", fn,
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
dep_info = {
'ecs': [],
@@ -1078,8 +1149,8 @@ def _easyconfigs_pr_common(paths, ecs, start_branch=None, pr_branch=None, start_
# only consider new easyconfig files for dependencies (not updated ones)
for idx in range(len(all_dep_info['ecs'])):
if all_dep_info['new'][idx]:
- for key in dep_info:
- dep_info[key].append(all_dep_info[key][idx])
+ for key, info in dep_info.items():
+ info.append(all_dep_info[key][idx])
# checkout target branch
if pr_branch is None:
@@ -1116,8 +1187,11 @@ def _easyconfigs_pr_common(paths, ecs, start_branch=None, pr_branch=None, start_
diff_stat = git_repo.git.diff(cached=True, stat=True)
if not diff_stat:
- raise EasyBuildError("No changed files found when comparing to current develop branch. "
- "Refused to make empty pull request.")
+ raise EasyBuildError(
+ f"No changed files found when comparing to current {start_branch} branch. "
+ "Refused to make empty pull request.",
+ exit_code=EasyBuildExit.FAIL_GITHUB
+ )
# commit
git_repo.index.commit(commit_msg)
@@ -1148,7 +1222,9 @@ def create_remote(git_repo, account, repo, https=False):
try:
remote = git_repo.create_remote(remote_name, github_url)
except GitCommandError as err:
- raise EasyBuildError("Failed to create remote %s for %s: %s", remote_name, github_url, err)
+ raise EasyBuildError(
+ "Failed to create remote %s for %s: %s", remote_name, github_url, err, exit_code=EasyBuildExit.FAIL_GITHUB
+ )
return remote
@@ -1163,7 +1239,9 @@ def push_branch_to_github(git_repo, target_account, target_repo, branch):
:param branch: name of branch to push
"""
if target_account is None:
- raise EasyBuildError("target_account not specified in push_branch_to_github!")
+ raise EasyBuildError(
+ "target_account not specified in push_branch_to_github!", exit_code=EasyBuildExit.OPTION_ERROR
+ )
# push to GitHub
remote = create_remote(git_repo, target_account, target_repo)
@@ -1180,17 +1258,24 @@ def push_branch_to_github(git_repo, target_account, target_repo, branch):
try:
res = remote.push(branch)
except GitCommandError as err:
- raise EasyBuildError("Failed to push branch '%s' to GitHub (%s): %s", branch, github_url, err)
+ raise EasyBuildError(
+ "Failed to push branch '%s' to GitHub (%s): %s", branch, github_url, err,
+ exit_code=EasyBuildExit.FAIL_GITHUB
+ )
if res:
if res[0].ERROR & res[0].flags:
- raise EasyBuildError("Pushing branch '%s' to remote %s (%s) failed: %s",
- branch, remote, github_url, res[0].summary)
+ raise EasyBuildError(
+ "Pushing branch '%s' to remote %s (%s) failed: %s", branch, remote, github_url, res[0].summary,
+ exit_code=EasyBuildExit.FAIL_GITHUB
+ )
else:
_log.debug("Pushed branch %s to remote %s (%s): %s", branch, remote, github_url, res[0].summary)
else:
- raise EasyBuildError("Pushing branch '%s' to remote %s (%s) failed: empty result",
- branch, remote, github_url)
+ raise EasyBuildError(
+ "Pushing branch '%s' to remote %s (%s) failed: empty result", branch, remote, github_url,
+ exit_code=EasyBuildExit.FAIL_GITHUB
+ )
def is_patch_for(patch_name, ec):
@@ -1247,7 +1332,10 @@ def det_patch_specs(patch_paths, file_info, ec_dirs):
patch_specs.append((patch_path, soft_name))
else:
# still nothing found
- raise EasyBuildError("Failed to determine software name to which patch file %s relates", patch_path)
+ raise EasyBuildError(
+ "Failed to determine software name to which patch file %s relates", patch_path,
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
return patch_specs
@@ -1514,7 +1602,7 @@ def close_pr(pr, motivation_msg=None):
"""
github_user = build_option('github_user')
if github_user is None:
- raise EasyBuildError("GitHub user must be specified to use --close-pr")
+ raise EasyBuildError("GitHub user must be specified to use --close-pr", exit_code=EasyBuildExit.OPTION_ERROR)
pr_target_account = build_option('pr_target_account')
pr_target_repo = build_option('pr_target_repo') or GITHUB_EASYCONFIGS_REPO
@@ -1522,7 +1610,10 @@ def close_pr(pr, motivation_msg=None):
pr_data, _ = fetch_pr_data(pr, pr_target_account, pr_target_repo, github_user, full=True)
if pr_data['state'] == GITHUB_STATE_CLOSED:
- raise EasyBuildError("PR #%d from %s/%s is already closed.", pr, pr_target_account, pr_target_repo)
+ raise EasyBuildError(
+ "PR #%d from %s/%s is already closed.", pr, pr_target_account, pr_target_repo,
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
pr_owner = pr_data['user']['login']
msg = "\n%s/%s PR #%s was submitted by %s, " % (pr_target_account, pr_target_repo, pr, pr_owner)
@@ -1539,8 +1630,10 @@ def close_pr(pr, motivation_msg=None):
possible_reasons = reasons_for_closing(pr_data)
if not possible_reasons:
- raise EasyBuildError("No reason specified and none found from PR data, "
- "please use --close-pr-reasons or --close-pr-msg")
+ raise EasyBuildError(
+ "No reason specified and none found from PR data, please use --close-pr-reasons or --close-pr-msg",
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
else:
motivation_msg = ", ".join([VALID_CLOSE_PR_REASONS[reason] for reason in possible_reasons])
print_msg("\nNo reason specified but found possible reasons: %s.\n" % motivation_msg, prefix=False)
@@ -1559,18 +1652,26 @@ def close_pr(pr, motivation_msg=None):
else:
github_token = fetch_github_token(github_user)
if github_token is None:
- raise EasyBuildError("GitHub token for user '%s' must be available to use --close-pr", github_user)
+ raise EasyBuildError(
+ "GitHub token for user '%s' must be available to use --close-pr", github_user,
+ exit_code=EasyBuildExit.FAIL_GITHUB
+ )
g = RestClient(GITHUB_API_URL, username=github_user, token=github_token)
pull_url = g.repos[pr_target_account][pr_target_repo].pulls[pr]
body = {'state': 'closed'}
status, data = pull_url.post(body=body)
if not status == HTTP_STATUS_OK:
- raise EasyBuildError("Failed to close PR #%s; status %s, data: %s", pr, status, data)
+ raise EasyBuildError(
+ "Failed to close PR #%s; status %s, data: %s", pr, status, data, exit_code=EasyBuildExit.FAIL_GITHUB
+ )
if reopen:
body = {'state': 'open'}
status, data = pull_url.post(body=body)
if not status == HTTP_STATUS_OK:
- raise EasyBuildError("Failed to reopen PR #%s; status %s, data: %s", pr, status, data)
+ raise EasyBuildError(
+ "Failed to reopen PR #%s; status %s, data: %s", pr, status, data,
+ exit_code=EasyBuildExit.FAIL_GITHUB
+ )
def list_prs(params, per_page=GITHUB_MAX_PER_PAGE, github_user=None):
@@ -1606,7 +1707,7 @@ def merge_pr(pr):
"""
github_user = build_option('github_user')
if github_user is None:
- raise EasyBuildError("GitHub user must be specified to use --merge-pr")
+ raise EasyBuildError("GitHub user must be specified to use --merge-pr", exit_code=EasyBuildExit.OPTION_ERROR)
pr_target_account = build_option('pr_target_account')
pr_target_repo = build_option('pr_target_repo') or GITHUB_EASYCONFIGS_REPO
@@ -1618,16 +1719,16 @@ def merge_pr(pr):
msg += "\nPR title: %s\n\n" % pr_data['title']
print_msg(msg, prefix=False)
if pr_data['user']['login'] == github_user:
- raise EasyBuildError("Please do not merge your own PRs!")
+ raise EasyBuildError("Please do not merge your own PRs!", exit_code=EasyBuildExit.OPTION_ERROR)
force = build_option('force')
dry_run = build_option('dry_run') or build_option('extended_dry_run')
if not dry_run:
if pr_data['merged']:
- raise EasyBuildError("This PR is already merged.")
+ raise EasyBuildError("This PR is already merged.", exit_code=EasyBuildExit.OPTION_ERROR)
elif pr_data['state'] == GITHUB_STATE_CLOSED:
- raise EasyBuildError("This PR is closed.")
+ raise EasyBuildError("This PR is closed.", exit_code=EasyBuildExit.OPTION_ERROR)
def merge_url(gh):
"""Utility function to fetch merge URL for a specific PR."""
@@ -1692,7 +1793,7 @@ def post_pr_labels(pr, labels):
pr_url = g.repos[pr_target_account][pr_target_repo].issues[pr]
try:
- status, data = pr_url.labels.post(body=labels)
+ status, _ = pr_url.labels.post(body=labels)
if status == HTTP_STATUS_OK:
print_msg("Added labels %s to PR#%s" % (', '.join(labels), pr), log=_log, prefix=False)
return True
@@ -1711,7 +1812,10 @@ def add_pr_labels(pr, branch=GITHUB_DEVELOP_BRANCH):
"""
pr_target_repo = build_option('pr_target_repo') or GITHUB_EASYCONFIGS_REPO
if pr_target_repo != GITHUB_EASYCONFIGS_REPO:
- raise EasyBuildError("Adding labels to PRs for repositories other than easyconfigs hasn't been implemented yet")
+ raise EasyBuildError(
+ "Adding labels to PRs for repositories other than easyconfigs hasn't been implemented yet",
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
tmpdir = tempfile.mkdtemp()
@@ -1819,11 +1923,17 @@ def new_pr_from_branch(branch_name, title=None, descr=None, pr_target_repo=None,
# fetch GitHub token (required to perform actions on GitHub)
github_user = build_option('github_user')
if github_user is None:
- raise EasyBuildError("GitHub user must be specified to open a pull request")
+ raise EasyBuildError(
+ "GitHub user must be specified to open a pull request",
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
github_token = fetch_github_token(github_user)
if github_token is None:
- raise EasyBuildError("GitHub token for user '%s' must be available to open a pull request", github_user)
+ raise EasyBuildError(
+ "GitHub token for user '%s' must be available to open a pull request", github_user,
+ exit_code=EasyBuildExit.FAIL_GITHUB
+ )
# GitHub organisation or GitHub user where branch is located
github_account = build_option('github_org') or github_user
@@ -1885,7 +1995,10 @@ def new_pr_from_branch(branch_name, title=None, descr=None, pr_target_repo=None,
print_msg('\n'.join(msg), log=_log)
else:
- raise EasyBuildError("No changes in '%s' branch compared to current 'develop' branch!", branch_name)
+ raise EasyBuildError(
+ f"No changes in '{branch_name}' branch compared to current '{pr_target_branch}' branch!",
+ exit_code=EasyBuildExit.FAIL_GITHUB
+ )
# copy repo while target branch is still checked out
tmpdir = tempfile.mkdtemp()
@@ -1918,8 +2031,10 @@ def new_pr_from_branch(branch_name, title=None, descr=None, pr_target_repo=None,
title = "new easyblock%s for %s" % (plural, (', '.join(file_info['eb_names'])))
if title is None:
- raise EasyBuildError("Don't know how to make a PR title for this PR. "
- "Please include a title (use --pr-title)")
+ raise EasyBuildError(
+ "Don't know how to make a PR title for this PR. Please include a title (use --pr-title)",
+ exit_code=EasyBuildExit.FAIL_GITHUB
+ )
full_descr = "(created using `eb --new-pr`)\n"
if descr is not None:
@@ -1956,7 +2071,10 @@ def new_pr_from_branch(branch_name, title=None, descr=None, pr_target_repo=None,
}
status, data = pulls_url.post(body=body)
if not status == HTTP_STATUS_CREATED:
- raise EasyBuildError("Failed to open PR for branch %s; status %s, data: %s", branch_name, status, data)
+ raise EasyBuildError(
+ "Failed to open PR for branch %s; status %s, data: %s", branch_name, status, data,
+ exit_code=EasyBuildExit.FAIL_GITHUB
+ )
print_msg("Opened pull request: %s" % data['html_url'], log=_log, prefix=False)
@@ -1991,11 +2109,11 @@ def new_pr(paths, ecs, title=None, descr=None, commit_msg=None):
patch = patch[0]
elif isinstance(patch, dict):
patch_info = {}
- for key in patch.keys():
- patch_info[key] = patch[key]
- if 'name' not in patch_info.keys():
- raise EasyBuildError("Wrong patch spec '%s', when using a dict 'name' entry must be supplied",
- str(patch))
+ for key, cur_patch in patch.items():
+ patch_info[key] = cur_patch
+ if 'name' not in patch_info:
+ msg = f"Wrong patch spec '{patch}', when using a dict 'name' entry must be supplied"
+ raise EasyBuildError(msg, exit_code=EasyBuildExit.EASYCONFIG_ERROR)
patch = patch_info['name']
if patch not in paths['patch_files'] and not os.path.isfile(os.path.join(os.path.dirname(ec_path),
@@ -2014,7 +2132,7 @@ def det_account_branch_for_pr(pr_id, github_user=None, pr_target_repo=None):
github_user = build_option('github_user')
if github_user is None:
- raise EasyBuildError("GitHub username (--github-user) must be specified!")
+ raise EasyBuildError("GitHub username (--github-user) must be specified!", exit_code=EasyBuildExit.OPTION_ERROR)
pr_target_account = build_option('pr_target_account')
if pr_target_repo is None:
@@ -2086,7 +2204,10 @@ def update_branch(branch_name, paths, ecs, github_account=None, commit_msg=None)
commit_msg = build_option('pr_commit_msg')
if commit_msg is None:
- raise EasyBuildError("A meaningful commit message must be specified via --pr-commit-msg when using --update-pr")
+ raise EasyBuildError(
+ "A meaningful commit message must be specified via --pr-commit-msg when using --update-pr",
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
if github_account is None:
github_account = build_option('github_user') or build_option('github_org')
@@ -2117,7 +2238,10 @@ def update_pr(pr_id, paths, ecs, commit_msg=None):
pr_target_repo = det_pr_target_repo(paths)
if pr_target_repo is None:
- raise EasyBuildError("Failed to determine target repository, please specify it via --pr-target-repo!")
+ raise EasyBuildError(
+ "Failed to determine target repository, please specify it via --pr-target-repo!",
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
github_account, branch_name = det_account_branch_for_pr(pr_id, pr_target_repo=pr_target_repo)
@@ -2179,7 +2303,9 @@ def check_github():
print_msg("OK\n", log=_log, prefix=False)
else:
print_msg("FAIL (%s)", ', '.join(online_state), log=_log, prefix=False)
- raise EasyBuildError("checking status of GitHub integration must be done online")
+ raise EasyBuildError(
+ "checking status of GitHub integration must be done online", exit_code=EasyBuildExit.FAIL_GITHUB
+ )
# GitHub user
print_msg("* GitHub user...", log=_log, prefix=False, newline=False)
@@ -2356,7 +2482,7 @@ def check_github():
msg = '\n'.join([
'',
"One or more checks FAILed, GitHub configuration not fully complete!",
- "See http://easybuild.readthedocs.org/en/latest/Integration_with_GitHub.html#configuration for help.",
+ "See https://docs.easybuild.io/integration-with-github/#github_configuration for help.",
'',
])
print_msg(msg, log=_log, prefix=False)
@@ -2413,7 +2539,9 @@ def install_github_token(github_user, silent=False):
:param silent: keep quiet (don't print any messages)
"""
if github_user is None:
- raise EasyBuildError("GitHub user must be specified to install GitHub token")
+ raise EasyBuildError(
+ "GitHub user must be specified to install GitHub token", exit_code=EasyBuildExit.OPTION_ERROR
+ )
# check if there's a token available already
current_token = fetch_github_token(github_user)
@@ -2424,8 +2552,10 @@ def install_github_token(github_user, silent=False):
msg = "WARNING: overwriting installed token '%s' for user '%s'..." % (current_token, github_user)
print_msg(msg, prefix=False, silent=silent)
else:
- raise EasyBuildError("Installed token '%s' found for user '%s', not overwriting it without --force",
- current_token, github_user)
+ raise EasyBuildError(
+ "Installed token '%s' found for user '%s', not overwriting it without --force",
+ current_token, github_user, exit_code=EasyBuildExit.OPTION_ERROR
+ )
# get token to install
token = getpass.getpass(prompt="Token: ").strip()
@@ -2436,7 +2566,10 @@ def install_github_token(github_user, silent=False):
if valid_token:
print_msg("Token seems to be valid, installing it.", prefix=False, silent=silent)
else:
- raise EasyBuildError("Token validation failed, not installing it. Please verify your token and try again.")
+ raise EasyBuildError(
+ "Token validation failed, not installing it. Please verify your token and try again.",
+ exit_code=EasyBuildExit.FAIL_GITHUB
+ )
# install token
keyring.set_password(KEYRING_GITHUB_TOKEN, github_user, token)
@@ -2510,7 +2643,7 @@ def find_easybuild_easyconfig(github_user=None):
if file_versions:
fn = sorted(file_versions)[-1][1]
else:
- raise EasyBuildError("Couldn't find any EasyBuild easyconfigs")
+ raise EasyBuildError("Couldn't find any EasyBuild easyconfigs", exit_code=EasyBuildExit.MISSING_EASYCONFIG)
eb_file = os.path.join(eb_parent_path, fn)
return eb_file
@@ -2537,8 +2670,11 @@ def check_suites_url(gh):
# first check combined commit status (set by e.g. Travis CI)
status, commit_status_data = github_api_get_request(commit_status_url, github_user)
if status != HTTP_STATUS_OK:
- raise EasyBuildError("Failed to get status of commit %s from %s/%s (status: %d %s)",
- commit_sha, account, repo, status, commit_status_data)
+ raise EasyBuildError(
+ "Failed to get status of commit %s from %s/%s (status: %d %s)",
+ commit_sha, account, repo, status, commit_status_data,
+ exit_code=EasyBuildExit.FAIL_GITHUB
+ )
commit_status_count = commit_status_data['total_count']
combined_commit_status = commit_status_data['state']
@@ -2586,7 +2722,9 @@ def check_suites_url(gh):
break
else:
app_name = check_suite_data.get('app', {}).get('name', 'UNKNOWN')
- raise EasyBuildError("Unknown check suite status set by %s: '%s'", app_name, status)
+ raise EasyBuildError(
+ "Unknown check suite status set by %s: '%s'", app_name, status, exit_code=EasyBuildExit.FAIL_GITHUB
+ )
return result
@@ -2604,13 +2742,16 @@ def pr_url(gh):
try:
status, pr_data = github_api_get_request(pr_url, github_user, **parameters)
except HTTPError as err:
- raise EasyBuildError("Failed to get data for PR #%d from %s/%s (%s)\n"
- "Please check PR #, account and repo.",
- pr, pr_target_account, pr_target_repo, err)
+ raise EasyBuildError(
+ "Failed to get data for PR #%d from %s/%s (%s)\nPlease check PR #, account and repo.",
+ pr, pr_target_account, pr_target_repo, err, exit_code=EasyBuildExit.FAIL_GITHUB
+ )
if status != HTTP_STATUS_OK:
- raise EasyBuildError("Failed to get data for PR #%d from %s/%s (status: %d %s)",
- pr, pr_target_account, pr_target_repo, status, pr_data)
+ raise EasyBuildError(
+ "Failed to get data for PR #%d from %s/%s (status: %d %s)",
+ pr, pr_target_account, pr_target_repo, status, pr_data, exit_code=EasyBuildExit.FAIL_GITHUB
+ )
if full:
# also fetch status of last commit
@@ -2624,8 +2765,10 @@ def comments_url(gh):
status, comments_data = github_api_get_request(comments_url, github_user, **parameters)
if status != HTTP_STATUS_OK:
- raise EasyBuildError("Failed to get comments for PR #%d from %s/%s (status: %d %s)",
- pr, pr_target_account, pr_target_repo, status, comments_data)
+ raise EasyBuildError(
+ "Failed to get comments for PR #%d from %s/%s (status: %d %s)",
+ pr, pr_target_account, pr_target_repo, status, comments_data, exit_code=EasyBuildExit.FAIL_GITHUB
+ )
pr_data['issue_comments'] = comments_data
# also fetch reviews
@@ -2635,8 +2778,10 @@ def reviews_url(gh):
status, reviews_data = github_api_get_request(reviews_url, github_user, **parameters)
if status != HTTP_STATUS_OK:
- raise EasyBuildError("Failed to get reviews for PR #%d from %s/%s (status: %d %s)",
- pr, pr_target_account, pr_target_repo, status, reviews_data)
+ raise EasyBuildError(
+ "Failed to get reviews for PR #%d from %s/%s (status: %d %s)",
+ pr, pr_target_account, pr_target_repo, status, reviews_data, exit_code=EasyBuildExit.FAIL_GITHUB
+ )
pr_data['reviews'] = reviews_data
return pr_data, pr_url
@@ -2683,7 +2828,9 @@ def sync_pr_with_develop(pr_id):
"""Sync pull request with specified ID with current develop branch."""
github_user = build_option('github_user')
if github_user is None:
- raise EasyBuildError("GitHub user must be specified to use --sync-pr-with-develop")
+ raise EasyBuildError(
+ "GitHub user must be specified to use --sync-pr-with-develop", exit_code=EasyBuildExit.OPTION_ERROR
+ )
target_account = build_option('pr_target_account')
target_repo = build_option('pr_target_repo') or GITHUB_EASYCONFIGS_REPO
@@ -2706,7 +2853,9 @@ def sync_branch_with_develop(branch_name):
"""Sync branch with specified name with current develop branch."""
github_user = build_option('github_user')
if github_user is None:
- raise EasyBuildError("GitHub user must be specified to use --sync-branch-with-develop")
+ raise EasyBuildError(
+ "GitHub user must be specified to use --sync-branch-with-develop", exit_code=EasyBuildExit.OPTION_ERROR
+ )
target_account = build_option('pr_target_account')
target_repo = build_option('pr_target_repo') or GITHUB_EASYCONFIGS_REPO
diff --git a/easybuild/tools/hooks.py b/easybuild/tools/hooks.py
index f018c40607..0165717b39 100644
--- a/easybuild/tools/hooks.py
+++ b/easybuild/tools/hooks.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2017-2024 Ghent University
+# Copyright 2017-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -33,9 +33,9 @@
import os
from easybuild.base import fancylogger
-from easybuild.tools.py2vs3 import load_source
from easybuild.tools.build_log import EasyBuildError, print_msg
from easybuild.tools.config import build_option
+from importlib.util import spec_from_file_location, module_from_spec
_log = fancylogger.getLogger('hooks', fname=False)
@@ -44,6 +44,7 @@
CLEANUP_STEP = 'cleanup'
CONFIGURE_STEP = 'configure'
EXTENSIONS_STEP = 'extensions'
+EXTRACT_STEP = 'extract'
FETCH_STEP = 'fetch'
INSTALL_STEP = 'install'
MODULE_STEP = 'module'
@@ -55,7 +56,6 @@
PREPARE_STEP = 'prepare'
READY_STEP = 'ready'
SANITYCHECK_STEP = 'sanitycheck'
-SOURCE_STEP = 'source'
TEST_STEP = 'test'
TESTCASES_STEP = 'testcases'
@@ -67,6 +67,7 @@
END = 'end'
CANCEL = 'cancel'
+CRASH = 'crash'
FAIL = 'fail'
RUN_SHELL_CMD = 'run_shell_cmd'
@@ -76,7 +77,7 @@
HOOK_SUFF = '_hook'
# list of names for steps in installation procedure (in order of execution)
-STEP_NAMES = [FETCH_STEP, READY_STEP, SOURCE_STEP, PATCH_STEP, PREPARE_STEP, CONFIGURE_STEP, BUILD_STEP, TEST_STEP,
+STEP_NAMES = [FETCH_STEP, READY_STEP, EXTRACT_STEP, PATCH_STEP, PREPARE_STEP, CONFIGURE_STEP, BUILD_STEP, TEST_STEP,
INSTALL_STEP, EXTENSIONS_STEP, POSTITER_STEP, POSTPROC_STEP, SANITYCHECK_STEP, CLEANUP_STEP, MODULE_STEP,
PERMISSIONS_STEP, PACKAGE_STEP, TESTCASES_STEP]
@@ -107,6 +108,7 @@
POST_PREF + BUILD_AND_INSTALL_LOOP,
END,
CANCEL,
+ CRASH,
FAIL,
PRE_PREF + RUN_SHELL_CMD,
POST_PREF + RUN_SHELL_CMD,
@@ -118,6 +120,14 @@
_cached_hooks = {}
+def load_source(filename, path):
+ """Load file as Python module"""
+ spec = spec_from_file_location(filename, path)
+ module = module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
+
+
def load_hooks(hooks_path):
"""Load defined hooks (if any)."""
diff --git a/easybuild/tools/include.py b/easybuild/tools/include.py
index 0171d74f10..96e5bfd66c 100644
--- a/easybuild/tools/include.py
+++ b/easybuild/tools/include.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python
# #
-# Copyright 2015-2024 Ghent University
+# Copyright 2015-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -37,7 +37,7 @@
from easybuild.base import fancylogger
from easybuild.tools.build_log import EasyBuildError
-from easybuild.tools.filetools import expand_glob_paths, read_file, symlink
+from easybuild.tools.filetools import expand_glob_paths, read_file, symlink, EASYBLOCK_CLASS_PREFIX
# these are imported just to we can reload them later
import easybuild.tools.module_naming_scheme
import easybuild.toolchains
@@ -157,7 +157,8 @@ def verify_imports(pymods, pypkg, from_path):
def is_software_specific_easyblock(module):
"""Determine whether Python module at specified location is a software-specific easyblock."""
- return bool(re.search(r'^class EB_.*\(.*\):\s*$', read_file(module), re.M))
+ # All software-specific easyblocks start with the prefix and derive from another class, at least EasyBlock
+ return bool(re.search(r"^class %s[^(:]+\([^)]+\):\s*$" % EASYBLOCK_CLASS_PREFIX, read_file(module), re.M))
def include_easyblocks(tmpdir, paths):
@@ -198,10 +199,8 @@ def include_easyblocks(tmpdir, paths):
# hard inject location to included (generic) easyblocks into Python search path
# only prepending to sys.path is not enough due to 'pkgutil.extend_path' in easybuild/easyblocks/__init__.py
- new_path = os.path.join(easyblocks_path, 'easybuild', 'easyblocks')
- easybuild.easyblocks.__path__.insert(0, new_path)
- new_path = os.path.join(new_path, 'generic')
- easybuild.easyblocks.generic.__path__.insert(0, new_path)
+ easybuild.easyblocks.__path__.insert(0, easyblocks_dir)
+ easybuild.easyblocks.generic.__path__.insert(0, os.path.join(easyblocks_dir, 'generic'))
# sanity check: verify that included easyblocks can be imported (from expected location)
for subdir, ebs in [('', included_ebs), ('generic', included_generic_ebs)]:
diff --git a/easybuild/tools/jenkins.py b/easybuild/tools/jenkins.py
index 50ee563083..771331118a 100644
--- a/easybuild/tools/jenkins.py
+++ b/easybuild/tools/jenkins.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/tools/job/backend.py b/easybuild/tools/job/backend.py
index 1219883740..9b372c5848 100644
--- a/easybuild/tools/job/backend.py
+++ b/easybuild/tools/job/backend.py
@@ -1,5 +1,5 @@
##
-# Copyright 2015-2024 Ghent University
+# Copyright 2015-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -32,6 +32,7 @@
"""
from abc import ABCMeta, abstractmethod
+from types import SimpleNamespace
from easybuild.base import fancylogger
from easybuild.tools.config import get_job_backend
@@ -69,7 +70,7 @@ def make_job(self, script, name, env_vars=None, hours=None, cores=None):
See the `Job`:class: constructor for an explanation of what
the arguments are.
"""
- pass
+ return SimpleNamespace()
@abstractmethod
def queue(self, job, dependencies=frozenset()):
diff --git a/easybuild/tools/job/gc3pie.py b/easybuild/tools/job/gc3pie.py
index f6b57e0f2c..b912490d1f 100644
--- a/easybuild/tools/job/gc3pie.py
+++ b/easybuild/tools/job/gc3pie.py
@@ -1,5 +1,5 @@
##
-# Copyright 2015-2024 Ghent University
+# Copyright 2015-2025 Ghent University
# Copyright 2015 S3IT, University of Zurich
#
# This file is part of EasyBuild,
@@ -104,6 +104,10 @@ def __init__(self, *args, **kwargs):
def _check_version(self):
"""Check whether GC3Pie version complies with required version."""
+ deprecation_msg = "The GC3Pie job backend is no longer maintained and will be removed"
+ deprecation_msg += ", please use a different job backend"
+ _log.deprecated(deprecation_msg, '6.0')
+
try:
from pkg_resources import get_distribution, DistributionNotFound
pkg = get_distribution('gc3pie')
diff --git a/easybuild/tools/job/pbs_python.py b/easybuild/tools/job/pbs_python.py
index bf90b57063..222479bd29 100644
--- a/easybuild/tools/job/pbs_python.py
+++ b/easybuild/tools/job/pbs_python.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -39,6 +39,7 @@
from easybuild.base import fancylogger
from easybuild.tools.build_log import EasyBuildError, print_msg
from easybuild.tools.config import JOB_DEPS_TYPE_ABORT_ON_ERROR, JOB_DEPS_TYPE_ALWAYS_RUN, build_option
+from easybuild.tools.filetools import get_cwd
from easybuild.tools.job.backend import JobBackend
from easybuild.tools.utilities import only_if_module_is_available
@@ -320,8 +321,8 @@ def _submit(self):
self.log.debug("Job hold attributes: %s" % hold_attributes[0].value)
# add a bunch of variables (added by qsub)
- # also set PBS_O_WORKDIR to os.getcwd()
- os.environ.setdefault('WORKDIR', os.getcwd())
+ # also set PBS_O_WORKDIR to current working dir
+ os.environ.setdefault('WORKDIR', get_cwd())
defvars = ['MAIL', 'HOME', 'PATH', 'SHELL', 'WORKDIR']
pbsvars = ["PBS_O_%s=%s" % (x, os.environ.get(x, 'NOTFOUND_%s' % x)) for x in defvars]
diff --git a/easybuild/tools/job/slurm.py b/easybuild/tools/job/slurm.py
index 2666cbd6af..28d39d278e 100644
--- a/easybuild/tools/job/slurm.py
+++ b/easybuild/tools/job/slurm.py
@@ -1,5 +1,5 @@
##
-# Copyright 2018-2024 Ghent University
+# Copyright 2018-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -37,7 +37,7 @@
from easybuild.tools.config import JOB_DEPS_TYPE_ABORT_ON_ERROR, JOB_DEPS_TYPE_ALWAYS_RUN, build_option
from easybuild.tools.job.backend import JobBackend
from easybuild.tools.filetools import which
-from easybuild.tools.run import run_cmd
+from easybuild.tools.run import run_shell_cmd
_log = fancylogger.getLogger('slurm', fname=False)
@@ -78,8 +78,8 @@ def __init__(self, *args, **kwargs):
def _check_version(self):
"""Check whether version of Slurm complies with required version."""
- (out, _) = run_cmd("sbatch --version", trace=False)
- slurm_ver = out.strip().split(' ')[-1]
+ res = run_shell_cmd("sbatch --version", hidden=True)
+ slurm_ver = res.output.strip().split(' ')[-1]
self.log.info("Found Slurm version %s", slurm_ver)
if LooseVersion(slurm_ver) < LooseVersion(self.REQ_VERSION):
@@ -116,16 +116,16 @@ def queue(self, job, dependencies=frozenset()):
else:
submit_cmd += ' --%s "%s"' % (key, job.job_specs[key])
- (out, _) = run_cmd(submit_cmd, trace=False)
+ cmd_res = run_shell_cmd(submit_cmd, hidden=True)
jobid_regex = re.compile("^Submitted batch job (?P[0-9]+)")
- res = jobid_regex.search(out)
- if res:
- job.jobid = res.group('jobid')
+ regex_res = jobid_regex.search(cmd_res.output)
+ if regex_res:
+ job.jobid = regex_res.group('jobid')
self.log.info("Job submitted, got job ID %s", job.jobid)
else:
- raise EasyBuildError("Failed to determine job ID from output of submission command: %s", out)
+ raise EasyBuildError("Failed to determine job ID from output of submission command: %s", cmd_res.output)
self._submitted.append(job)
@@ -142,7 +142,7 @@ def complete(self):
job_ids.append(job.jobid)
if job_ids:
- run_cmd("scontrol release %s" % ' '.join(job_ids), trace=False)
+ run_shell_cmd("scontrol release %s" % ' '.join(job_ids), hidden=True)
submitted_jobs = '; '.join(["%s (%s): %s" % (job.name, job.module, job.jobid) for job in self._submitted])
print_msg("List of submitted jobs (%d): %s" % (len(self._submitted), submitted_jobs), log=self.log)
diff --git a/easybuild/tools/loose_version.py b/easybuild/tools/loose_version.py
index 1855fee74a..a454912ed0 100644
--- a/easybuild/tools/loose_version.py
+++ b/easybuild/tools/loose_version.py
@@ -1,21 +1,18 @@
-# This file contains the LooseVersion class based on the class with the same name
-# as present in Python 3.7.4 distutils.
-# The original class is licensed under the Python Software Foundation License Version 2.
-# It was slightly simplified as needed to make it shorter and easier to read.
-# In particular the following changes were made:
-# - Subclass object directly instead of abstract Version class
-# - Fully init the class in the constructor removing the parse method
-# - Always set self.vstring and self.version
-# - Shorten the comparison operators as the NotImplemented case doesn't apply anymore
-# - Changes to documentation and formatting
+"""
+This file contains the LooseVersion class based on the class with the same name
+as present in Python 3.7.4 distutils.
+The original class is licensed under the Python Software Foundation License Version 2.
+It was slightly simplified as needed to make it shorter and easier to read.
+In particular the following changes were made:
+- Subclass object directly instead of abstract Version class
+- Fully init the class in the constructor removing the parse method
+- Always set self.vstring and self.version
+- Shorten the comparison operators as the NotImplemented case doesn't apply anymore
+- Changes to documentation and formatting
+"""
import re
-# Modified: Make this compatible with Python 2
-try:
- from itertools import zip_longest
-except ImportError:
- # Python 2
- from itertools import izip_longest as zip_longest
+from itertools import zip_longest
class LooseVersion(object):
@@ -53,6 +50,22 @@ def version(self):
"""Readonly access to the parsed version (list or None)"""
return self._version
+ def is_prerelease(self, other, markers):
+ """Check if this is a prerelease of other
+
+ Markers is a list of strings that denote a prerelease
+ """
+ if isinstance(other, str):
+ vstring = other
+ else:
+ vstring = other._vstring
+ if self._vstring.startswith(vstring):
+ prerelease = self._vstring[len(vstring):]
+ for marker in markers:
+ if prerelease.startswith(marker):
+ return True
+ return False
+
def __str__(self):
return self._vstring
@@ -64,17 +77,19 @@ def _cmp(self, other):
if isinstance(other, str):
other = LooseVersion(other)
- # Modified: Behave the same in Python 2 & 3 when parts are of different types
- # Taken from https://bugs.python.org/issue14894
- for i, j in zip_longest(self.version, other.version, fillvalue=''):
- if not type(i) is type(j):
+ # Modified: Use string comparison for different types and fill with zeroes/empty strings
+ # Based on https://bugs.python.org/issue14894
+ for i, j in zip_longest(self.version, other.version):
+ if i is None:
+ i = 0 if isinstance(j, int) else ''
+ elif j is None:
+ j = 0 if isinstance(i, int) else ''
+ elif not type(i) is type(j):
i = str(i)
j = str(j)
- if i == j:
- continue
- elif i < j:
+ if i < j:
return -1
- else: # i > j
+ if i > j:
return 1
return 0
diff --git a/easybuild/tools/module_generator.py b/easybuild/tools/module_generator.py
index 5704d24cb0..eebafb3c4e 100644
--- a/easybuild/tools/module_generator.py
+++ b/easybuild/tools/module_generator.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -36,19 +36,20 @@
* Damian Alvarez (Forschungszentrum Juelich GmbH)
"""
import copy
+import itertools
import os
import re
import tempfile
+from collections import defaultdict
from contextlib import contextmanager
-from easybuild.tools import LooseVersion
from textwrap import wrap
from easybuild.base import fancylogger
+from easybuild.tools import LooseVersion
from easybuild.tools.build_log import EasyBuildError, print_warning
from easybuild.tools.config import build_option, get_module_syntax, install_path
from easybuild.tools.filetools import convert_name, mkdir, read_file, remove_file, resolve_path, symlink, write_file
from easybuild.tools.modules import ROOT_ENV_VAR_NAME_PREFIX, EnvironmentModulesC, Lmod, modules_tool
-from easybuild.tools.py2vs3 import string_type
from easybuild.tools.utilities import get_subclasses, nub, quote_str
@@ -154,7 +155,7 @@ def start_module_creation(self):
raise EasyBuildError('Module creation already in process. '
'You cannot create multiple modules at the same time!')
# Mapping of keys/env vars to paths already added
- self.added_paths_per_key = dict()
+ self.added_paths_per_key = defaultdict(set)
txt = self.MODULE_SHEBANG
if txt:
txt += '\n'
@@ -206,16 +207,20 @@ def get_modules_path(self, fake=False, mod_path_suffix=None):
return os.path.join(mod_path, mod_path_suffix)
- def _filter_paths(self, key, paths):
- """Filter out paths already added to key and return the remaining ones"""
+ def _filter_paths(self, key, paths, warn_exists=True):
+ """
+ Filter out paths already added to key and return the remaining ones
+
+ :param warn_exists: Show a warning for paths already added to the key
+ """
if self.added_paths_per_key is None:
# For compatibility this is only a warning for now and we don't filter any paths
print_warning('Module creation has not been started. Call start_module_creation first!')
return paths
- added_paths = self.added_paths_per_key.setdefault(key, set())
+ added_paths = self.added_paths_per_key[key]
# paths can be a string
- if isinstance(paths, string_type):
+ if isinstance(paths, str):
if paths in added_paths:
filtered_paths = None
else:
@@ -227,15 +232,17 @@ def _filter_paths(self, key, paths):
paths = list(paths)
filtered_paths = [x for x in paths if x not in added_paths and not added_paths.add(x)]
if filtered_paths != paths:
- removed_paths = paths if filtered_paths is None else [x for x in paths if x not in filtered_paths]
- print_warning("Suppressed adding the following path(s) to $%s of the module as they were already added: %s",
- key, removed_paths,
- log=self.log)
+ if warn_exists:
+ removed_paths = paths if filtered_paths is None else [x for x in paths if x not in filtered_paths]
+ print_warning("Suppressed adding the following path(s) to $%s of the module "
+ "as they were already added: %s",
+ key, removed_paths,
+ log=self.log)
if not filtered_paths:
filtered_paths = None
return filtered_paths
- def append_paths(self, key, paths, allow_abs=False, expand_relpaths=True):
+ def append_paths(self, key, paths, allow_abs=False, expand_relpaths=True, delim=':', warn_exists=True):
"""
Generate append-path statements for the given list of paths.
@@ -243,13 +250,16 @@ def append_paths(self, key, paths, allow_abs=False, expand_relpaths=True):
:param paths: list of paths to append
:param allow_abs: allow providing of absolute paths
:param expand_relpaths: expand relative paths into absolute paths (by prefixing install dir)
+ :param delim: delimiter used between paths
+ :param warn_exists: Show a warning if any path was already added to the variable
"""
- paths = self._filter_paths(key, paths)
+ paths = self._filter_paths(key, paths, warn_exists=warn_exists)
if paths is None:
return ''
- return self.update_paths(key, paths, prepend=False, allow_abs=allow_abs, expand_relpaths=expand_relpaths)
+ return self.update_paths(key, paths, prepend=False, allow_abs=allow_abs, expand_relpaths=expand_relpaths,
+ delim=delim)
- def prepend_paths(self, key, paths, allow_abs=False, expand_relpaths=True):
+ def prepend_paths(self, key, paths, allow_abs=False, expand_relpaths=True, delim=':', warn_exists=True):
"""
Generate prepend-path statements for the given list of paths.
@@ -257,11 +267,14 @@ def prepend_paths(self, key, paths, allow_abs=False, expand_relpaths=True):
:param paths: list of paths to append
:param allow_abs: allow providing of absolute paths
:param expand_relpaths: expand relative paths into absolute paths (by prefixing install dir)
+ :param delim: delimiter used between paths
+ :param warn_exists: Show a warning if any path was already added to the variable
"""
- paths = self._filter_paths(key, paths)
+ paths = self._filter_paths(key, paths, warn_exists=warn_exists)
if paths is None:
return ''
- return self.update_paths(key, paths, prepend=True, allow_abs=allow_abs, expand_relpaths=expand_relpaths)
+ return self.update_paths(key, paths, prepend=True, allow_abs=allow_abs, expand_relpaths=expand_relpaths,
+ delim=delim)
def _modulerc_check_module_version(self, module_version):
"""
@@ -347,7 +360,7 @@ def modulerc(self, module_version=None, filepath=None, modulerc_txt=None):
module_version_statement = "module-version %(modname)s %(sym_version)s"
- # for Environment Modules we need to guard the module-version statement,
+ # for EnvironmentModulesC we need to guard the module-version statement,
# to avoid "Duplicate version symbol" warning messages where EasyBuild trips over,
# which occur because the .modulerc is parsed twice
# "module-info version " returns its argument if that argument is not a symbolic version (yet),
@@ -388,7 +401,7 @@ def is_loaded(self, mod_names):
:param mod_names: (list of) module name(s) to check load status for
"""
- if isinstance(mod_names, string_type):
+ if isinstance(mod_names, str):
res = self.IS_LOADED_TEMPLATE % mod_names
else:
res = [self.IS_LOADED_TEMPLATE % m for m in mod_names]
@@ -416,7 +429,7 @@ def unpack_setenv_value(self, env_var_name, env_var_val):
use_pushenv = False
# value may be specified as a string, or as a dict for special cases
- if isinstance(env_var_val, string_type):
+ if isinstance(env_var_val, str):
value = env_var_val
elif isinstance(env_var_val, dict):
@@ -484,13 +497,13 @@ def getenv_cmd(self, envvar, default=None):
"""
raise NotImplementedError
- def load_module(self, mod_name, recursive_unload=False, depends_on=False, unload_modules=None, multi_dep_mods=None):
+ def load_module(self, mod_name, recursive_unload=False, depends_on=None, unload_modules=None, multi_dep_mods=None):
"""
Generate load statement for specified module.
:param mod_name: name of module to generate load statement for
:param recursive_unload: boolean indicating whether the 'load' statement should be reverted on unload
- :param depends_on: use depends_on statements rather than (guarded) load statements
+ :param depends_on: use depends_on statements rather than (guarded) load statements (DEPRECATED)
:param unload_modules: name(s) of module to unload first
:param multi_dep_mods: list of module names in multi_deps context, to use for guarding load statement
"""
@@ -552,15 +565,16 @@ def unload_module(self, mod_name):
"""
raise NotImplementedError
- def update_paths(self, key, paths, prepend=True, allow_abs=False, expand_relpaths=True):
+ def update_paths(self, key, paths, prepend=True, allow_abs=False, expand_relpaths=True, delim=':'):
"""
Generate prepend-path or append-path statements for the given list of paths.
:param key: environment variable to prepend/append paths to
- :param paths: list of paths to prepend
+ :param paths: list of paths to prepend/append
:param prepend: whether to prepend (True) or append (False) paths
:param allow_abs: allow providing of absolute paths
:param expand_relpaths: expand relative paths into absolute paths (by prefixing install dir)
+ :param delim: delimiter used between paths
"""
raise NotImplementedError
@@ -610,21 +624,6 @@ def use(self, paths, prefix=None, guarded=False, user_modpath=None):
"""
raise NotImplementedError
- def _generate_extension_list(self):
- """
- Generate a string with a list of extensions.
-
- The name and version are separated by name_version_sep and each extension is separated by ext_sep
- """
- return self.app.make_extension_string()
-
- def _generate_extensions_list(self):
- """
- Generate a list of all extensions in name/version format
- """
- exts_str = self.app.make_extension_string(name_version_sep='/', ext_sep=',')
- return exts_str.split(',') if exts_str else []
-
def _generate_help_text(self):
"""
Generate syntax-independent help text used for `module help`.
@@ -671,7 +670,7 @@ def _generate_help_text(self):
lines.extend(self._generate_section("Compatible modules", compatible_modules_txt))
# Extensions (if any)
- extensions = self._generate_extension_list()
+ extensions = self.app.make_extension_string()
lines.extend(self._generate_section("Included extensions", '\n'.join(wrap(extensions, 78))))
return '\n'.join(lines)
@@ -682,21 +681,13 @@ def _generate_multi_deps_list(self):
"""
multi_deps = []
if self.app.cfg['multi_deps']:
- for key in sorted(self.app.cfg['multi_deps'].keys()):
- mod_list = []
- txt = ''
- vlist = self.app.cfg['multi_deps'].get(key)
- for idx in range(len(vlist)):
- for deplist in self.app.cfg.multi_deps:
- for dep in deplist:
- if dep['name'] == key and dep['version'] == vlist[idx]:
- modname = dep['short_mod_name']
- # indicate which version is loaded by default (unless that's disabled)
- if idx == 0 and self.app.cfg['multi_deps_load_default']:
- modname += ' (default)'
- mod_list.append(modname)
- txt += ', '.join(mod_list)
- multi_deps.append(txt)
+ for name in sorted(self.app.cfg['multi_deps']):
+ mod_list = [dep['short_mod_name'] for dep in itertools.chain.from_iterable(self.app.cfg.multi_deps)
+ if dep['name'] == name]
+ # indicate that the first version is loaded by default if enabled
+ if self.app.cfg['multi_deps_load_default']:
+ mod_list[0] += ' (default)'
+ multi_deps.append(', '.join(mod_list))
return multi_deps
@@ -728,7 +719,7 @@ def _generate_whatis_lines(self):
if multi_deps:
whatis.append("Compatible modules: %s" % ', '.join(multi_deps))
- extensions = self._generate_extension_list()
+ extensions = self.app.make_extension_string()
if extensions:
whatis.append("Extensions: %s" % extensions)
@@ -758,8 +749,18 @@ def check_group(self, group, error_msg=None):
:param group: string with the group name
:param error_msg: error message to print for users outside that group
"""
- self.log.warning("Can't generate robust check in TCL modules for users belonging to group %s.", group)
- return ''
+ if self.modules_tool.supports_tcl_check_group:
+ if error_msg is None:
+ error_msg = "You are not part of '%s' group of users that have access to this software; " % group
+ error_msg += "Please consult with user support how to become a member of this group"
+
+ error_msg = 'error "%s"' % error_msg
+ res = self.conditional_statement('module-info usergroups %s' % group, error_msg, negative=True)
+ else:
+ self.log.warning("Can't generate robust check in Tcl modules for users belonging to group %s.", group)
+ res = ''
+
+ return res
def comment(self, msg):
"""Return string containing given message as a comment."""
@@ -778,7 +779,7 @@ def conditional_statement(self, conditions, body, negative=False, else_body=None
:param cond_or: combine multiple conditions using 'or' (default is to combine with 'and')
:param cond_tmpl: template for condition expression (default: '%s')
"""
- if isinstance(conditions, string_type):
+ if isinstance(conditions, str):
conditions = [conditions]
if cond_or:
@@ -817,19 +818,20 @@ def get_description(self, conflict=True):
"""
Generate a description.
"""
- txt = '\n'.join([
+ lines = [
"proc ModulesHelp { } {",
" puts stderr {%s" % re.sub(r'([{}\[\]])', r'\\\1', self._generate_help_text()),
" }",
'}',
'',
+ ]
+
+ lines.extend([
+ "module-whatis {%s}" % re.sub(r'([{}\[\]])', r'\\\1', line)
+ for line in self._generate_whatis_lines()
])
- lines = [
- '%(whatis_lines)s',
- '',
- "set root %(installdir)s",
- ]
+ lines.extend(['', "set root " + self.app.installdir])
if self.app.cfg['moduleloadnoconflict']:
cond_unload = self.conditional_statement(self.is_loaded('%(name)s'), "module unload %(name)s")
@@ -846,41 +848,36 @@ def get_description(self, conflict=True):
# - 'conflict Compiler/GCC/4.8.2/OpenMPI' for 'Compiler/GCC/4.8.2/OpenMPI/1.6.4'
lines.extend(['', "conflict %s" % os.path.dirname(self.app.short_mod_name)])
- whatis_lines = [
- "module-whatis {%s}" % re.sub(r'([{}\[\]])', r'\\\1', line)
- for line in self._generate_whatis_lines()
- ]
- txt += '\n'.join([''] + lines + ['']) % {
- 'name': self.app.name,
- 'version': self.app.version,
- 'whatis_lines': '\n'.join(whatis_lines),
- 'installdir': self.app.installdir,
- }
-
- return txt
+ return '\n'.join(lines + [''])
def getenv_cmd(self, envvar, default=None):
"""
Return module-syntax specific code to get value of specific environment variable.
"""
if default is None:
- cmd = '$::env(%s)' % envvar
+ if self.modules_tool.supports_tcl_getenv:
+ cmd = '[getenv %s]' % envvar
+ else:
+ cmd = '$::env(%s)' % envvar
else:
- values = {
- 'default': default,
- 'envvar': '::env(%s)' % envvar,
- }
- cmd = '[if { [info exists %(envvar)s] } { concat $%(envvar)s } else { concat "%(default)s" } ]' % values
+ if self.modules_tool.supports_tcl_getenv:
+ cmd = '[getenv %s "%s"]' % (envvar, default)
+ else:
+ values = {
+ 'default': default,
+ 'envvar': '::env(%s)' % envvar,
+ }
+ cmd = '[if { [info exists %(envvar)s] } { concat $%(envvar)s } else { concat "%(default)s" } ]' % values
return cmd
- def load_module(self, mod_name, recursive_unload=None, depends_on=False, unload_modules=None, multi_dep_mods=None):
+ def load_module(self, mod_name, recursive_unload=None, depends_on=None, unload_modules=None, multi_dep_mods=None):
"""
Generate load statement for specified module.
:param mod_name: name of module to generate load statement for
:param recursive_unload: boolean indicating whether the 'load' statement should be reverted on unload
(if None: enable if recursive_mod_unload build option or depends_on is True)
- :param depends_on: use depends_on statements rather than (guarded) load statements
+ :param depends_on: use depends_on statements rather than (guarded) load statements (DEPRECATED)
:param unload_modules: name(s) of module to unload first
:param multi_dep_mods: list of module names in multi_deps context, to use for guarding load statement
"""
@@ -889,7 +886,10 @@ def load_module(self, mod_name, recursive_unload=None, depends_on=False, unload_
body.extend([self.unload_module(m).strip() for m in unload_modules])
load_template = self.LOAD_TEMPLATE
# Lmod 7.6.1+ supports depends-on which does this most nicely:
- if build_option('mod_depends_on') or depends_on:
+ if (build_option('mod_depends_on') and self.modules_tool.supports_depends_on) or depends_on:
+ if depends_on is not None:
+ depr_msg = "'depends_on' argument of module generator method 'load_module' should not be used anymore"
+ self.log.deprecated(depr_msg, '6.0')
if not self.modules_tool.supports_depends_on:
raise EasyBuildError("depends-on statements in generated module are not supported by modules tool")
load_template = self.LOAD_TEMPLATE_DEPENDS_ON
@@ -900,8 +900,13 @@ def load_module(self, mod_name, recursive_unload=None, depends_on=False, unload_
cond_tmpl = None
+ # Environment Modules v4+ safely handles automatic module load by not reloading already
+ # loaded module. No safe guard test is required and it should even be avoided to get the
+ # module dependency correctly tracked.
+ safe_auto_load = self.modules_tool.supports_safe_auto_load
+
if recursive_unload is None:
- recursive_unload = build_option('recursive_mod_unload') or depends_on
+ recursive_unload = build_option('recursive_mod_unload') or depends_on or safe_auto_load
if recursive_unload:
# wrapping the 'module load' statement with an 'is-loaded or mode == unload'
@@ -912,7 +917,7 @@ def load_module(self, mod_name, recursive_unload=None, depends_on=False, unload_
# see also http://lmod.readthedocs.io/en/latest/210_load_storms.html
cond_tmpl = "[ module-info mode remove ] || %s"
- if depends_on:
+ if depends_on or safe_auto_load:
if multi_dep_mods and len(multi_dep_mods) > 1:
parent_mod_name = os.path.dirname(mod_name)
guard = self.is_loaded(multi_dep_mods[1:])
@@ -956,15 +961,16 @@ def msg_on_unload(self, msg):
print_cmd = "puts stderr %s" % quote_str(msg, tcl=True)
return '\n'.join(['', self.conditional_statement("module-info mode unload", print_cmd, indent=False)])
- def update_paths(self, key, paths, prepend=True, allow_abs=False, expand_relpaths=True):
+ def update_paths(self, key, paths, prepend=True, allow_abs=False, expand_relpaths=True, delim=':'):
"""
Generate prepend-path or append-path statements for the given list of paths.
:param key: environment variable to prepend/append paths to
- :param paths: list of paths to prepend
+ :param paths: list of paths to prepend/append
:param prepend: whether to prepend (True) or append (False) paths
:param allow_abs: allow providing of absolute paths
:param expand_relpaths: expand relative paths into absolute paths (by prefixing install dir)
+ :param delim: delimiter used between paths
"""
if prepend:
update_type = 'prepend'
@@ -975,7 +981,7 @@ def update_paths(self, key, paths, prepend=True, allow_abs=False, expand_relpath
self.log.info("Not including statement to %s environment variable $%s, as specified", update_type, key)
return ''
- if isinstance(paths, string_type):
+ if isinstance(paths, str):
self.log.debug("Wrapping %s into a list before using it to %s path %s", paths, update_type, key)
paths = [paths]
@@ -996,7 +1002,8 @@ def update_paths(self, key, paths, prepend=True, allow_abs=False, expand_relpath
else:
abspaths.append(path)
- statements = ['%s-path\t%s\t\t%s\n' % (update_type, key, p) for p in abspaths]
+ delim_opt = '' if delim == ':' else f' -d "{delim}"'
+ statements = [f'{update_type}-path{delim_opt}\t{key}\t\t{p}\n' for p in abspaths]
return ''.join(statements)
def set_alias(self, key, value):
@@ -1008,7 +1015,7 @@ def set_alias(self, key, value):
def set_as_default(self, module_dir_path, module_version, mod_symlink_paths=None):
"""
- Create a .version file inside the package module folder in order to set the default version for TMod
+ Create a .version file inside the package module folder in order to set the default version
:param module_dir_path: module directory path, e.g. $HOME/easybuild/modules/all/Bison
:param module_version: module version, e.g. 3.0.4
@@ -1147,6 +1154,7 @@ class ModuleGeneratorLua(ModuleGenerator):
PATH_JOIN_TEMPLATE = 'pathJoin(root, "%s")'
UPDATE_PATH_TEMPLATE = '%s_path("%s", %s)'
+ UPDATE_PATH_TEMPLATE_DELIM = '%s_path("%s", %s, "%s")'
START_STR = '[==['
END_STR = ']==]'
@@ -1181,21 +1189,12 @@ def check_group(self, group, error_msg=None):
:param group: string with the group name
:param error_msg: error message to print for users outside that group
"""
- lmod_version = self.modules_tool.version
- min_lmod_version = '6.0.8'
+ if error_msg is None:
+ error_msg = "You are not part of '%s' group of users that have access to this software; " % group
+ error_msg += "Please consult with user support how to become a member of this group"
- if LooseVersion(lmod_version) >= LooseVersion(min_lmod_version):
- if error_msg is None:
- error_msg = "You are not part of '%s' group of users that have access to this software; " % group
- error_msg += "Please consult with user support how to become a member of this group"
-
- error_msg = 'LmodError("' + error_msg + '")'
- res = self.conditional_statement('userInGroup("%s")' % group, error_msg, negative=True)
- else:
- warn_msg = "Can't generate robust check in Lua modules for users belonging to group %s. "
- warn_msg += "Lmod version not recent enough (%s), should be >= %s"
- self.log.warning(warn_msg, group, lmod_version, min_lmod_version)
- res = ''
+ error_msg = 'LmodError("' + error_msg + '")'
+ res = self.conditional_statement('userInGroup("%s")' % group, error_msg, negative=True)
return res
@@ -1223,7 +1222,7 @@ def conditional_statement(self, conditions, body, negative=False, else_body=None
:param cond_or: combine multiple conditions using 'or' (default is to combine with 'and')
:param cond_tmpl: template for condition expression (default: '%s')
"""
- if isinstance(conditions, string_type):
+ if isinstance(conditions, str):
conditions = [conditions]
if cond_or:
@@ -1262,48 +1261,32 @@ def get_description(self, conflict=True):
"""
Generate a description.
"""
- txt = '\n'.join([
+ lines = [
'help(%s%s' % (self.START_STR, self.check_str(self._generate_help_text())),
'%s)' % self.END_STR,
'',
- ])
-
- lines = [
- "%(whatis_lines)s",
- '',
- 'local root = "%(installdir)s"',
]
+ for line in self._generate_whatis_lines():
+ lines.append("whatis(%s%s%s)" % (self.START_STR, self.check_str(line), self.END_STR))
+
+ lines.extend(['', 'local root = "%s"' % self.app.installdir])
+
if self.app.cfg['moduleloadnoconflict']:
self.log.info("Nothing to do to ensure no conflicts can occur on load when using Lua modules files/Lmod")
elif conflict:
# conflict on 'name' part of module name (excluding version part at the end)
lines.extend(['', 'conflict("%s")' % os.path.dirname(self.app.short_mod_name)])
-
- whatis_lines = []
- for line in self._generate_whatis_lines():
- whatis_lines.append("whatis(%s%s%s)" % (self.START_STR, self.check_str(line), self.END_STR))
-
- if build_option('module_extensions'):
- extensions_list = self._generate_extensions_list()
-
+ extensions_list = self.app.make_extension_string(name_version_sep='/', ext_sep=',')
if extensions_list:
- extensions_stmt = 'extensions("%s")' % ','.join([str(x) for x in extensions_list])
+ extensions_stmt = 'extensions("%s")' % extensions_list
# put this behind a Lmod version check as 'extensions' is only (well) supported since Lmod 8.2.8,
# see https://lmod.readthedocs.io/en/latest/330_extensions.html#module-extensions and
# https://github.com/TACC/Lmod/issues/428
lines.extend(['', self.conditional_statement(self.check_version("8", "2", "8"), extensions_stmt)])
- txt += '\n'.join([''] + lines + ['']) % {
- 'name': self.app.name,
- 'version': self.app.version,
- 'whatis_lines': '\n'.join(whatis_lines),
- 'installdir': self.app.installdir,
- 'homepage': self.app.cfg['homepage'],
- }
-
- return txt
+ return '\n'.join(lines + [''])
def getenv_cmd(self, envvar, default=None):
"""
@@ -1315,14 +1298,14 @@ def getenv_cmd(self, envvar, default=None):
cmd = 'os.getenv("%s") or "%s"' % (envvar, default)
return cmd
- def load_module(self, mod_name, recursive_unload=None, depends_on=False, unload_modules=None, multi_dep_mods=None):
+ def load_module(self, mod_name, recursive_unload=None, depends_on=None, unload_modules=None, multi_dep_mods=None):
"""
Generate load statement for specified module.
:param mod_name: name of module to generate load statement for
:param recursive_unload: boolean indicating whether the 'load' statement should be reverted on unload
(if None: enable if recursive_mod_unload build option or depends_on is True)
- :param depends_on: use depends_on statements rather than (guarded) load statements
+ :param depends_on: use depends_on statements rather than (guarded) load statements (DEPRECATED)
:param unload_modules: name(s) of module to unload first
:param multi_dep_mods: list of module names in multi_deps context, to use for guarding load statement
"""
@@ -1332,7 +1315,10 @@ def load_module(self, mod_name, recursive_unload=None, depends_on=False, unload_
load_template = self.LOAD_TEMPLATE
# Lmod 7.6+ supports depends_on which does this most nicely:
- if build_option('mod_depends_on') or depends_on:
+ if (build_option('mod_depends_on') and self.modules_tool.supports_depends_on) or depends_on:
+ if depends_on is not None:
+ depr_msg = "'depends_on' argument of module generator method 'load_module' should not be used anymore"
+ self.log.deprecated(depr_msg, '6.0')
if not self.modules_tool.supports_depends_on:
raise EasyBuildError("depends_on statements in generated module are not supported by modules tool")
load_template = self.LOAD_TEMPLATE_DEPENDS_ON
@@ -1427,7 +1413,7 @@ def modulerc(self, module_version=None, filepath=None, modulerc_txt=None):
return super(ModuleGeneratorLua, self).modulerc(module_version=module_version, filepath=filepath,
modulerc_txt=modulerc_txt)
- def update_paths(self, key, paths, prepend=True, allow_abs=False, expand_relpaths=True):
+ def update_paths(self, key, paths, prepend=True, allow_abs=False, expand_relpaths=True, delim=':'):
"""
Generate prepend_path or append_path statements for the given list of paths
@@ -1436,6 +1422,7 @@ def update_paths(self, key, paths, prepend=True, allow_abs=False, expand_relpath
:param prepend: whether to prepend (True) or append (False) paths
:param allow_abs: allow providing of absolute paths
:param expand_relpaths: expand relative paths into absolute paths (by prefixing install dir)
+ :param delim: delimiter used between paths
"""
if prepend:
update_type = 'prepend'
@@ -1446,7 +1433,7 @@ def update_paths(self, key, paths, prepend=True, allow_abs=False, expand_relpath
self.log.info("Not including statement to %s environment variable $%s, as specified", update_type, key)
return ''
- if isinstance(paths, string_type):
+ if isinstance(paths, str):
self.log.debug("Wrapping %s into a list before using it to %s path %s", update_type, paths, key)
paths = [paths]
@@ -1468,7 +1455,10 @@ def update_paths(self, key, paths, prepend=True, allow_abs=False, expand_relpath
else:
abspaths.append('root')
- statements = [self.UPDATE_PATH_TEMPLATE % (update_type, key, p) for p in abspaths]
+ if delim != ':':
+ statements = [self.UPDATE_PATH_TEMPLATE_DELIM % (update_type, key, p, delim) for p in abspaths]
+ else:
+ statements = [self.UPDATE_PATH_TEMPLATE % (update_type, key, p) for p in abspaths]
statements.append('')
return '\n'.join(statements)
diff --git a/easybuild/tools/module_naming_scheme/__init__.py b/easybuild/tools/module_naming_scheme/__init__.py
index e54f04c31e..646366409d 100644
--- a/easybuild/tools/module_naming_scheme/__init__.py
+++ b/easybuild/tools/module_naming_scheme/__init__.py
@@ -1,5 +1,5 @@
##
-# Copyright 2011-2024 Ghent University
+# Copyright 2011-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/tools/module_naming_scheme/categorized_mns.py b/easybuild/tools/module_naming_scheme/categorized_mns.py
index cdafc97cbb..b3eff8ed69 100644
--- a/easybuild/tools/module_naming_scheme/categorized_mns.py
+++ b/easybuild/tools/module_naming_scheme/categorized_mns.py
@@ -1,5 +1,5 @@
##
-# Copyright 2016-2024 Ghent University
+# Copyright 2016-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/tools/module_naming_scheme/easybuild_mns.py b/easybuild/tools/module_naming_scheme/easybuild_mns.py
index 93ff663e77..c3d3acc2d2 100644
--- a/easybuild/tools/module_naming_scheme/easybuild_mns.py
+++ b/easybuild/tools/module_naming_scheme/easybuild_mns.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/tools/module_naming_scheme/hierarchical_mns.py b/easybuild/tools/module_naming_scheme/hierarchical_mns.py
index d0e4ea5966..b14425d9ce 100644
--- a/easybuild/tools/module_naming_scheme/hierarchical_mns.py
+++ b/easybuild/tools/module_naming_scheme/hierarchical_mns.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -204,10 +204,10 @@ def det_modpath_extensions(self, ec):
comp_name_ver = None
if ec['name'] in extend_comps:
- for key in COMP_NAME_VERSION_TEMPLATES:
+ for key, comp_tmpl in COMP_NAME_VERSION_TEMPLATES.items():
comp_names = key.split(',')
if ec['name'] in comp_names:
- comp_name, comp_ver_tmpl = COMP_NAME_VERSION_TEMPLATES[key]
+ comp_name, comp_ver_tmpl = comp_tmpl
comp_versions = {ec['name']: self.det_full_version(ec)}
if ec['name'] == 'ifort':
# 'icc' key should be provided since it's the only one used in the template
diff --git a/easybuild/tools/module_naming_scheme/migrate_from_eb_to_hmns.py b/easybuild/tools/module_naming_scheme/migrate_from_eb_to_hmns.py
index 5603b7db47..c90717a4eb 100644
--- a/easybuild/tools/module_naming_scheme/migrate_from_eb_to_hmns.py
+++ b/easybuild/tools/module_naming_scheme/migrate_from_eb_to_hmns.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/tools/module_naming_scheme/mns.py b/easybuild/tools/module_naming_scheme/mns.py
index fd60dbbe90..cf7bfcd393 100644
--- a/easybuild/tools/module_naming_scheme/mns.py
+++ b/easybuild/tools/module_naming_scheme/mns.py
@@ -1,5 +1,5 @@
##
-# Copyright 2011-2024 Ghent University
+# Copyright 2011-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -33,9 +33,9 @@
import re
from easybuild.base import fancylogger
+from easybuild.base.wrapper import create_base_metaclass
from easybuild.tools.build_log import EasyBuildError
from easybuild.tools.config import Singleton
-from easybuild.tools.py2vs3 import create_base_metaclass
DEVEL_MODULE_SUFFIX = '-easybuild-devel'
diff --git a/easybuild/tools/module_naming_scheme/toolchain.py b/easybuild/tools/module_naming_scheme/toolchain.py
index b85032e6e6..e0627090d0 100644
--- a/easybuild/tools/module_naming_scheme/toolchain.py
+++ b/easybuild/tools/module_naming_scheme/toolchain.py
@@ -1,5 +1,5 @@
##
-# Copyright 2014-2024 Ghent University
+# Copyright 2014-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/tools/module_naming_scheme/utilities.py b/easybuild/tools/module_naming_scheme/utilities.py
index 4fff90f690..7061f5cebb 100644
--- a/easybuild/tools/module_naming_scheme/utilities.py
+++ b/easybuild/tools/module_naming_scheme/utilities.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -40,7 +40,6 @@
from easybuild.base import fancylogger
from easybuild.tools.build_log import EasyBuildError
from easybuild.tools.module_naming_scheme.mns import ModuleNamingScheme
-from easybuild.tools.py2vs3 import string_type
from easybuild.tools.toolchain.toolchain import SYSTEM_TOOLCHAIN_NAME, is_system_toolchain
from easybuild.tools.utilities import get_subclasses, import_available_modules
@@ -64,12 +63,12 @@ def det_full_ec_version(ec):
# prepend/append version prefix/suffix
versionprefix = ec.get('versionprefix', '')
- if versionprefix and not isinstance(versionprefix, string_type):
+ if versionprefix and not isinstance(versionprefix, str):
raise EasyBuildError("versionprefix value should be a string, found '%s': %s (full spec: %s)",
type(versionprefix).__name__, versionprefix, ec)
versionsuffix = ec.get('versionsuffix', '')
- if versionsuffix and not isinstance(versionsuffix, string_type):
+ if versionsuffix and not isinstance(versionsuffix, str):
raise EasyBuildError("versionsuffix value should be a string, found '%s': %s (full spec: %s)",
type(versionsuffix).__name__, versionsuffix, ec)
@@ -94,7 +93,7 @@ def avail_module_naming_schemes():
def is_valid_module_name(mod_name):
"""Check whether the specified value is a valid module name."""
# module name must be a string
- if not isinstance(mod_name, string_type):
+ if not isinstance(mod_name, str):
_log.warning("Wrong type for module name %s (%s), should be a string" % (mod_name, type(mod_name)))
return False
# module name must be relative path
diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py
index 8929ccc560..237b234e39 100644
--- a/easybuild/tools/modules.py
+++ b/easybuild/tools/modules.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -41,21 +41,24 @@
import os
import re
import shlex
+from enum import Enum
from easybuild.base import fancylogger
-from easybuild.tools import StrictVersion
-from easybuild.tools.build_log import EasyBuildError, print_warning
-from easybuild.tools.config import ERROR, IGNORE, PURGE, UNLOAD, UNSET
-from easybuild.tools.config import EBROOT_ENV_VAR_ACTIONS, LOADED_MODULES_ACTIONS
+from easybuild.tools import LooseVersion
+from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, print_warning
+from easybuild.tools.config import ERROR, EBROOT_ENV_VAR_ACTIONS, IGNORE, LOADED_MODULES_ACTIONS, PURGE
+from easybuild.tools.config import SEARCH_PATH_BIN_DIRS, SEARCH_PATH_HEADER_DIRS, SEARCH_PATH_LIB_DIRS, UNLOAD, UNSET
from easybuild.tools.config import build_option, get_modules_tool, install_path
from easybuild.tools.environment import ORIG_OS_ENVIRON, restore_env, setvar, unset_env_vars
from easybuild.tools.filetools import convert_name, mkdir, normalize_path, path_matches, read_file, which, write_file
from easybuild.tools.module_naming_scheme.mns import DEVEL_MODULE_SUFFIX
-from easybuild.tools.py2vs3 import subprocess_popen_text
-from easybuild.tools.run import run_cmd
+from easybuild.tools.run import run_shell_cmd
from easybuild.tools.systemtools import get_shared_lib_ext
from easybuild.tools.utilities import get_subclasses, nub
+
+MODULE_LOAD_ENV_HEADERS = 'CPP_HEADERS'
+
# software root/version environment variable name prefixes
ROOT_ENV_VAR_NAME_PREFIX = "EBROOT"
VERSION_ENV_VAR_NAME_PREFIX = "EBVERSION"
@@ -132,6 +135,355 @@
_log = fancylogger.getLogger('modules', fname=False)
+class ModEnvVarType(Enum):
+ """
+ Possible types of ModuleEnvironmentVariable:
+ - STRING: (list of) strings with no further meaning
+ - PATH: (list of) of paths to existing directories or files
+ - PATH_WITH_FILES: (list of) of paths to existing directories containing
+ one or more files
+ - PATH_WITH_TOP_FILES: (list of) of paths to existing directories
+ containing one or more files in its top directory
+ - """
+ STRING, PATH, PATH_WITH_FILES, PATH_WITH_TOP_FILES = range(0, 4)
+
+
+class ModuleEnvironmentVariable:
+ """
+ Environment variable data structure for modules
+ Contents of environment variable is a list of unique strings
+ """
+
+ def __init__(self, contents, delimiter=os.pathsep, prepend=True, var_type=ModEnvVarType.PATH_WITH_FILES):
+ """
+ Initialize new environment variable
+ Actual contents of the environment variable are held in self.contents
+ By default, the environment variable is a list of paths with files in them
+ Existence of paths and their contents are not checked at init
+ """
+ self.contents = contents
+ self.delimiter = delimiter
+ self.mod_prepend = prepend
+ self.type = var_type
+
+ self.log = fancylogger.getLogger(self.__class__.__name__, fname=False)
+
+ def __repr__(self):
+ return repr(self.contents)
+
+ def __str__(self):
+ return self.delimiter.join(self.contents)
+
+ def __iter__(self):
+ return iter(self.contents)
+
+ @property
+ def contents(self):
+ return self._contents
+
+ @contents.setter
+ def contents(self, value):
+ """Enforce that contents is a list of strings"""
+ if isinstance(value, str):
+ value = [value]
+
+ try:
+ str_list = [str(path) for path in value]
+ except TypeError as err:
+ raise TypeError("ModuleEnvironmentVariable.contents must be a list of strings") from err
+
+ self._contents = nub(str_list) # remove duplicates and keep order
+
+ @property
+ def type(self):
+ return self._type
+
+ @type.setter
+ def type(self, value):
+ """Convert type to VarType"""
+ if isinstance(value, ModEnvVarType):
+ self._type = value
+ else:
+ try:
+ self._type = ModEnvVarType[value]
+ except KeyError as err:
+ raise EasyBuildError(f"Cannot create ModuleEnvironmentVariable with type {value}") from err
+
+ def append(self, item):
+ """Shortcut to append to list of contents"""
+ self.contents += [item]
+
+ def extend(self, item):
+ """Shortcut to extend list of contents"""
+ self.contents += item
+
+ def prepend(self, item):
+ """Shortcut to prepend item to list of contents"""
+ self.contents = [item] + self.contents
+
+ def update(self, item):
+ """Shortcut to replace list of contents with item"""
+ self.contents = item
+
+ def remove(self, *args):
+ """Shortcut to remove items from list of contents"""
+ try:
+ self.contents.remove(*args)
+ except ValueError:
+ # item is not in the list, move along
+ self.log.debug(f"ModuleEnvironmentVariable does not contain item: {' '.join(args)}")
+
+ @property
+ def is_path(self):
+ path_like_types = [
+ ModEnvVarType.PATH,
+ ModEnvVarType.PATH_WITH_FILES,
+ ModEnvVarType.PATH_WITH_TOP_FILES,
+ ]
+ return self.type in path_like_types
+
+
+class ModuleLoadEnvironment:
+ """
+ Changes to environment variables that should be made when environment module is loaded.
+ - Environment variables are defined as ModuleEnvironmentVariables instances
+ with attribute name equal to environment variable name.
+ - Aliases are arbitrary names that serve to apply changes to lists of
+ environment variables
+ - Environment variables are public attributes, with names containing
+ uppercase letters and '_'
+ - Other attributes like aliases are private, with names starting with '_'
+ - Environment variables are kept in a private dict to avoid name collisions
+ """
+
+ def __init__(self, aliases=None):
+ """
+ Initialize default environment definition
+
+ :aliases: dict defining environment variables aliases
+ """
+ # The following regex patterns are needed to properly distinguish
+ # between public environment variables and private attributes. Set them
+ # directly into __dict__ to bypass class getter and setter.
+ self.__dict__['regex'] = {}
+ self.regex['mangled_attr'] = re.compile('^_[A-Za-z]+__') # mangled attributes: _ClassName__VAR_NAME
+ self.regex['private_attr'] = re.compile('^_[a-z][a-z_]+$') # private attributes: _var_name
+ self.regex['env_var_name'] = re.compile('^[A-Z_]+[A-Z0-9_]+$') # environment variables: {__}VAR_NAME_00_SUFFIX
+
+ self._log = fancylogger.getLogger(self.__class__.__name__, fname=False)
+
+ self._aliases = {}
+ if aliases is not None:
+ try:
+ for alias_name, alias_vars in aliases.items():
+ self.update_alias(alias_name, alias_vars)
+ except AttributeError as err:
+ raise EasyBuildError(
+ "Wrong format for aliases defitions passed to ModuleLoadEnvironment. "
+ f"Expected a dictionary but got: {type(aliases)}."
+ ) from err
+
+ self._env_vars = {}
+ self.ACLOCAL_PATH = [os.path.join('share', 'aclocal')]
+ self.CLASSPATH = ['*.jar']
+ self.CMAKE_LIBRARY_PATH = ['lib64'] # only needed for installations with standalone lib64
+ self.CMAKE_PREFIX_PATH = ['']
+ self.GI_TYPELIB_PATH = [os.path.join(x, 'girepository-*') for x in SEARCH_PATH_LIB_DIRS]
+ self.LD_LIBRARY_PATH = SEARCH_PATH_LIB_DIRS
+ self.LIBRARY_PATH = SEARCH_PATH_LIB_DIRS
+ self.MANPATH = ['man', os.path.join('share', 'man')]
+ self.PATH = SEARCH_PATH_BIN_DIRS + ['sbin']
+ self.PKG_CONFIG_PATH = [os.path.join(x, 'pkgconfig') for x in SEARCH_PATH_LIB_DIRS + ['share']]
+ self.XDG_DATA_DIRS = ['share']
+
+ # environment variables with known aliases
+ # e.g. search paths to C/C++ headers
+ for envar_name in self._aliases.get(MODULE_LOAD_ENV_HEADERS, []):
+ setattr(self, envar_name, SEARCH_PATH_HEADER_DIRS)
+
+ def __getattr__(self, name):
+ """
+ Return requested attribute from either the private attributes in self.__dict__
+ or the public ModuleEnvironmentVariables in self._env_vars
+ """
+ if self.regex['private_attr'].match(name):
+ return self.__dict__[name]
+
+ name = self._unmangle_env_var_name(name)
+ return self.__dict__['_env_vars'][name]
+
+ def __setattr__(self, name, value):
+ """
+ Specific restrictions for ModuleLoadEnvironment attributes:
+ - public attributes are instances of ModuleEnvironmentVariable
+ - private attributes are allowed with lowercase names starting with single underscore
+ """
+ if self.regex['private_attr'].match(name):
+ # do not control protected/private attributes
+ return super().__setattr__(name, value)
+
+ try:
+ # set public environment variable
+ name = self._unmangle_env_var_name(name)
+ self._env_vars[name] = self._set_module_environment_variable(name, value)
+ except TypeError as err:
+ raise EasyBuildError(
+ f"Cannot define ModuleEnvironmentVariable ${name} with the following attributes: {value}"
+ ) from err
+
+ return True
+
+ def __delattr__(self, name):
+ """
+ Delete private attributes or public ModuleEnvironmentVariables
+ Fails on missing attributes
+ """
+
+ if self.regex['private_attr'].match(name):
+ del self.__dict__[name]
+ return True
+
+ name = self._unmangle_env_var_name(name)
+ try:
+ del self.__dict__['_env_vars'][name]
+ except KeyError as err:
+ raise EasyBuildError(
+ f"Cannot delete environment variable from ModuleEnvironmentVariable: {name} not found."
+ ) from err
+ return True
+
+ def _unmangle_env_var_name(self, name):
+ """
+ Unmangle environment variable names that were originally set with a leading double underscore
+ """
+ if self.regex['mangled_attr'].match(name):
+ # environment variable with 2+ leading underscores: __UPPER_CASE
+ # undo mangling into _ClassName__UPPER_CASE
+ split_name = name.split('__')
+ split_name[0] = ''
+ name = '__'.join(split_name)
+ return name
+
+ def _set_module_environment_variable(self, name, value):
+ """
+ Specific restrictions for ModuleEnvironmentVariable attributes:
+ - attribute names are uppercase with underscores
+ - dictionaries are unpacked into arguments of ModuleEnvironmentVariable
+ - controls variables with special types (e.g. PATH, LD_LIBRARY_PATH)
+ """
+ if not self.regex['env_var_name'].match(name):
+ raise EasyBuildError(
+ "Name of ModuleLoadEnvironment attribute does not conform to shell naming rules, "
+ f"it must only have upper-case letters and underscores: '{name}'"
+ )
+
+ if not isinstance(value, dict):
+ value = {'contents': value}
+
+ # special variables that require files in their top directories
+ if name in ('LD_LIBRARY_PATH', 'PATH'):
+ value.update({'var_type': ModEnvVarType.PATH_WITH_TOP_FILES})
+
+ return ModuleEnvironmentVariable(**value)
+
+ @property
+ def vars(self):
+ """Return list of public ModuleEnvironmentVariable"""
+ return list(self._env_vars)
+
+ def __iter__(self):
+ """Make the class iterable"""
+ yield from self.vars
+
+ def items(self):
+ """
+ Return key-value pairs for each attribute that is a ModuleEnvironmentVariable
+ - key = attribute name
+ - value = its "contents" attribute
+ """
+ for attr in self.vars:
+ yield attr, self._env_vars[attr]
+
+ def update(self, new_env):
+ """Update contents of environment from given dictionary"""
+ try:
+ for envar_name, envar_contents in new_env.items():
+ setattr(self, envar_name, envar_contents)
+ except AttributeError as err:
+ raise EasyBuildError("Cannot update ModuleLoadEnvironment from a non-dict variable") from err
+
+ def replace(self, new_env):
+ """Replace contents of environment with given dictionary"""
+ for var in self.vars:
+ self.remove(var)
+ self.update(new_env)
+
+ def remove(self, var_name):
+ """
+ Remove ModuleEnvironmentVariable attribute from instance
+ Silently goes through if attribute is already missing
+ """
+ if var_name in self.vars:
+ del self._env_vars[var_name]
+
+ @property
+ def as_dict(self):
+ """
+ Return dict with mapping of ModuleEnvironmentVariables names with their contents
+ """
+ return dict(self.items())
+
+ @property
+ def environ(self):
+ """
+ Return dict with mapping of ModuleEnvironmentVariables names with their contents
+ Equivalent in shape to os.environ
+ """
+ return {envar_name: str(envar_contents) for envar_name, envar_contents in self.items()}
+
+ def alias(self, alias):
+ """
+ Return iterator to search path variables for given alias
+ """
+ try:
+ yield from [self._env_vars[var_name] for var_name in self._aliases[alias]]
+ except KeyError as err:
+ raise EasyBuildError(f"Unknown search path alias: {alias}") from err
+ except AttributeError as err:
+ raise EasyBuildError(f"Missing environment variable in '{alias} alias") from err
+
+ def alias_vars(self, alias):
+ """
+ Return list of environment variable names aliased by given alias
+ """
+ try:
+ return self._aliases[alias]
+ except KeyError as err:
+ raise EasyBuildError(f"Unknown search path alias: {alias}") from err
+
+ def update_alias(self, alias, value):
+ """
+ Update existing or non-existing alias with given search paths variables
+ """
+ if isinstance(value, str):
+ value = [value]
+
+ try:
+ self._aliases[alias] = [str(envar) for envar in value]
+ except TypeError as err:
+ raise TypeError("ModuleLoadEnvironment aliases must be a list of strings") from err
+
+ def set_alias_vars(self, alias, value):
+ """
+ Set value of search paths variables for given alias
+ """
+ try:
+ for envar_name in self._aliases[alias]:
+ setattr(self, envar_name, value)
+ except KeyError as err:
+ raise EasyBuildError(f"Unknown search path alias: {alias}") from err
+
+
class ModulesTool(object):
"""An abstract interface to a tool that deals with modules."""
# name of this modules tool (used in log/warning/error messages)
@@ -147,11 +499,13 @@ class ModulesTool(object):
COMMAND_SHELL = None
# option to determine the version
VERSION_OPTION = '--version'
- # minimal required version (StrictVersion; suffix rc replaced with b (and treated as beta by StrictVersion))
+ # minimal required version (cannot include -beta or rc)
REQ_VERSION = None
+ # minimal required version to check user's group in modulefile
+ REQ_VERSION_TCL_CHECK_GROUP = None
# deprecated version limit (support for versions below this version is deprecated)
DEPR_VERSION = None
- # maximum version allowed (StrictVersion; suffix rc replaced with b (and treated as beta by StrictVersion))
+ # maximum version allowed (cannot include -beta or rc)
MAX_VERSION = None
# the regexp, should have a "version" group (multiline search)
VERSION_REGEXP = None
@@ -210,6 +564,9 @@ def __init__(self, mod_paths=None, testing=False):
self.check_module_function(allow_mismatch=build_option('allow_modules_tool_mismatch'))
self.set_and_check_version()
self.supports_depends_on = False
+ self.supports_tcl_getenv = False
+ self.supports_tcl_check_group = False
+ self.supports_safe_auto_load = False
def __str__(self):
"""String representation of this ModulesTool instance."""
@@ -242,14 +599,6 @@ def set_and_check_version(self):
if res:
self.version = res.group('version')
self.log.info("Found %s version %s", self.NAME, self.version)
-
- # make sure version is a valid StrictVersion (e.g., 5.7.3.1 is invalid),
- # and replace 'rc' by 'b', to make StrictVersion treat it as a beta-release
- self.version = self.version.replace('rc', 'b').replace('-beta', 'b1')
- if len(self.version.split('.')) > 3:
- self.version = '.'.join(self.version.split('.')[:3])
-
- self.log.info("Converted actual version to '%s'" % self.version)
else:
raise EasyBuildError("Failed to determine %s version from option '%s' output: %s",
self.NAME, self.VERSION_OPTION, txt)
@@ -262,9 +611,10 @@ def set_and_check_version(self):
elif build_option('modules_tool_version_check'):
self.log.debug("Checking whether %s version %s meets requirements", self.NAME, self.version)
+ version = LooseVersion(self.version)
if self.REQ_VERSION is not None:
self.log.debug("Required minimum %s version defined: %s", self.NAME, self.REQ_VERSION)
- if StrictVersion(self.version) < StrictVersion(self.REQ_VERSION):
+ if version < self.REQ_VERSION or version.is_prerelease(self.REQ_VERSION, ['rc', '-beta']):
raise EasyBuildError("EasyBuild requires %s >= v%s, found v%s",
self.NAME, self.REQ_VERSION, self.version)
else:
@@ -272,18 +622,14 @@ def set_and_check_version(self):
if self.DEPR_VERSION is not None:
self.log.debug("Deprecated %s version limit defined: %s", self.NAME, self.DEPR_VERSION)
- if StrictVersion(self.version) < StrictVersion(self.DEPR_VERSION):
+ if version < self.DEPR_VERSION or version.is_prerelease(self.DEPR_VERSION, ['rc', '-beta']):
depr_msg = "Support for %s version < %s is deprecated, " % (self.NAME, self.DEPR_VERSION)
depr_msg += "found version %s" % self.version
-
- if self.version.startswith('6') and 'Lmod6' in build_option('silence_deprecation_warnings'):
- self.log.warning(depr_msg)
- else:
- self.log.deprecated(depr_msg, '5.0')
+ self.log.deprecated(depr_msg, '6.0')
if self.MAX_VERSION is not None:
self.log.debug("Maximum allowed %s version defined: %s", self.NAME, self.MAX_VERSION)
- if StrictVersion(self.version) > StrictVersion(self.MAX_VERSION):
+ if self.version > self.MAX_VERSION and not version.is_prerelease(self.MAX_VERSION, ['rc', '-beta']):
raise EasyBuildError("EasyBuild requires %s <= v%s, found v%s",
self.NAME, self.MAX_VERSION, self.version)
else:
@@ -310,31 +656,32 @@ def check_module_function(self, allow_mismatch=False, regex=None):
if self.testing:
# grab 'module' function definition from environment if it's there; only during testing
try:
- out, ec = os.environ['module'], 0
+ output, exit_code = os.environ['module'], EasyBuildExit.SUCCESS
except KeyError:
- out, ec = None, 1
+ output, exit_code = None, EasyBuildExit.FAIL_SYSTEM_CHECK
else:
cmd = "type module"
- out, ec = run_cmd(cmd, simple=False, log_ok=False, log_all=False, force_in_dry_run=True, trace=False)
+ res = run_shell_cmd(cmd, fail_on_error=False, in_dry_run=True, hidden=True, output_file=False)
+ output, exit_code = res.output, res.exit_code
if regex is None:
regex = r".*%s" % os.path.basename(self.cmd)
mod_cmd_re = re.compile(regex, re.M)
mod_details = "pattern '%s' (%s)" % (mod_cmd_re.pattern, self.NAME)
- if ec == 0:
- if mod_cmd_re.search(out):
+ if exit_code == EasyBuildExit.SUCCESS:
+ if mod_cmd_re.search(output):
self.log.debug("Found pattern '%s' in defined 'module' function." % mod_cmd_re.pattern)
else:
msg = "%s not found in defined 'module' function.\n" % mod_details
msg += "Specify the correct modules tool to avoid weird problems due to this mismatch, "
msg += "see the --modules-tool and --avail-modules-tools command line options.\n"
if allow_mismatch:
- msg += "Obtained definition of 'module' function: %s" % out
+ msg += "Obtained definition of 'module' function: %s" % output
self.log.warning(msg)
else:
msg += "Or alternatively, use --allow-modules-tool-mismatch to stop treating this as an error. "
- msg += "Obtained definition of 'module' function: %s" % out
+ msg += "Obtained definition of 'module' function: %s" % output
raise EasyBuildError(msg)
else:
# module function may not be defined (weird, but fine)
@@ -551,18 +898,14 @@ def module_wrapper_exists(self, mod_name, modulerc_fn='.modulerc', mod_wrapper_r
return wrapped_mod
- def exist(self, mod_names, mod_exists_regex_template=None, skip_avail=False, maybe_partial=True):
+ def exist(self, mod_names, skip_avail=False, maybe_partial=True):
"""
Check if modules with specified names exists.
:param mod_names: list of module names
- :param mod_exists_regex_template: DEPRECATED and unused
:param skip_avail: skip checking through 'module avail', only check via 'module show'
:param maybe_partial: indicates if the module name may be a partial module name
"""
- if mod_exists_regex_template is not None:
- self.log.deprecated('mod_exists_regex_template is no longer used', '5.0')
-
def mod_exists_via_show(mod_name):
"""
Helper function to check whether specified module name exists through 'module show'.
@@ -598,7 +941,8 @@ def mod_exists_via_show(mod_name):
self.log.debug("Skipping warning line '%s'", line)
continue
- # skip lines that start with 'module-' (like 'module-version'),
+ # skip lines that start with 'module-' (like 'module-version')
+ # that may appear with EnvironmentModulesC or EnvironmentModulesTcl,
# see https://github.com/easybuilders/easybuild-framework/issues/3376
if line.startswith('module-'):
self.log.debug("Skipping line '%s' since it starts with 'module-'", line)
@@ -718,7 +1062,8 @@ def show(self, mod_name):
ans = MODULE_SHOW_CACHE[key]
self.log.debug("Found cached result for 'module show %s' with key '%s': %s", mod_name, key, ans)
else:
- ans = self.run_module('show', mod_name, check_output=False, return_stderr=True)
+ ans = self.run_module('show', mod_name, check_output=False, return_stderr=True,
+ check_exit_code=False)
MODULE_SHOW_CACHE[key] = ans
self.log.debug("Cached result for 'module show %s' with key '%s': %s", mod_name, key, ans)
@@ -753,7 +1098,7 @@ def modulefile_path(self, mod_name, strip_ext=False):
:param mod_name: module name
:param strip_ext: strip (.lua) extension from module fileame (if present)"""
# (possible relative) path is always followed by a ':', and may be prepended by whitespace
- # this works for both environment modules and Lmod
+ # this works for both Environment Modules and Lmod
modpath_re = re.compile(r'^\s*(?P[^/\n]*/[^\s]+):$', re.M)
modpath = self.get_value_from_modulefile(mod_name, modpath_re)
@@ -824,24 +1169,22 @@ def run_module(self, *args, **kwargs):
key, old_value, new_value)
cmd_list = self.compose_cmd_list(args)
- full_cmd = ' '.join(cmd_list)
- self.log.debug("Running module command '%s' from %s" % (full_cmd, os.getcwd()))
-
- proc = subprocess_popen_text(cmd_list, env=environ)
+ cmd = ' '.join(cmd_list)
+ # note: module commands are always run in dry mode, and are kept hidden in trace and dry run output
+ res = run_shell_cmd(cmd_list, env=environ, fail_on_error=False, use_bash=False, split_stderr=True,
+ hidden=True, in_dry_run=True, output_file=False)
# stdout will contain python code (to change environment etc)
# stderr will contain text (just like the normal module command)
- (stdout, stderr) = proc.communicate()
- self.log.debug("Output of module command '%s': stdout: %s; stderr: %s" % (full_cmd, stdout, stderr))
+ stdout, stderr = res.output, res.stderr
# also catch and check exit code
- exit_code = proc.returncode
- if kwargs.get('check_exit_code', True) and exit_code != 0:
+ if kwargs.get('check_exit_code', True) and res.exit_code != EasyBuildExit.SUCCESS:
raise EasyBuildError("Module command '%s' failed with exit code %s; stderr: %s; stdout: %s",
- ' '.join(cmd_list), exit_code, stderr, stdout)
+ cmd, res.exit_code, stderr, stdout)
if kwargs.get('check_output', True):
- self.check_module_output(full_cmd, stdout, stderr)
+ self.check_module_output(cmd, stdout, stderr)
if kwargs.get('return_stderr', False):
return stderr
@@ -958,7 +1301,7 @@ def check_loaded_modules(self):
"use the --allow-loaded-modules configuration option.",
"To specify action to take when loaded modules are detected, use %s." % opt,
'',
- "See http://easybuild.readthedocs.io/en/latest/Detecting_loaded_modules.html for more information.",
+ "See https://docs.easybuild.io/detecting-loaded-modules/ for more information.",
])
action = build_option('detect_loaded_modules')
@@ -1189,11 +1532,12 @@ def update(self):
class EnvironmentModulesC(ModulesTool):
- """Interface to (C) environment modules (modulecmd)."""
+ """Interface to (C) Environment Modules (modulecmd)."""
NAME = "Environment Modules"
COMMAND = "modulecmd"
REQ_VERSION = '3.2.10'
MAX_VERSION = '3.99'
+ DEPR_VERSION = '3.999'
VERSION_REGEXP = r'^\s*(VERSION\s*=\s*)?(?P\d\S*)\s*'
def run_module(self, *args, **kwargs):
@@ -1203,7 +1547,7 @@ def run_module(self, *args, **kwargs):
if isinstance(args[0], (list, tuple,)):
args = args[0]
- # some versions of Cray's environment modules tool (3.2.10.x) include a "source */init/bash" command
+ # some versions of Cray's Environment Modules tool (3.2.10.x) include a "source */init/bash" command
# in the output of some "modulecmd python load" calls, which is not a valid Python command,
# which must be stripped out to avoid "invalid syntax" errors when evaluating the output
def tweak_stdout(txt):
@@ -1245,9 +1589,9 @@ def get_setenv_value_from_modulefile(self, mod_name, var_name):
class EnvironmentModulesTcl(EnvironmentModulesC):
- """Interface to (Tcl) environment modules (modulecmd.tcl)."""
+ """Interface to (ancient Tcl-only) Environment Modules (modulecmd.tcl)."""
NAME = "ancient Tcl-only Environment Modules"
- # Tcl environment modules have no --terse (yet),
+ # ancient Tcl-only Environment Modules have no --terse (yet),
# -t must be added after the command ('avail', 'list', etc.)
TERSE_OPTION = (1, '-t')
COMMAND = 'modulecmd.tcl'
@@ -1255,12 +1599,13 @@ class EnvironmentModulesTcl(EnvironmentModulesC):
COMMAND_SHELL = ['tclsh']
VERSION_OPTION = ''
REQ_VERSION = None
+ DEPR_VERSION = '9999.9'
VERSION_REGEXP = r'^Modules\s+Release\s+Tcl\s+(?P\d\S*)\s'
def set_path_env_var(self, key, paths):
"""Set environment variable with given name to the given list of paths."""
super(EnvironmentModulesTcl, self).set_path_env_var(key, paths)
- # for Tcl environment modules, we need to make sure the _modshare env var is kept in sync
+ # for Tcl Environment Modules, we need to make sure the _modshare env var is kept in sync
setvar('%s_modshare' % key, ':1:'.join(paths), verbose=False)
def run_module(self, *args, **kwargs):
@@ -1323,13 +1668,15 @@ def remove_module_path(self, path, set_mod_paths=True):
self.set_mod_paths()
-class EnvironmentModules(EnvironmentModulesTcl):
- """Interface to environment modules 4.0+"""
+class EnvironmentModules(ModulesTool):
+ """Interface to Environment Modules 4.0+"""
NAME = "Environment Modules"
COMMAND = os.path.join(os.getenv('MODULESHOME', 'MODULESHOME_NOT_DEFINED'), 'libexec', 'modulecmd.tcl')
COMMAND_ENVIRONMENT = 'MODULES_CMD'
- REQ_VERSION = '4.0.0'
+ REQ_VERSION = '4.3.0'
+ DEPR_VERSION = '4.3.0'
MAX_VERSION = None
+ REQ_VERSION_TCL_CHECK_GROUP = '4.6.0'
VERSION_REGEXP = r'^Modules\s+Release\s+(?P\d[^+\s]*)(\+\S*)?\s'
SHOW_HIDDEN_OPTION = '--all'
@@ -1356,6 +1703,10 @@ def __init__(self, *args, **kwargs):
setvar('MODULES_LIST_TERSE_OUTPUT', '', verbose=False)
super(EnvironmentModules, self).__init__(*args, **kwargs)
+ version = LooseVersion(self.version)
+ self.supports_tcl_getenv = True
+ self.supports_tcl_check_group = version >= LooseVersion(self.REQ_VERSION_TCL_CHECK_GROUP)
+ self.supports_safe_auto_load = True
def check_module_function(self, allow_mismatch=False, regex=None):
"""Check whether selected module tool matches 'module' function definition."""
@@ -1369,7 +1720,8 @@ def check_module_function(self, allow_mismatch=False, regex=None):
out, ec = None, 1
else:
cmd = "type _module_raw"
- out, ec = run_cmd(cmd, simple=False, log_ok=False, log_all=False, force_in_dry_run=True, trace=False)
+ res = run_shell_cmd(cmd, fail_on_error=False, in_dry_run=True, hidden=True, output_file=False)
+ out, ec = res.output, res.exit_code
if regex is None:
regex = r".*%s" % os.path.basename(self.cmd)
@@ -1399,7 +1751,7 @@ def available(self, mod_name=None, extra_args=None):
if extra_args is None:
extra_args = []
# make hidden modules visible (requires Environment Modules 4.6.0)
- if StrictVersion(self.version) >= StrictVersion('4.6.0'):
+ if LooseVersion(self.version) >= LooseVersion('4.6.0'):
extra_args.append(self.SHOW_HIDDEN_OPTION)
return super(EnvironmentModules, self).available(mod_name=mod_name, extra_args=extra_args)
@@ -1417,23 +1769,53 @@ def get_setenv_value_from_modulefile(self, mod_name, var_name):
# - line starts with 'setenv'
# - whitespace (spaces & tabs) around variable name
# - curly braces around value if it contain spaces
- value = super(EnvironmentModules, self).get_setenv_value_from_modulefile(mod_name=mod_name,
- var_name=var_name)
+ regex = re.compile(r'^setenv\s+%s\s+(?P.+)' % var_name, re.M)
+ value = self.get_value_from_modulefile(mod_name, regex, strict=False)
if value:
- value = value.strip('{}')
+ value = value.strip(' {}')
return value
+ def remove_module_path(self, path, set_mod_paths=True):
+ """
+ Remove specified module path (using 'module unuse').
+
+ :param path: path to remove from $MODULEPATH via 'unuse'
+ :param set_mod_paths: (re)set self.mod_paths
+ """
+ # remove module path via 'module use' and make sure self.mod_paths is synced
+ # Environment Modules <5.0 keeps track of how often a path was added via 'module use',
+ # so we need to check to make sure it's really removed
+ path = normalize_path(path)
+ while True:
+ try:
+ # Unuse the path that is actually present in the environment
+ module_path = next(p for p in curr_module_paths() if normalize_path(p) == path)
+ except StopIteration:
+ break
+ self.unuse(module_path)
+ if set_mod_paths:
+ self.set_mod_paths()
+
+ def update(self):
+ """Update after new modules were added."""
+
+ version = LooseVersion(self.version)
+ if build_option('update_modules_tool_cache') and version >= LooseVersion('5.3.0'):
+ out = self.run_module('cachebuild', return_stderr=True, check_output=False)
+
+ if self.testing:
+ return out
+
class Lmod(ModulesTool):
"""Interface to Lmod."""
NAME = "Lmod"
COMMAND = 'lmod'
COMMAND_ENVIRONMENT = 'LMOD_CMD'
- REQ_VERSION = '6.5.1'
- DEPR_VERSION = '7.0.0'
- REQ_VERSION_DEPENDS_ON = '7.6.1'
+ REQ_VERSION = '8.0.0'
+ DEPR_VERSION = '8.0.0'
VERSION_REGEXP = r"^Modules\s+based\s+on\s+Lua:\s+Version\s+(?P\d\S*)\s"
SHOW_HIDDEN_OPTION = '--show-hidden'
@@ -1453,11 +1835,11 @@ def __init__(self, *args, **kwargs):
setvar('LMOD_TERSE_DECORATIONS', 'no', verbose=False)
super(Lmod, self).__init__(*args, **kwargs)
- version = StrictVersion(self.version)
+ version = LooseVersion(self.version)
- self.supports_depends_on = version >= self.REQ_VERSION_DEPENDS_ON
+ self.supports_depends_on = True
# See https://lmod.readthedocs.io/en/latest/125_personal_spider_cache.html
- if version >= '8.7.12':
+ if version >= LooseVersion('8.7.12'):
self.USER_CACHE_DIR = os.path.join(os.path.expanduser('~'), '.cache', 'lmod')
else:
self.USER_CACHE_DIR = os.path.join(os.path.expanduser('~'), '.lmod.d', '.cache')
@@ -1522,14 +1904,16 @@ def update(self):
if build_option('update_modules_tool_cache'):
spider_cmd = os.path.join(os.path.dirname(self.cmd), 'spider')
- cmd = [spider_cmd, '-o', 'moduleT', os.environ['MODULEPATH']]
- self.log.debug("Running command '%s'..." % ' '.join(cmd))
+ cmd_list = [spider_cmd, '-o', 'moduleT', os.environ['MODULEPATH']]
+ cmd = ' '.join(cmd_list)
+ self.log.debug("Running command '%s'...", cmd)
- proc = subprocess_popen_text(cmd, env=os.environ)
- (stdout, stderr) = proc.communicate()
+ res = run_shell_cmd(cmd_list, env=os.environ, fail_on_error=False, use_bash=False, split_stderr=True,
+ hidden=True)
+ stdout, stderr = res.output, res.stderr
if stderr:
- raise EasyBuildError("An error occurred when running '%s': %s", ' '.join(cmd), stderr)
+ raise EasyBuildError("An error occurred when running '%s': %s", cmd, stderr)
if self.testing:
# don't actually update local cache when testing, just return the cache contents
@@ -1537,7 +1921,7 @@ def update(self):
else:
suffix = build_option('module_cache_suffix') or ''
cache_fp = os.path.join(self.USER_CACHE_DIR, 'moduleT%s.lua' % suffix)
- self.log.debug("Updating Lmod spider cache %s with output from '%s'" % (cache_fp, ' '.join(cmd)))
+ self.log.debug("Updating Lmod spider cache %s with output from '%s'", cache_fp, cmd)
cache_dir = os.path.dirname(cache_fp)
if not os.path.exists(cache_dir):
mkdir(cache_dir, parents=True)
@@ -1612,13 +1996,9 @@ def module_wrapper_exists(self, mod_name):
Determine whether a module wrapper with specified name exists.
First check for wrapper defined in .modulerc.lua, fall back to also checking .modulerc (Tcl syntax).
"""
- res = None
-
- # first consider .modulerc.lua with Lmod 7.8 (or newer)
- if StrictVersion(self.version) >= StrictVersion('7.8'):
- mod_wrapper_regex_template = r'^module_version\("(?P.*)", "%s"\)$'
- res = super(Lmod, self).module_wrapper_exists(mod_name, modulerc_fn='.modulerc.lua',
- mod_wrapper_regex_template=mod_wrapper_regex_template)
+ mod_wrapper_regex_template = r'^module_version\("(?P.*)", "%s"\)$'
+ res = super(Lmod, self).module_wrapper_exists(mod_name, modulerc_fn='.modulerc.lua',
+ mod_wrapper_regex_template=mod_wrapper_regex_template)
# fall back to checking for .modulerc in Tcl syntax
if res is None:
@@ -1670,7 +2050,7 @@ def get_software_root(name, with_env_var=False):
return res
-def get_software_libdir(name, only_one=True, fs=None):
+def get_software_libdir(name, only_one=True, fs=None, full_path=False):
"""
Find library subdirectories for the specified software package.
@@ -1681,51 +2061,57 @@ def get_software_libdir(name, only_one=True, fs=None):
:param name: name of the software package
:param only_one: indicates whether only one lib path is expected to be found
:param fs: only retain library subdirs that contain one of the files in this list
+ :param full_path: Include the software root in the returned path, or just return the subfolder found
"""
lib_subdirs = ['lib', 'lib64']
root = get_software_root(name)
- res = []
- if root:
- for lib_subdir in lib_subdirs:
- lib_dir_path = os.path.join(root, lib_subdir)
- if os.path.exists(lib_dir_path):
- # take into account that lib64 could be a symlink to lib (or vice versa)
- # see https://github.com/easybuilders/easybuild-framework/issues/3139
- if any(os.path.samefile(lib_dir_path, os.path.join(root, x)) for x in res):
- _log.debug("%s is the same as one of the other paths, so skipping it", lib_dir_path)
-
- elif fs is None or any(os.path.exists(os.path.join(lib_dir_path, f)) for f in fs):
- _log.debug("Retaining library subdir '%s' (found at %s)", lib_subdir, lib_dir_path)
- res.append(lib_subdir)
-
- elif build_option('extended_dry_run'):
- res.append(lib_subdir)
- break
-
- # if no library subdir was found, return None
- if not res:
- return None
- if only_one:
- if len(res) == 1:
- res = res[0]
- else:
- if fs is None and len(res) == 2:
- # if both lib and lib64 were found, check if only one (exactly) has libraries;
- # this is needed for software with library archives in lib64 but other files/directories in lib
- lib_glob = ['*.%s' % ext for ext in ['a', get_shared_lib_ext()]]
- has_libs = [any(glob.glob(os.path.join(root, subdir, f)) for f in lib_glob) for subdir in res]
- if has_libs[0] and not has_libs[1]:
- return res[0]
- elif has_libs[1] and not has_libs[0]:
- return res[1]
-
- raise EasyBuildError("Multiple library subdirectories found for %s in %s: %s",
- name, root, ', '.join(res))
- return res
- else:
+ if not root:
# return None if software package root could not be determined
return None
+ found_subdirs = []
+ for lib_subdir in lib_subdirs:
+ lib_dir_path = os.path.join(root, lib_subdir)
+ if os.path.exists(lib_dir_path):
+ # take into account that lib64 could be a symlink to lib (or vice versa)
+ # see https://github.com/easybuilders/easybuild-framework/issues/3139
+ if any(os.path.samefile(lib_dir_path, os.path.join(root, x)) for x in found_subdirs):
+ _log.debug("%s is the same as one of the other paths, so skipping it", lib_dir_path)
+
+ elif fs is None or any(os.path.exists(os.path.join(lib_dir_path, f)) for f in fs):
+ _log.debug("Retaining library subdir '%s' (found at %s)", lib_subdir, lib_dir_path)
+ found_subdirs.append(lib_subdir)
+
+ elif build_option('extended_dry_run'):
+ found_subdirs.append(lib_subdir)
+ break
+
+ # if no library subdir was found, return None
+ if not found_subdirs:
+ return None
+ if full_path:
+ res = [os.path.join(root, subdir) for subdir in found_subdirs]
+ else:
+ res = found_subdirs
+ if only_one:
+ if len(res) == 1:
+ res = res[0]
+ else:
+ if fs is None and len(res) == 2:
+ # if both lib and lib64 were found, check if only one (exactly) has libraries;
+ # this is needed for software with library archives in lib64 but other files/directories in lib
+ lib_glob = ['*.%s' % ext for ext in ['a', get_shared_lib_ext()]]
+ has_libs = [any(glob.glob(os.path.join(root, subdir, f)) for f in lib_glob)
+ for subdir in found_subdirs]
+ if has_libs[0] and not has_libs[1]:
+ return res[0]
+ if has_libs[1] and not has_libs[0]:
+ return res[1]
+
+ raise EasyBuildError("Multiple library subdirectories found for %s in %s: %s",
+ name, root, ', '.join(found_subdirs))
+ return res
+
def get_software_version_env_var_name(name):
"""Return name of environment variable for software root."""
@@ -1780,7 +2166,7 @@ def avail_modules_tools():
def modules_tool(mod_paths=None, testing=False):
"""
- Return interface to modules tool (environment modules (C, Tcl), or Lmod)
+ Return interface to modules tool (EnvironmentModules, Lmod, ...)
"""
# get_modules_tool might return none (e.g. if config was not initialized yet)
modules_tool = get_modules_tool()
diff --git a/easybuild/tools/multidiff.py b/easybuild/tools/multidiff.py
index 99ee949e56..e8881275c9 100644
--- a/easybuild/tools/multidiff.py
+++ b/easybuild/tools/multidiff.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2014-2024 Ghent University
+# Copyright 2014-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -291,8 +291,8 @@ def multidiff(base, files, colored=True):
offset -= 1
# construct the multi-diff based on the constructed dict
- for line_no in local_diff:
- for (line, filename) in local_diff[line_no]:
+ for line_no, line_infos in local_diff.items():
+ for (line, filename) in line_infos:
mdiff.parse_line(line_no, line.rstrip(), filename, squigly_dict.get(line, '').rstrip())
return str(mdiff)
diff --git a/easybuild/tools/options.py b/easybuild/tools/options.py
index fd22e384e5..4068aa1ad6 100644
--- a/easybuild/tools/options.py
+++ b/easybuild/tools/options.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -51,16 +51,15 @@
from easybuild.base import fancylogger # build_log should always stay there, to ensure EasyBuildLog
from easybuild.base.fancylogger import setLogLevel
from easybuild.base.generaloption import GeneralOption
-from easybuild.framework.easyblock import MODULE_ONLY_STEPS, SOURCE_STEP, FETCH_STEP, EasyBlock
+from easybuild.framework.easyblock import MODULE_ONLY_STEPS, EXTRACT_STEP, FETCH_STEP, EasyBlock
from easybuild.framework.easyconfig import EASYCONFIGS_PKG_SUBDIR
from easybuild.framework.easyconfig.easyconfig import HAVE_AUTOPEP8
from easybuild.framework.easyconfig.format.one import EB_FORMAT_EXTENSION
from easybuild.framework.easyconfig.format.pyheaderconfigobj import build_easyconfig_constants_dict
-from easybuild.framework.easyconfig.format.yeb import YEB_FORMAT_EXTENSION
from easybuild.framework.easyconfig.tools import alt_easyconfig_paths, get_paths_for
from easybuild.toolchains.compiler.systemcompiler import TC_CONSTANT_SYSTEM
from easybuild.tools import LooseVersion, build_log, run # build_log should always stay there, to ensure EasyBuildLog
-from easybuild.tools.build_log import DEVEL_LOG_LEVEL, EasyBuildError
+from easybuild.tools.build_log import DEVEL_LOG_LEVEL, EasyBuildError, EasyBuildExit
from easybuild.tools.build_log import init_logging, log_start, print_msg, print_warning, raise_easybuilderror
from easybuild.tools.config import CHECKSUM_PRIORITY_CHOICES, DEFAULT_CHECKSUM_PRIORITY
from easybuild.tools.config import CONT_IMAGE_FORMATS, CONT_TYPES, DEFAULT_CONT_TYPE, DEFAULT_ALLOW_LOADED_MODULES
@@ -68,25 +67,27 @@
from easybuild.tools.config import DEFAULT_ENV_FOR_SHEBANG, DEFAULT_ENVVAR_USERS_MODULES
from easybuild.tools.config import DEFAULT_FORCE_DOWNLOAD, DEFAULT_INDEX_MAX_AGE, DEFAULT_JOB_BACKEND
from easybuild.tools.config import DEFAULT_JOB_EB_CMD, DEFAULT_LOGFILE_FORMAT, DEFAULT_MAX_FAIL_RATIO_PERMS
-from easybuild.tools.config import DEFAULT_MINIMAL_BUILD_ENV, DEFAULT_MNS, DEFAULT_MODULE_SYNTAX, DEFAULT_MODULES_TOOL
+from easybuild.tools.config import DEFAULT_MAX_PARALLEL, DEFAULT_MINIMAL_BUILD_ENV, DEFAULT_MNS
+from easybuild.tools.config import DEFAULT_MOD_SEARCH_PATH_HEADERS, DEFAULT_MODULE_SYNTAX, DEFAULT_MODULES_TOOL
from easybuild.tools.config import DEFAULT_MODULECLASSES, DEFAULT_PATH_SUBDIRS, DEFAULT_PKG_RELEASE, DEFAULT_PKG_TOOL
from easybuild.tools.config import DEFAULT_PKG_TYPE, DEFAULT_PNS, DEFAULT_PREFIX, DEFAULT_EXTRA_SOURCE_URLS
from easybuild.tools.config import DEFAULT_REPOSITORY, DEFAULT_WAIT_ON_LOCK_INTERVAL, DEFAULT_WAIT_ON_LOCK_LIMIT
from easybuild.tools.config import DEFAULT_PR_TARGET_ACCOUNT, DEFAULT_FILTER_RPATH_SANITY_LIBS
from easybuild.tools.config import EBROOT_ENV_VAR_ACTIONS, ERROR, FORCE_DOWNLOAD_CHOICES, GENERAL_CLASS, IGNORE
from easybuild.tools.config import JOB_DEPS_TYPE_ABORT_ON_ERROR, JOB_DEPS_TYPE_ALWAYS_RUN, LOADED_MODULES_ACTIONS
-from easybuild.tools.config import LOCAL_VAR_NAMING_CHECK_WARN, LOCAL_VAR_NAMING_CHECKS
-from easybuild.tools.config import OUTPUT_STYLE_AUTO, OUTPUT_STYLES, WARN
+from easybuild.tools.config import LOCAL_VAR_NAMING_CHECK_WARN, LOCAL_VAR_NAMING_CHECKS, MOD_SEARCH_PATH_HEADERS
+from easybuild.tools.config import OUTPUT_STYLE_AUTO, OUTPUT_STYLES, WARN, build_option
from easybuild.tools.config import get_pretend_installpath, init, init_build_options, mk_full_default_path
from easybuild.tools.config import BuildOptions, ConfigurationVariables
+from easybuild.tools.config import PYTHON_SEARCH_PATH_TYPES, PYTHONPATH
from easybuild.tools.configobj import ConfigObj, ConfigObjError
from easybuild.tools.docs import FORMAT_JSON, FORMAT_MD, FORMAT_RST, FORMAT_TXT
from easybuild.tools.docs import avail_cfgfile_constants, avail_easyconfig_constants, avail_easyconfig_licenses
from easybuild.tools.docs import avail_toolchain_opts, avail_easyconfig_params, avail_easyconfig_templates
from easybuild.tools.docs import list_easyblocks, list_toolchains
from easybuild.tools.environment import restore_env, unset_env_vars
-from easybuild.tools.filetools import CHECKSUM_TYPE_SHA256, CHECKSUM_TYPES, expand_glob_paths, install_fake_vsc
-from easybuild.tools.filetools import move_file, which
+from easybuild.tools.filetools import CHECKSUM_TYPE_SHA256, CHECKSUM_TYPES, expand_glob_paths, get_cwd
+from easybuild.tools.filetools import install_fake_vsc, move_file, which, is_parent_path
from easybuild.tools.github import GITHUB_PR_DIRECTION_DESC, GITHUB_PR_ORDER_CREATED
from easybuild.tools.github import GITHUB_PR_STATE_OPEN, GITHUB_PR_STATES, GITHUB_PR_ORDERS, GITHUB_PR_DIRECTIONS
from easybuild.tools.github import HAVE_GITHUB_API, HAVE_KEYRING, VALID_CLOSE_PR_REASONS
@@ -98,15 +99,16 @@
from easybuild.tools.module_generator import ModuleGeneratorLua, avail_module_generators
from easybuild.tools.module_naming_scheme.utilities import avail_module_naming_schemes
from easybuild.tools.modules import Lmod
-from easybuild.tools.py2vs3 import string_type
from easybuild.tools.robot import det_robot_path
-from easybuild.tools.run import run_cmd
+from easybuild.tools.run import run_shell_cmd
from easybuild.tools.package.utilities import avail_package_naming_schemes
from easybuild.tools.toolchain.compiler import DEFAULT_OPT_LEVEL, OPTARCH_MAP_CHAR, OPTARCH_SEP, Compiler
+from easybuild.tools.toolchain.toolchain import DEFAULT_SEARCH_PATH_CPP_HEADERS, DEFAULT_SEARCH_PATH_LINKER, SEARCH_PATH
from easybuild.tools.toolchain.toolchain import SYSTEM_TOOLCHAIN_NAME
from easybuild.tools.repository.repository import avail_repositories
-from easybuild.tools.systemtools import UNKNOWN, check_python_version, get_cpu_architecture, get_cpu_family
-from easybuild.tools.systemtools import get_cpu_features, get_gpu_info, get_system_info
+from easybuild.tools.systemtools import DARWIN, UNKNOWN, check_python_version, get_cpu_architecture, get_cpu_family
+from easybuild.tools.systemtools import get_cpu_features, get_gpu_info, get_os_type, get_system_info
+from easybuild.tools.utilities import flatten
from easybuild.tools.version import this_is_easybuild
@@ -125,14 +127,17 @@ def terminal_supports_colors(stream):
CONFIG_ENV_VAR_PREFIX = 'EASYBUILD'
XDG_CONFIG_HOME = os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), ".config"))
-XDG_CONFIG_DIRS = os.environ.get('XDG_CONFIG_DIRS', '/etc').split(os.pathsep)
-DEFAULT_SYS_CFGFILES = [f for d in XDG_CONFIG_DIRS for f in sorted(glob.glob(os.path.join(d, 'easybuild.d', '*.cfg')))]
+XDG_CONFIG_DIRS = os.environ.get('XDG_CONFIG_DIRS', '/etc/xdg').split(os.pathsep)
+DEFAULT_SYS_CFGFILES = [[f for f in sorted(glob.glob(os.path.join(d, 'easybuild.d', '*.cfg')))]
+ for d in XDG_CONFIG_DIRS]
DEFAULT_USER_CFGFILE = os.path.join(XDG_CONFIG_HOME, 'easybuild', 'config.cfg')
DEFAULT_LIST_PR_STATE = GITHUB_PR_STATE_OPEN
DEFAULT_LIST_PR_ORDER = GITHUB_PR_ORDER_CREATED
DEFAULT_LIST_PR_DIREC = GITHUB_PR_DIRECTION_DESC
+RPATH_DEFAULT = False if get_os_type() == DARWIN else True
+
_log = fancylogger.getLogger('options', fname=False)
@@ -212,7 +217,17 @@ class EasyBuildOptions(GeneralOption):
VERSION = this_is_easybuild()
DEFAULT_LOGLEVEL = 'INFO'
- DEFAULT_CONFIGFILES = DEFAULT_SYS_CFGFILES[:]
+ # https://specifications.freedesktop.org/basedir-spec/latest/
+ # says precedence should be
+ # XDG_CONFIG_HOME > 1st entry of XDG_CONFIG_DIRS > 2nd entry ...
+ # EasyBuild parses this list backwards, gives priority to last entry
+ DEFAULT_CONFIGFILES = flatten(DEFAULT_SYS_CFGFILES[::-1])
+ if 'XDG_CONFIG_DIRS' not in os.environ:
+ old_etc_location = os.path.join('/etc', 'easybuild.d')
+ if os.path.isdir(old_etc_location) and glob.glob(os.path.join(old_etc_location, '*.cfg')):
+ _log.deprecated(f"Using {old_etc_location} is deprecated. Please use "
+ "/etc/xdg/easybuild.d instead or add /etc to XDG_CONFIG_DIRS", '6.0')
+
if os.path.exists(DEFAULT_USER_CFGFILE):
DEFAULT_CONFIGFILES.append(DEFAULT_USER_CFGFILE)
@@ -286,7 +301,7 @@ def basic_options(self):
'skip': ("Skip existing software (useful for installing additional packages)",
None, 'store_true', False, 'k'),
'stop': ("Stop the installation after certain step",
- 'choice', 'store_or_None', SOURCE_STEP, 's', all_stops),
+ 'choice', 'store_or_None', EXTRACT_STEP, 's', all_stops),
'strict': ("Set strictness level", 'choice', 'store', WARN, strictness_options),
})
@@ -341,19 +356,16 @@ def override_options(self):
# override options
descr = ("Override options", "Override default EasyBuild behavior.")
- all_deprecations = ('python2', 'Lmod6', 'easyconfig', 'toolchain')
+ all_deprecations = ('easyconfig', 'toolchain')
opts = OrderedDict({
- 'accept-eula': ("Accept EULA for specified software [DEPRECATED, use --accept-eula-for instead!]",
- 'strlist', 'store', []),
'accept-eula-for': ("Accept EULA for specified software", 'strlist', 'store', []),
- 'add-dummy-to-minimal-toolchains': ("Include dummy toolchain in minimal toolchain searches "
- "[DEPRECATED, use --add-system-to-minimal-toolchains instead!]",
- None, 'store_true', False),
'add-system-to-minimal-toolchains': ("Include system toolchain in minimal toolchain searches",
None, 'store_true', False),
'allow-loaded-modules': ("List of software names for which to allow loaded modules in initial environment",
'strlist', 'store', DEFAULT_ALLOW_LOADED_MODULES),
+ 'allow-unresolved-templates': ("Don't error out when templates such as %(name)s in EasyConfigs "
+ "could not be resolved", None, 'store_true', False),
'allow-modules-tool-mismatch': ("Allow mismatch of modules tool and definition of 'module' function",
None, 'store_true', False),
'allow-use-as-root-and-accept-consequences': ("Allow using of EasyBuild as root (NOT RECOMMENDED!)",
@@ -389,7 +401,7 @@ def override_options(self):
"for example: 3.5,5.0,7.2", 'strlist', 'extend', None),
'debug-lmod': ("Run Lmod modules tool commands in debug module", None, 'store_true', False),
'default-opt-level': ("Specify default optimisation level", 'choice', 'store', DEFAULT_OPT_LEVEL,
- Compiler.COMPILER_OPT_FLAGS),
+ Compiler.COMPILER_OPT_OPTIONS),
'deprecated': ("Run pretending to be (future) version, to test removal of deprecated code.",
None, 'store', None),
'detect-loaded-modules': ("Detect loaded EasyBuild-generated modules, act accordingly; "
@@ -410,6 +422,8 @@ def override_options(self):
'strlist', 'extend', None),
"extra-source-urls": ("Specify URLs to fetch sources from in addition to those in the easyconfig",
"urltuple", "add_flex", DEFAULT_EXTRA_SOURCE_URLS, {'metavar': 'URL[|URL]'}),
+ 'fail-on-mod-files-gcccore': ("Fail if .mod files are detected in a GCCcore install", None, 'store_true',
+ False),
'fetch': ("Allow downloading sources ignoring OS and modules tool dependencies, "
"implies --stop=fetch, --ignore-osdeps and ignore modules tool", None, 'store_true', False),
'filter-deps': ("List of dependencies that you do *not* want to install with EasyBuild, "
@@ -452,6 +466,7 @@ def override_options(self):
'insecure-download': ("Don't check the server certificate against the available certificate authorities.",
None, 'store_true', False),
'install-latest-eb-release': ("Install latest known version of easybuild", None, 'store_true', False),
+ 'keep-debug-symbols': ("Sets default value of debug toolchain option", None, 'store_true', False),
'lib-lib64-symlink': ("Automatically create symlinks for lib/ pointing to lib64/ if the former is missing",
None, 'store_true', True),
'lib64-fallback-sanity-check': ("Fallback in sanity check to lib64/ equivalent for missing libraries",
@@ -460,6 +475,8 @@ def override_options(self):
None, 'store_true', True),
'max-fail-ratio-adjust-permissions': ("Maximum ratio for failures to allow when adjusting permissions",
'float', 'store', DEFAULT_MAX_FAIL_RATIO_PERMS),
+ 'max-parallel': ("Specify maximum level of parallelism that should be used during build procedure",
+ 'int', 'store', DEFAULT_MAX_PARALLEL),
'minimal-build-env': ("Minimal build environment to define when using system toolchain, "
"specified as a comma-separated list that defines a mapping between name of "
"environment variable and its value separated by a colon (':')",
@@ -481,12 +498,18 @@ def override_options(self):
'output-style': ("Control output style; auto implies using Rich if available to produce rich output, "
"with fallback to basic colored output",
'choice', 'store', OUTPUT_STYLE_AUTO, OUTPUT_STYLES),
- 'parallel': ("Specify (maximum) level of parallelism used during build procedure",
+ 'parallel': ("Specify level of parallelism that should be used during build procedure, "
+ "(bypasses auto-detection of number of available cores; "
+ "actual value is determined by this value + 'max_parallel' easyconfig parameter)",
'int', 'store', None),
'parallel-extensions-install': ("Install list of extensions in parallel (if supported)",
None, 'store_true', False),
'pre-create-installdir': ("Create installation directory before submitting build jobs",
None, 'store_true', True),
+ 'prefer-python-search-path': (("Prefer using specified environment variable when possible to specify where"
+ " Python packages were installed; see also "
+ "https://docs.easybuild.io/python-search-path"),
+ 'choice', 'store_or_None', PYTHONPATH, PYTHON_SEARCH_PATH_TYPES),
'pretend': (("Does the build/installation in a test directory located in $HOME/easybuildinstall"),
None, 'store_true', False, 'p'),
'read-only-installdir': ("Set read-only permissions on installation directory after installation",
@@ -497,7 +520,7 @@ def override_options(self):
'required-linked-shared-libs': ("Comma-separated list of shared libraries (names, file names, or paths) "
"which must be linked in all installed binaries/libraries",
'strlist', 'extend', None),
- 'rpath': ("Enable use of RPATH for linking with libraries", None, 'store_true', False),
+ 'rpath': ("Enable use of RPATH for linking with libraries", None, 'store_true', RPATH_DEFAULT),
'rpath-filter': ("List of regex patterns to use for filtering out RPATH paths", 'strlist', 'store', None),
'rpath-override-dirs': ("Path(s) to be prepended when linking with RPATH (string, colon-separated)",
None, 'store', None),
@@ -521,9 +544,12 @@ def override_options(self):
"Git commit to use for the target software build (robot capabilities are automatically disabled)",
None, 'store', None),
'sticky-bit': ("Set sticky bit on newly created directories", None, 'store_true', False),
+ 'strict-rpath-sanity-check': ("Perform strict RPATH sanity check, which involces unsetting "
+ "$LD_LIBRARY_PATH before checking whether all required libraries are found",
+ None, 'store_true', False),
'sysroot': ("Location root directory of system, prefix for standard paths like /usr/lib and /usr/include",
None, 'store', None),
- 'trace': ("Provide more information in output to stdout on progress", None, 'store_true', False, 'T'),
+ 'trace': ("Provide more information in output to stdout on progress", None, 'store_true', True, 'T'),
'umask': ("umask to use (e.g. '022'); non-user write permissions on install directories are removed",
None, 'store', None),
'update-modules-tool-cache': ("Update modules tool cache file(s) after generating module file",
@@ -537,10 +563,6 @@ def override_options(self):
None, 'store_true', False),
'verify-easyconfig-filenames': ("Verify whether filename of specified easyconfigs matches with contents",
None, 'store_true', False),
- 'wait-on-lock': ("Wait for lock to be released; 0 implies no waiting (exit with an error if the lock "
- "already exists), non-zero value specified waiting interval [DEPRECATED: "
- "use --wait-on-lock-interval and --wait-on-lock-limit instead]",
- int, 'store_or_None', None),
'wait-on-lock-interval': ("Wait interval (in seconds) to use when waiting for existing lock to be removed",
int, 'store', DEFAULT_WAIT_ON_LOCK_INTERVAL),
'wait-on-lock-limit': ("Maximum amount of time (in seconds) to wait until lock is released (0 means no "
@@ -573,6 +595,12 @@ def config_options(self):
[DEFAULT_ENVVAR_USERS_MODULES]),
'external-modules-metadata': ("List of (glob patterns for) paths to files specifying metadata "
"for external modules (INI format)", 'strlist', 'store', None),
+ 'failed-install-build-dirs-path': ("Location where build directories are copied if installation fails; "
+ "an empty value disables copying of build directories",
+ None, 'store', None, {'metavar': "PATH"}),
+ 'failed-install-logs-path': ("Location where log files are copied if installation fails; "
+ "an empty value disables copying of log files",
+ None, 'store', None, {'metavar': "PATH"}),
'hooks': ("Location of Python module with hook implementations", 'str', 'store', None),
'ignore-dirs': ("Directory names to ignore when searching for files/dirs",
'strlist', 'store', ['.git', '.svn']),
@@ -594,10 +622,13 @@ def config_options(self):
'strtuple', 'store', DEFAULT_LOGFILE_FORMAT[:], {'metavar': 'DIR,FORMAT'}),
'module-depends-on': ("Use depends_on (Lmod 7.6.1+) for dependencies in all generated modules "
"(implies recursive unloading of modules).",
- None, 'store_true', False),
+ None, 'store_true', True),
'module-extensions': ("Include 'extensions' statement in generated module file (Lua syntax only)",
- None, 'store_true', False),
+ None, 'store_true', True),
'module-naming-scheme': ("Module naming scheme to use", None, 'store', DEFAULT_MNS),
+ 'module-search-path-headers': ("Environment variable set by modules on load with search paths "
+ "to header files", 'choice', 'store', DEFAULT_MOD_SEARCH_PATH_HEADERS,
+ sorted(MOD_SEARCH_PATH_HEADERS.keys())),
'module-syntax': ("Syntax to be used for module files", 'choice', 'store', DEFAULT_MODULE_SYNTAX,
sorted(avail_module_generators().keys())),
'moduleclasses': (("Extend supported module classes "
@@ -624,6 +655,10 @@ def config_options(self):
"(is passed as list of arguments to create the repository instance). "
"For more info, use --avail-repositories."),
'strlist', 'store', self.default_repositorypath),
+ 'search-path-cpp-headers': ("Search path used at build time for include directories", 'choice',
+ 'store', DEFAULT_SEARCH_PATH_CPP_HEADERS, [*SEARCH_PATH["cpp_headers"]]),
+ 'search-path-linker': ("Search path used at build time by the linker for libraries", 'choice',
+ 'store', DEFAULT_SEARCH_PATH_LINKER, [*SEARCH_PATH["linker"]]),
'sourcepath': ("Path(s) to where sources should be downloaded (string, colon-separated)",
None, 'store', mk_full_default_path('sourcepath')),
'subdir-modules': ("Installpath subdir for modules", None, 'store', DEFAULT_PATH_SUBDIRS['subdir_modules']),
@@ -845,7 +880,7 @@ def job_options(self):
'eb-cmd': ("EasyBuild command to use in jobs", 'str', 'store', DEFAULT_JOB_EB_CMD),
'max-jobs': ("Maximum number of concurrent jobs (queued and running, 0 = unlimited)", 'int', 'store', 0),
'max-walltime': ("Maximum walltime for jobs (in hours)", 'int', 'store', 24),
- 'output-dir': ("Output directory for jobs (default: current directory)", None, 'store', os.getcwd()),
+ 'output-dir': ("Output directory for jobs (default: current directory)", None, 'store', get_cwd()),
'polling-interval': ("Interval between polls for status of jobs (in seconds)", float, 'store', 30.0),
'target-resource': ("Target resource for jobs", None, 'store', None),
})
@@ -916,7 +951,10 @@ def validate(self):
error_msgs.append(error_msg % (cuda_cc_regex.pattern, ', '.join(faulty_cuda_ccs)))
if error_msgs:
- raise EasyBuildError("Found problems validating the options: %s", '\n'.join(error_msgs))
+ raise EasyBuildError(
+ "Found problems validating the options: %s", '\n'.join(error_msgs),
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
def postprocess(self):
"""Do some postprocessing, in particular print stuff"""
@@ -941,10 +979,6 @@ def postprocess(self):
# set tmpdir
self.tmpdir = set_tmpdir(self.options.tmpdir)
- # early check for opt-in to installing extensions in parallel (experimental feature)
- if self.options.parallel_extensions_install:
- self.log.experimental("installing extensions in parallel")
-
# take --include options into account (unless instructed otherwise)
if self.with_include:
self._postprocess_include()
@@ -1004,15 +1038,20 @@ def _postprocess_optarch(self):
n_parts = len(optarch_parts)
map_char_cnts = [p.count(OPTARCH_MAP_CHAR) for p in optarch_parts]
if (n_parts > 1 and any(c != 1 for c in map_char_cnts)) or (n_parts == 1 and map_char_cnts[0] > 1):
- raise EasyBuildError("The optarch option has an incorrect syntax: %s", self.options.optarch)
+ raise EasyBuildError(
+ "The optarch option has an incorrect syntax: %s", self.options.optarch,
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
else:
# if there are options for different compilers, we set up a dict
if OPTARCH_MAP_CHAR in optarch_parts[0]:
optarch_dict = {}
for compiler, compiler_opt in [p.split(OPTARCH_MAP_CHAR) for p in optarch_parts]:
if compiler in optarch_dict:
- raise EasyBuildError("The optarch option contains duplicated entries for compiler %s: %s",
- compiler, self.options.optarch)
+ raise EasyBuildError(
+ "The optarch option contains duplicated entries for compiler %s: %s",
+ compiler, self.options.optarch, exit_code=EasyBuildExit.OPTION_ERROR
+ )
else:
optarch_dict[compiler] = compiler_opt
self.options.optarch = optarch_dict
@@ -1024,13 +1063,17 @@ def _postprocess_optarch(self):
def _postprocess_close_pr_reasons(self):
"""Postprocess --close-pr-reasons options"""
if self.options.close_pr_msg:
- raise EasyBuildError("Please either specify predefined reasons with --close-pr-reasons or " +
- "a custom message with--close-pr-msg")
+ raise EasyBuildError(
+ "Please either select a reason with --close-pr-reasons or add a custom message with--close-pr-msg",
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
reasons = self.options.close_pr_reasons.split(',')
if any([reason not in VALID_CLOSE_PR_REASONS.keys() for reason in reasons]):
- raise EasyBuildError("Argument to --close-pr_reasons must be a comma separated list of valid reasons " +
- "among %s" % VALID_CLOSE_PR_REASONS.keys())
+ raise EasyBuildError(
+ "Argument to --close-pr_reasons must be a comma separated list of valid reasons among %s",
+ VALID_CLOSE_PR_REASONS.keys(), exit_code=EasyBuildExit.OPTION_ERROR
+ )
self.options.close_pr_msg = ", ".join([VALID_CLOSE_PR_REASONS[reason] for reason in reasons])
def _postprocess_list_prs(self):
@@ -1039,18 +1082,30 @@ def _postprocess_list_prs(self):
nparts = len(list_pr_parts)
if nparts > 3:
- raise EasyBuildError("Argument to --list-prs must be in the format 'state[,order[,direction]]")
+ raise EasyBuildError(
+ "Argument to --list-prs must be in the format 'state[,order[,direction]]",
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
list_pr_state = list_pr_parts[0]
list_pr_order = list_pr_parts[1] if nparts > 1 else DEFAULT_LIST_PR_ORDER
list_pr_direc = list_pr_parts[2] if nparts > 2 else DEFAULT_LIST_PR_DIREC
if list_pr_state not in GITHUB_PR_STATES:
- raise EasyBuildError("1st item in --list-prs ('%s') must be one of %s", list_pr_state, GITHUB_PR_STATES)
+ raise EasyBuildError(
+ "1st item in --list-prs ('%s') must be one of %s", list_pr_state, GITHUB_PR_STATES,
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
if list_pr_order not in GITHUB_PR_ORDERS:
- raise EasyBuildError("2nd item in --list-prs ('%s') must be one of %s", list_pr_order, GITHUB_PR_ORDERS)
+ raise EasyBuildError(
+ "2nd item in --list-prs ('%s') must be one of %s", list_pr_order, GITHUB_PR_ORDERS,
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
if list_pr_direc not in GITHUB_PR_DIRECTIONS:
- raise EasyBuildError("3rd item in --list-prs ('%s') must be one of %s", list_pr_direc, GITHUB_PR_DIRECTIONS)
+ raise EasyBuildError(
+ "3rd item in --list-prs ('%s') must be one of %s", list_pr_direc, GITHUB_PR_DIRECTIONS,
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
self.options.list_prs = (list_pr_state, list_pr_order, list_pr_direc)
@@ -1072,41 +1127,62 @@ def _postprocess_checks(self):
# fail early if required dependencies for functionality requiring using GitHub API are not available:
if self.options.from_pr or self.options.include_easyblocks_from_pr or self.options.upload_test_report:
if not HAVE_GITHUB_API:
- raise EasyBuildError("Required support for using GitHub API is not available (see warnings)")
+ raise EasyBuildError(
+ "Required support for using GitHub API is not available (see warnings)",
+ exit_code=EasyBuildExit.FAIL_GITHUB
+ )
# using Lua module syntax only makes sense when modules tool being used is Lmod
if self.options.module_syntax == ModuleGeneratorLua.SYNTAX and self.options.modules_tool != Lmod.__name__:
error_msg = "Generating Lua module files requires Lmod as modules tool; "
mod_syntaxes = ', '.join(sorted(avail_module_generators().keys()))
error_msg += "use --module-syntax to specify a different module syntax to use (%s)" % mod_syntaxes
- raise EasyBuildError(error_msg)
+ raise EasyBuildError(error_msg, exit_code=EasyBuildExit.OPTION_ERROR)
# check whether specified action --detect-loaded-modules is valid
if self.options.detect_loaded_modules not in LOADED_MODULES_ACTIONS:
error_msg = "Unknown action specified to --detect-loaded-modules: %s (known values: %s)"
- raise EasyBuildError(error_msg % (self.options.detect_loaded_modules, ', '.join(LOADED_MODULES_ACTIONS)))
+ raise EasyBuildError(
+ error_msg, self.options.detect_loaded_modules, ', '.join(LOADED_MODULES_ACTIONS),
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
# make sure a GitHub token is available when it's required
if self.options.upload_test_report:
if not HAVE_KEYRING:
- raise EasyBuildError("Python 'keyring' module required for obtaining GitHub token is not available")
+ raise EasyBuildError(
+ "Python 'keyring' module required for obtaining GitHub token is not available",
+ exit_code=EasyBuildExit.MISSING_EB_DEPENDENCY
+ )
if self.options.github_user is None:
- raise EasyBuildError("No GitHub user name provided, required for fetching GitHub token")
+ raise EasyBuildError(
+ "No GitHub user name provided, required for fetching GitHub token",
+ exit_code=EasyBuildExit.FAIL_GITHUB
+ )
token = fetch_github_token(self.options.github_user)
if token is None:
- raise EasyBuildError("Failed to obtain required GitHub token for user '%s'" % self.options.github_user)
+ raise EasyBuildError(
+ "Failed to obtain required GitHub token for user '%s'", self.options.github_user,
+ exit_code=EasyBuildExit.FAIL_GITHUB
+ )
# make sure autopep8 is available when it needs to be
if self.options.dump_autopep8:
if not HAVE_AUTOPEP8:
- raise EasyBuildError("Python 'autopep8' module required to reformat dumped easyconfigs as requested")
+ raise EasyBuildError(
+ "Python 'autopep8' module required to reformat dumped easyconfigs as requested",
+ exit_code=EasyBuildExit.MISSING_EB_DEPENDENCY
+ )
# if a path is specified to --sysroot, it must exist
if self.options.sysroot:
if os.path.exists(self.options.sysroot):
self.log.info("Specified sysroot '%s' exists: OK", self.options.sysroot)
else:
- raise EasyBuildError("Specified sysroot '%s' does not exist!", self.options.sysroot)
+ raise EasyBuildError(
+ "Specified sysroot '%s' does not exist!", self.options.sysroot,
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
# 'software_commit' is specific to a particular software package and cannot be used with 'robot'
if self.options.software_commit:
@@ -1131,14 +1207,17 @@ def _ensure_abs_path(self, opt_name):
opt_val = getattr(self.options, opt_name)
if opt_val:
- if isinstance(opt_val, string_type):
+ if isinstance(opt_val, str):
setattr(self.options, opt_name, self.get_cfg_opt_abs_path(opt_name, opt_val))
elif isinstance(opt_val, list):
abs_paths = [self.get_cfg_opt_abs_path(opt_name, p) for p in opt_val]
setattr(self.options, opt_name, abs_paths)
else:
error_msg = "Don't know how to ensure absolute path(s) for '%s' configuration option (value type: %s)"
- raise EasyBuildError(error_msg, opt_name, type(opt_val))
+ raise EasyBuildError(
+ error_msg, opt_name, type(opt_val),
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
def _postprocess_config(self):
"""Postprocessing of configuration options"""
@@ -1147,22 +1226,23 @@ def _postprocess_config(self):
# to avoid incorrect paths being used when EasyBuild changes the current working directory
# (see https://github.com/easybuilders/easybuild-framework/issues/3619);
# ensuring absolute paths for 'robot' is handled separately below,
- # because we need to be careful with the argument pass to --robot;
+ # because we need to be careful with the argument passed to --robot;
# note: repositorypath is purposely not listed here, because it's a special case:
# - the value could consist of a 2-tuple (, );
# - the could also specify the location of a *remote* (Git( repository,
# which can be done in variety of formats (git@:/), https://, etc.)
# (see also https://github.com/easybuilders/easybuild-framework/issues/3892);
- path_opt_names = ['buildpath', 'containerpath', 'git_working_dirs_path', 'installpath',
- 'installpath_modules', 'installpath_software', 'prefix', 'packagepath',
- 'robot_paths', 'sourcepath']
+ path_opt_names = ['buildpath', 'containerpath', 'failed_install_build_dirs_path', 'failed_install_logs_path',
+ 'git_working_dirs_path', 'installpath', 'installpath_modules', 'installpath_software',
+ 'prefix', 'packagepath', 'robot_paths', 'sourcepath']
for opt_name in path_opt_names:
self._ensure_abs_path(opt_name)
if self.options.prefix is not None:
- # prefix applies to all paths, and repository has to be reinitialised to take new repositorypath in account
- # in the legacy-style configuration, repository is initialised in configuration file itself
+ # prefix applies to selected path configuration options;
+ # repository has to be reinitialised to take new repositorypath in account;
+ # in the legacy-style configuration, repository is initialised in configuration file itself;
path_opts = ['buildpath', 'containerpath', 'installpath', 'packagepath', 'repository', 'repositorypath',
'sourcepath']
for dest in path_opts:
@@ -1187,18 +1267,21 @@ def _postprocess_config(self):
# which makes it susceptible to 'eating' the following argument/option;
# for example: with 'eb -r foo', 'foo' must be an existing directory (or 'eb foo -r' should be used);
# when multiple directories are specified, we deliberately do not enforce that all of them exist;
- # if a single argument is passed to --robot/-r that ends with '.eb' or '.yeb', we assume it's an easyconfig
+ # if a single argument is passed to --robot/-r that ends with '.eb' we assume it's an easyconfig
if len(self.options.robot) == 1:
robot_arg = self.options.robot[0]
if not os.path.isdir(robot_arg):
- if robot_arg.endswith(EB_FORMAT_EXTENSION) or robot_arg.endswith(YEB_FORMAT_EXTENSION):
+ if robot_arg.endswith(EB_FORMAT_EXTENSION):
info_msg = "Sole --robot argument %s is not an existing directory, "
info_msg += "promoting it to a stand-alone argument since it looks like an easyconfig file name"
self.log.info(info_msg, robot_arg)
self.args.append(robot_arg)
self.options.robot = []
else:
- raise EasyBuildError("Argument passed to --robot is not an existing directory: %s", robot_arg)
+ raise EasyBuildError(
+ "Argument passed to --robot is not an existing directory: %s", robot_arg,
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
# paths specified to --robot have preference over --robot-paths
# keep both values in sync if robot is enabled, which implies enabling dependency resolver
@@ -1220,6 +1303,20 @@ def _postprocess_config(self):
if self.options.inject_checksums or self.options.inject_checksums_to_json:
self.options.pre_create_installdir = False
+ # Prevent that build directories and logs for failed installations are copied to location for build directories
+ if self.options.buildpath and self.options.failed_install_logs_path:
+ if is_parent_path(self.options.buildpath, self.options.failed_install_logs_path):
+ raise EasyBuildError(
+ f"The --failed-install-logs-path ('{self.options.failed_install_logs_path}') "
+ f"cannot reside in a subdirectory of the --buildpath ('{self.options.buildpath}')"
+ )
+ if self.options.buildpath and self.options.failed_install_build_dirs_path:
+ if is_parent_path(self.options.buildpath, self.options.failed_install_build_dirs_path):
+ raise EasyBuildError(
+ f"The --failed-install-build-dirs-path ('{self.options.failed_install_build_dirs_path}') "
+ f"cannot reside in a subdirectory of the --buildpath ('{self.options.buildpath}')"
+ )
+
def _postprocess_list_avail(self):
"""Create all the additional info that can be requested (exit at the end)"""
msg = ''
@@ -1335,10 +1432,11 @@ def show_default_configfiles(self):
'',
"* user-level: %s" % os.path.join('${XDG_CONFIG_HOME:-$HOME/.config}', 'easybuild', 'config.cfg'),
" -> %s => %s" % (DEFAULT_USER_CFGFILE, ('not found', 'found')[os.path.exists(DEFAULT_USER_CFGFILE)]),
- "* system-level: %s" % os.path.join('${XDG_CONFIG_DIRS:-/etc}', 'easybuild.d', '*.cfg'),
- " -> %s => %s" % (system_cfg_glob_paths, ', '.join(DEFAULT_SYS_CFGFILES) or "(no matches)"),
+ "* system-level: %s" % os.path.join('${XDG_CONFIG_DIRS:-/etc/xdg}', 'easybuild.d', '*.cfg'),
+ " -> %s => %s" % (system_cfg_glob_paths, ', '.join(flatten(DEFAULT_SYS_CFGFILES)) or "(no matches)"),
'',
- "Default list of existing configuration files (%d): %s" % (found_cfgfile_cnt, found_cfgfile_list),
+ "Default list of existing configuration files (%d, most important last):" % found_cfgfile_cnt,
+ found_cfgfile_list,
]
return '\n'.join(lines)
@@ -1387,9 +1485,9 @@ def show_system_info(self):
'',
"* GPU:",
])
- for vendor in gpu_info:
+ for vendor, vendor_gpu in gpu_info.items():
lines.append(" -> %s" % vendor)
- for gpu, num in gpu_info[vendor].items():
+ for gpu, num in vendor_gpu.items():
lines.append(" -> %sx %s" % (num, gpu))
lines.extend([
@@ -1409,7 +1507,8 @@ def show_config(self):
# options that should never/always be printed
ignore_opts = ['show_config', 'show_full_config']
- include_opts = ['buildpath', 'containerpath', 'installpath', 'repositorypath', 'robot_paths', 'sourcepath']
+ include_opts = ['buildpath', 'containerpath', 'installpath', 'repositorypath', 'robot_paths',
+ 'rpath', 'sourcepath']
cmdline_opts_dict = self.dict_by_prefix()
def reparse_cfg(args=None, withcfg=True):
@@ -1523,7 +1622,11 @@ def parse_options(args=None, with_include=True):
go_args=eb_args, error_env_options=True, error_env_option_method=raise_easybuilderror,
with_include=with_include)
except EasyBuildError as err:
- raise EasyBuildError("Failed to parse configuration options: %s" % err)
+ try:
+ exit_code = err.exit_code
+ except AttributeError:
+ exit_code = EasyBuildExit.OPTION_ERROR
+ raise EasyBuildError("Failed to parse configuration options: %s", err, exit_code=exit_code)
return eb_go
@@ -1533,12 +1636,15 @@ def check_options(options):
Check configuration options, some combinations are not allowed.
"""
if options.from_commit and options.from_pr:
- raise EasyBuildError("--from-commit and --from-pr should not be used together, pick one")
+ raise EasyBuildError(
+ "--from-commit and --from-pr should not be used together, pick one",
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
if options.include_easyblocks_from_commit and options.include_easyblocks_from_pr:
error_msg = "--include-easyblocks-from-commit and --include-easyblocks-from-pr "
error_msg += "should not be used together, pick one"
- raise EasyBuildError(error_msg)
+ raise EasyBuildError(error_msg, exit_code=EasyBuildExit.OPTION_ERROR)
def check_root_usage(allow_use_as_root=False):
@@ -1572,6 +1678,7 @@ def check_included_multiple(included_easyblocks_from, source):
print_warning(warning_msg)
if options.include_easyblocks_from_pr or options.include_easyblocks_from_commit:
+ terse = build_option('terse')
if options.include_easyblocks:
# check if you are including the same easyblock twice
@@ -1582,7 +1689,10 @@ def check_included_multiple(included_easyblocks_from, source):
try:
easyblock_prs = [int(x) for x in options.include_easyblocks_from_pr]
except ValueError:
- raise EasyBuildError("Argument to --include-easyblocks-from-pr must be a comma separated list of PR #s")
+ raise EasyBuildError(
+ "Argument to --include-easyblocks-from-pr must be a comma separated list of PR #s",
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
for easyblock_pr in easyblock_prs:
easyblocks_from_pr = fetch_easyblocks_from_pr(easyblock_pr)
@@ -1593,7 +1703,7 @@ def check_included_multiple(included_easyblocks_from, source):
included_easyblocks |= included_from_pr
for easyblock in included_from_pr:
- print_msg("easyblock %s included from PR #%s" % (easyblock, easyblock_pr), log=log)
+ print_msg("easyblock %s included from PR #%s" % (easyblock, easyblock_pr), log=log, silent=terse)
include_easyblocks(options.tmpdir, easyblocks_from_pr)
@@ -1606,7 +1716,8 @@ def check_included_multiple(included_easyblocks_from, source):
check_included_multiple(included_from_commit, "commit %s" % easyblock_commit)
for easyblock in included_from_commit:
- print_msg("easyblock %s included from commit %s" % (easyblock, easyblock_commit), log=log)
+ print_msg("easyblock %s included from commit %s" % (easyblock, easyblock_commit),
+ log=log, silent=terse)
include_easyblocks(options.tmpdir, easyblocks_from_commit)
@@ -1677,12 +1788,18 @@ def set_up_configuration(args=None, logfile=None, testing=False, silent=False, r
try:
from_prs = [int(x) for x in eb_go.options.from_pr]
except ValueError:
- raise EasyBuildError("Argument to --from-pr must be a comma separated list of PR #s.")
+ raise EasyBuildError(
+ "Argument to --from-pr must be a comma separated list of PR #s.",
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
try:
review_pr = (lambda x: int(x) if x else None)(eb_go.options.review_pr)
except ValueError:
- raise EasyBuildError("Argument to --review-pr must be an integer PR #.")
+ raise EasyBuildError(
+ "Argument to --review-pr must be an integer PR #.",
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
# determine robot path
# --try-X, --dep-graph, --search use robot path for searching, so enable it with path of installed easyconfigs
@@ -1857,7 +1974,10 @@ def parse_external_modules_metadata(cfgs):
paths.extend(res)
else:
# if there are no matches, we report an error to avoid silently ignores faulty paths
- raise EasyBuildError("Specified path for file with external modules metadata does not exist: %s", cfg)
+ raise EasyBuildError(
+ "Specified path for file with external modules metadata does not exist: %s", cfg,
+ exit_code=EasyBuildExit.OPTION_ERROR
+ )
cfgs = paths
# use external modules metadata configuration files that are available by default, unless others are specified
@@ -1889,7 +2009,10 @@ def parse_external_modules_metadata(cfgs):
try:
parsed_metadata.merge(ConfigObj(cfg))
except ConfigObjError as err:
- raise EasyBuildError("Failed to parse %s with external modules metadata: %s", cfg, err)
+ raise EasyBuildError(
+ "Failed to parse %s with external modules metadata: %s", cfg, err,
+ exit_code=EasyBuildExit.MODULE_ERROR
+ )
known_metadata_keys = ['name', 'prefix', 'version']
unknown_keys = {}
@@ -1902,7 +2025,7 @@ def parse_external_modules_metadata(cfgs):
unknown_keys.setdefault(mod, []).append(key)
for key in ['name', 'version']:
- if isinstance(entry.get(key), string_type):
+ if isinstance(entry.get(key), str):
entry[key] = [entry[key]]
_log.debug("Transformed external module metadata value %s for %s into a single-value list: %s",
key, mod, entry[key])
@@ -1910,14 +2033,17 @@ def parse_external_modules_metadata(cfgs):
# if both names and versions are available, lists must be of same length
names, versions = entry.get('name'), entry.get('version')
if names is not None and versions is not None and len(names) != len(versions):
- raise EasyBuildError("Different length for lists of names/versions in metadata for external module %s: "
- "names: %s; versions: %s", mod, names, versions)
+ raise EasyBuildError(
+ "Different length for lists of names/versions in metadata for external module %s: ; "
+ "names: %s; versions: %s", mod, names, versions,
+ exit_code=EasyBuildExit.MODULE_ERROR
+ )
if unknown_keys:
error_msg = "Found metadata entries with unknown keys:"
for mod in sorted(unknown_keys.keys()):
error_msg += "\n* %s: %s" % (mod, ', '.join(sorted(unknown_keys[mod])))
- raise EasyBuildError(error_msg)
+ raise EasyBuildError(error_msg, exit_code=EasyBuildExit.MODULE_ERROR)
_log.debug("External modules metadata: %s", parsed_metadata)
return parsed_metadata
@@ -1968,8 +2094,9 @@ def set_tmpdir(tmpdir=None, raise_error=False):
fd, tmptest_file = tempfile.mkstemp()
os.close(fd)
os.chmod(tmptest_file, 0o700)
- if not run_cmd(tmptest_file, simple=True, log_ok=False, regexp=False, force_in_dry_run=True, trace=False,
- stream_output=False, with_hooks=False, with_sysroot=False):
+ res = run_shell_cmd(tmptest_file, fail_on_error=False, in_dry_run=True, hidden=True, stream_output=False,
+ with_hooks=False)
+ if res.exit_code != EasyBuildExit.SUCCESS:
msg = "The temporary directory (%s) does not allow to execute files. " % tempfile.gettempdir()
msg += "This can cause problems in the build process, consider using --tmpdir."
if raise_error:
@@ -2002,6 +2129,7 @@ def opts_dict_to_eb_opts(args_dict):
:return: a list of strings representing command-line options for the 'eb' command
"""
+ allow_multiple_calls = ['amend', 'try-amend']
_log.debug("Converting dictionary %s to argument list" % args_dict)
args = []
for arg in sorted(args_dict):
@@ -2011,14 +2139,18 @@ def opts_dict_to_eb_opts(args_dict):
prefix = '--'
option = prefix + str(arg)
value = args_dict[arg]
- if isinstance(value, (list, tuple)):
- value = ','.join(str(x) for x in value)
- if value in [True, None]:
+ if str(arg) in allow_multiple_calls:
+ if not isinstance(value, (list, tuple)):
+ value = [value]
+ args.extend(option + '=' + str(x) for x in value)
+ elif value in [True, None]:
args.append(option)
elif value is False:
args.append('--disable-' + option[2:])
elif value is not None:
+ if isinstance(value, (list, tuple)):
+ value = ','.join(str(x) for x in value)
args.append(option + '=' + str(value))
_log.debug("Converted dictionary %s to argument list %s" % (args_dict, args))
diff --git a/easybuild/tools/output.py b/easybuild/tools/output.py
index a8f98480f9..8439d8f08b 100644
--- a/easybuild/tools/output.py
+++ b/easybuild/tools/output.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# #
-# Copyright 2021-2024 Ghent University
+# Copyright 2021-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -33,6 +33,7 @@
"""
import functools
from collections import OrderedDict
+import sys
from easybuild.tools.build_log import EasyBuildError
from easybuild.tools.config import OUTPUT_STYLE_RICH, build_option, get_output_style
@@ -328,9 +329,7 @@ def print_checks(checks_data):
if use_rich():
console = Console()
- # don't use console.print, which causes SyntaxError in Python 2
- console_print = getattr(console, 'print') # noqa: B009
- console_print('')
+ console.print('')
for section in checks_data:
section_checks = checks_data[section]
@@ -382,11 +381,25 @@ def print_checks(checks_data):
lines.append('')
if use_rich():
- console_print(table)
+ console.print(table)
else:
print('\n'.join(lines))
+def print_error(error_msg, rich_highlight=True):
+ """
+ Print error message, using a Rich Console instance if possible.
+ Newlines before/after message are automatically added.
+
+ :param rich_highlight: boolean indicating whether automatic highlighting by Rich should be enabled
+ """
+ if use_rich():
+ console = Console(stderr=True)
+ console.print('\n\n' + error_msg + '\n', highlight=rich_highlight)
+ else:
+ sys.stderr.write('\n' + error_msg + '\n\n')
+
+
# this constant must be defined at the end, since functions used as values need to be defined
PROGRESS_BAR_TYPES = {
PROGRESS_BAR_DOWNLOAD_ALL: download_all_progress_bar,
diff --git a/easybuild/tools/package/package_naming_scheme/easybuild_deb_friendly_pns.py b/easybuild/tools/package/package_naming_scheme/easybuild_deb_friendly_pns.py
index 033687fac1..d106a46554 100644
--- a/easybuild/tools/package/package_naming_scheme/easybuild_deb_friendly_pns.py
+++ b/easybuild/tools/package/package_naming_scheme/easybuild_deb_friendly_pns.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- vim: set fileencoding=utf-8
##
-# Copyright 2015-2024 Ghent University
+# Copyright 2015-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/tools/package/package_naming_scheme/easybuild_pns.py b/easybuild/tools/package/package_naming_scheme/easybuild_pns.py
index 7a53a86101..5136a5d89e 100644
--- a/easybuild/tools/package/package_naming_scheme/easybuild_pns.py
+++ b/easybuild/tools/package/package_naming_scheme/easybuild_pns.py
@@ -1,5 +1,5 @@
##
-# Copyright 2015-2024 Ghent University
+# Copyright 2015-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/tools/package/package_naming_scheme/pns.py b/easybuild/tools/package/package_naming_scheme/pns.py
index 8683ae8d37..fbc503be72 100644
--- a/easybuild/tools/package/package_naming_scheme/pns.py
+++ b/easybuild/tools/package/package_naming_scheme/pns.py
@@ -1,5 +1,5 @@
##
-# Copyright 2015-2024 Ghent University
+# Copyright 2015-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/tools/package/utilities.py b/easybuild/tools/package/utilities.py
index bf55fdcff1..5f55983f05 100644
--- a/easybuild/tools/package/utilities.py
+++ b/easybuild/tools/package/utilities.py
@@ -1,5 +1,5 @@
##
-# Copyright 2015-2024 Ghent University
+# Copyright 2015-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -39,13 +39,13 @@
import pprint
from easybuild.base import fancylogger
+from easybuild.base.wrapper import create_base_metaclass
from easybuild.tools.config import PKG_TOOL_FPM, PKG_TYPE_RPM, Singleton
from easybuild.tools.config import build_option, get_package_naming_scheme, log_path
from easybuild.tools.build_log import EasyBuildError
from easybuild.tools.filetools import change_dir, which
from easybuild.tools.package.package_naming_scheme.pns import PackageNamingScheme
-from easybuild.tools.py2vs3 import create_base_metaclass
-from easybuild.tools.run import run_cmd
+from easybuild.tools.run import run_shell_cmd
from easybuild.tools.utilities import get_subclasses, import_available_modules
@@ -145,7 +145,7 @@ def package_with_fpm(easyblock):
])
cmd = ' '.join(cmdlist)
_log.debug("The flattened cmdlist looks like: %s", cmd)
- run_cmd(cmdlist, log_all=True, simple=True, shell=False)
+ run_shell_cmd(cmdlist, use_bash=False)
_log.info("Created %s package(s) in %s", pkgtype, workdir)
diff --git a/easybuild/tools/parallelbuild.py b/easybuild/tools/parallelbuild.py
index 3ab31e1efc..94e971e7a6 100644
--- a/easybuild/tools/parallelbuild.py
+++ b/easybuild/tools/parallelbuild.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -33,6 +33,7 @@
* Toon Willems (Ghent University)
* Kenneth Hoste (Ghent University)
* Stijn De Weirdt (Ghent University)
+* Bart Oldeman (McGill University, Calcul Quebec, Digital Research Alliance of Canada)
"""
import math
import os
@@ -43,8 +44,9 @@
from easybuild.framework.easyconfig.easyconfig import ActiveMNS
from easybuild.tools.build_log import EasyBuildError
from easybuild.tools.config import build_option, get_repository, get_repositorypath
+from easybuild.tools.filetools import get_cwd
from easybuild.tools.module_naming_scheme.utilities import det_full_ec_version
-from easybuild.tools.job.backend import job_backend
+from easybuild.tools.job.backend import job_backend, JobBackend
from easybuild.tools.repository.repository import init_repository
@@ -56,7 +58,8 @@ def _to_key(dep):
return ActiveMNS().det_full_module_name(dep)
-def build_easyconfigs_in_parallel(build_command, easyconfigs, output_dir='easybuild-build', prepare_first=True):
+def build_easyconfigs_in_parallel(build_command, easyconfigs, output_dir='easybuild-build', testing=False,
+ prepare_first=True, tweak_map=None, try_opts=''):
"""
Build easyconfigs in parallel by submitting jobs to a batch-queuing system.
Return list of jobs submitted.
@@ -68,11 +71,14 @@ def build_easyconfigs_in_parallel(build_command, easyconfigs, output_dir='easybu
:param build_command: build command to use
:param easyconfigs: list of easyconfig files
:param output_dir: output directory
+ :param testing: If `True`, skip actual job submission
:param prepare_first: prepare by runnning fetch step first for each easyconfig
+ :param tweak_map: Mapping from tweaked to original easyconfigs
+ :param try_opts: --try-* options to pass if the easyconfig is tweaked
"""
_log.info("going to build these easyconfigs in parallel: %s", [os.path.basename(ec['spec']) for ec in easyconfigs])
- active_job_backend = job_backend()
+ active_job_backend = JobBackend() if testing else job_backend()
if active_job_backend is None:
raise EasyBuildError("Can not use --job if no job backend is available.")
@@ -92,12 +98,17 @@ def build_easyconfigs_in_parallel(build_command, easyconfigs, output_dir='easybu
# this is very important, otherwise we might have race conditions
# e.g. GCC-4.5.3 finds cloog.tar.gz but it was incorrectly downloaded by GCC-4.6.3
# running this step here, prevents this
- if prepare_first:
+ if prepare_first and not testing:
prepare_easyconfig(easyconfig)
+ # convert to to avoid needing a shared tmpdir
+ spec = easyconfig['spec']
+ if spec in (tweak_map or {}):
+ spec = tweak_map[spec] + try_opts
+
# the new job will only depend on already submitted jobs
- _log.info("creating job for ec: %s" % os.path.basename(easyconfig['spec']))
- new_job = create_job(active_job_backend, build_command, easyconfig, output_dir=output_dir)
+ _log.info("creating job for ec: %s using %s" % (os.path.basename(easyconfig['spec']), spec))
+ new_job = create_job(active_job_backend, build_command, easyconfig, output_dir=output_dir, spec=spec)
# filter out dependencies marked as external modules
deps = [d for d in easyconfig['ec'].all_dependencies if not d.get('external_module', False)]
@@ -115,24 +126,27 @@ def build_easyconfigs_in_parallel(build_command, easyconfigs, output_dir='easybu
active_job_backend.complete()
- return jobs
+ return build_command if testing else jobs
-def submit_jobs(ordered_ecs, cmd_line_opts, testing=False, prepare_first=True):
+def submit_jobs(ordered_ecs, cmd_line_opts, testing=False, prepare_first=True, tweak_map=None):
"""
Submit jobs.
:param ordered_ecs: list of easyconfigs, in the order they should be processed
:param cmd_line_opts: list of command line options (in 'longopt=value' form)
:param testing: If `True`, skip actual job submission
:param prepare_first: prepare by runnning fetch step first for each easyconfig
+ :param tweak_map: Mapping from tweaked to original easyconfigs
"""
- curdir = os.getcwd()
+ curdir = get_cwd()
- # regex pattern for options to ignore (help options can't reach here)
+ # regex patterns for options to ignore and tweak options (help options can't reach here)
ignore_opts = re.compile('^--robot$|^--job|^--try-.*$|^--easystack$')
+ try_opts_re = re.compile('^--try-.*$')
# generate_cmd_line returns the options in form --longopt=value
opts = [o for o in cmd_line_opts if not ignore_opts.match(o.split('=')[0])]
+ try_opts = [o for o in cmd_line_opts if try_opts_re.match(o.split('=')[0])]
# add --disable-job to make sure the submitted job doesn't submit a job itself,
# resulting in an infinite cycle of jobs;
@@ -142,6 +156,7 @@ def submit_jobs(ordered_ecs, cmd_line_opts, testing=False, prepare_first=True):
# compose string with command line options, properly quoted and with '%' characters escaped
opts_str = ' '.join(opts).replace('%', '%%')
+ try_opts_str = ' ' + ' '.join(try_opts).replace('%', '%%')
eb_cmd = build_option('job_eb_cmd')
@@ -153,12 +168,11 @@ def submit_jobs(ordered_ecs, cmd_line_opts, testing=False, prepare_first=True):
_log.info("Command template for jobs: %s", command)
if testing:
_log.debug("Skipping actual submission of jobs since testing mode is enabled")
- return command
- else:
- return build_easyconfigs_in_parallel(command, ordered_ecs, prepare_first=prepare_first)
+ return build_easyconfigs_in_parallel(command, ordered_ecs, testing=testing, prepare_first=prepare_first,
+ tweak_map=tweak_map, try_opts=try_opts_str)
-def create_job(job_backend, build_command, easyconfig, output_dir='easybuild-build'):
+def create_job(job_backend, build_command, easyconfig, output_dir='easybuild-build', spec=''):
"""
Creates a job to build a *single* easyconfig.
@@ -166,6 +180,7 @@ def create_job(job_backend, build_command, easyconfig, output_dir='easybuild-bui
:param build_command: format string for command, full path to an easyconfig file will be substituted in it
:param easyconfig: easyconfig as processed by process_easyconfig
:param output_dir: optional output path; --regtest-output-dir will be used inside the job with this variable
+ :param spec: untweaked easyconfig name with optional --try-* options
returns the job
"""
@@ -182,7 +197,7 @@ def create_job(job_backend, build_command, easyconfig, output_dir='easybuild-bui
command = build_command % {
'add_opts': add_opts,
'output_dir': os.path.join(os.path.abspath(output_dir), name),
- 'spec': easyconfig['spec'],
+ 'spec': spec or easyconfig['spec'],
}
# just use latest build stats
diff --git a/easybuild/tools/py2vs3/__init__.py b/easybuild/tools/py2vs3/__init__.py
index 5d45e11dc3..c0cc7a61f4 100644
--- a/easybuild/tools/py2vs3/__init__.py
+++ b/easybuild/tools/py2vs3/__init__.py
@@ -1,5 +1,5 @@
#
-# Copyright 2019-2024 Ghent University
+# Copyright 2019-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -24,38 +24,23 @@
#
import sys
-# all functionality provided by the py2 and py3 modules is made available via the easybuild.tools.py2vs3 namespace
-if sys.version_info[0] >= 3:
- from easybuild.tools.py2vs3.py3 import * # noqa
-else:
- from easybuild.tools.py2vs3.py2 import * # noqa
+from easybuild.base import fancylogger
+from easybuild.base.wrapper import create_base_metaclass # noqa
-# based on six's 'with_metaclass' function
-# see also https://stackoverflow.com/questions/18513821/python-metaclass-understanding-the-with-metaclass
-def create_base_metaclass(base_class_name, metaclass, *bases):
- """Create new class with specified metaclass based on specified base class(es)."""
- return metaclass(base_class_name, bases, {})
+# all functionality provided by the py3 modules is made available via the easybuild.tools.py2vs3 namespace
+from easybuild.tools.py2vs3.py3 import * # noqa
+
+
+_log = fancylogger.getLogger('py2vs3', fname=False)
+_log.deprecated("Using py2vs3 is deprecated, since EasyBuild no longer runs on Python 2.", '6.0')
def python2_is_deprecated():
"""
- Print warning when using Python 2, since the support for running EasyBuild with it is deprecated.
+ Exit with an error when using Python 2, since EasyBuild does not support it.
+ We preserve the function name here in here EB5, to maintain the API, even though it now exits.
"""
if sys.version_info[0] == 2:
- full_py_ver = '.'.join(str(x) for x in sys.version_info[:3])
- warning_lines = [
- "Running EasyBuild with Python v2.x is deprecated, found Python v%s." % full_py_ver,
- "Support for running EasyBuild with Python v2.x will be removed in EasyBuild v5.0.",
- '',
- "It is strongly recommended to start using Python v3.x for running EasyBuild,",
- "see https://docs.easybuild.io/en/latest/Python-2-3-compatibility.html for more information.",
- ]
- max_len = max(len(x) for x in warning_lines)
- for i in range(len(warning_lines)):
- line_len = len(warning_lines[i])
- warning_lines[i] = '!!! ' + warning_lines[i] + ' ' * (max_len - line_len) + ' !!!'
- max_len = max(len(x) for x in warning_lines)
- warning_lines.insert(0, '!' * max_len)
- warning_lines.append('!' * max_len)
- sys.stderr.write('\n\n' + '\n'.join(warning_lines) + '\n\n\n')
+ sys.stderr.write('\n\nEasyBuild v5.0+ is not compatible with Python v2. Use Python >= 3.6.\n\n\n')
+ sys.exit(1)
diff --git a/easybuild/tools/py2vs3/py2.py b/easybuild/tools/py2vs3/py2.py
deleted file mode 100644
index b52f55a1a2..0000000000
--- a/easybuild/tools/py2vs3/py2.py
+++ /dev/null
@@ -1,129 +0,0 @@
-#
-# Copyright 2019-2024 Ghent University
-#
-# This file is part of EasyBuild,
-# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
-# with support of Ghent University (http://ugent.be/hpc),
-# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
-# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
-# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
-#
-# https://github.com/easybuilders/easybuild
-#
-# EasyBuild is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation v2.
-#
-# EasyBuild is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with EasyBuild. If not, see .
-#
-"""
-Functionality to facilitate keeping code compatible with Python 2 & Python 3.
-
-Implementations for Python 2.
-
-Authors:
-
-* Kenneth Hoste (Ghent University)
-"""
-# these are not used here, but imported from here in other places
-import ConfigParser as configparser # noqa
-import imp
-import json
-import os
-import subprocess
-import time
-import urllib2 as std_urllib # noqa
-from collections import Mapping, OrderedDict # noqa
-from HTMLParser import HTMLParser # noqa
-from string import letters as ascii_letters # noqa
-from string import lowercase as ascii_lowercase # noqa
-from StringIO import StringIO # noqa
-from urllib import urlencode # noqa
-from urllib2 import HTTPError, HTTPSHandler, Request, URLError, build_opener, urlopen # noqa
-
-# Use the safe version. In Python 3.2+ this is the default already
-ConfigParser = configparser.SafeConfigParser
-
-
-# reload function (built-in in Python 2)
-reload = reload # noqa: F821
-
-# string type that can be used in 'isinstance' calls
-string_type = basestring
-
-# trivial wrapper for json.loads (Python 3 version is less trivial)
-json_loads = json.loads
-
-
-def load_source(filename, path):
- """Load Python module"""
- return imp.load_source(filename, path)
-
-
-def subprocess_popen_text(cmd, **kwargs):
- """Call subprocess.Popen with specified named arguments."""
- kwargs.setdefault('stderr', subprocess.PIPE)
- return subprocess.Popen(cmd, stdout=subprocess.PIPE, **kwargs)
-
-
-def subprocess_terminate(proc, timeout):
- """Terminate the subprocess if it hasn't finished after the given timeout"""
- res = None
- for pipe in (proc.stdout, proc.stderr, proc.stdin):
- if pipe:
- pipe.close()
- while timeout > 0:
- res = proc.poll()
- if res is not None:
- break
- delay = min(timeout, 0.1)
- time.sleep(delay)
- timeout -= delay
- if res is None:
- proc.terminate()
-
-
-# Wrapped in exec to avoid invalid syntax warnings for Python 3
-exec('''
-def raise_with_traceback(exception_class, message, traceback):
- """Raise exception of specified class with given message and traceback."""
- raise exception_class, message, traceback # noqa: E999
-''')
-
-
-def extract_method_name(method_func):
- """Extract method name from lambda function."""
- return '_'.join(method_func.func_code.co_names)
-
-
-def mk_wrapper_baseclass(metaclass):
-
- class WrapperBase(object):
- """
- Wrapper class that provides proxy access to an instance of some internal instance.
- """
- __metaclass__ = metaclass
- __wraps__ = None
-
- return WrapperBase
-
-
-def sort_looseversions(looseversions):
- """Sort list of (values including) LooseVersion instances."""
- # with Python 2, we can safely use 'sorted' on LooseVersion instances
- # (but we can't in Python 3, see https://bugs.python.org/issue14894)
- return sorted(looseversions)
-
-
-def makedirs(name, mode=0o777, exist_ok=False):
- try:
- os.makedirs(name, mode)
- except OSError:
- if not exist_ok or not os.path.isdir(name):
- raise
diff --git a/easybuild/tools/py2vs3/py3.py b/easybuild/tools/py2vs3/py3.py
index 62e7a93723..01c5a3a397 100644
--- a/easybuild/tools/py2vs3/py3.py
+++ b/easybuild/tools/py2vs3/py3.py
@@ -1,5 +1,5 @@
#
-# Copyright 2019-2024 Ghent University
+# Copyright 2019-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -34,10 +34,8 @@
# these are not used here, but imported from here in other places
import configparser # noqa
import json
-import subprocess
import sys
import urllib.request as std_urllib # noqa
-from collections import OrderedDict # noqa
from collections.abc import Mapping # noqa
from functools import cmp_to_key
from importlib.util import spec_from_file_location, module_from_spec
@@ -61,10 +59,14 @@
except ImportError:
HAVE_DISTUTILS = False
+from easybuild.base.wrapper import mk_wrapper_baseclass # noqa
+from easybuild.tools.run import subprocess_popen_text, subprocess_terminate # noqa
+
# string type that can be used in 'isinstance' calls
string_type = str
+# note: also available in easybuild.tools.filetools, should be imported from there!
def load_source(filename, path):
"""Load file as Python module"""
spec = spec_from_file_location(filename, path)
@@ -85,24 +87,6 @@ def json_loads(body):
return json.loads(body)
-def subprocess_popen_text(cmd, **kwargs):
- """Call subprocess.Popen in text mode with specified named arguments."""
- # open stdout/stderr in text mode in Popen when using Python 3
- kwargs.setdefault('stderr', subprocess.PIPE)
- return subprocess.Popen(cmd, stdout=subprocess.PIPE, universal_newlines=True, **kwargs)
-
-
-def subprocess_terminate(proc, timeout):
- """Terminate the subprocess if it hasn't finished after the given timeout"""
- try:
- proc.communicate(timeout=timeout)
- except subprocess.TimeoutExpired:
- for pipe in (proc.stdout, proc.stderr, proc.stdin):
- if pipe:
- pipe.close()
- proc.terminate()
-
-
def raise_with_traceback(exception_class, message, traceback):
"""Raise exception of specified class with given message and traceback."""
raise exception_class(message).with_traceback(traceback)
@@ -113,17 +97,6 @@ def extract_method_name(method_func):
return '_'.join(method_func.__code__.co_names)
-def mk_wrapper_baseclass(metaclass):
-
- class WrapperBase(object, metaclass=metaclass):
- """
- Wrapper class that provides proxy access to an instance of some internal instance.
- """
- __wraps__ = None
-
- return WrapperBase
-
-
def safe_cmp_looseversions(v1, v2):
"""Safe comparison function for two (values containing) LooseVersion instances."""
diff --git a/easybuild/tools/repository/filerepo.py b/easybuild/tools/repository/filerepo.py
index b18c3c6159..0a40cce578 100644
--- a/easybuild/tools/repository/filerepo.py
+++ b/easybuild/tools/repository/filerepo.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -43,7 +43,6 @@
from easybuild.framework.easyconfig.easyconfig import EasyConfig
from easybuild.framework.easyconfig.format.one import EB_FORMAT_EXTENSION
-from easybuild.framework.easyconfig.format.yeb import YEB_FORMAT_EXTENSION, is_yeb_format
from easybuild.framework.easyconfig.tools import stats_to_str
from easybuild.tools.filetools import copy_file, mkdir, read_file, write_file
from easybuild.tools.repository.repository import Repository
@@ -85,14 +84,8 @@ def add_easyconfig(self, cfg, name, version, stats, previous):
# create directory for eb file
full_path = os.path.join(self.wc, self.subdir, name)
- yeb_format = is_yeb_format(cfg, None)
- if yeb_format:
- extension = YEB_FORMAT_EXTENSION
- prefix = "buildstats: ["
-
- else:
- extension = EB_FORMAT_EXTENSION
- prefix = "buildstats = ["
+ extension = EB_FORMAT_EXTENSION
+ prefix = "buildstats = ["
# destination
dest = os.path.join(full_path, "%s-%s%s" % (name, version, extension))
@@ -109,10 +102,10 @@ def add_easyconfig(self, cfg, name, version, stats, previous):
if previous:
statstxt = statscomment + statsprefix + '\n'
for entry in previous + [stats]:
- statstxt += stats_to_str(entry, isyeb=yeb_format) + ',\n'
+ statstxt += stats_to_str(entry) + ',\n'
statstxt += statssuffix
else:
- statstxt = statscomment + statsprefix + stats_to_str(stats, isyeb=yeb_format) + statssuffix
+ statstxt = statscomment + statsprefix + stats_to_str(stats) + statssuffix
txt += statstxt
write_file(dest, txt)
diff --git a/easybuild/tools/repository/gitrepo.py b/easybuild/tools/repository/gitrepo.py
index feaf8cb2ad..1e343c2375 100644
--- a/easybuild/tools/repository/gitrepo.py
+++ b/easybuild/tools/repository/gitrepo.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/tools/repository/hgrepo.py b/easybuild/tools/repository/hgrepo.py
index 83a7890cc3..a3e678df4a 100644
--- a/easybuild/tools/repository/hgrepo.py
+++ b/easybuild/tools/repository/hgrepo.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/tools/repository/repository.py b/easybuild/tools/repository/repository.py
index e84c9d3cc5..23f4f53cb8 100644
--- a/easybuild/tools/repository/repository.py
+++ b/easybuild/tools/repository/repository.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -38,7 +38,6 @@
"""
from easybuild.base import fancylogger
from easybuild.tools.build_log import EasyBuildError
-from easybuild.tools.py2vs3 import string_type
from easybuild.tools.utilities import get_subclasses, import_available_modules
_log = fancylogger.getLogger('repository', fname=False)
@@ -159,10 +158,10 @@ def init_repository(repository, repository_path):
inited_repo = None
if isinstance(repository, Repository):
inited_repo = repository
- elif isinstance(repository, string_type):
+ elif isinstance(repository, str):
repo = avail_repositories().get(repository)
try:
- if isinstance(repository_path, string_type):
+ if isinstance(repository_path, str):
inited_repo = repo(repository_path)
elif isinstance(repository_path, (tuple, list)) and len(repository_path) <= 2:
inited_repo = repo(*repository_path)
diff --git a/easybuild/tools/repository/svnrepo.py b/easybuild/tools/repository/svnrepo.py
index 79f593b1ee..63e04b6b89 100644
--- a/easybuild/tools/repository/svnrepo.py
+++ b/easybuild/tools/repository/svnrepo.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -46,7 +46,7 @@
from easybuild.base import fancylogger
from easybuild.tools.build_log import EasyBuildError
-from easybuild.tools.filetools import remove_dir
+from easybuild.tools.filetools import get_cwd, remove_dir
from easybuild.tools.repository.filerepo import FileRepository
from easybuild.tools.utilities import only_if_module_is_available
@@ -145,7 +145,7 @@ def stage_file(self, path):
"""
if self.client and not self.client.status(path)[0].is_versioned:
# add it to version control
- self.log.debug("Going to add %s (working copy: %s, cwd %s)" % (path, self.wc, os.getcwd()))
+ self.log.debug("Going to add %s (working copy: %s, cwd %s)" % (path, self.wc, get_cwd()))
self.client.add(path)
def add_easyconfig(self, cfg, name, version, stats, previous_stats):
diff --git a/easybuild/tools/robot.py b/easybuild/tools/robot.py
index 5189769929..1d0546de8e 100644
--- a/easybuild/tools/robot.py
+++ b/easybuild/tools/robot.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -43,9 +43,9 @@
from easybuild.framework.easyconfig.easyconfig import EASYCONFIGS_ARCHIVE_DIR, ActiveMNS, process_easyconfig
from easybuild.framework.easyconfig.easyconfig import robot_find_easyconfig, verify_easyconfig_filename
from easybuild.framework.easyconfig.tools import find_resolved_modules, skip_available
-from easybuild.tools.build_log import EasyBuildError
+from easybuild.tools.build_log import EasyBuildError, EasyBuildExit
from easybuild.tools.config import build_option
-from easybuild.tools.filetools import det_common_path_prefix, search_file
+from easybuild.tools.filetools import det_common_path_prefix, get_cwd, search_file
from easybuild.tools.module_naming_scheme.easybuild_mns import EasyBuildMNS
from easybuild.tools.module_naming_scheme.utilities import det_full_ec_version
from easybuild.tools.utilities import flatten, nub
@@ -242,7 +242,7 @@ def dry_run(easyconfigs, modtool, short=False):
:param short: use short format for overview: use a variable for common prefixes
"""
lines = []
- if build_option('robot_path') is None:
+ if build_option('robot') is None:
lines.append("Dry run: printing build status of easyconfigs")
all_specs = easyconfigs
else:
@@ -325,7 +325,7 @@ def raise_error_missing_deps(missing_deps, extra_msg=None):
error_msg = "Missing dependencies: %s" % mod_names
if extra_msg:
error_msg += ' (%s)' % extra_msg
- raise EasyBuildError(error_msg)
+ raise EasyBuildError(error_msg, exit_code=EasyBuildExit.MISSING_DEPENDENCY)
def resolve_dependencies(easyconfigs, modtool, retain_all_deps=False, raise_error_missing_ecs=True):
@@ -491,7 +491,7 @@ def search_easyconfigs(query, short=False, filename_only=False, terse=False, con
"""
search_path = build_option('robot_path')
if not search_path:
- search_path = [os.getcwd()]
+ search_path = [get_cwd()]
extra_search_paths = build_option('search_paths')
# If we're returning a list of possible resolutions by the robot, don't include the extra_search_paths
if extra_search_paths and consider_extra_paths:
diff --git a/easybuild/tools/run.py b/easybuild/tools/run.py
index f8d034f981..2e118e0860 100644
--- a/easybuild/tools/run.py
+++ b/easybuild/tools/run.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -35,36 +35,45 @@
* Toon Willems (Ghent University)
* Ward Poelmans (Ghent University)
"""
-import contextlib
import functools
+import inspect
+import locale
import os
import re
-import signal
+import shlex
+import shutil
+import string
import subprocess
-import sys
import tempfile
import time
+from collections import namedtuple
from datetime import datetime
-import easybuild.tools.asyncprocess as asyncprocess
+# import deprecated functions so they can still be imported from easybuild.tools.run, for now
+from easybuild._deprecated import check_async_cmd, check_log_for_errors, complete_cmd, extract_errors_from_log # noqa
+from easybuild._deprecated import get_output_from_process, parse_cmd_output, parse_log_for_error # noqa
+from easybuild._deprecated import run_cmd, run_cmd_qa # noqa
+
+try:
+ # get_native_id is only available in Python >= 3.8
+ from threading import get_native_id as get_thread_id
+except ImportError:
+ # get_ident is available in Python >= 3.3
+ from threading import get_ident as get_thread_id
+
from easybuild.base import fancylogger
-from easybuild.tools.build_log import EasyBuildError, dry_run_msg, print_msg, time_str_since
-from easybuild.tools.config import ERROR, IGNORE, WARN, build_option
+from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, CWD_NOTFOUND_ERROR
+from easybuild.tools.build_log import dry_run_msg, time_str_since
+from easybuild.tools.config import build_option
from easybuild.tools.hooks import RUN_SHELL_CMD, load_hooks, run_hook
-from easybuild.tools.py2vs3 import string_type
-from easybuild.tools.utilities import nub, trace_msg
+from easybuild.tools.output import COLOR_RED, COLOR_YELLOW, colorize, print_error
+from easybuild.tools.utilities import trace_msg
_log = fancylogger.getLogger('run', fname=False)
-errors_found_in_log = 0
-
-# default strictness level
-strictness = WARN
-
-
-CACHED_COMMANDS = [
+CACHED_COMMANDS = (
"sysctl -n hw.cpufrequency_max", # used in get_cpu_speed (OS X)
"sysctl -n hw.memsize", # used in get_total_memory (OS X)
"sysctl -n hw.ncpu", # used in get_avail_core_count (OS X)
@@ -73,10 +82,99 @@
"type module", # used in ModulesTool.check_module_function
"type _module_raw", # used in EnvironmentModules.check_module_function
"ulimit -u", # used in det_parallelism
-]
+)
+
+RunShellCmdResult = namedtuple('RunShellCmdResult', ('cmd', 'exit_code', 'output', 'stderr', 'work_dir',
+ 'out_file', 'err_file', 'cmd_sh', 'thread_id', 'task_id'))
+RunShellCmdResult.__doc__ = """A namedtuple that represents the result of a call to run_shell_cmd,
+with the following fields:
+- cmd: the command that was executed;
+- exit_code: the exit code of the command (zero if it was successful, non-zero if not);
+- output: output of the command (stdout+stderr combined, only stdout if stderr was caught separately);
+- stderr: stderr output produced by the command, if caught separately (None otherwise);
+- work_dir: the working directory of the command;
+- out_file: path to file with output of command (stdout+stderr combined, only stdout if stderr was caught separately);
+- err_file: path to file with stderr output of command, if caught separately (None otherwise);
+- cmd_sh: path to script to set up interactive shell with environment in which command was executed;
+- thread_id: thread ID of command that was executed (None unless asynchronous mode was enabled for running command);
+- task_id: task ID of command, if it was specified (None otherwise);
+"""
+
+
+class RunShellCmdError(BaseException):
+
+ def __init__(self, cmd_result, caller_info, *args, **kwargs):
+ """Constructor for RunShellCmdError."""
+ self.cmd = cmd_result.cmd
+ self.cmd_name = os.path.basename(self.cmd.split(' ')[0])
+ self.exit_code = cmd_result.exit_code
+ self.work_dir = cmd_result.work_dir
+ self.output = cmd_result.output
+ self.out_file = cmd_result.out_file
+ self.stderr = cmd_result.stderr
+ self.err_file = cmd_result.err_file
+ self.cmd_sh = cmd_result.cmd_sh
+
+ self.caller_info = caller_info
+
+ msg = f"Shell command '{self.cmd_name}' failed!"
+ super(RunShellCmdError, self).__init__(msg, *args, **kwargs)
+
+ def print(self):
+ """
+ Report failed shell command for this RunShellCmdError instance
+ """
+
+ def pad_4_spaces(msg, color=None):
+ padded_msg = ' ' * 4 + msg
+ if color:
+ return colorize(padded_msg, color)
+ else:
+ return padded_msg
+
+ caller_file_name, caller_line_nr, caller_function_name = self.caller_info
+ called_from_info = f"'{caller_function_name}' function in {caller_file_name} (line {caller_line_nr})"
+
+ error_info = [
+ colorize("ERROR: Shell command failed!", COLOR_RED),
+ pad_4_spaces(f"full command -> {self.cmd}"),
+ pad_4_spaces(f"exit code -> {self.exit_code}"),
+ pad_4_spaces(f"called from -> {called_from_info}"),
+ pad_4_spaces(f"working directory -> {self.work_dir}"),
+ ]
+
+ if self.out_file is not None:
+ # if there's no separate file for error/warnings, then out_file includes both stdout + stderr
+ out_info_msg = "output (stdout + stderr)" if self.err_file is None else "output (stdout) "
+ error_info.append(pad_4_spaces(f"{out_info_msg} -> {self.out_file}", color=COLOR_YELLOW))
+ if self.err_file is not None:
+ error_info.append(pad_4_spaces(f"error/warnings (stderr) -> {self.err_file}", color=COLOR_YELLOW))
-def run_cmd_cache(func):
+ if self.cmd_sh is not None:
+ error_info.append(pad_4_spaces(f"interactive shell script -> {self.cmd_sh}", color=COLOR_YELLOW))
+
+ print_error('\n'.join(error_info), rich_highlight=False)
+
+
+def raise_run_shell_cmd_error(cmd_res):
+ """
+ Raise RunShellCmdError for failed shell command, after collecting additional caller info
+ """
+
+ # figure out where failing command was run
+ # need to go 3 levels down:
+ # 1) this function
+ # 2) run_shell_cmd function
+ # 3) run_shell_cmd_cache decorator
+ # 4) actual caller site
+ frameinfo = inspect.getouterframes(inspect.currentframe())[3]
+ caller_info = (frameinfo.filename, frameinfo.lineno, frameinfo.function)
+
+ raise RunShellCmdError(cmd_res, caller_info)
+
+
+def run_shell_cmd_cache(func):
"""Function decorator to cache (and retrieve cached) results of running commands."""
cache = {}
@@ -84,9 +182,9 @@ def run_cmd_cache(func):
def cache_aware_func(cmd, *args, **kwargs):
"""Retrieve cached result of selected commands, or run specified and collect & cache result."""
# cache key is combination of command and input provided via stdin
- key = (cmd, kwargs.get('inp', None))
+ key = (cmd, kwargs.get('stdin', None))
# fetch from cache if available, cache it if it's not, but only on cmd strings
- if isinstance(cmd, string_type) and key in cache:
+ if isinstance(cmd, str) and key in cache:
_log.debug("Using cached value for command '%s': %s", cmd, cache[key])
return cache[key]
else:
@@ -102,725 +200,488 @@ def cache_aware_func(cmd, *args, **kwargs):
return cache_aware_func
-def get_output_from_process(proc, read_size=None, asynchronous=False):
+def fileprefix_from_cmd(cmd, allowed_chars=False):
"""
- Get output from running process (that was opened with subprocess.Popen).
+ Simplify the cmd to only the allowed_chars we want in a filename
- :param proc: process to get output from
- :param read_size: number of bytes of output to read (if None: read all output)
- :param asynchronous: get output asynchronously
+ :param cmd: the cmd (string)
+ :param allowed_chars: characters allowed in filename (defaults to string.ascii_letters + string.digits + "_-")
"""
+ if not allowed_chars:
+ allowed_chars = f"{string.ascii_letters}{string.digits}_-"
- if asynchronous:
- # e=False is set to avoid raising an exception when command has completed;
- # that's needed to ensure we get all output,
- # see https://github.com/easybuilders/easybuild-framework/issues/3593
- output = asyncprocess.recv_some(proc, e=False)
- elif read_size:
- output = proc.stdout.read(read_size)
- else:
- output = proc.stdout.read()
+ return ''.join([c for c in cmd if c in allowed_chars])
- # need to be careful w.r.t. encoding since we want to obtain a string value,
- # and the output may include non UTF-8 characters
- # * in Python 2, .decode() returns a value of type 'unicode',
- # but we really want a regular 'str' value (which is also why we use 'ignore' for encoding errors)
- # * in Python 3, .decode() returns a 'str' value when called on the 'bytes' value obtained from .read()
- output = str(output.decode('ascii', 'ignore'))
- return output
+def create_cmd_scripts(cmd_str, work_dir, env, tmpdir, out_file, err_file):
+ """
+ Create helper scripts for specified command in specified directory:
+ - env.sh which can be sourced to define environment in which command was run;
+ - cmd.sh to create interactive (bash) shell session with working directory and environment,
+ and with the command in shell history;
+ """
+ # Save environment variables in env.sh which can be sourced to restore environment
+ if env is None:
+ env = os.environ.copy()
+
+ # Decode any declared bash functions
+ proc = subprocess.Popen('declare -f', stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
+ env=env, shell=True, executable='bash')
+ (bash_functions, _) = proc.communicate()
+
+ env_fp = os.path.join(tmpdir, 'env.sh')
+ with open(env_fp, 'w') as fid:
+ # unset all environment variables in current environment first to start from a clean slate;
+ # we need to be careful to filter out functions definitions, so first undefine those
+ fid.write('\n'.join([
+ 'for var in $(compgen -e); do',
+ ' unset "$var"',
+ 'done',
+ ]) + '\n')
+ # also unset any bash functions
+ fid.write('\n'.join([
+ 'for func in $(compgen -A function); do',
+ ' if [[ $func != _* ]]; then',
+ ' unset -f "$func"',
+ ' fi',
+ 'done',
+ ]) + '\n')
+
+ # excludes bash functions (environment variables ending with %)
+ fid.write('\n'.join(f'export {key}={shlex.quote(value)}' for key, value in sorted(env.items())
+ if not key.endswith('%')) + '\n')
+
+ fid.write(bash_functions.decode(errors='ignore') + '\n')
+
+ fid.write('\n\nPS1="eb-shell> "')
+
+ # define $EB_CMD_OUT_FILE to contain path to file with command output
+ fid.write(f'\nEB_CMD_OUT_FILE="{out_file}"')
+ # define $EB_CMD_ERR_FILE to contain path to file with command stderr output (if available)
+ if err_file:
+ fid.write(f'\nEB_CMD_ERR_FILE="{err_file}"')
+
+ # also change to working directory (to ensure that working directory is correct for interactive bash shell)
+ fid.write(f'\ncd "{work_dir}"')
+
+ # reset shell history to only include executed command
+ fid.write(f'\nhistory -s {shlex.quote(cmd_str)}')
+
+ # Make script that sets up bash shell with specified environment and working directory
+ cmd_fp = os.path.join(tmpdir, 'cmd.sh')
+ with open(cmd_fp, 'w') as fid:
+ fid.write('#!/usr/bin/env bash\n')
+ fid.write('# Run this script to set up a shell environment that EasyBuild used to run the shell command\n')
+ fid.write('\n'.join([
+ 'EB_SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )',
+ f'echo "# Shell for the command: \'"{shlex.quote(cmd_str)}"\'"',
+ 'echo "# Use command history, exit to stop"',
+ # using -i to force interactive shell, so env.sh is also sourced when -c is used to run commands
+ 'bash --rcfile $EB_SCRIPT_DIR/env.sh -i "$@"',
+ ]))
+ os.chmod(cmd_fp, 0o775)
+
+ return cmd_fp
+
+
+def _answer_question(stdout, proc, qa_patterns, qa_wait_patterns):
+ """
+ Private helper function to try and answer questions raised in interactive shell commands.
+ """
+ match_found = False
+
+ space_line_break_pattern = r'[\s\n]+'
+ space_line_break_regex = re.compile(space_line_break_pattern)
+
+ stdout_end = stdout.decode(errors='ignore')[-1000:]
+ for question, answers in qa_patterns:
+ # first replace hard spaces by regular spaces, since they would mess up the join/split below
+ question = question.replace(r'\ ', ' ')
+ # replace spaces/line breaks with regex pattern that matches one or more spaces/line breaks,
+ # and allow extra whitespace at the end
+ question = space_line_break_pattern.join(space_line_break_regex.split(question)) + r'[\s\n]*$'
+ _log.debug(f"Checking for question pattern '{question}'...")
+ regex = re.compile(question.encode())
+ res = regex.search(stdout)
+ if res:
+ _log.debug(f"Found match for question pattern '{question}' at end of stdout: {stdout_end}")
+ # if answer is specified as a list, we take the first item as current answer,
+ # and add it to the back of the list (so we cycle through answers)
+ if isinstance(answers, list):
+ answer = answers.pop(0)
+ answers.append(answer)
+ elif isinstance(answers, str):
+ answer = answers
+ else:
+ raise EasyBuildError(f"Unknown type of answers encountered for question ({question}): {answers}")
+ # answer may need to be completed via pattern extracted from question
+ _log.debug(f"Raw answer for question pattern '{question}': {answer}")
+ answer = answer % {k: v.decode() for (k, v) in res.groupdict().items()}
+ answer += '\n'
+ _log.info(f"Found match for question pattern '{question}', replying with: {answer}")
-@run_cmd_cache
-def run_cmd(cmd, log_ok=True, log_all=False, simple=False, inp=None, regexp=True, log_output=False, path=None,
- force_in_dry_run=False, verbose=True, shell=None, trace=True, stream_output=None, asynchronous=False,
- with_hooks=True, with_sysroot=True):
+ try:
+ os.write(proc.stdin.fileno(), answer.encode())
+ except OSError as err:
+ raise EasyBuildError("Failed to answer question raised by interactive command: %s", err)
+
+ match_found = True
+ break
+ else:
+ _log.debug(f"No match for question pattern '{question}' at end of stdout: {stdout_end}")
+ else:
+ _log.info("No match found for question patterns, considering question wait patterns")
+ # if no match was found among question patterns,
+ # take into account patterns for non-questions (qa_wait_patterns)
+ for pattern in qa_wait_patterns:
+ # first replace hard spaces by regular spaces, since they would mess up the join/split below
+ pattern = pattern.replace(r'\ ', ' ')
+ # replace spaces/line breaks with regex pattern that matches one or more spaces/line breaks,
+ # and allow extra whitespace at the end
+ pattern = space_line_break_pattern.join(space_line_break_regex.split(pattern)) + r'[\s\n]*$'
+ regex = re.compile(pattern.encode())
+ _log.debug(f"Checking for question wait pattern '{pattern}'...")
+ if regex.search(stdout):
+ _log.info(f"Found match for question wait pattern '{pattern}'")
+ _log.debug(f"Found match for question wait pattern '{pattern}' at end of stdout: {stdout_end}")
+ match_found = True
+ break
+ else:
+ _log.debug(f"No match for question wait pattern '{pattern}' at end of stdout: {stdout_end}")
+ else:
+ _log.info("No match found for question wait patterns")
+ _log.debug(f"No match found in question (wait) patterns at end of stdout: {stdout_end}")
+
+ return match_found
+
+
+@run_shell_cmd_cache
+def run_shell_cmd(cmd, fail_on_error=True, split_stderr=False, stdin=None, env=None,
+ hidden=False, in_dry_run=False, verbose_dry_run=False, work_dir=None, use_bash=True,
+ output_file=True, stream_output=None, asynchronous=False, task_id=None, with_hooks=True,
+ qa_patterns=None, qa_wait_patterns=None, qa_timeout=100):
"""
- Run specified command (in a subshell)
- :param cmd: command to run
- :param log_ok: only run output/exit code for failing commands (exit code non-zero)
- :param log_all: always log command output and exit code
- :param simple: if True, just return True/False to indicate success, else return a tuple: (output, exit_code)
- :param inp: the input given to the command via stdin
- :param regexp: regex used to check the output for errors; if True it will use the default (see parse_log_for_error)
- :param log_output: indicate whether all output of command should be logged to a separate temporary logfile
- :param path: path to execute the command in; current working directory is used if unspecified
- :param force_in_dry_run: force running the command during dry run
- :param verbose: include message on running the command in dry run output
- :param shell: allow commands to not run in a shell (especially useful for cmd lists), defaults to True
- :param trace: print command being executed as part of trace output
- :param stream_output: enable streaming command output to stdout
- :param asynchronous: run command asynchronously (returns subprocess.Popen instance if set to True)
+ Run specified (interactive) shell command, and capture output + exit code.
+
+ :param fail_on_error: fail on non-zero exit code (enabled by default)
+ :param split_stderr: split of stderr from stdout output
+ :param stdin: input to be sent to stdin (nothing if set to None)
+ :param env: environment to use to run command (if None, inherit current process environment)
+ :param hidden: do not show command in terminal output (when using --trace, or with --extended-dry-run / -x)
+ :param in_dry_run: also run command in dry run mode
+ :param verbose_dry_run: show that command is run in dry run mode (overrules 'hidden')
+ :param work_dir: working directory to run command in (current working directory if None)
+ :param use_bash: execute command through bash shell (enabled by default)
+ :param output_file: collect command output in temporary output file
+ :param stream_output: stream command output to stdout (auto-enabled with --logtostdout if None)
+ :param asynchronous: indicate that command is being run asynchronously
+ :param task_id: task ID for specified shell command (included in return value)
:param with_hooks: trigger pre/post run_shell_cmd hooks (if defined)
- :param with_sysroot: prepend sysroot to exec_cmd (if defined)
+ :param qa_patterns: list of 2-tuples with patterns for questions + corresponding answers
+ :param qa_wait_patterns: list of strings with patterns for non-questions
+ :param qa_timeout: amount of seconds to wait until more output is produced when there is no matching question
+
+ :return: Named tuple with:
+ - output: command output, stdout+stderr combined if split_stderr is disabled, only stdout otherwise
+ - exit_code: exit code of command (integer)
+ - stderr: stderr output if split_stderr is enabled, None otherwise
"""
- cwd = os.getcwd()
+ def to_cmd_str(cmd):
+ """
+ Helper function to create string representation of specified command.
+ """
+ if isinstance(cmd, str):
+ cmd_str = cmd.strip()
+ elif isinstance(cmd, list):
+ cmd_str = ' '.join(cmd)
+ else:
+ raise EasyBuildError(f"Unknown command type ('{type(cmd)}'): {cmd}")
- if isinstance(cmd, string_type):
- cmd_msg = cmd.strip()
- elif isinstance(cmd, list):
- cmd_msg = ' '.join(cmd)
- else:
- raise EasyBuildError("Unknown command type ('%s'): %s", type(cmd), cmd)
-
- if shell is None:
- shell = True
- if isinstance(cmd, list):
- raise EasyBuildError("When passing cmd as a list then `shell` must be set explictely! "
- "Note that all elements of the list but the first are treated as arguments "
- "to the shell and NOT to the command to be executed!")
-
- if log_output or (trace and build_option('trace')):
- # collect output of running command in temporary log file, if desired
- fd, cmd_log_fn = tempfile.mkstemp(suffix='.log', prefix='easybuild-run_cmd-')
- os.close(fd)
- try:
- cmd_log = open(cmd_log_fn, 'w')
- except IOError as err:
- raise EasyBuildError("Failed to open temporary log file for output of command: %s", err)
- _log.debug('run_cmd: Output of "%s" will be logged to %s' % (cmd, cmd_log_fn))
- else:
- cmd_log_fn, cmd_log = None, None
+ return cmd_str
- # auto-enable streaming of command output under --logtostdout/-l, unless it was disabled explicitely
- if stream_output is None and build_option('logtostdout'):
- _log.info("Auto-enabling streaming output of '%s' command because logging to stdout is enabled", cmd_msg)
- stream_output = True
+ # make sure that qa_patterns is a list of 2-tuples (not a dict, or something else)
+ if qa_patterns:
+ if not isinstance(qa_patterns, list) or any(not isinstance(x, tuple) or len(x) != 2 for x in qa_patterns):
+ raise EasyBuildError("qa_patterns passed to run_shell_cmd should be a list of 2-tuples!")
- if stream_output:
- print_msg("(streaming) output for command '%s':" % cmd_msg)
+ interactive = bool(qa_patterns)
- start_time = datetime.now()
- if trace:
- trace_txt = "running command:\n"
- trace_txt += "\t[started at: %s]\n" % start_time.strftime('%Y-%m-%d %H:%M:%S')
- trace_txt += "\t[working dir: %s]\n" % (path or os.getcwd())
- if inp:
- trace_txt += "\t[input: %s]\n" % inp
- trace_txt += "\t[output logged in %s]\n" % cmd_log_fn
- trace_msg(trace_txt + '\t' + cmd_msg)
-
- # early exit in 'dry run' mode, after printing the command that would be run (unless running the command is forced)
- if not force_in_dry_run and build_option('extended_dry_run'):
- if path is None:
- path = cwd
- if verbose:
- dry_run_msg(" running command \"%s\"" % cmd_msg, silent=build_option('silent'))
- dry_run_msg(" (in %s)" % path, silent=build_option('silent'))
-
- # make sure we get the type of the return value right
- if simple:
- return True
- else:
- # output, exit code
- return ('', 0)
+ if qa_wait_patterns is None:
+ qa_wait_patterns = []
+ # keep path to current working dir in case we need to come back to it
try:
- if path:
- os.chdir(path)
-
- _log.debug("run_cmd: running cmd %s (in %s)" % (cmd, os.getcwd()))
- except OSError as err:
- _log.warning("Failed to change to %s: %s" % (path, err))
- _log.info("running cmd %s in non-existing directory, might fail!", cmd)
-
- if cmd_log:
- cmd_log.write("# output for command: %s\n\n" % cmd_msg)
-
- exec_cmd = "/bin/bash"
-
- # if EasyBuild is configured to use an alternate sysroot,
- # we should also run shell commands using the bash shell provided in there,
- # since /bin/bash may not be compatible with the alternate sysroot
- if with_sysroot:
- sysroot = build_option('sysroot')
- if sysroot:
- sysroot_bin_bash = os.path.join(sysroot, 'bin', 'bash')
- if os.path.exists(sysroot_bin_bash):
- exec_cmd = sysroot_bin_bash
-
- if not shell:
- if isinstance(cmd, list):
- exec_cmd = None
- cmd.insert(0, '/usr/bin/env')
- elif isinstance(cmd, string_type):
- cmd = '/usr/bin/env %s' % cmd
- else:
- raise EasyBuildError("Don't know how to prefix with /usr/bin/env for commands of type %s", type(cmd))
+ initial_work_dir = os.getcwd()
+ except FileNotFoundError:
+ raise EasyBuildError(CWD_NOTFOUND_ERROR)
- _log.info("Using %s as shell for running cmd: %s", exec_cmd, cmd)
+ if work_dir is None:
+ work_dir = initial_work_dir
if with_hooks:
hooks = load_hooks(build_option('hooks'))
- hook_res = run_hook(RUN_SHELL_CMD, hooks, pre_step_hook=True, args=[cmd], kwargs={'work_dir': os.getcwd()})
- if isinstance(hook_res, string_type):
+ kwargs = {
+ 'interactive': interactive,
+ 'work_dir': work_dir,
+ }
+ hook_res = run_hook(RUN_SHELL_CMD, hooks, pre_step_hook=True, args=[cmd], kwargs=kwargs)
+ if hook_res:
cmd, old_cmd = hook_res, cmd
_log.info("Command to run was changed by pre-%s hook: '%s' (was: '%s')", RUN_SHELL_CMD, cmd, old_cmd)
- _log.info('running cmd: %s ' % cmd)
- try:
- proc = subprocess.Popen(cmd, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
- stdin=subprocess.PIPE, close_fds=True, executable=exec_cmd)
- except OSError as err:
- raise EasyBuildError("run_cmd init cmd %s failed:%s", cmd, err)
-
- if inp:
- proc.stdin.write(inp.encode())
- proc.stdin.close()
+ cmd_str = to_cmd_str(cmd)
+ thread_id = None
if asynchronous:
- return (proc, cmd, cwd, start_time, cmd_log)
- else:
- return complete_cmd(proc, cmd, cwd, start_time, cmd_log, log_ok=log_ok, log_all=log_all, simple=simple,
- regexp=regexp, stream_output=stream_output, trace=trace, with_hook=with_hooks)
+ thread_id = get_thread_id()
+ _log.info(f"Initiating running of shell command '{cmd_str}' via thread with ID {thread_id}")
+ # auto-enable streaming of command output under --logtostdout/-l, unless it was disabled explicitely
+ if stream_output is None and build_option('logtostdout'):
+ _log.info(f"Auto-enabling streaming output of '{cmd_str}' command because logging to stdout is enabled")
+ stream_output = True
-def check_async_cmd(proc, cmd, owd, start_time, cmd_log, fail_on_error=True, output_read_size=1024, output=''):
- """
- Check status of command that was started asynchronously.
+ # temporary output file(s) for command output, along with helper scripts
+ if output_file:
+ toptmpdir = os.path.join(tempfile.gettempdir(), 'run-shell-cmd-output')
+ os.makedirs(toptmpdir, exist_ok=True)
+ cmd_name = fileprefix_from_cmd(os.path.basename(cmd_str.split(' ')[0]))
+ tmpdir = tempfile.mkdtemp(dir=toptmpdir, prefix=f'{cmd_name}-')
- :param proc: subprocess.Popen instance representing asynchronous command
- :param cmd: command being run
- :param owd: original working directory
- :param start_time: start time of command (datetime instance)
- :param cmd_log: log file to print command output to
- :param fail_on_error: raise EasyBuildError when command exited with an error
- :param output_read_size: number of bytes to read from output
- :param output: already collected output for this command
-
- :result: dict value with result of the check (boolean 'done', 'exit_code', 'output')
- """
- # use small read size, to avoid waiting for a long time until sufficient output is produced
- if output_read_size:
- if not isinstance(output_read_size, int) or output_read_size < 0:
- raise EasyBuildError("Number of output bytes to read should be a positive integer value (or zero)")
- add_out = get_output_from_process(proc, read_size=output_read_size)
- _log.debug("Additional output from asynchronous command '%s': %s" % (cmd, add_out))
- output += add_out
-
- exit_code = proc.poll()
- if exit_code is None:
- _log.debug("Asynchronous command '%s' still running..." % cmd)
- done = False
+ _log.info(f'run_shell_cmd: command environment of "{cmd_str}" will be saved to {tmpdir}')
+
+ cmd_out_fp = os.path.join(tmpdir, 'out.txt')
+ _log.info(f'run_shell_cmd: Output of "{cmd_str}" will be logged to {cmd_out_fp}')
+ if split_stderr:
+ cmd_err_fp = os.path.join(tmpdir, 'err.txt')
+ _log.info(f'run_shell_cmd: Errors and warnings of "{cmd_str}" will be logged to {cmd_err_fp}')
+ else:
+ cmd_err_fp = None
+
+ cmd_sh = create_cmd_scripts(cmd_str, work_dir, env, tmpdir, cmd_out_fp, cmd_err_fp)
else:
- _log.debug("Asynchronous command '%s' completed!", cmd)
- output, _ = complete_cmd(proc, cmd, owd, start_time, cmd_log, output=output,
- simple=False, trace=False, log_ok=fail_on_error)
- done = True
-
- res = {
- 'done': done,
- 'exit_code': exit_code,
- 'output': output,
- }
- return res
+ tmpdir, cmd_out_fp, cmd_err_fp, cmd_sh = None, None, None, None
+ interactive_msg = 'interactive ' if interactive else ''
-def complete_cmd(proc, cmd, owd, start_time, cmd_log, log_ok=True, log_all=False, simple=False,
- regexp=True, stream_output=None, trace=True, output='', with_hook=True):
- """
- Complete running of command represented by passed subprocess.Popen instance.
+ # early exit in 'dry run' mode, after printing the command that would be run (unless 'hidden' is enabled)
+ if not in_dry_run and build_option('extended_dry_run'):
+ if not hidden or verbose_dry_run:
+ silent = build_option('silent')
+ msg = f" running {interactive_msg}shell command \"{cmd_str}\"\n"
+ msg += f" (in {work_dir})"
+ dry_run_msg(msg, silent=silent)
- :param proc: subprocess.Popen instance representing running command
- :param cmd: command being run
- :param owd: original working directory
- :param start_time: start time of command (datetime instance)
- :param cmd_log: log file to print command output to
- :param log_ok: only run output/exit code for failing commands (exit code non-zero)
- :param log_all: always log command output and exit code
- :param simple: if True, just return True/False to indicate success, else return a tuple: (output, exit_code)
- :param regexp: regex used to check the output for errors; if True it will use the default (see parse_log_for_error)
- :param stream_output: enable streaming command output to stdout
- :param trace: print command being executed as part of trace output
- :param with_hook: trigger post run_shell_cmd hooks (if defined)
- """
- # use small read size when streaming output, to make it stream more fluently
- # read size should not be too small though, to avoid too much overhead
- if stream_output:
- read_size = 128
+ return RunShellCmdResult(cmd=cmd_str, exit_code=0, output='', stderr=None, work_dir=work_dir,
+ out_file=cmd_out_fp, err_file=cmd_err_fp, cmd_sh=cmd_sh,
+ thread_id=thread_id, task_id=task_id)
+
+ start_time = datetime.now()
+ if not hidden:
+ _cmd_trace_msg(cmd_str, start_time, work_dir, stdin, tmpdir, thread_id, interactive=interactive)
+
+ # use bash as shell instead of the default /bin/sh used by subprocess.run
+ # (which could be dash instead of bash, like on Ubuntu, see https://wiki.ubuntu.com/DashAsBinSh)
+ # stick to None (default value) when not running command via a shell
+ if use_bash:
+ bash = shutil.which('bash')
+ _log.info(f"Path to bash that will be used to run shell commands: {bash}")
+ executable, shell = bash, True
else:
- read_size = 1024 * 8
+ executable, shell = None, False
- stdouterr = output
+ stderr_handle = subprocess.PIPE if split_stderr else subprocess.STDOUT
+ stdin_handle = subprocess.PIPE if stdin or qa_patterns else subprocess.DEVNULL
- try:
- ec = proc.poll()
- while ec is None:
- # need to read from time to time.
- # - otherwise the stdout/stderr buffer gets filled and it all stops working
- output = get_output_from_process(proc, read_size=read_size)
- if cmd_log:
- cmd_log.write(output)
- if stream_output:
- sys.stdout.write(output)
- stdouterr += output
- ec = proc.poll()
-
- # read remaining data (all of it)
- output = get_output_from_process(proc)
- finally:
- proc.stdout.close()
-
- if cmd_log:
- cmd_log.write(output)
- cmd_log.close()
- if stream_output:
- sys.stdout.write(output)
- stdouterr += output
-
- if with_hook:
- hooks = load_hooks(build_option('hooks'))
- run_hook_kwargs = {
- 'exit_code': ec,
- 'output': stdouterr,
- 'work_dir': os.getcwd(),
- }
- run_hook(RUN_SHELL_CMD, hooks, post_step_hook=True, args=[cmd], kwargs=run_hook_kwargs)
+ log_msg = f"Running {interactive_msg}shell command '{cmd_str}' in {work_dir}"
+ if thread_id:
+ log_msg += f" (via thread with ID {thread_id})"
+ _log.info(log_msg)
- if trace:
- trace_msg("command completed: exit %s, ran in %s" % (ec, time_str_since(start_time)))
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=stderr_handle, stdin=stdin_handle,
+ cwd=work_dir, env=env, shell=shell, executable=executable)
- try:
- os.chdir(owd)
- except OSError as err:
- raise EasyBuildError("Failed to return to %s after executing command: %s", owd, err)
+ # 'input' value fed to subprocess.run must be a byte sequence
+ if stdin:
+ stdin = stdin.encode()
- return parse_cmd_output(cmd, stdouterr, ec, simple, log_all, log_ok, regexp)
+ if stream_output or qa_patterns:
+ # enable non-blocking access to stdout, stderr, stdin
+ for channel in (proc.stdout, proc.stdin, proc.stderr):
+ if channel is not None:
+ os.set_blocking(channel.fileno(), False)
+ if stdin:
+ proc.stdin.write(stdin)
+ proc.stdin.flush()
+ if not qa_patterns:
+ proc.stdin.close()
-def run_cmd_qa(cmd, qa, no_qa=None, log_ok=True, log_all=False, simple=False, regexp=True, std_qa=None, path=None,
- maxhits=50, trace=True):
- """
- Run specified interactive command (in a subshell)
- :param cmd: command to run
- :param qa: dictionary which maps question to answers
- :param no_qa: list of patters that are not questions
- :param log_ok: only run output/exit code for failing commands (exit code non-zero)
- :param log_all: always log command output and exit code
- :param simple: if True, just return True/False to indicate success, else return a tuple: (output, exit_code)
- :param regexp: regex used to check the output for errors; if True it will use the default (see parse_log_for_error)
- :param std_qa: dictionary which maps question regex patterns to answers
- :param path: path to execute the command is; current working directory is used if unspecified
- :param maxhits: maximum number of cycles (seconds) without being able to find a known question
- :param trace: print command being executed as part of trace output
- """
- cwd = os.getcwd()
+ exit_code = None
+ stdout, stderr = b'', b''
+ check_interval_secs = 0.1
+ time_no_match = 0
+ prev_stdout = ''
+
+ while exit_code is None:
+ # collect output line by line, while checking for questions to answer (if qa_patterns is provided)
+ for line in iter(proc.stdout.readline, b''):
+ _log.debug(f"Captured stdout: {line.decode(errors='ignore').rstrip()}")
+ stdout += line
+
+ # note: we assume that there won't be any questions in stderr output
+ if split_stderr:
+ for line in iter(proc.stderr.readline, b''):
+ stderr += line
+
+ if qa_patterns:
+ # only check for question patterns if additional output is available
+ # compared to last time a question was answered;
+ # use empty list of question patterns if no extra output (except for whitespace) is available
+ # we do always need to check for wait patterns though!
+ active_qa_patterns = qa_patterns if stdout.strip() != prev_stdout else []
+
+ if _answer_question(stdout, proc, active_qa_patterns, qa_wait_patterns):
+ time_no_match = 0
+ prev_stdout = stdout.strip()
+ else:
+ # this will only run if the for loop above was *not* stopped by the break statement
+ time_no_match += check_interval_secs
+ if time_no_match > qa_timeout:
+ error_msg = "No matching questions found for current command output, "
+ error_msg += f"giving up after {qa_timeout} seconds!"
+ raise EasyBuildError(error_msg)
+ _log.debug(f"{time_no_match:0.1f} seconds without match in output of interactive shell command")
+
+ time.sleep(check_interval_secs)
+
+ exit_code = proc.poll()
+
+ # collect last bit of output once processed has exited
+ for line in iter(proc.stdout.readline, b''):
+ _log.debug(f"Captured stdout: {line.decode(errors='ignore').rstrip()}")
+ stdout += line
+ if split_stderr:
+ stderr += proc.stderr.read() or b''
+ else:
+ (stdout, stderr) = proc.communicate(input=stdin)
- if not isinstance(cmd, string_type) and len(cmd) > 1:
- # We use shell=True and hence we should really pass the command as a string
- # When using a list then every element past the first is passed to the shell itself, not the command!
- raise EasyBuildError("The command passed must be a string!")
+ # return output as a regular string rather than a byte sequence (and non-UTF-8 characters get stripped out)
+ # getpreferredencoding normally gives 'utf-8' but can be ASCII (ANSI_X3.4-1968)
+ # for Python 3.6 and older with LC_ALL=C
+ encoding = locale.getpreferredencoding(False)
+ output = stdout.decode(encoding, 'ignore')
+ stderr = stderr.decode(encoding, 'ignore') if split_stderr else None
- if log_all or (trace and build_option('trace')):
- # collect output of running command in temporary log file, if desired
- fd, cmd_log_fn = tempfile.mkstemp(suffix='.log', prefix='easybuild-run_cmd_qa-')
- os.close(fd)
+ # store command output to temporary file(s)
+ if output_file:
try:
- cmd_log = open(cmd_log_fn, 'w')
+ with open(cmd_out_fp, 'w') as fp:
+ fp.write(output)
+ if split_stderr:
+ with open(cmd_err_fp, 'w') as fp:
+ fp.write(stderr)
except IOError as err:
- raise EasyBuildError("Failed to open temporary log file for output of interactive command: %s", err)
- _log.debug('run_cmd_qa: Output of "%s" will be logged to %s' % (cmd, cmd_log_fn))
+ raise EasyBuildError(f"Failed to dump command output to temporary file: {err}")
+
+ res = RunShellCmdResult(cmd=cmd_str, exit_code=proc.returncode, output=output, stderr=stderr,
+ work_dir=work_dir, out_file=cmd_out_fp, err_file=cmd_err_fp, cmd_sh=cmd_sh,
+ thread_id=thread_id, task_id=task_id)
+
+ # always log command output
+ cmd_name = cmd_str.split(' ')[0]
+ if split_stderr:
+ _log.info(f"Output of '{cmd_name} ...' shell command (stdout only):\n{res.output}")
+ _log.info(f"Warnings and errors of '{cmd_name} ...' shell command (stderr only):\n{res.stderr}")
else:
- cmd_log_fn, cmd_log = None, None
+ _log.info(f"Output of '{cmd_name} ...' shell command (stdout + stderr):\n{res.output}")
- start_time = datetime.now()
- if trace:
- trace_txt = "running interactive command:\n"
- trace_txt += "\t[started at: %s]\n" % start_time.strftime('%Y-%m-%d %H:%M:%S')
- trace_txt += "\t[working dir: %s]\n" % (path or os.getcwd())
- trace_txt += "\t[output logged in %s]\n" % cmd_log_fn
- trace_msg(trace_txt + '\t' + cmd.strip())
-
- # early exit in 'dry run' mode, after printing the command that would be run
- if build_option('extended_dry_run'):
- if path is None:
- path = cwd
- dry_run_msg(" running interactive command \"%s\"" % cmd, silent=build_option('silent'))
- dry_run_msg(" (in %s)" % path, silent=build_option('silent'))
- if cmd_log:
- cmd_log.close()
- if simple:
- return True
- else:
- # output, exit code
- return ('', 0)
+ if res.exit_code == EasyBuildExit.SUCCESS:
+ _log.info(f"Shell command completed successfully (see output above): {cmd_str}")
+ else:
+ _log.warning(f"Shell command FAILED (exit code {res.exit_code}, see output above): {cmd_str}")
+ if fail_on_error:
+ raise_run_shell_cmd_error(res)
+ # check that we still are in a sane environment after command execution
+ # safeguard against commands that deleted the work dir or missbehaving filesystems
try:
- if path:
- os.chdir(path)
-
- _log.debug("run_cmd_qa: running cmd %s (in %s)" % (cmd, os.getcwd()))
- except OSError as err:
- _log.warning("Failed to change to %s: %s" % (path, err))
- _log.info("running cmd %s in non-existing directory, might fail!" % cmd)
-
- # Part 1: process the QandA dictionary
- # given initial set of Q and A (in dict), return dict of reg. exp. and A
- #
- # make regular expression that matches the string with
- # - replace whitespace
- # - replace newline
-
- def escape_special(string):
- return re.sub(r"([\+\?\(\)\[\]\*\.\\\$])", r"\\\1", string)
-
- split = r'[\s\n]+'
- regSplit = re.compile(r"" + split)
-
- def process_QA(q, a_s):
- splitq = [escape_special(x) for x in regSplit.split(q)]
- regQtxt = split.join(splitq) + split.rstrip('+') + "*$"
- # add optional split at the end
- for i in [idx for idx, a in enumerate(a_s) if not a.endswith('\n')]:
- a_s[i] += '\n'
- regQ = re.compile(r"" + regQtxt)
- if regQ.search(q):
- return (a_s, regQ)
- else:
- raise EasyBuildError("runqanda: Question %s converted in %s does not match itself", q, regQtxt)
-
- def check_answers_list(answers):
- """Make sure we have a list of answers (as strings)."""
- if isinstance(answers, string_type):
- answers = [answers]
- elif not isinstance(answers, list):
- if cmd_log:
- cmd_log.close()
- raise EasyBuildError("Invalid type for answer on %s, no string or list: %s (%s)",
- question, type(answers), answers)
- # list is manipulated when answering matching question, so return a copy
- return answers[:]
-
- new_qa = {}
- _log.debug("new_qa: ")
- for question, answers in qa.items():
- answers = check_answers_list(answers)
- (answers, regQ) = process_QA(question, answers)
- new_qa[regQ] = answers
- _log.debug("new_qa[%s]: %s" % (regQ.pattern, new_qa[regQ]))
-
- new_std_qa = {}
- if std_qa:
- for question, answers in std_qa.items():
- regQ = re.compile(r"" + question + r"[\s\n]*$")
- answers = check_answers_list(answers)
- for i in [idx for idx, a in enumerate(answers) if not a.endswith('\n')]:
- answers[i] += '\n'
- new_std_qa[regQ] = answers
- _log.debug("new_std_qa[%s]: %s" % (regQ.pattern, new_std_qa[regQ]))
-
- new_no_qa = []
- if no_qa:
- # simple statements, can contain wildcards
- new_no_qa = [re.compile(r"" + x + r"[\s\n]*$") for x in no_qa]
-
- _log.debug("New noQandA list is: %s" % [x.pattern for x in new_no_qa])
-
- # Part 2: Run the command and answer questions
- # - this needs asynchronous stdout
-
- hooks = load_hooks(build_option('hooks'))
- run_hook_kwargs = {
- 'interactive': True,
- 'work_dir': os.getcwd(),
- }
- hook_res = run_hook(RUN_SHELL_CMD, hooks, pre_step_hook=True, args=[cmd], kwargs=run_hook_kwargs)
- if isinstance(hook_res, string_type):
- cmd, old_cmd = hook_res, cmd
- _log.info("Interactive command to run was changed by pre-%s hook: '%s' (was: '%s')",
- RUN_SHELL_CMD, cmd, old_cmd)
-
- # # Log command output
- if cmd_log:
- cmd_log.write("# output for interactive command: %s\n\n" % cmd)
-
- # Make sure we close the proc handles and the cmd_log file
- @contextlib.contextmanager
- def get_proc():
+ os.getcwd()
+ except FileNotFoundError:
+ _log.warning(
+ f"Shell command `{cmd_str}` completed successfully but left the system in an unknown working directory. "
+ f"Changing back to initial working directory: {initial_work_dir}"
+ )
try:
- proc = asyncprocess.Popen(cmd, shell=True, stdout=asyncprocess.PIPE, stderr=asyncprocess.STDOUT,
- stdin=asyncprocess.PIPE, close_fds=True, executable='/bin/bash')
+ os.chdir(initial_work_dir)
except OSError as err:
- if cmd_log:
- cmd_log.close()
- raise EasyBuildError("run_cmd_qa init cmd %s failed:%s", cmd, err)
- try:
- yield proc
- finally:
- if proc.stdout:
- proc.stdout.close()
- if proc.stdin:
- proc.stdin.close()
- if cmd_log:
- cmd_log.close()
-
- with get_proc() as proc:
- ec = proc.poll()
- stdout_err = ''
- old_len_out = -1
- hit_count = 0
-
- while ec is None:
- # need to read from time to time.
- # - otherwise the stdout/stderr buffer gets filled and it all stops working
- try:
- out = get_output_from_process(proc, asynchronous=True)
-
- if cmd_log:
- cmd_log.write(out)
- stdout_err += out
- # recv_some used by get_output_from_process for getting asynchronous output may throw exception
- except (IOError, Exception) as err:
- _log.debug("run_cmd_qa cmd %s: read failed: %s", cmd, err)
- out = None
-
- hit = False
- for question, answers in new_qa.items():
- res = question.search(stdout_err)
- if out and res:
- fa = answers[0] % res.groupdict()
- # cycle through list of answers
- last_answer = answers.pop(0)
- answers.append(last_answer)
- _log.debug("List of answers for question %s after cycling: %s", question.pattern, answers)
-
- _log.debug("run_cmd_qa answer %s question %s out %s", fa, question.pattern, stdout_err[-50:])
- asyncprocess.send_all(proc, fa)
- hit = True
- break
- if not hit:
- for question, answers in new_std_qa.items():
- res = question.search(stdout_err)
- if out and res:
- fa = answers[0] % res.groupdict()
- # cycle through list of answers
- last_answer = answers.pop(0)
- answers.append(last_answer)
- _log.debug("List of answers for question %s after cycling: %s", question.pattern, answers)
-
- _log.debug("run_cmd_qa answer %s std question %s out %s",
- fa, question.pattern, stdout_err[-50:])
- asyncprocess.send_all(proc, fa)
- hit = True
- break
- if not hit:
- if len(stdout_err) > old_len_out:
- old_len_out = len(stdout_err)
- else:
- noqa = False
- for r in new_no_qa:
- if r.search(stdout_err):
- _log.debug("runqanda: noQandA found for out %s", stdout_err[-50:])
- noqa = True
- if not noqa:
- hit_count += 1
- else:
- hit_count = 0
- else:
- hit_count = 0
-
- if hit_count > maxhits:
- # explicitly kill the child process before exiting
- try:
- os.killpg(proc.pid, signal.SIGKILL)
- os.kill(proc.pid, signal.SIGKILL)
- except OSError as err:
- _log.debug("run_cmd_qa exception caught when killing child process: %s", err)
- _log.debug("run_cmd_qa: full stdouterr: %s", stdout_err)
- raise EasyBuildError("run_cmd_qa: cmd %s : Max nohits %s reached: end of output %s",
- cmd, maxhits, stdout_err[-500:])
-
- # the sleep below is required to avoid exiting on unknown 'questions' too early (see above)
- time.sleep(1)
- ec = proc.poll()
-
- # Process stopped. Read all remaining data
- try:
- if proc.stdout:
- out = get_output_from_process(proc)
- stdout_err += out
- if cmd_log:
- cmd_log.write(out)
- except IOError as err:
- _log.debug("runqanda cmd %s: remaining data read failed: %s", cmd, err)
+ raise EasyBuildError(f"Failed to return to {initial_work_dir} after executing command `{cmd_str}`: {err}")
- run_hook_kwargs.update({
- 'interactive': True,
- 'exit_code': ec,
- 'output': stdout_err,
- })
- run_hook(RUN_SHELL_CMD, hooks, post_step_hook=True, args=[cmd], kwargs=run_hook_kwargs)
-
- if trace:
- trace_msg("interactive command completed: exit %s, ran in %s" % (ec, time_str_since(start_time)))
+ if with_hooks:
+ run_hook_kwargs = {
+ 'exit_code': res.exit_code,
+ 'interactive': interactive,
+ 'output': res.output,
+ 'stderr': res.stderr,
+ 'work_dir': res.work_dir,
+ }
+ run_hook(RUN_SHELL_CMD, hooks, post_step_hook=True, args=[cmd], kwargs=run_hook_kwargs)
- try:
- os.chdir(cwd)
- except OSError as err:
- raise EasyBuildError("Failed to return to %s after executing command: %s", cwd, err)
+ if not hidden:
+ time_since_start = time_str_since(start_time)
+ trace_msg(f"command completed: exit {res.exit_code}, ran in {time_since_start}")
- return parse_cmd_output(cmd, stdout_err, ec, simple, log_all, log_ok, regexp)
+ return res
-def parse_cmd_output(cmd, stdouterr, ec, simple, log_all, log_ok, regexp):
+def _cmd_trace_msg(cmd, start_time, work_dir, stdin, tmpdir, thread_id, interactive=False):
"""
- Parse command output and construct return value.
- :param cmd: executed command
- :param stdouterr: combined stdout/stderr of executed command
- :param ec: exit code of executed command
- :param simple: if True, just return True/False to indicate success, else return a tuple: (output, exit_code)
- :param log_all: always log command output and exit code
- :param log_ok: only run output/exit code for failing commands (exit code non-zero)
- :param regexp: regex used to check the output for errors; if True it will use the default (see parse_log_for_error)
- """
- if strictness == IGNORE:
- check_ec = False
- fail_on_error_match = False
- elif strictness == WARN:
- check_ec = True
- fail_on_error_match = False
- elif strictness == ERROR:
- check_ec = True
- fail_on_error_match = True
- else:
- raise EasyBuildError("invalid strictness setting: %s", strictness)
-
- # allow for overriding the regexp setting
- if not regexp:
- fail_on_error_match = False
-
- if ec and (log_all or log_ok):
- # We don't want to error if the user doesn't care
- if check_ec:
- raise EasyBuildError('cmd "%s" exited with exit code %s and output:\n%s', cmd, ec, stdouterr)
- else:
- _log.warning('cmd "%s" exited with exit code %s and output:\n%s' % (cmd, ec, stdouterr))
- elif not ec:
- if log_all:
- _log.info('cmd "%s" exited with exit code %s and output:\n%s' % (cmd, ec, stdouterr))
- else:
- _log.debug('cmd "%s" exited with exit code %s and output:\n%s' % (cmd, ec, stdouterr))
-
- # parse the stdout/stderr for errors when strictness dictates this or when regexp is passed in
- if fail_on_error_match or regexp:
- res = parse_log_for_error(stdouterr, regexp, stdout=False)
- if res:
- errors = "\n\t" + "\n\t".join([r[0] for r in res])
- error_str = "error" if len(res) == 1 else "errors"
- if fail_on_error_match:
- raise EasyBuildError("Found %s %s in output of %s:%s", len(res), error_str, cmd, errors)
- else:
- _log.warning("Found %s potential %s (some may be harmless) in output of %s:%s",
- len(res), error_str, cmd, errors)
-
- if simple:
- if ec:
- # If the user does not care -> will return true
- return not check_ec
- else:
- return True
- else:
- # Because we are not running in simple mode, we return the output and ec to the user
- return (stdouterr, ec)
+ Helper function to construct and print trace message for command being run
-
-def parse_log_for_error(txt, regExp=None, stdout=True, msg=None):
- """
- txt is multiline string.
- - in memory
- regExp is a one-line regular expression
- - default
+ :param cmd: command being run
+ :param start_time: datetime object indicating when command was started
+ :param work_dir: path of working directory in which command is run
+ :param stdin: stdin input value for command
+ :param tmpdir: path to temporary output directory for command
+ :param thread_id: thread ID (None when not running shell command asynchronously)
+ :param interactive: boolean indicating whether it is an interactive command, or not
"""
- global errors_found_in_log
+ start_time = start_time.strftime('%Y-%m-%d %H:%M:%S')
- if regExp and isinstance(regExp, bool):
- regExp = r"(? 0:
- core_cnt = int(out)
+ if int(res.output) > 0:
+ core_cnt = int(res.output)
except ValueError:
pass
@@ -313,10 +313,9 @@ def get_total_memory():
elif os_type == DARWIN:
cmd = "sysctl -n hw.memsize"
_log.debug("Trying to determine total memory size on Darwin via cmd '%s'", cmd)
- out, ec = run_cmd(cmd, force_in_dry_run=True, trace=False, stream_output=False, with_hooks=False,
- with_sysroot=False)
- if ec == 0:
- memtotal = int(out.strip()) // (1024**2)
+ res = run_shell_cmd(cmd, in_dry_run=True, hidden=True, with_hooks=False, output_file=False, stream_output=False)
+ if res.exit_code == EasyBuildExit.SUCCESS:
+ memtotal = int(res.output.strip()) // (1024**2)
if memtotal is None:
memtotal = UNKNOWN
@@ -396,18 +395,18 @@ def get_cpu_vendor():
elif os_type == DARWIN:
cmd = "sysctl -n machdep.cpu.vendor"
- out, ec = run_cmd(cmd, force_in_dry_run=True, trace=False, stream_output=False, log_ok=False,
- with_hooks=False, with_sysroot=False)
- out = out.strip()
- if ec == 0 and out in VENDOR_IDS:
+ res = run_shell_cmd(cmd, fail_on_error=False, in_dry_run=True, hidden=True, with_hooks=False,
+ output_file=False, stream_output=False)
+ out = res.output.strip()
+ if res.exit_code == EasyBuildExit.SUCCESS and out in VENDOR_IDS:
vendor = VENDOR_IDS[out]
_log.debug("Determined CPU vendor on DARWIN as being '%s' via cmd '%s" % (vendor, cmd))
else:
cmd = "sysctl -n machdep.cpu.brand_string"
- out, ec = run_cmd(cmd, force_in_dry_run=True, trace=False, stream_output=False, log_ok=False,
- with_hooks=False, with_sysroot=False)
- out = out.strip().split(' ')[0]
- if ec == 0 and out in CPU_VENDORS:
+ res = run_shell_cmd(cmd, fail_on_error=False, in_dry_run=True, hidden=True, with_hooks=False,
+ output_file=False, stream_output=False)
+ out = res.output.strip().split(' ')[0]
+ if res.exit_code == EasyBuildExit.SUCCESS and out in CPU_VENDORS:
vendor = out
_log.debug("Determined CPU vendor on DARWIN as being '%s' via cmd '%s" % (vendor, cmd))
@@ -508,10 +507,9 @@ def get_cpu_model():
elif os_type == DARWIN:
cmd = "sysctl -n machdep.cpu.brand_string"
- out, ec = run_cmd(cmd, force_in_dry_run=True, trace=False, stream_output=False, with_hooks=False,
- with_sysroot=False)
- if ec == 0:
- model = out.strip()
+ res = run_shell_cmd(cmd, in_dry_run=True, hidden=True, with_hooks=False, output_file=False, stream_output=False)
+ if res.exit_code == EasyBuildExit.SUCCESS:
+ model = res.output.strip()
_log.debug("Determined CPU model on Darwin using cmd '%s': %s" % (cmd, model))
if model is None:
@@ -554,11 +552,10 @@ def get_cpu_speed():
elif os_type == DARWIN:
cmd = "sysctl -n hw.cpufrequency_max"
_log.debug("Trying to determine CPU frequency on Darwin via cmd '%s'" % cmd)
- out, ec = run_cmd(cmd, force_in_dry_run=True, trace=False, stream_output=False, with_hooks=False,
- with_sysroot=False)
- out = out.strip()
+ res = run_shell_cmd(cmd, in_dry_run=True, hidden=True, with_hooks=False, output_file=False, stream_output=False)
+ out = res.output.strip()
cpu_freq = None
- if ec == 0 and out:
+ if res.exit_code == EasyBuildExit.SUCCESS and out:
# returns clock frequency in cycles/sec, but we want MHz
cpu_freq = float(out) // (1000 ** 2)
@@ -603,10 +600,10 @@ def get_cpu_features():
for feature_set in ['extfeatures', 'features', 'leaf7_features']:
cmd = "sysctl -n machdep.cpu.%s" % feature_set
_log.debug("Trying to determine CPU features on Darwin via cmd '%s'", cmd)
- out, ec = run_cmd(cmd, force_in_dry_run=True, trace=False, stream_output=False, log_ok=False,
- with_hooks=False, with_sysroot=False)
- if ec == 0:
- cpu_feat.extend(out.strip().lower().split())
+ res = run_shell_cmd(cmd, in_dry_run=True, hidden=True, fail_on_error=False, with_hooks=False,
+ output_file=False, stream_output=False)
+ if res.exit_code == EasyBuildExit.SUCCESS:
+ cpu_feat.extend(res.output.strip().lower().split())
cpu_feat.sort()
@@ -631,36 +628,68 @@ def get_gpu_info():
try:
cmd = "nvidia-smi --query-gpu=gpu_name,driver_version --format=csv,noheader"
_log.debug("Trying to determine NVIDIA GPU info on Linux via cmd '%s'", cmd)
- out, ec = run_cmd(cmd, simple=False, log_ok=False, log_all=False, force_in_dry_run=True,
- trace=False, stream_output=False, with_hooks=False, with_sysroot=False)
- if ec == 0:
- for line in out.strip().split('\n'):
+ res = run_shell_cmd(cmd, fail_on_error=False, in_dry_run=True, hidden=True, with_hooks=False,
+ output_file=False, stream_output=False)
+ if res.exit_code == EasyBuildExit.SUCCESS:
+ for line in res.output.strip().split('\n'):
nvidia_gpu_info = gpu_info.setdefault('NVIDIA', {})
nvidia_gpu_info.setdefault(line, 0)
nvidia_gpu_info[line] += 1
else:
- _log.debug("None zero exit (%s) from nvidia-smi: %s", ec, out)
- except Exception as err:
+ _log.debug("None zero exit (%s) from nvidia-smi: %s", res.exit_code, res.output)
+ except EasyBuildError as err:
_log.debug("Exception was raised when running nvidia-smi: %s", err)
_log.info("No NVIDIA GPUs detected")
+ amdgpu_checked = False
+ if not which('amd-smi', on_error=IGNORE):
+ _log.info("amd-smi not found. Trying to detect AMD GPUs via rocm-smi")
+ else:
+ try:
+ cmd = "amd-smi static --driver --board --asic --csv"
+ _log.debug("Trying to determine AMD GPU info on Linux via cmd '%s'", cmd)
+ res = run_shell_cmd(cmd, fail_on_error=False, in_dry_run=True, hidden=True, with_hooks=False,
+ output_file=False, stream_output=False)
+ if res.exit_code == EasyBuildExit.SUCCESS:
+ csv_reader = csv.DictReader(io.StringIO(res.output.strip()))
+
+ for row in csv_reader:
+ amd_card_series = row['product_name']
+ amd_card_device_id = row['device_id']
+ amd_card_gfx = row['target_graphics_version']
+ amd_card_driver = row['version']
+
+ amd_gpu = ("%s (device id: %s, gfx: %s, driver: %s)" %
+ (amd_card_series, amd_card_device_id, amd_card_gfx, amd_card_driver))
+ amd_gpu_info = gpu_info.setdefault('AMD', {})
+ amd_gpu_info.setdefault(amd_gpu, 0)
+ amd_gpu_info[amd_gpu] += 1
+ amdgpu_checked = True
+ else:
+ _log.debug("None zero exit (%s) from amd-smi: %s.", res.exit_code, res.output)
+ except EasyBuildError as err:
+ _log.debug("Exception was raised when running amd-smi: %s", err)
+ _log.info("No AMD GPUs detected via amd-smi.")
+ except KeyError as err:
+ _log.warning("Failed to extract AMD GPU info from amd-smi output: %s.", err)
+
if not which('rocm-smi', on_error=IGNORE):
_log.info("rocm-smi not found. Cannot detect AMD GPUs")
- else:
+ elif not amdgpu_checked:
try:
cmd = "rocm-smi --showdriverversion --csv"
_log.debug("Trying to determine AMD GPU driver on Linux via cmd '%s'", cmd)
- out, ec = run_cmd(cmd, simple=False, log_ok=False, log_all=False, force_in_dry_run=True,
- trace=False, stream_output=False, with_hooks=False, with_sysroot=False)
- if ec == 0:
- amd_driver = out.strip().split('\n')[1].split(',')[1]
+ res = run_shell_cmd(cmd, fail_on_error=False, in_dry_run=True, hidden=True, with_hooks=False,
+ output_file=False, stream_output=False)
+ if res.exit_code == EasyBuildExit.SUCCESS:
+ amd_driver = res.output.strip().split('\n')[1].split(',')[1]
cmd = "rocm-smi --showproductname --csv"
_log.debug("Trying to determine AMD GPU info on Linux via cmd '%s'", cmd)
- out, ec = run_cmd(cmd, simple=False, log_ok=False, log_all=False, force_in_dry_run=True,
- trace=False, stream_output=False, with_hooks=False, with_sysroot=False)
- if ec == 0:
- for line in out.strip().split('\n')[1:]:
+ res = run_shell_cmd(cmd, fail_on_error=False, in_dry_run=True, hidden=True, with_hooks=False,
+ output_file=False, stream_output=False)
+ if res.exit_code == EasyBuildExit.SUCCESS:
+ for line in res.output.strip().split('\n')[1:]:
amd_card_series = line.split(',')[1]
amd_card_model = line.split(',')[2]
amd_gpu = "%s (model: %s, driver: %s)" % (amd_card_series, amd_card_model, amd_driver)
@@ -668,8 +697,8 @@ def get_gpu_info():
amd_gpu_info.setdefault(amd_gpu, 0)
amd_gpu_info[amd_gpu] += 1
else:
- _log.debug("None zero exit (%s) from rocm-smi: %s", ec, out)
- except Exception as err:
+ _log.debug("None zero exit (%s) from rocm-smi: %s", res.exit_code, res.output)
+ except EasyBuildError as err:
_log.debug("Exception was raised when running rocm-smi: %s", err)
_log.info("No AMD GPUs detected")
@@ -868,16 +897,17 @@ def check_os_dependency(dep):
for pkg_cmd in pkg_cmds:
if which(pkg_cmd):
- cmd = [
+ cmd = ' '.join([
# unset $LD_LIBRARY_PATH to avoid broken rpm command due to loaded dependencies
# see https://github.com/easybuilders/easybuild-easyconfigs/pull/4179
'unset LD_LIBRARY_PATH &&',
pkg_cmd,
pkg_cmd_flag.get(pkg_cmd),
dep,
- ]
- found = run_cmd(' '.join(cmd), simple=True, log_all=False, log_ok=False,
- force_in_dry_run=True, trace=False, stream_output=False)
+ ])
+ res = run_shell_cmd(cmd, fail_on_error=False, in_dry_run=True, hidden=True,
+ output_file=False, stream_output=False)
+ found = res.exit_code == EasyBuildExit.SUCCESS
if found:
break
@@ -888,10 +918,10 @@ def check_os_dependency(dep):
# try locate if it's available
if not found and which('locate'):
cmd = 'locate -c --regexp "/%s$"' % dep
- out, ec = run_cmd(cmd, simple=False, log_all=False, log_ok=False, force_in_dry_run=True, trace=False,
- stream_output=False)
+ res = run_shell_cmd(cmd, fail_on_error=False, in_dry_run=True, hidden=True,
+ output_file=False, stream_output=False)
try:
- found = (ec == 0 and int(out.strip()) > 0)
+ found = (res.exit_code == EasyBuildExit.SUCCESS and int(res.output.strip()) > 0)
except ValueError:
# Returned something else than an int -> Error
found = False
@@ -904,41 +934,41 @@ def get_tool_version(tool, version_option='--version', ignore_ec=False):
Get output of running version option for specific command line tool.
Output is returned as a single-line string (newlines are replaced by '; ').
"""
- out, ec = run_cmd(' '.join([tool, version_option]), simple=False, log_ok=False, force_in_dry_run=True,
- trace=False, stream_output=False, with_hooks=False, with_sysroot=False)
- if not ignore_ec and ec:
- _log.warning("Failed to determine version of %s using '%s %s': %s" % (tool, tool, version_option, out))
+ res = run_shell_cmd(' '.join([tool, version_option]), fail_on_error=False, in_dry_run=True,
+ hidden=True, with_hooks=False, output_file=False, stream_output=False)
+ if not ignore_ec and res.exit_code != EasyBuildExit.SUCCESS:
+ _log.warning("Failed to determine version of %s using '%s %s': %s" % (tool, tool, version_option, res.output))
return UNKNOWN
else:
- return '; '.join(out.split('\n'))
+ return '; '.join(res.output.split('\n'))
def get_gcc_version():
"""
Process `gcc --version` and return the GCC version.
"""
- out, ec = run_cmd('gcc --version', simple=False, log_ok=False, force_in_dry_run=True, verbose=False, trace=False,
- stream_output=False)
- res = None
- if ec:
- _log.warning("Failed to determine the version of GCC: %s", out)
- res = UNKNOWN
+ res = run_shell_cmd('gcc --version', fail_on_error=False, in_dry_run=True, hidden=True,
+ output_file=False, stream_output=False)
+ gcc_ver = None
+ if res.exit_code != EasyBuildExit.SUCCESS:
+ _log.warning("Failed to determine the version of GCC: %s", res.output)
+ gcc_ver = UNKNOWN
# Fedora: gcc (GCC) 5.1.1 20150618 (Red Hat 5.1.1-4)
# Debian: gcc (Debian 4.9.2-10) 4.9.2
- find_version = re.search(r"^gcc\s+\([^)]+\)\s+(?P[^\s]+)\s+", out)
+ find_version = re.search(r"^gcc\s+\([^)]+\)\s+(?P[^\s]+)\s+", res.output)
if find_version:
- res = find_version.group('version')
- _log.debug("Found GCC version: %s from %s", res, out)
+ gcc_ver = find_version.group('version')
+ _log.debug("Found GCC version: %s from %s", res, res.output)
else:
# Apple likes to install clang but call it gcc.
if get_os_type() == DARWIN:
_log.warning("On recent version of Mac OS, gcc is actually clang, returning None as GCC version")
- res = None
+ gcc_ver = None
else:
- raise EasyBuildError("Failed to determine the GCC version from: %s", out)
+ raise EasyBuildError("Failed to determine the GCC version from: %s", res.output)
- return res
+ return gcc_ver
def get_glibc_version():
@@ -974,9 +1004,9 @@ def get_linked_libs_raw(path):
or None for other types of files.
"""
- file_cmd_out, ec = run_cmd("file %s" % path, simple=False, trace=False)
- if ec:
- fail_msg = "Failed to run 'file %s': %s" % (path, file_cmd_out)
+ res = run_shell_cmd("file %s" % path, fail_on_error=False, hidden=True, output_file=False, stream_output=False)
+ if res.exit_code != EasyBuildExit.SUCCESS:
+ fail_msg = "Failed to run 'file %s': %s" % (path, res.output)
_log.warning(fail_msg)
os_type = get_os_type()
@@ -987,7 +1017,7 @@ def get_linked_libs_raw(path):
# /usr/bin/ls: ELF 64-bit LSB executable, x86-64, ..., dynamically linked (uses shared libs), ...
# example output for shared libraries:
# /lib64/libc-2.17.so: ELF 64-bit LSB shared object, x86-64, ..., dynamically linked (uses shared libs), ...
- if "dynamically linked" in file_cmd_out:
+ if "dynamically linked" in res.output:
# determine linked libraries via 'ldd'
linked_libs_cmd = "ldd %s" % path
else:
@@ -999,7 +1029,7 @@ def get_linked_libs_raw(path):
# example output for shared libraries:
# /usr/lib/libz.dylib: Mach-O 64-bit dynamically linked shared library x86_64
bin_lib_regex = re.compile('(Mach-O .* executable)|(dynamically linked)', re.M)
- if bin_lib_regex.search(file_cmd_out):
+ if bin_lib_regex.search(res.output):
linked_libs_cmd = "otool -L %s" % path
else:
return None
@@ -1009,12 +1039,12 @@ def get_linked_libs_raw(path):
# take into account that 'ldd' may fail for strange reasons,
# like printing 'not a dynamic executable' when not enough memory is available
# (see also https://bugzilla.redhat.com/show_bug.cgi?id=1817111)
- out, ec = run_cmd(linked_libs_cmd, simple=False, trace=False, log_ok=False, log_all=False)
- if ec == 0:
- linked_libs_out = out
+ res = run_shell_cmd(linked_libs_cmd, fail_on_error=False, hidden=True, output_file=False, stream_output=False)
+ if res.exit_code == EasyBuildExit.SUCCESS:
+ linked_libs_out = res.output
else:
- fail_msg = "Determining linked libraries for %s via '%s' failed! Output: '%s'" % (path, linked_libs_cmd, out)
- print_warning(fail_msg)
+ fail_msg = "Determining linked libraries for %s via '%s' failed! Output: '%s'"
+ print_warning(fail_msg % (path, linked_libs_cmd, res.output))
linked_libs_out = None
return linked_libs_out
@@ -1033,12 +1063,12 @@ def check_linked_shared_libs(path, required_patterns=None, banned_patterns=None)
if required_patterns is None:
required_regexs = []
else:
- required_regexs = [re.compile(p) if isinstance(p, string_type) else p for p in required_patterns]
+ required_regexs = [re.compile(p) if isinstance(p, str) else p for p in required_patterns]
if banned_patterns is None:
banned_regexs = []
else:
- banned_regexs = [re.compile(p) if isinstance(p, string_type) else p for p in banned_patterns]
+ banned_regexs = [re.compile(p) if isinstance(p, str) else p for p in banned_patterns]
# resolve symbolic links (unless they're broken)
if os.path.islink(path) and os.path.exists(path):
@@ -1190,20 +1220,22 @@ def get_default_parallelism():
except AttributeError:
# No cache -> Calculate value from current system values
par = get_avail_core_count()
- # check ulimit -u
- out, ec = run_cmd('ulimit -u', force_in_dry_run=True, trace=False, stream_output=False)
+ # determine max user processes via ulimit -u
+ res = run_shell_cmd("ulimit -u", in_dry_run=True, hidden=True, output_file=False, stream_output=False)
try:
- if out.startswith("unlimited"):
+ if res.output.startswith("unlimited"):
maxuserproc = 2 ** 32 - 1
else:
- maxuserproc = int(out)
+ maxuserproc = int(res.output)
except ValueError as err:
- raise EasyBuildError("Failed to determine max user processes (%s, %s): %s", ec, out, err)
+ raise EasyBuildError(
+ "Failed to determine max user processes (%s, %s): %s", res.exit_code, res.output, err
+ )
# assume 6 processes per build thread + 15 overhead
par_guess = (maxuserproc - 15) // 6
if par_guess < par:
par = par_guess
- _log.info("Limit parallel builds to %s because max user processes is %s", par, out)
+ _log.info("Limit parallel builds to %s because max user processes is %s", par, res.output)
# Cache value
det_parallelism._default_parallelism = par
return par
@@ -1217,6 +1249,8 @@ def get_default_parallelism():
raise EasyBuildError("Specified level of parallelism '%s' is not an integer value: %s", par, err)
if maxpar is not None and maxpar < par:
+ if maxpar is False:
+ maxpar = 1
_log.info("Limiting parallelism from %s to %s", par, maxpar)
par = maxpar
@@ -1249,17 +1283,13 @@ def check_python_version():
python_ver = '%d.%d' % (python_maj_ver, python_min_ver)
_log.info("Found Python version %s", python_ver)
- if python_maj_ver == 2:
- if python_min_ver < 7:
- raise EasyBuildError("Python 2.7 is required when using Python 2, found Python %s", python_ver)
- else:
- _log.info("Running EasyBuild with Python 2 (version %s)", python_ver)
-
- elif python_maj_ver == 3:
- if python_min_ver < 5:
- raise EasyBuildError("Python 3.5 or higher is required when using Python 3, found Python %s", python_ver)
+ if python_maj_ver == 3:
+ if python_min_ver < 6:
+ raise EasyBuildError("Python 3.6 or higher is required, found Python %s", python_ver)
else:
_log.info("Running EasyBuild with Python 3 (version %s)", python_ver)
+ elif python_maj_ver < 3:
+ raise EasyBuildError("EasyBuild is not compatible with Python %s", python_ver)
else:
raise EasyBuildError("EasyBuild is not compatible (yet) with Python %s", python_ver)
@@ -1320,7 +1350,7 @@ def pick_dep_version(dep_version):
result = None
else:
result = pick_system_specific_value("version", dep_version)
- if not isinstance(result, string_type) and result is not False:
+ if not isinstance(result, str) and result is not False:
typ = type(dep_version)
raise EasyBuildError("Unknown value type for version: %s (%s), should be string value", typ, dep_version)
@@ -1373,9 +1403,9 @@ def extract_version(tool):
python_version = extract_version(sys.executable)
opt_dep_versions = {}
- for key in EASYBUILD_OPTIONAL_DEPENDENCIES:
+ for key, opt_dep in EASYBUILD_OPTIONAL_DEPENDENCIES.items():
- pkg = EASYBUILD_OPTIONAL_DEPENDENCIES[key][0]
+ pkg = opt_dep[0]
if pkg is None:
pkg = key.lower()
@@ -1401,8 +1431,8 @@ def extract_version(tool):
opt_deps_key = "Optional dependencies"
checks_data[opt_deps_key] = {}
- for key in opt_dep_versions:
- checks_data[opt_deps_key][key] = (opt_dep_versions[key], EASYBUILD_OPTIONAL_DEPENDENCIES[key][1])
+ for key, version in opt_dep_versions.items():
+ checks_data[opt_deps_key][key] = (version, EASYBUILD_OPTIONAL_DEPENDENCIES[key][1])
sys_tools_key = "System tools"
checks_data[sys_tools_key] = {}
diff --git a/easybuild/tools/testing.py b/easybuild/tools/testing.py
index 881788337e..2f0b0c8a6f 100644
--- a/easybuild/tools/testing.py
+++ b/easybuild/tools/testing.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -47,7 +47,7 @@
from easybuild.framework.easyconfig.tools import skip_available
from easybuild.tools.build_log import EasyBuildError
from easybuild.tools.config import build_option
-from easybuild.tools.filetools import find_easyconfigs, mkdir, read_file, write_file
+from easybuild.tools.filetools import find_easyconfigs, get_cwd, mkdir, read_file, write_file
from easybuild.tools.github import GITHUB_EASYBLOCKS_REPO, GITHUB_EASYCONFIGS_REPO, create_gist, post_comment_in_issue
from easybuild.tools.jenkins import aggregate_xml_in_dirs
from easybuild.tools.parallelbuild import build_easyconfigs_in_parallel
@@ -67,7 +67,7 @@ def regtest(easyconfig_paths, modtool, build_specs=None):
:param build_specs: dictionary specifying build specifications (e.g. version, toolchain, ...)
"""
- cur_dir = os.getcwd()
+ cur_dir = get_cwd()
aggregate_regtest = build_option('aggregate_regtest')
if aggregate_regtest is not None:
@@ -321,8 +321,8 @@ def post_pr_test_report(pr_nrs, repo_type, test_report, msg, init_session_state,
gpu_info = get_gpu_info()
gpu_str = ""
if gpu_info:
- for vendor in gpu_info:
- for gpu, num in gpu_info[vendor].items():
+ for vendor, vendor_gpu in gpu_info.items():
+ for gpu, num in vendor_gpu.items():
gpu_str += ", %s x %s %s" % (num, vendor, gpu)
os_info = '%(hostname)s - %(os_type)s %(os_name)s %(os_version)s' % system_info
diff --git a/easybuild/tools/toolchain/__init__.py b/easybuild/tools/toolchain/__init__.py
index cf755a8e40..928a2252af 100644
--- a/easybuild/tools/toolchain/__init__.py
+++ b/easybuild/tools/toolchain/__init__.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/tools/toolchain/compiler.py b/easybuild/tools/toolchain/compiler.py
index faf8f03d88..5619ecc733 100644
--- a/easybuild/tools/toolchain/compiler.py
+++ b/easybuild/tools/toolchain/compiler.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -32,13 +32,12 @@
* Damian Alvarez (Forschungszentrum Juelich GmbH)
"""
from easybuild.tools import systemtools
-from easybuild.tools.build_log import EasyBuildError
+from easybuild.tools.build_log import EasyBuildError, print_warning
from easybuild.tools.config import build_option
-from easybuild.tools.py2vs3 import string_type
from easybuild.tools.toolchain.constants import COMPILER_VARIABLES
from easybuild.tools.toolchain.toolchain import Toolchain
-# default optimization 'level' (see COMPILER_SHARED_OPTION_MAP/COMPILER_OPT_FLAGS)
+# default optimization 'level' (see COMPILER_SHARED_OPTION_MAP/COMPILER_OPT_OPTIONS)
DEFAULT_OPT_LEVEL = 'defaultopt'
# 'GENERIC' can be used to enable generic compilation instead of optimized compilation (which is the default)
@@ -83,18 +82,19 @@ class Compiler(Toolchain):
'loose': (False, "Loose precision"),
'veryloose': (False, "Very loose precision"),
'verbose': (False, "Verbose output"),
- 'debug': (False, "Enable debug"),
+ 'debug': (None, "Keep debug symbols"), # default value set by build option keep-debug-symbols
'i8': (False, "Integers are 8 byte integers"), # fortran only -> no: MKL and icc give -DMKL_ILP64
'r8': (False, "Real is 8 byte real"), # fortran only
'unroll': (False, "Unroll loops"),
'cstd': (None, "Specify C standard"),
'shared': (False, "Build shared library"),
'static': (False, "Build static library"),
- '32bit': (False, "Compile 32bit target"), # LA, FFTW
'openmp': (False, "Enable OpenMP"),
'vectorize': (None, "Enable compiler auto-vectorization, default except for noopt and lowopt"),
'packed-linker-options': (False, "Pack the linker options as comma separated list"), # ScaLAPACK mainly
'rpath': (True, "Use RPATH wrappers when --rpath is enabled in EasyBuild configuration"),
+ 'search-path-cpp-headers': (None, "Search path used at build time for include directories"),
+ 'search-path-linker': (None, "Search path used at build time by the linker for libraries"),
'extra_cflags': (None, "Specify extra CFLAGS options."),
'extra_cxxflags': (None, "Specify extra CXXFLAGS options."),
'extra_fflags': (None, "Specify extra FFLAGS options."),
@@ -104,19 +104,18 @@ class Compiler(Toolchain):
COMPILER_UNIQUE_OPTION_MAP = None
COMPILER_SHARED_OPTION_MAP = {
- DEFAULT_OPT_LEVEL: 'O2',
- '32bit': 'm32',
- 'cstd': 'std=%(value)s',
- 'debug': 'g',
- 'lowopt': 'O1',
- 'noopt': 'O0',
- 'openmp': 'fopenmp',
- 'opt': 'O3',
- 'pic': 'fPIC',
- 'shared': 'shared',
- 'static': 'static',
- 'unroll': 'unroll',
- 'verbose': 'v',
+ DEFAULT_OPT_LEVEL: '-O2',
+ 'cstd': '-std=%(value)s',
+ 'debug': '-g',
+ 'lowopt': '-O1',
+ 'noopt': '-O0',
+ 'openmp': '-fopenmp',
+ 'opt': '-O3',
+ 'pic': '-fPIC',
+ 'shared': '-shared',
+ 'static': '-static',
+ 'unroll': '-unroll',
+ 'verbose': '-v',
'extra_cflags': '%(value)s',
'extra_cxxflags': '%(value)s',
'extra_fflags': '%(value)s',
@@ -127,20 +126,20 @@ class Compiler(Toolchain):
COMPILER_OPTIMAL_ARCHITECTURE_OPTION = None
COMPILER_GENERIC_OPTION = None
- COMPILER_FLAGS = ['debug', 'ieee', 'openmp', 'pic', 'shared', 'static', 'unroll', 'verbose'] # any compiler
- COMPILER_OPT_FLAGS = ['noopt', 'lowopt', DEFAULT_OPT_LEVEL, 'opt'] # optimisation args, ordered !
- COMPILER_PREC_FLAGS = ['strict', 'precise', 'defaultprec', 'loose', 'veryloose'] # precision flags, ordered !
+ COMPILER_OPTIONS = ['debug', 'ieee', 'openmp', 'pic', 'shared', 'static', 'unroll', 'verbose'] # any compiler
+ COMPILER_OPT_OPTIONS = ['noopt', 'lowopt', DEFAULT_OPT_LEVEL, 'opt'] # optimisation args, ordered !
+ COMPILER_PREC_OPTIONS = ['strict', 'precise', 'defaultprec', 'loose', 'veryloose'] # precision flags, ordered !
COMPILER_CC = None
COMPILER_CXX = None
- COMPILER_C_FLAGS = ['cstd']
- COMPILER_C_UNIQUE_FLAGS = []
+ COMPILER_C_OPTIONS = ['cstd']
+ COMPILER_C_UNIQUE_OPTIONS = []
COMPILER_F77 = None
COMPILER_F90 = None
COMPILER_FC = None
- COMPILER_F_FLAGS = ['i8', 'r8']
- COMPILER_F_UNIQUE_FLAGS = []
+ COMPILER_F_OPTIONS = ['i8', 'r8']
+ COMPILER_F_UNIQUE_OPTIONS = []
LINKER_TOGGLE_STATIC_DYNAMIC = None
LINKER_TOGGLE_START_STOP_GROUP = {
@@ -178,6 +177,10 @@ def set_variables(self):
def _set_compiler_toolchainoptions(self):
"""Set the compiler related toolchain options"""
+ # Initialize default value of debug symbols based on global build option
+ if self.COMPILER_SHARED_OPTS and 'debug' in self.COMPILER_SHARED_OPTS:
+ _, desc = self.COMPILER_SHARED_OPTS['debug']
+ self.COMPILER_SHARED_OPTS['debug'] = (build_option('keep_debug_symbols'), desc)
self.options.add_options(self.COMPILER_SHARED_OPTS, self.COMPILER_SHARED_OPTION_MAP)
# always include empty infix first for non-prefixed compilers (e.g., GCC, Intel, ...)
@@ -190,10 +193,6 @@ def _set_compiler_toolchainoptions(self):
def _set_compiler_vars(self):
"""Set the compiler variables"""
- is32bit = self.options.get('32bit', None)
- if is32bit:
- self.log.debug("_set_compiler_vars: 32bit set: changing compiler definitions")
-
comp_var_tmpl_dict = {}
# always include empty infix first for non-prefixed compilers (e.g., GCC, Intel, ...)
@@ -215,8 +214,6 @@ def _set_compiler_vars(self):
self.log.warning("_set_compiler_vars: %s compiler variable %s undefined", infix, var)
self.variables[pref_var] = value
- if is32bit:
- self.variables.nappend_el(pref_var, self.options.option('32bit'))
# update dictionary to complete compiler variable template
# to produce e.g. 'nvcc -ccbin=icpc' from 'nvcc -ccbin=%(CXX_base)'
@@ -246,23 +243,34 @@ def _set_compiler_vars(self):
def _set_compiler_flags(self):
"""Collect the flags set, and add them as variables too"""
-
- flags = [self.options.option(x) for x in self.COMPILER_FLAGS if self.options.get(x, False)]
- cflags = [self.options.option(x) for x in self.COMPILER_C_FLAGS + self.COMPILER_C_UNIQUE_FLAGS
+ variants = ['', '_F', '_F_UNIQUE', '_C', '_C_UNIQUE', '_OPT', '_PREC']
+ for variant in variants:
+ old_var = getattr(self, f'COMPILER{variant}_FLAGS', None)
+ if old_var is not None:
+ self.log.deprecated(f'COMPILER{variant}_FLAGS has been renamed to COMPILER{variant}_OPTIONS.', '6.0')
+ setattr(self, f'COMPILER{variant}_OPTIONS', old_var)
+
+ flags = [self.options.option(x) for x in self.COMPILER_OPTIONS if self.options.get(x, False)]
+ cflags = [self.options.option(x) for x in self.COMPILER_C_OPTIONS + self.COMPILER_C_UNIQUE_OPTIONS
if self.options.get(x, False)]
- fflags = [self.options.option(x) for x in self.COMPILER_F_FLAGS + self.COMPILER_F_UNIQUE_FLAGS
+ fflags = [self.options.option(x) for x in self.COMPILER_F_OPTIONS + self.COMPILER_F_UNIQUE_OPTIONS
if self.options.get(x, False)]
# Allow a user-defined default optimisation
default_opt_level = build_option('default_opt_level')
- if default_opt_level not in self.COMPILER_OPT_FLAGS:
+ if default_opt_level not in self.COMPILER_OPT_OPTIONS:
raise EasyBuildError("Unknown value for default optimisation: %s (possibilities are %s)" %
- (default_opt_level, self.COMPILER_OPT_FLAGS))
+ (default_opt_level, self.COMPILER_OPT_OPTIONS))
# 1st one is the one to use. add default at the end so len is at least 1
- optflags = ([self.options.option(x) for x in self.COMPILER_OPT_FLAGS if self.options.get(x, False)] +
+ optflags = ([self.options.option(x) for x in self.COMPILER_OPT_OPTIONS if self.options.get(x, False)] +
[self.options.option(default_opt_level)])[:1]
+ # Normal compiler flags need to include "-" starting with EB 5.0, check the first as a sanity check.
+ # Avoiding all flags as there may be legitimate use for flags that lack -
+ if optflags and optflags[0] and not optflags[0][0].startswith('-'):
+ print_warning(f'Compiler flag "{optflags[0][0]}" does not start with a dash. See changes in EasyBuild 5.')
+
# only apply if the vectorize toolchainopt is explicitly set
# otherwise the individual compiler toolchain file should make sure that
# vectorization is disabled for noopt and lowopt, and enabled otherwise.
@@ -282,7 +290,7 @@ def _set_compiler_flags(self):
elif self.options.get('optarch', False):
optarchflags.append(self.options.option('optarch'))
- precflags = [self.options.option(x) for x in self.COMPILER_PREC_FLAGS if self.options.get(x, False)] + \
+ precflags = [self.options.option(x) for x in self.COMPILER_PREC_OPTIONS if self.options.get(x, False)] + \
[self.options.option('defaultprec')]
self.variables.nextend('OPTFLAGS', optflags + optarchflags)
@@ -301,7 +309,7 @@ def _set_compiler_flags(self):
extraflags = self.options.option(extra)
if not extraflags or extraflags[0] != '-':
raise EasyBuildError("toolchainopts %s: '%s' must start with a '-'." % (extra, extraflags))
- self.variables.nappend_el(var, extraflags[1:])
+ self.variables.nappend_el(var, extraflags)
def _set_optimal_architecture(self, default_optarch=None):
"""
@@ -311,7 +319,7 @@ def _set_optimal_architecture(self, default_optarch=None):
(--optarch and --optarch=GENERIC still override this value)
"""
ec_optarch = self.options.get('optarch', False)
- if isinstance(ec_optarch, string_type):
+ if isinstance(ec_optarch, str):
if OPTARCH_MAP_CHAR in ec_optarch:
error_msg = "When setting optarch in the easyconfig (found %s), " % ec_optarch
error_msg += "the syntax is not allowed. " % OPTARCH_MAP_CHAR
@@ -338,7 +346,7 @@ def _set_optimal_architecture(self, default_optarch=None):
self.log.info("_set_optimal_architecture: no optarch found for compiler %s. Ignoring option.",
current_compiler)
- if isinstance(optarch, string_type):
+ if isinstance(optarch, str):
use_generic = (optarch == OPTARCH_GENERIC)
elif optarch is None:
use_generic = False
@@ -358,6 +366,11 @@ def _set_optimal_architecture(self, default_optarch=None):
optarch = self.COMPILER_OPTIMAL_ARCHITECTURE_OPTION[(self.arch, self.cpu_family)]
if optarch is not None:
+ if optarch and not optarch.startswith('-'):
+ self.log.deprecated(f'Specifying optarch "{optarch}" without initial dash is deprecated.', '6.0')
+ # Add flags for backwards compatibility
+ optarch = '-' + optarch
+
optarch_log_str = optarch or 'no flags'
self.log.info("_set_optimal_architecture: using %s as optarch for %s/%s.",
optarch_log_str, self.arch, self.cpu_family)
@@ -366,9 +379,9 @@ def _set_optimal_architecture(self, default_optarch=None):
optarch_flags_str = "%soptarch flags" % ('', 'generic ')[use_generic]
error_msg = "Don't know how to set %s for %s/%s! " % (optarch_flags_str, self.arch, self.cpu_family)
error_msg += "Use --optarch='' to override (see "
- error_msg += "http://easybuild.readthedocs.io/en/latest/Controlling_compiler_optimization_flags.html "
+ error_msg += "https://docs.easybuild.io/controlling-compiler-optimization-flags/ "
error_msg += "for details) and consider contributing your settings back (see "
- error_msg += "http://easybuild.readthedocs.io/en/latest/Contributing.html)."
+ error_msg += "https://docs.easybuild.io/contributing/)."
raise EasyBuildError(error_msg)
def comp_family(self, prefix=None):
diff --git a/easybuild/tools/toolchain/constants.py b/easybuild/tools/toolchain/constants.py
index 643e8b4d21..47f06c7113 100644
--- a/easybuild/tools/toolchain/constants.py
+++ b/easybuild/tools/toolchain/constants.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -31,9 +31,9 @@
* Kenneth Hoste (Ghent University)
"""
-from easybuild.tools.variables import AbsPathList
-from easybuild.tools.toolchain.variables import CommandFlagList, CommaSharedLibs, CommaStaticLibs
-from easybuild.tools.toolchain.variables import FlagList, IncludePaths, LibraryList, LinkLibraryPaths
+from easybuild.tools.toolchain.variables import CommaSharedLibs, CommaStaticLibs
+from easybuild.tools.toolchain.variables import IncludePaths, LibraryList, LinkLibraryPaths, SearchPaths
+from easybuild.tools.variables import AbsPathList, StrList
COMPILER_VARIABLES = [
@@ -53,32 +53,36 @@
]
COMPILER_MAP_CLASS = {
- FlagList: [
+ StrList: [
('OPTFLAGS', 'Optimization flags'),
('PRECFLAGS', 'FP precision flags'),
- ] + COMPILER_FLAGS,
+ ] + COMPILER_FLAGS + COMPILER_VARIABLES,
LibraryList: [
- ('LIBS', 'Libraries'), # TODO: where are these used? ld?
- ('FLIBS', 'Fortran libraries'), # TODO: where are these used? gfortran only?
+ ('LIBS', 'Libraries'), # -l options to pass to the linker (C/C++/Fortran)
+ ('FLIBS', 'Fortran libraries'), # linker flags (e.g. -L and -l) for Fortran libraries
],
LinkLibraryPaths: [
- ('LDFLAGS', 'Flags passed to linker'), # TODO: overridden by command line?
+ ('LDFLAGS', 'Linker flags'),
],
IncludePaths: [
- ('CPPFLAGS', 'Precompiler flags'),
+ ('CPPFLAGS', 'Preprocessor flags'),
+ ],
+ SearchPaths: [
+ ('CPATH', 'Location of C/C++ header files'),
+ ('C_INCLUDE_PATH', 'Location of C header files'),
+ ('CPLUS_INCLUDE_PATH', 'Location of C++ header files'),
+ ('OBJC_INCLUDE_PATH', 'Location of Objective C header files'),
+ ('LIBRARY_PATH', 'Location of linker files'),
],
- CommandFlagList: COMPILER_VARIABLES,
}
CO_COMPILER_MAP_CLASS = {
- CommandFlagList: [
+ StrList: [
('CUDA_CC', 'CUDA C compiler command'),
('CUDA_CXX', 'CUDA C++ compiler command'),
('CUDA_F77', 'CUDA Fortran 77 compiler command'),
('CUDA_F90', 'CUDA Fortran 90 compiler command'),
('CUDA_FC', 'CUDA Fortran 77/90 compiler command'),
- ],
- FlagList: [
('CUDA_CFLAGS', 'CUDA C compiler flags'),
('CUDA_CXXFLAGS', 'CUDA C++ compiler flags'),
('CUDA_FCFLAGS', 'CUDA Fortran 77/90 compiler flags'),
@@ -103,7 +107,7 @@
('MPI_LIB_DIR', 'MPI library directory'),
('MPI_INC_DIR', 'MPI include directory'),
],
- CommandFlagList: MPI_COMPILER_VARIABLES + SEQ_COMPILER_VARIABLES,
+ StrList: MPI_COMPILER_VARIABLES + SEQ_COMPILER_VARIABLES,
}
BLAS_MAP_CLASS = {
diff --git a/easybuild/tools/toolchain/fft.py b/easybuild/tools/toolchain/fft.py
index b7191d5a26..53bce4cd8c 100644
--- a/easybuild/tools/toolchain/fft.py
+++ b/easybuild/tools/toolchain/fft.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/tools/toolchain/linalg.py b/easybuild/tools/toolchain/linalg.py
index fd74615c4a..bbf158d9e5 100644
--- a/easybuild/tools/toolchain/linalg.py
+++ b/easybuild/tools/toolchain/linalg.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/tools/toolchain/mpi.py b/easybuild/tools/toolchain/mpi.py
index 0ea2714490..aaaf4e7078 100644
--- a/easybuild/tools/toolchain/mpi.py
+++ b/easybuild/tools/toolchain/mpi.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -193,10 +193,6 @@ def set_variables(self):
def _set_mpi_compiler_variables(self):
"""Set the MPI compiler variables"""
- is32bit = self.options.get('32bit', None)
- if is32bit:
- self.log.debug("_set_mpi_compiler_variables: 32bit set: changing compiler definitions")
-
for var_tuple in COMPILER_VARIABLES:
c_var = var_tuple[0] # [1] is the description
var = MPI_COMPILER_TEMPLATE % {'c_var': c_var}
@@ -214,9 +210,6 @@ def _set_mpi_compiler_variables(self):
self.variables.nappend_el(var, self.options.option('_opt_%s' % var, templatedict=templatedict))
- if is32bit:
- self.variables.nappend_el(var, self.options.option('32bit'))
-
if self.options.get('usempi', None):
var_seq = SEQ_COMPILER_TEMPLATE % {'c_var': c_var}
self.log.debug("usempi set: defining %s as %s", var_seq, self.variables[c_var])
@@ -238,9 +231,7 @@ def _set_mpi_variables(self):
lib_dir = ['lib']
incl_dir = ['include']
- suffix = None
- if not self.options.get('32bit', None):
- suffix = '64'
+ suffix = '64'
# take into account that MPI_MODULE_NAME could be None (see Cray toolchains)
for root in self.get_software_root(self.MPI_MODULE_NAME or []):
diff --git a/easybuild/tools/toolchain/options.py b/easybuild/tools/toolchain/options.py
index 391a2831e0..cc0e515301 100644
--- a/easybuild/tools/toolchain/options.py
+++ b/easybuild/tools/toolchain/options.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -39,7 +39,6 @@
from easybuild.base import fancylogger
from easybuild.tools.build_log import EasyBuildError
-from easybuild.tools.py2vs3 import string_type
class ToolchainOptions(dict):
@@ -105,7 +104,7 @@ def option(self, name, templatedict=None):
'value': value,
})
- if isinstance(res, string_type):
+ if isinstance(res, str):
# allow for template
res = self.options_map[name] % templatedict
elif isinstance(res, (list, tuple,)):
diff --git a/easybuild/tools/toolchain/toolchain.py b/easybuild/tools/toolchain/toolchain.py
index f928215fd4..c13f9e10d7 100644
--- a/easybuild/tools/toolchain/toolchain.py
+++ b/easybuild/tools/toolchain/toolchain.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -69,17 +69,11 @@
from easybuild.tools.systemtools import LINUX, get_os_type
from easybuild.tools.toolchain.options import ToolchainOptions
from easybuild.tools.toolchain.toolchainvariables import ToolchainVariables
-from easybuild.tools.utilities import nub, trace_msg
+from easybuild.tools.utilities import nub, unique_ordered_extend, trace_msg
_log = fancylogger.getLogger('tools.toolchain', fname=False)
-# name/version for dummy toolchain
-# if name==DUMMY_TOOLCHAIN_NAME and version==DUMMY_TOOLCHAIN_VERSION, do not load dependencies
-# NOTE: use of 'dummy' toolchain is deprecated, replaced by 'system' toolchain (which always loads dependencies)
-DUMMY_TOOLCHAIN_NAME = 'dummy'
-DUMMY_TOOLCHAIN_VERSION = 'dummy'
-
SYSTEM_TOOLCHAIN_NAME = 'system'
CCACHE = 'ccache'
@@ -101,11 +95,27 @@
TOOLCHAIN_CAPABILITY_LAPACK_FAMILY,
TOOLCHAIN_CAPABILITY_MPI_FAMILY,
]
+# modes to handle header and linker search paths
+# see: https://gcc.gnu.org/onlinedocs/cpp/Environment-Variables.html
+# supported on Linux by: GCC, GFortran, oneAPI C/C++ Compilers, oneAPI Fortran Compiler, LLVM-based
+SEARCH_PATH = {
+ "cpp_headers": {
+ "flags": ["CPPFLAGS"],
+ "cpath": ["CPATH"],
+ "include_paths": ["C_INCLUDE_PATH", "CPLUS_INCLUDE_PATH", "OBJC_INCLUDE_PATH"],
+ },
+ "linker": {
+ "flags": ["LDFLAGS"],
+ "library_path": ["LIBRARY_PATH"],
+ },
+}
+DEFAULT_SEARCH_PATH_CPP_HEADERS = "flags"
+DEFAULT_SEARCH_PATH_LINKER = "flags"
def is_system_toolchain(tc_name):
"""Return whether toolchain with specified name is a system toolchain or not."""
- return tc_name in [DUMMY_TOOLCHAIN_NAME, SYSTEM_TOOLCHAIN_NAME]
+ return tc_name in [SYSTEM_TOOLCHAIN_NAME]
def env_vars_external_module(name, version, metadata):
@@ -197,10 +207,6 @@ def __init__(self, name=None, version=None, mns=None, class_constants=None, tcde
if name is None:
raise EasyBuildError("Toolchain init: no name provided")
self.name = name
- if self.name == DUMMY_TOOLCHAIN_NAME:
- self.log.deprecated("Use of 'dummy' toolchain is deprecated, use 'system' toolchain instead", '5.0',
- silent=build_option('silent'))
- self.name = SYSTEM_TOOLCHAIN_NAME
if version is None:
version = self.VERSION
@@ -224,6 +230,11 @@ def __init__(self, name=None, version=None, mns=None, class_constants=None, tcde
self.use_rpath = False
+ self.search_path = {
+ "cpp_headers": DEFAULT_SEARCH_PATH_CPP_HEADERS,
+ "linker": DEFAULT_SEARCH_PATH_LINKER,
+ }
+
self.mns = mns
self.mod_full_name = None
self.mod_short_name = None
@@ -237,7 +248,7 @@ def __init__(self, name=None, version=None, mns=None, class_constants=None, tcde
self.init_modpaths = self.mns.det_init_modulepaths(tc_dict)
def is_system_toolchain(self):
- """Return boolean to indicate whether this toolchain is a system(/dummy) toolchain."""
+ """Return boolean to indicate whether this toolchain is a system toolchain."""
return is_system_toolchain(self.name)
def set_minimal_build_env(self):
@@ -371,9 +382,11 @@ def get_variable(self, name, typ=str):
return res
def set_variables(self):
- """Do nothing? Everything should have been set by others
- Needs to be defined for super() relations
"""
+ No generic toolchain variables set.
+ Post-process variables set by child Toolchain classes.
+ """
+
if self.options.option('packed-linker-options'):
self.log.devel("set_variables: toolchain variables. packed-linker-options.")
self.variables.try_function_on_element('set_packed_linker_options')
@@ -564,16 +577,6 @@ def _check_dependencies(self, dependencies):
return deps
- def add_dependencies(self, dependencies):
- """
- [DEPRECATED] Verify if the given dependencies exist, and return them.
-
- This method is deprecated.
- You should pass the dependencies to the 'prepare' method instead, via the 'deps' named argument.
- """
- self.log.deprecated("use of 'Toolchain.add_dependencies' method", '4.0')
- self.dependencies = self._check_dependencies(dependencies)
-
def is_required(self, name):
"""Determine whether this is a required toolchain element."""
# default: assume every element is required
@@ -608,8 +611,8 @@ def _simulated_load_dependency_module(self, name, version, metadata, verbose=Fal
self.log.debug("Defining $EB* environment variables for software named %s", name)
env_vars = env_vars_external_module(name, version, metadata)
- for key in env_vars:
- setvar(key, env_vars[key], verbose=verbose)
+ for var, value in env_vars.items():
+ setvar(var, value, verbose=verbose)
def _load_toolchain_module(self, silent=False):
"""Load toolchain module."""
@@ -768,6 +771,27 @@ def _verify_toolchain(self):
raise EasyBuildError("List of toolchain dependency modules and toolchain definition do not match "
"(found %s vs expected %s)", self.toolchain_dep_mods, toolchain_definition)
+ def _validate_search_path(self):
+ """
+ Validate search path toolchain options.
+ Toolchain option has precedence over build option
+ """
+ for search_path in self.search_path:
+ sp_build_opt = f"search_path_{search_path}"
+ sp_toolchain_opt = sp_build_opt.replace("_", "-")
+ if self.options.get(sp_toolchain_opt) is not None:
+ self.search_path[search_path] = self.options.option(sp_toolchain_opt)
+ elif build_option(sp_build_opt) is not None:
+ self.search_path[search_path] = build_option(sp_build_opt)
+
+ if self.search_path[search_path] not in SEARCH_PATH[search_path]:
+ raise EasyBuildError(
+ "Unknown value selected for toolchain option %s: %s. Choose one of: %s",
+ sp_toolchain_opt, self.search_path[search_path], ", ".join(SEARCH_PATH[search_path])
+ )
+
+ self.log.debug("%s toolchain option set to: %s", sp_toolchain_opt, self.search_path[search_path])
+
def symlink_commands(self, paths):
"""
Create a symlink for each command to binary/script at specified path.
@@ -847,7 +871,6 @@ def prepare(self, onlymod=None, deps=None, silent=False, loadmod=True,
self._load_modules(silent=silent)
if self.is_system_toolchain():
-
# define minimal build environment when using system toolchain;
# this is mostly done to try controlling which compiler commands are being used,
# cfr. https://github.com/easybuilders/easybuild-framework/issues/3398
@@ -860,6 +883,7 @@ def prepare(self, onlymod=None, deps=None, silent=False, loadmod=True,
self._verify_toolchain()
# Generate the variables to be set
+ self._validate_search_path()
self.set_variables()
# set the variables
@@ -870,7 +894,7 @@ def prepare(self, onlymod=None, deps=None, silent=False, loadmod=True,
else:
self.log.debug("prepare: set additional variables onlymod=%s", onlymod)
- # add LDFLAGS and CPPFLAGS from dependencies to self.vars
+ # add linker and preprocessor paths of dependencies to self.vars
self._add_dependency_variables()
self.generate_vars()
self._setenv_variables(onlymod, verbose=not silent)
@@ -1048,7 +1072,7 @@ def prepare_rpath_wrappers(self, rpath_filter_dirs=None, rpath_include_dirs=None
def handle_sysroot(self):
"""
- Extra stuff to be done when alternate system root is specified via --sysroot EasyBuild configuration option.
+ Extra stuff to be done when alternative system root is specified via --sysroot EasyBuild configuration option.
* Update $PKG_CONFIG_PATH to include sysroot location to pkg-config files (*.pc).
"""
@@ -1069,41 +1093,54 @@ def handle_sysroot(self):
setvar('PKG_CONFIG_PATH', os.pathsep.join(pkg_config_path))
def _add_dependency_variables(self, names=None, cpp=None, ld=None):
- """ Add LDFLAGS and CPPFLAGS to the self.variables based on the dependencies
- names should be a list of strings containing the name of the dependency
"""
- cpp_paths = ['include']
- ld_paths = ['lib']
- if not self.options.get('32bit', None):
- ld_paths.insert(0, 'lib64')
-
- if cpp is not None:
- for p in cpp:
- if p not in cpp_paths:
- cpp_paths.append(p)
- if ld is not None:
- for p in ld:
- if p not in ld_paths:
- ld_paths.append(p)
-
- if not names:
- deps = self.dependencies
- else:
- deps = [{'name': name} for name in names if name is not None]
+ Add linker and preprocessor paths of dependencies to self.variables
+ :names: list of strings containing the name of the dependency
+ """
+ # collect dependencies
+ dependencies = self.dependencies if names is None else [{"name": name} for name in names if name]
# collect software install prefixes for dependencies
- roots = []
- for dep in deps:
- if dep.get('external_module', False):
+ dependency_roots = []
+ for dep in dependencies:
+ if dep.get("external_module", False):
# for software names provided via external modules, install prefix may be unknown
- names = dep['external_module_metadata'].get('name', [])
- roots.extend([root for root in self.get_software_root(names) if root is not None])
+ names = dep["external_module_metadata"].get("name", [])
+ dependency_roots.extend([root for root in self.get_software_root(names) if root is not None])
else:
- roots.extend(self.get_software_root(dep['name']))
+ dependency_roots.extend(self.get_software_root(dep["name"]))
+
+ for root in dependency_roots:
+ self._add_dependency_cpp_headers(root, extra_dirs=cpp)
+ self._add_dependency_linker_paths(root, extra_dirs=ld)
+
+ def _add_dependency_cpp_headers(self, dep_root, extra_dirs=None):
+ """
+ Append prepocessor paths for given dependency root directory
+ """
+ if extra_dirs is None:
+ extra_dirs = ()
+
+ header_dirs = ["include"]
+ header_dirs = unique_ordered_extend(header_dirs, extra_dirs)
+
+ for env_var in SEARCH_PATH["cpp_headers"][self.search_path["cpp_headers"]]:
+ self.log.debug("Adding header paths to toolchain variable '%s': %s", env_var, dep_root)
+ self.variables.append_subdirs(env_var, dep_root, subdirs=header_dirs)
+
+ def _add_dependency_linker_paths(self, dep_root, extra_dirs=None):
+ """
+ Append linker paths for given dependency root directory
+ """
+ if extra_dirs is None:
+ extra_dirs = ()
+
+ lib_dirs = ["lib64", "lib"]
+ lib_dirs = unique_ordered_extend(lib_dirs, extra_dirs)
- for root in roots:
- self.variables.append_subdirs("CPPFLAGS", root, subdirs=cpp_paths)
- self.variables.append_subdirs("LDFLAGS", root, subdirs=ld_paths)
+ for env_var in SEARCH_PATH["linker"][self.search_path["linker"]]:
+ self.log.debug("Adding lib paths to toolchain variable '%s': %s", env_var, dep_root)
+ self.variables.append_subdirs(env_var, dep_root, subdirs=lib_dirs)
def _setenv_variables(self, donotset=None, verbose=True):
"""Actually set the environment variables"""
@@ -1136,9 +1173,9 @@ def _setenv_variables(self, donotset=None, verbose=True):
def get_flag(self, name):
"""Get compiler flag(s) for a certain option."""
if isinstance(self.options.option(name), list):
- return " ".join("-%s" % x for x in list(self.options.option(name)))
+ return " ".join(self.options.option(name))
else:
- return "-%s" % self.options.option(name)
+ return self.options.option(name)
def toolchain_family(self):
"""Return toolchain family for this toolchain."""
diff --git a/easybuild/tools/toolchain/toolchainvariables.py b/easybuild/tools/toolchain/toolchainvariables.py
index dbb5686040..a5a78ab9fd 100644
--- a/easybuild/tools/toolchain/toolchainvariables.py
+++ b/easybuild/tools/toolchain/toolchainvariables.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -33,7 +33,8 @@
from easybuild.tools.variables import Variables, join_map_class
from easybuild.tools.toolchain.constants import ALL_MAP_CLASSES
-from easybuild.tools.toolchain.variables import LinkerFlagList, FlagList
+from easybuild.tools.toolchain.variables import LinkerFlagList
+from easybuild.tools.variables import StrList
class ToolchainVariables(Variables):
@@ -42,7 +43,7 @@ class ToolchainVariables(Variables):
in context of compilers (i.e. the generated string are e.g. compiler options or link flags)
"""
MAP_CLASS = join_map_class(ALL_MAP_CLASSES) # join_map_class strips explanation
- DEFAULT_CLASS = FlagList
+ DEFAULT_CLASS = StrList
LINKER_TOGGLE_START_STOP_GROUP = None
LINKER_TOGGLE_STATIC_DYNAMIC = None
diff --git a/easybuild/tools/toolchain/utilities.py b/easybuild/tools/toolchain/utilities.py
index 8047778391..90a0c99583 100644
--- a/easybuild/tools/toolchain/utilities.py
+++ b/easybuild/tools/toolchain/utilities.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/tools/toolchain/variables.py b/easybuild/tools/toolchain/variables.py
index 482f989480..16487de7ff 100644
--- a/easybuild/tools/toolchain/variables.py
+++ b/easybuild/tools/toolchain/variables.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -46,6 +46,11 @@ class LinkLibraryPaths(AbsPathList):
PREFIX = '-L'
+class SearchPaths(AbsPathList):
+ """Colon-separated list of absolute paths"""
+ SEPARATOR = ':'
+
+
class FlagList(StrList):
"""Flag list"""
PREFIX = "-"
diff --git a/easybuild/tools/utilities.py b/easybuild/tools/utilities.py
index 8f6407e55d..ae570f3dc0 100644
--- a/easybuild/tools/utilities.py
+++ b/easybuild/tools/utilities.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -34,12 +34,11 @@
import os
import re
import sys
-from string import digits
+from string import ascii_letters, digits
from easybuild.base import fancylogger
from easybuild.tools.build_log import EasyBuildError, print_msg
from easybuild.tools.config import build_option
-from easybuild.tools.py2vs3 import ascii_letters, string_type
_log = fancylogger.getLogger('tools.utilities')
@@ -72,7 +71,7 @@ def quote_str(val, escape_newline=False, prefer_single_quotes=False, escape_back
:param tcl: Boolean for whether we are quoting for Tcl syntax
"""
- if isinstance(val, string_type):
+ if isinstance(val, str):
# escape backslashes
if escape_backslash:
val = val.replace('\\', '\\\\')
@@ -164,7 +163,7 @@ def only_if_module_is_available(modnames, pkgname=None, url=None):
if pkgname and url is None:
url = 'https://pypi.python.org/pypi/%s' % pkgname
- if isinstance(modnames, string_type):
+ if isinstance(modnames, str):
modnames = (modnames,)
def wrap(orig):
@@ -205,23 +204,35 @@ def trace_msg(message, silent=False):
def nub(list_):
- """Returns the unique items of a list of hashables, while preserving order of
- the original list, i.e. the first unique element encoutered is
- retained.
+ """Returns the unique items of a list of hashables, while preserving order of the original list,
+ i.e. the first unique element encoutered is retained.
- Code is taken from
- http://stackoverflow.com/questions/480214/how-do-you-remove-duplicates-from-a-list-in-python-whilst-preserving-order
+ Code is taken from https://www.peterbe.com/plog/fastest-way-to-uniquify-a-list-in-python-3.6
- Supposedly, this is one of the fastest ways to determine the
- unique elements of a list.
+ Supposedly, this is one of the fastest ways to determine the unique elements of a list.
@type list_: a list :-)
:return: a new list with each element from `list` appearing only once (cfr. Michelle Dubois).
"""
- seen = set()
- seen_add = seen.add
- return [x for x in list_ if x not in seen and not seen_add(x)]
+ return list(dict.fromkeys(list_))
+
+
+def unique_ordered_extend(base, affix):
+ """Extend base list with elements of affix list keeping order and without duplicates"""
+ if isinstance(affix, str):
+ # avoid extending with strings, as iterables generate wrong result without error
+ raise EasyBuildError(f"given affix list is a string: {affix}")
+
+ try:
+ ext_base = base.copy()
+ ext_base.extend(affix)
+ except TypeError as err:
+ raise EasyBuildError(f"given affix list is not iterable: {affix}") from err
+ except AttributeError as err:
+ raise EasyBuildError(f"given base cannot be extended: {base}") from err
+
+ return nub(ext_base) # remove duplicates
def get_class_for(modulepath, class_name):
diff --git a/easybuild/tools/variables.py b/easybuild/tools/variables.py
index cef771f3d8..423f7adb36 100644
--- a/easybuild/tools/variables.py
+++ b/easybuild/tools/variables.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/easybuild/tools/version.py b/easybuild/tools/version.py
index 681233536b..7b1716a497 100644
--- a/easybuild/tools/version.py
+++ b/easybuild/tools/version.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -45,7 +45,7 @@
# recent setuptools versions will *TRANSFORM* something like 'X.Y.Zdev' into 'X.Y.Z.dev0', with a warning like
# UserWarning: Normalizing '2.4.0dev' to '2.4.0.dev0'
# This causes problems further up the dependency chain...
-VERSION = LooseVersion('4.9.4')
+VERSION = LooseVersion('5.0.0')
UNKNOWN = 'UNKNOWN'
UNKNOWN_EASYBLOCKS_VERSION = '0.0.UNKNOWN.EASYBLOCKS'
@@ -93,8 +93,7 @@ def get_git_revision():
def this_is_easybuild():
"""Standard starting message"""
- top_version = max(FRAMEWORK_VERSION, EASYBLOCKS_VERSION)
- # !!! bootstrap_eb.py script checks hard on the string below, so adjust with sufficient care !!!
+ top_version = max(FRAMEWORK_VERSION, LooseVersion(EASYBLOCKS_VERSION))
msg = "This is EasyBuild %s (framework: %s, easyblocks: %s) on host %s."
msg = msg % (top_version, FRAMEWORK_VERSION, EASYBLOCKS_VERSION, gethostname())
diff --git a/eb b/eb
index 0a510c70ad..132cf92ecf 100755
--- a/eb
+++ b/eb
@@ -1,6 +1,6 @@
-#!/bin/bash
+#!/usr/bin/env bash
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -32,6 +32,7 @@
# @author: Kenneth Hoste (Ghent University)
# @author: Pieter De Baets (Ghent University)
# @author: Jens Timmerman (Ghent University)
+# @author: Simon Branford (University of Birmingham)
keyboard_interrupt() {
echo "Keyboard interrupt!"
@@ -40,9 +41,8 @@ keyboard_interrupt() {
trap keyboard_interrupt SIGINT
-# Python 2.7+ or 3.5+ required
-REQ_MIN_PY2VER=7
-REQ_MIN_PY3VER=5
+# Python 3.6+ required
+REQ_MIN_PY3VER=6
EASYBUILD_MAIN='easybuild.main'
@@ -60,7 +60,7 @@ PYTHON=
# - EB_INSTALLPYTHON is set when EasyBuild is installed as a module (by EasyBuild). It is set to the PYTHON
# used during that installation (for example, you could override PYTHON using EB_PYTHON at installation
# time, this variable preserves that choice).
-for python_cmd in "${EB_PYTHON}" "${EB_INSTALLPYTHON}" 'python' 'python3' 'python2'; do
+for python_cmd in "${EB_PYTHON}" "${EB_INSTALLPYTHON}" 'python3' 'python'; do
# Only consider non-empty values, i.e. continue if e.g. $EB_PYTHON is not set
[ -n "${python_cmd}" ] || continue
@@ -76,10 +76,7 @@ for python_cmd in "${EB_PYTHON}" "${EB_INSTALLPYTHON}" 'python' 'python3' 'pytho
pyver_maj=$(echo "${pyver}" | cut -f1 -d'.')
pyver_min=$(echo "${pyver}" | cut -f2 -d'.')
- if [ "${pyver_maj}" -eq 2 ] && [ "${pyver_min}" -ge "${REQ_MIN_PY2VER}" ]; then
- verbose "'${python_cmd}' version: ${pyver}, which matches Python 2 version requirement (>= 2.${REQ_MIN_PY2VER})"
- PYTHON="${python_cmd}"
- elif [ "${pyver_maj}" -eq 3 ] && [ "${pyver_min}" -ge "${REQ_MIN_PY3VER}" ]; then
+ if [ "${pyver_maj}" -eq 3 ] && [ "${pyver_min}" -ge "${REQ_MIN_PY3VER}" ]; then
verbose "'${python_cmd}' version: ${pyver}, which matches Python 3 version requirement (>= 3.${REQ_MIN_PY3VER})"
PYTHON="${python_cmd}"
fi
@@ -106,7 +103,7 @@ done
if [ -z "${PYTHON}" ]; then
echo -n "ERROR: No compatible 'python' command found via \$PATH " >&2
- echo "(EasyBuild requires Python 2.${REQ_MIN_PY2VER}+ or 3.${REQ_MIN_PY3VER}+)" >&2
+ echo "(EasyBuild requires Python 3.${REQ_MIN_PY3VER}+)" >&2
exit 1
else
verbose "Selected Python command: ${python_cmd} ($(command -v "${python_cmd}"))"
diff --git a/requirements.txt b/requirements.txt
index d2ce2ccdc1..699cc90372 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,20 +1,10 @@
# keyring is required to provide GitHub token to EasyBuild;
# for recent versions of keyring, keyrings.alt must be installed too
-# 19.0 dropped Python 2 support
-keyring<19.0; python_version < '3.0'
-keyring; python_version >= '3.0'
+keyring
keyrings.alt
-# GitPython 3.1.15 deprecates Python 3.5
-GitPython<3.1.15; python_version >= '3.0' and python_version < '3.6'
-GitPython; python_version >= '3.6' or python_version <= '3.0'
-
-# autopep8
-# stick to older autopep8 with Python 2.7, since autopep8 1.7.0 requires pycodestyle>=2.9.1 (which is Python 3 only)
-autopep8<1.7.0; python_version < '3.0'
-autopep8; python_version >= '3.0'
-
-# PyYAML
+GitPython
+autopep8
PyYAML
# optional Python packages for EasyBuild
@@ -22,18 +12,11 @@ PyYAML
# flake8 is a superset of pycodestyle
flake8
-# 2.6.7 uses invalid Python 2 syntax
-GC3Pie!=2.6.7; python_version < '3.0'
-GC3Pie; python_version >= '3.0' and python_version < '3.11'
+GC3Pie; python_version < '3.11'
python-graph-dot
python-hglib
requests
archspec
-# cryptography 3.4.0 no longer supports Python 2.7
-cryptography==3.3.2; python_version == '2.7'
-cryptography; python_version >= '3.5' and python_version < '3.11'
-
-# rich is only supported for Python 3.6+
-rich; python_version >= '3.6'
+rich
diff --git a/setup.py b/setup.py
index 82bae3aa2f..c305735e13 100644
--- a/setup.py
+++ b/setup.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -24,16 +24,17 @@
##
"""
This script can be used to install easybuild-framework, e.g. using:
- easy_install --user .
-or
- python setup.py --prefix=$HOME/easybuild
+ python setup.py install --prefix=$HOME/easybuild
@author: Kenneth Hoste (Ghent University)
"""
import glob
import os
-from distutils import log
-from distutils.core import setup
+import logging
+try:
+ from distutils.core import setup
+except ImportError:
+ from setuptools import setup
from easybuild.tools.version import VERSION
@@ -45,8 +46,10 @@ def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
-# log levels: 0 = WARN (default), 1 = INFO, 2 = DEBUG
-log.set_verbosity(1)
+log = logging.getLogger("EasyBuild")
+
+# log levels: NOTSET (default), DEBUG, INFO, WARNING, ERROR, CRITICAL
+log.setLevel(logging.INFO)
log.info("Installing version %s (API version %s)" % (VERSION, API_VERSION))
@@ -96,7 +99,6 @@ def find_rel_test():
'eb_bash_completion.bash',
'eb_bash_completion_local.bash',
# utility scripts
- 'easybuild/scripts/bootstrap_eb.py',
'easybuild/scripts/install_eb_dep.sh',
],
data_files=[
@@ -111,14 +113,14 @@ def find_rel_test():
"Intended Audience :: System Administrators",
"License :: OSI Approved :: GNU General Public License v2 (GPLv2)",
"Operating System :: POSIX :: Linux",
- "Programming Language :: Python :: 2.7",
- "Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
"Topic :: Software Development :: Build Tools",
],
platforms="Linux",
diff --git a/test/__init__.py b/test/__init__.py
index 20c9130dcc..939c300f79 100644
--- a/test/__init__.py
+++ b/test/__init__.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/__init__.py b/test/framework/__init__.py
index fae73ca9e6..df6adea40d 100644
--- a/test/framework/__init__.py
+++ b/test/framework/__init__.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/asyncprocess.py b/test/framework/asyncprocess.py
index c7eced6c48..09045e0e01 100644
--- a/test/framework/asyncprocess.py
+++ b/test/framework/asyncprocess.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -35,7 +35,7 @@
import easybuild.tools.asyncprocess as p
from easybuild.tools.asyncprocess import Popen
-from easybuild.tools.py2vs3 import subprocess_terminate
+from easybuild.tools.run import subprocess_terminate
class AsyncProcessTest(EnhancedTestCase):
diff --git a/test/framework/build_log.py b/test/framework/build_log.py
index 15f6984099..4b29a2af26 100644
--- a/test/framework/build_log.py
+++ b/test/framework/build_log.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2015-2024 Ghent University
+# Copyright 2015-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -116,11 +116,12 @@ def test_easybuildlog(self):
stderr = self.get_stderr()
self.mock_stderr(False)
- more_info = "see http://easybuild.readthedocs.org/en/latest/Deprecated-functionality.html for more information"
+ more_info = "see https://docs.easybuild.io/deprecated-functionality/ for more information"
+ common_warning = "\nWARNING: Deprecated functionality, will no longer work in"
expected_stderr = '\n\n'.join([
- "\nWARNING: Deprecated functionality, will no longer work in v10000001: anotherwarning; " + more_info,
- "\nWARNING: Deprecated functionality, will no longer work in v2.0: onemorewarning",
- "\nWARNING: Deprecated functionality, will no longer work in v2.0: lastwarning",
+ common_warning + " EasyBuild v10000001: anotherwarning; " + more_info,
+ common_warning + " EasyBuild v2.0: onemorewarning",
+ common_warning + " EasyBuild v2.0: lastwarning",
]) + '\n\n'
self.assertEqual(stderr, expected_stderr)
@@ -139,8 +140,8 @@ def test_easybuildlog(self):
r"fancyroot.test_easybuildlog \[WARNING\] :: Deprecated functionality.*onemorewarning.*",
r"fancyroot.test_easybuildlog \[WARNING\] :: Deprecated functionality.*lastwarning.*",
r"fancyroot.test_easybuildlog \[WARNING\] :: Deprecated functionality.*thisisnotprinted.*",
- r"fancyroot.test_easybuildlog \[ERROR\] :: EasyBuild crashed with an error \(at .* in .*\): kaput",
- r"fancyroot.test_easybuildlog \[ERROR\] :: EasyBuild crashed with an error \(at .* in .*\): err: msg: %s",
+ r"fancyroot.test_easybuildlog \[ERROR\] :: EasyBuild encountered an error \(at .* in .*\): kaput",
+ r"fancyroot.test_easybuildlog \[ERROR\] :: EasyBuild encountered an error \(at .* in .*\): err: msg: %s",
r"fancyroot.test_easybuildlog \[ERROR\] :: .*EasyBuild encountered an exception \(at .* in .*\): oops",
'',
])
@@ -168,7 +169,7 @@ def test_easybuildlog(self):
r"fancyroot.test_easybuildlog \[WARNING\] :: bleh",
r"fancyroot.test_easybuildlog \[INFO\] :: 4\+2 = 42",
r"fancyroot.test_easybuildlog \[DEBUG\] :: this is just a test",
- r"fancyroot.test_easybuildlog \[ERROR\] :: EasyBuild crashed with an error \(at .* in .*\): foo baz baz",
+ r"fancyroot.test_easybuildlog \[ERROR\] :: EasyBuild encountered an error \(at .* in .*\): foo baz baz",
'',
])
logtxt_regex = re.compile(r'^%s' % expected_logtxt, re.M)
@@ -183,7 +184,7 @@ def test_easybuildlog(self):
self.mock_stderr(False)
logtxt = read_file(tmplog)
expected_logtxt = '\n'.join([
- "[WARNING] :: Deprecated functionality, will no longer work in v10000001: ",
+ "[WARNING] :: Deprecated functionality, will no longer work in EasyBuild v10000001: ",
"this is just a test",
"(see URLGOESHERE for more information)",
])
@@ -208,7 +209,7 @@ def test_log_levels(self):
log.error('kaput')
log.deprecated('almost kaput', '10000000000000')
log.raiseError = True
- log.warn('this is a warning')
+ log.warning('this is a warning')
log.info('fyi')
log.debug('gdb')
log.devel('tmi')
@@ -223,7 +224,7 @@ def test_log_levels(self):
info_msg = r"%s \[INFO\] :: fyi" % prefix
warning_msg = r"%s \[WARNING\] :: this is a warning" % prefix
deprecated_msg = r"%s \[WARNING\] :: Deprecated functionality, .*: almost kaput; see .*" % prefix
- error_msg = r"%s \[ERROR\] :: EasyBuild crashed with an error \(at .* in .*\): kaput" % prefix
+ error_msg = r"%s \[ERROR\] :: EasyBuild encountered an error \(at .* in .*\): kaput" % prefix
expected_logtxt = '\n'.join([
error_msg,
diff --git a/test/framework/config.py b/test/framework/config.py
index 55234d31b9..9614d4fb31 100644
--- a/test/framework/config.py
+++ b/test/framework/config.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -33,20 +33,19 @@
import shutil
import sys
import tempfile
+from importlib import reload
from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered, init_config
from unittest import TextTestRunner
import easybuild.tools.options as eboptions
-from easybuild.tools import run
from easybuild.tools.build_log import EasyBuildError
+from easybuild.tools.config import ERROR, IGNORE, WARN, BuildOptions, ConfigurationVariables
from easybuild.tools.config import build_option, build_path, get_build_log_path, get_log_filename, get_repositorypath
from easybuild.tools.config import install_path, log_file_format, log_path, source_paths
from easybuild.tools.config import update_build_option, update_build_options
-from easybuild.tools.config import BuildOptions, ConfigurationVariables
from easybuild.tools.config import DEFAULT_PATH_SUBDIRS, init_build_options
from easybuild.tools.filetools import copy_dir, mkdir, write_file
from easybuild.tools.options import CONFIG_ENV_VAR_PREFIX
-from easybuild.tools.py2vs3 import reload
class EasyBuildConfigTest(EnhancedTestCase):
@@ -449,7 +448,7 @@ def test_XDG_CONFIG_env_vars(self):
# $XDG_CONFIG_HOME not set, multiple directories listed in $XDG_CONFIG_DIRS
del os.environ['XDG_CONFIG_HOME'] # unset, so should become default
- os.environ['XDG_CONFIG_DIRS'] = os.pathsep.join([dir1, dir2, dir3])
+ os.environ['XDG_CONFIG_DIRS'] = os.pathsep.join([dir3, dir2, dir1])
cfg_files = [
os.path.join(dir1, 'easybuild.d', 'bar.cfg'),
os.path.join(dir1, 'easybuild.d', 'foo.cfg'),
@@ -580,9 +579,9 @@ def test_flex_robot_paths(self):
def test_strict(self):
"""Test use of --strict."""
# check default
- self.assertEqual(build_option('strict'), run.WARN)
+ self.assertEqual(build_option('strict'), WARN)
- for strict_str, strict_val in [('error', run.ERROR), ('ignore', run.IGNORE), ('warn', run.WARN)]:
+ for strict_str, strict_val in [('error', ERROR), ('ignore', IGNORE), ('warn', WARN)]:
options = init_config(args=['--strict=%s' % strict_str])
init_config(build_options={'strict': options.strict})
self.assertEqual(build_option('strict'), strict_val)
diff --git a/test/framework/containers.py b/test/framework/containers.py
index 1d9abbf8e4..321df783d2 100644
--- a/test/framework/containers.py
+++ b/test/framework/containers.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2018-2024 Ghent University
+# Copyright 2018-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/docs.py b/test/framework/docs.py
index 6b5b68ad17..4b281df14a 100644
--- a/test/framework/docs.py
+++ b/test/framework/docs.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -25,7 +25,6 @@
"""
Unit tests for docs.py.
"""
-import inspect
import os
import re
import sys
@@ -38,106 +37,197 @@
from easybuild.tools.docs import list_easyblocks, list_software, list_toolchains
from easybuild.tools.docs import md_title_and_table, rst_title_and_table
from easybuild.tools.options import EasyBuildOptions
-from easybuild.tools.utilities import import_available_modules, mk_md_table, mk_rst_table
+from easybuild.tools.utilities import mk_md_table, mk_rst_table
from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered, init_config
LIST_EASYBLOCKS_SIMPLE_TXT = """EasyBlock
|-- bar
+|-- Bundle
+|-- CMakeMake
+|-- CmdCp
|-- ConfigureMake
| |-- MakeCp
+|-- EB_binutils
+|-- EB_bzip2
+|-- EB_CMake
|-- EB_EasyBuildMeta
|-- EB_FFTW
+|-- EB_FFTW_period_MPI
+|-- EB_flex
|-- EB_foo
| |-- EB_foofoo
+|-- EB_freetype
|-- EB_GCC
|-- EB_HPL
|-- EB_libtoy
+|-- EB_libxml2
+|-- EB_LLVM
+|-- EB_Mesa
|-- EB_OpenBLAS
|-- EB_OpenMPI
+|-- EB_OpenSSL_wrapper
+|-- EB_Perl
+|-- EB_Python
|-- EB_ScaLAPACK
|-- EB_toy_buggy
+|-- EB_XCrySDen
|-- ExtensionEasyBlock
| |-- DummyExtension
+| | |-- CustomDummyExtension
+| | | |-- ChildCustomDummyExtension
+| | |-- DeprecatedDummyExtension
+| | | |-- ChildDeprecatedDummyExtension
| |-- EB_toy
+| | |-- EB_toy_deprecated
| | |-- EB_toy_eula
| | |-- EB_toytoy
| |-- Toy_Extension
+|-- MesonNinja
|-- ModuleRC
+|-- PerlBundle
|-- PythonBundle
+|-- PythonPackage
+|-- Tarball
|-- Toolchain
Extension
|-- ExtensionEasyBlock
| |-- DummyExtension
+| | |-- CustomDummyExtension
+| | | |-- ChildCustomDummyExtension
+| | |-- DeprecatedDummyExtension
+| | | |-- ChildDeprecatedDummyExtension
| |-- EB_toy
+| | |-- EB_toy_deprecated
| | |-- EB_toy_eula
| | |-- EB_toytoy
-| |-- Toy_Extension"""
+| |-- Toy_Extension""" # noqa
LIST_EASYBLOCKS_DETAILED_TXT = """EasyBlock (easybuild.framework.easyblock)
|-- bar (easybuild.easyblocks.generic.bar @ %(topdir)s/generic/bar.py)
+|-- Bundle (easybuild.easyblocks.generic.bundle @ %(topdir)s/generic/bundle.py)
+|-- CMakeMake (easybuild.easyblocks.generic.cmakemake @ %(topdir)s/generic/cmakemake.py)
+|-- CmdCp (easybuild.easyblocks.generic.cmdcp @ %(topdir)s/generic/cmdcp.py)
|-- ConfigureMake (easybuild.easyblocks.generic.configuremake @ %(topdir)s/generic/configuremake.py)
| |-- MakeCp (easybuild.easyblocks.generic.makecp @ %(topdir)s/generic/makecp.py)
+|-- EB_binutils (easybuild.easyblocks.binutils @ %(topdir)s/b/binutils.py)
+|-- EB_bzip2 (easybuild.easyblocks.bzip2 @ %(topdir)s/b/bzip2.py)
+|-- EB_CMake (easybuild.easyblocks.cmake @ %(topdir)s/c/cmake.py)
|-- EB_EasyBuildMeta (easybuild.easyblocks.easybuildmeta @ %(topdir)s/e/easybuildmeta.py)
|-- EB_FFTW (easybuild.easyblocks.fftw @ %(topdir)s/f/fftw.py)
+|-- EB_FFTW_period_MPI (easybuild.easyblocks.fftwmpi @ %(topdir)s/f/fftwmpi.py)
+|-- EB_flex (easybuild.easyblocks.flex @ %(topdir)s/f/flex.py)
|-- EB_foo (easybuild.easyblocks.foo @ %(topdir)s/f/foo.py)
| |-- EB_foofoo (easybuild.easyblocks.foofoo @ %(topdir)s/f/foofoo.py)
+|-- EB_freetype (easybuild.easyblocks.freetype @ %(topdir)s/f/freetype.py)
|-- EB_GCC (easybuild.easyblocks.gcc @ %(topdir)s/g/gcc.py)
|-- EB_HPL (easybuild.easyblocks.hpl @ %(topdir)s/h/hpl.py)
|-- EB_libtoy (easybuild.easyblocks.libtoy @ %(topdir)s/l/libtoy.py)
+|-- EB_libxml2 (easybuild.easyblocks.libxml2 @ %(topdir)s/l/libxml2.py)
+|-- EB_LLVM (easybuild.easyblocks.llvm @ %(topdir)s/l/llvm.py)
+|-- EB_Mesa (easybuild.easyblocks.mesa @ %(topdir)s/m/mesa.py)
|-- EB_OpenBLAS (easybuild.easyblocks.openblas @ %(topdir)s/o/openblas.py)
|-- EB_OpenMPI (easybuild.easyblocks.openmpi @ %(topdir)s/o/openmpi.py)
+|-- EB_OpenSSL_wrapper (easybuild.easyblocks.openssl_wrapper @ %(topdir)s/o/openssl_wrapper.py)
+|-- EB_Perl (easybuild.easyblocks.perl @ %(topdir)s/p/perl.py)
+|-- EB_Python (easybuild.easyblocks.python @ %(topdir)s/p/python.py)
|-- EB_ScaLAPACK (easybuild.easyblocks.scalapack @ %(topdir)s/s/scalapack.py)
|-- EB_toy_buggy (easybuild.easyblocks.toy_buggy @ %(topdir)s/t/toy_buggy.py)
+|-- EB_XCrySDen (easybuild.easyblocks.xcrysden @ %(topdir)s/x/xcrysden.py)
|-- ExtensionEasyBlock (easybuild.framework.extensioneasyblock )
| |-- DummyExtension (easybuild.easyblocks.generic.dummyextension @ %(topdir)s/generic/dummyextension.py)
+| | |-- CustomDummyExtension (easybuild.easyblocks.generic.customdummyextension @ %(topdir)s/generic/customdummyextension.py)
+| | | |-- ChildCustomDummyExtension (easybuild.easyblocks.generic.childcustomdummyextension @ %(topdir)s/generic/childcustomdummyextension.py)
+| | |-- DeprecatedDummyExtension (easybuild.easyblocks.generic.deprecateddummyextension @ %(topdir)s/generic/deprecateddummyextension.py)
+| | | |-- ChildDeprecatedDummyExtension (easybuild.easyblocks.generic.childdeprecateddummyextension @ %(topdir)s/generic/childdeprecateddummyextension.py)
| |-- EB_toy (easybuild.easyblocks.toy @ %(topdir)s/t/toy.py)
+| | |-- EB_toy_deprecated (easybuild.easyblocks.toy_deprecated @ %(topdir)s/t/toy_deprecated.py)
| | |-- EB_toy_eula (easybuild.easyblocks.toy_eula @ %(topdir)s/t/toy_eula.py)
| | |-- EB_toytoy (easybuild.easyblocks.toytoy @ %(topdir)s/t/toytoy.py)
| |-- Toy_Extension (easybuild.easyblocks.generic.toy_extension @ %(topdir)s/generic/toy_extension.py)
+|-- MesonNinja (easybuild.easyblocks.generic.mesonninja @ %(topdir)s/generic/mesonninja.py)
|-- ModuleRC (easybuild.easyblocks.generic.modulerc @ %(topdir)s/generic/modulerc.py)
+|-- PerlBundle (easybuild.easyblocks.generic.perlbundle @ %(topdir)s/generic/perlbundle.py)
|-- PythonBundle (easybuild.easyblocks.generic.pythonbundle @ %(topdir)s/generic/pythonbundle.py)
+|-- PythonPackage (easybuild.easyblocks.generic.pythonpackage @ %(topdir)s/generic/pythonpackage.py)
+|-- Tarball (easybuild.easyblocks.generic.tarball @ %(topdir)s/generic/tarball.py)
|-- Toolchain (easybuild.easyblocks.generic.toolchain @ %(topdir)s/generic/toolchain.py)
Extension (easybuild.framework.extension)
|-- ExtensionEasyBlock (easybuild.framework.extensioneasyblock )
| |-- DummyExtension (easybuild.easyblocks.generic.dummyextension @ %(topdir)s/generic/dummyextension.py)
+| | |-- CustomDummyExtension (easybuild.easyblocks.generic.customdummyextension @ %(topdir)s/generic/customdummyextension.py)
+| | | |-- ChildCustomDummyExtension (easybuild.easyblocks.generic.childcustomdummyextension @ %(topdir)s/generic/childcustomdummyextension.py)
+| | |-- DeprecatedDummyExtension (easybuild.easyblocks.generic.deprecateddummyextension @ %(topdir)s/generic/deprecateddummyextension.py)
+| | | |-- ChildDeprecatedDummyExtension (easybuild.easyblocks.generic.childdeprecateddummyextension @ %(topdir)s/generic/childdeprecateddummyextension.py)
| |-- EB_toy (easybuild.easyblocks.toy @ %(topdir)s/t/toy.py)
+| | |-- EB_toy_deprecated (easybuild.easyblocks.toy_deprecated @ %(topdir)s/t/toy_deprecated.py)
| | |-- EB_toy_eula (easybuild.easyblocks.toy_eula @ %(topdir)s/t/toy_eula.py)
| | |-- EB_toytoy (easybuild.easyblocks.toytoy @ %(topdir)s/t/toytoy.py)
-| |-- Toy_Extension (easybuild.easyblocks.generic.toy_extension @ %(topdir)s/generic/toy_extension.py)"""
+| |-- Toy_Extension (easybuild.easyblocks.generic.toy_extension @ %(topdir)s/generic/toy_extension.py)""" # noqa
LIST_EASYBLOCKS_SIMPLE_RST = """* **EasyBlock**
* bar
+ * Bundle
+ * CMakeMake
+ * CmdCp
* ConfigureMake
* MakeCp
+ * EB_binutils
+ * EB_bzip2
+ * EB_CMake
* EB_EasyBuildMeta
* EB_FFTW
+ * EB_FFTW_period_MPI
+ * EB_flex
* EB_foo
* EB_foofoo
+ * EB_freetype
* EB_GCC
* EB_HPL
* EB_libtoy
+ * EB_libxml2
+ * EB_LLVM
+ * EB_Mesa
* EB_OpenBLAS
* EB_OpenMPI
+ * EB_OpenSSL_wrapper
+ * EB_Perl
+ * EB_Python
* EB_ScaLAPACK
* EB_toy_buggy
+ * EB_XCrySDen
* ExtensionEasyBlock
* DummyExtension
+
+ * CustomDummyExtension
+
+ * ChildCustomDummyExtension
+
+ * DeprecatedDummyExtension
+
+ * ChildDeprecatedDummyExtension
+
+
* EB_toy
+ * EB_toy_deprecated
* EB_toy_eula
* EB_toytoy
* Toy_Extension
+ * MesonNinja
* ModuleRC
+ * PerlBundle
* PythonBundle
+ * PythonPackage
+ * Tarball
* Toolchain
* **Extension**
@@ -145,47 +235,89 @@
* ExtensionEasyBlock
* DummyExtension
+
+ * CustomDummyExtension
+
+ * ChildCustomDummyExtension
+
+ * DeprecatedDummyExtension
+
+ * ChildDeprecatedDummyExtension
+
+
* EB_toy
+ * EB_toy_deprecated
* EB_toy_eula
* EB_toytoy
* Toy_Extension
-"""
+""" # noqa
LIST_EASYBLOCKS_DETAILED_RST = """* **EasyBlock** (easybuild.framework.easyblock)
* bar (easybuild.easyblocks.generic.bar @ %(topdir)s/generic/bar.py)
+ * Bundle (easybuild.easyblocks.generic.bundle @ %(topdir)s/generic/bundle.py)
+ * CMakeMake (easybuild.easyblocks.generic.cmakemake @ %(topdir)s/generic/cmakemake.py)
+ * CmdCp (easybuild.easyblocks.generic.cmdcp @ %(topdir)s/generic/cmdcp.py)
* ConfigureMake (easybuild.easyblocks.generic.configuremake @ %(topdir)s/generic/configuremake.py)
* MakeCp (easybuild.easyblocks.generic.makecp @ %(topdir)s/generic/makecp.py)
+ * EB_binutils (easybuild.easyblocks.binutils @ %(topdir)s/b/binutils.py)
+ * EB_bzip2 (easybuild.easyblocks.bzip2 @ %(topdir)s/b/bzip2.py)
+ * EB_CMake (easybuild.easyblocks.cmake @ %(topdir)s/c/cmake.py)
* EB_EasyBuildMeta (easybuild.easyblocks.easybuildmeta @ %(topdir)s/e/easybuildmeta.py)
* EB_FFTW (easybuild.easyblocks.fftw @ %(topdir)s/f/fftw.py)
+ * EB_FFTW_period_MPI (easybuild.easyblocks.fftwmpi @ %(topdir)s/f/fftwmpi.py)
+ * EB_flex (easybuild.easyblocks.flex @ %(topdir)s/f/flex.py)
* EB_foo (easybuild.easyblocks.foo @ %(topdir)s/f/foo.py)
* EB_foofoo (easybuild.easyblocks.foofoo @ %(topdir)s/f/foofoo.py)
+ * EB_freetype (easybuild.easyblocks.freetype @ %(topdir)s/f/freetype.py)
* EB_GCC (easybuild.easyblocks.gcc @ %(topdir)s/g/gcc.py)
* EB_HPL (easybuild.easyblocks.hpl @ %(topdir)s/h/hpl.py)
* EB_libtoy (easybuild.easyblocks.libtoy @ %(topdir)s/l/libtoy.py)
+ * EB_libxml2 (easybuild.easyblocks.libxml2 @ %(topdir)s/l/libxml2.py)
+ * EB_LLVM (easybuild.easyblocks.llvm @ %(topdir)s/l/llvm.py)
+ * EB_Mesa (easybuild.easyblocks.mesa @ %(topdir)s/m/mesa.py)
* EB_OpenBLAS (easybuild.easyblocks.openblas @ %(topdir)s/o/openblas.py)
* EB_OpenMPI (easybuild.easyblocks.openmpi @ %(topdir)s/o/openmpi.py)
+ * EB_OpenSSL_wrapper (easybuild.easyblocks.openssl_wrapper @ %(topdir)s/o/openssl_wrapper.py)
+ * EB_Perl (easybuild.easyblocks.perl @ %(topdir)s/p/perl.py)
+ * EB_Python (easybuild.easyblocks.python @ %(topdir)s/p/python.py)
* EB_ScaLAPACK (easybuild.easyblocks.scalapack @ %(topdir)s/s/scalapack.py)
* EB_toy_buggy (easybuild.easyblocks.toy_buggy @ %(topdir)s/t/toy_buggy.py)
+ * EB_XCrySDen (easybuild.easyblocks.xcrysden @ %(topdir)s/x/xcrysden.py)
* ExtensionEasyBlock (easybuild.framework.extensioneasyblock )
* DummyExtension (easybuild.easyblocks.generic.dummyextension @ %(topdir)s/generic/dummyextension.py)
+
+ * CustomDummyExtension (easybuild.easyblocks.generic.customdummyextension @ %(topdir)s/generic/customdummyextension.py)
+
+ * ChildCustomDummyExtension (easybuild.easyblocks.generic.childcustomdummyextension @ %(topdir)s/generic/childcustomdummyextension.py)
+
+ * DeprecatedDummyExtension (easybuild.easyblocks.generic.deprecateddummyextension @ %(topdir)s/generic/deprecateddummyextension.py)
+
+ * ChildDeprecatedDummyExtension (easybuild.easyblocks.generic.childdeprecateddummyextension @ %(topdir)s/generic/childdeprecateddummyextension.py)
+
+
* EB_toy (easybuild.easyblocks.toy @ %(topdir)s/t/toy.py)
+ * EB_toy_deprecated (easybuild.easyblocks.toy_deprecated @ %(topdir)s/t/toy_deprecated.py)
* EB_toy_eula (easybuild.easyblocks.toy_eula @ %(topdir)s/t/toy_eula.py)
* EB_toytoy (easybuild.easyblocks.toytoy @ %(topdir)s/t/toytoy.py)
* Toy_Extension (easybuild.easyblocks.generic.toy_extension @ %(topdir)s/generic/toy_extension.py)
+ * MesonNinja (easybuild.easyblocks.generic.mesonninja @ %(topdir)s/generic/mesonninja.py)
* ModuleRC (easybuild.easyblocks.generic.modulerc @ %(topdir)s/generic/modulerc.py)
+ * PerlBundle (easybuild.easyblocks.generic.perlbundle @ %(topdir)s/generic/perlbundle.py)
* PythonBundle (easybuild.easyblocks.generic.pythonbundle @ %(topdir)s/generic/pythonbundle.py)
+ * PythonPackage (easybuild.easyblocks.generic.pythonpackage @ %(topdir)s/generic/pythonpackage.py)
+ * Tarball (easybuild.easyblocks.generic.tarball @ %(topdir)s/generic/tarball.py)
* Toolchain (easybuild.easyblocks.generic.toolchain @ %(topdir)s/generic/toolchain.py)
* **Extension** (easybuild.framework.extension)
@@ -193,82 +325,153 @@
* ExtensionEasyBlock (easybuild.framework.extensioneasyblock )
* DummyExtension (easybuild.easyblocks.generic.dummyextension @ %(topdir)s/generic/dummyextension.py)
+
+ * CustomDummyExtension (easybuild.easyblocks.generic.customdummyextension @ %(topdir)s/generic/customdummyextension.py)
+
+ * ChildCustomDummyExtension (easybuild.easyblocks.generic.childcustomdummyextension @ %(topdir)s/generic/childcustomdummyextension.py)
+
+ * DeprecatedDummyExtension (easybuild.easyblocks.generic.deprecateddummyextension @ %(topdir)s/generic/deprecateddummyextension.py)
+
+ * ChildDeprecatedDummyExtension (easybuild.easyblocks.generic.childdeprecateddummyextension @ %(topdir)s/generic/childdeprecateddummyextension.py)
+
+
* EB_toy (easybuild.easyblocks.toy @ %(topdir)s/t/toy.py)
+ * EB_toy_deprecated (easybuild.easyblocks.toy_deprecated @ %(topdir)s/t/toy_deprecated.py)
* EB_toy_eula (easybuild.easyblocks.toy_eula @ %(topdir)s/t/toy_eula.py)
* EB_toytoy (easybuild.easyblocks.toytoy @ %(topdir)s/t/toytoy.py)
* Toy_Extension (easybuild.easyblocks.generic.toy_extension @ %(topdir)s/generic/toy_extension.py)
-"""
+""" # noqa
LIST_EASYBLOCKS_SIMPLE_MD = """- **EasyBlock**
- bar
+ - Bundle
+ - CMakeMake
+ - CmdCp
- ConfigureMake
- MakeCp
+ - EB_binutils
+ - EB_bzip2
+ - EB_CMake
- EB_EasyBuildMeta
- EB_FFTW
+ - EB_FFTW_period_MPI
+ - EB_flex
- EB_foo
- EB_foofoo
+ - EB_freetype
- EB_GCC
- EB_HPL
- EB_libtoy
+ - EB_libxml2
+ - EB_LLVM
+ - EB_Mesa
- EB_OpenBLAS
- EB_OpenMPI
+ - EB_OpenSSL_wrapper
+ - EB_Perl
+ - EB_Python
- EB_ScaLAPACK
- EB_toy_buggy
+ - EB_XCrySDen
- ExtensionEasyBlock
- DummyExtension
+ - CustomDummyExtension
+ - ChildCustomDummyExtension
+ - DeprecatedDummyExtension
+ - ChildDeprecatedDummyExtension
- EB_toy
+ - EB_toy_deprecated
- EB_toy_eula
- EB_toytoy
- Toy_Extension
+ - MesonNinja
- ModuleRC
+ - PerlBundle
- PythonBundle
+ - PythonPackage
+ - Tarball
- Toolchain
- **Extension**
- ExtensionEasyBlock
- DummyExtension
+ - CustomDummyExtension
+ - ChildCustomDummyExtension
+ - DeprecatedDummyExtension
+ - ChildDeprecatedDummyExtension
- EB_toy
+ - EB_toy_deprecated
- EB_toy_eula
- EB_toytoy
- - Toy_Extension"""
+ - Toy_Extension""" # noqa
LIST_EASYBLOCKS_DETAILED_MD = """- **EasyBlock** (easybuild.framework.easyblock)
- bar (easybuild.easyblocks.generic.bar @ %(topdir)s/generic/bar.py)
+ - Bundle (easybuild.easyblocks.generic.bundle @ %(topdir)s/generic/bundle.py)
+ - CMakeMake (easybuild.easyblocks.generic.cmakemake @ %(topdir)s/generic/cmakemake.py)
+ - CmdCp (easybuild.easyblocks.generic.cmdcp @ %(topdir)s/generic/cmdcp.py)
- ConfigureMake (easybuild.easyblocks.generic.configuremake @ %(topdir)s/generic/configuremake.py)
- MakeCp (easybuild.easyblocks.generic.makecp @ %(topdir)s/generic/makecp.py)
+ - EB_binutils (easybuild.easyblocks.binutils @ %(topdir)s/b/binutils.py)
+ - EB_bzip2 (easybuild.easyblocks.bzip2 @ %(topdir)s/b/bzip2.py)
+ - EB_CMake (easybuild.easyblocks.cmake @ %(topdir)s/c/cmake.py)
- EB_EasyBuildMeta (easybuild.easyblocks.easybuildmeta @ %(topdir)s/e/easybuildmeta.py)
- EB_FFTW (easybuild.easyblocks.fftw @ %(topdir)s/f/fftw.py)
+ - EB_FFTW_period_MPI (easybuild.easyblocks.fftwmpi @ %(topdir)s/f/fftwmpi.py)
+ - EB_flex (easybuild.easyblocks.flex @ %(topdir)s/f/flex.py)
- EB_foo (easybuild.easyblocks.foo @ %(topdir)s/f/foo.py)
- EB_foofoo (easybuild.easyblocks.foofoo @ %(topdir)s/f/foofoo.py)
+ - EB_freetype (easybuild.easyblocks.freetype @ %(topdir)s/f/freetype.py)
- EB_GCC (easybuild.easyblocks.gcc @ %(topdir)s/g/gcc.py)
- EB_HPL (easybuild.easyblocks.hpl @ %(topdir)s/h/hpl.py)
- EB_libtoy (easybuild.easyblocks.libtoy @ %(topdir)s/l/libtoy.py)
+ - EB_libxml2 (easybuild.easyblocks.libxml2 @ %(topdir)s/l/libxml2.py)
+ - EB_LLVM (easybuild.easyblocks.llvm @ %(topdir)s/l/llvm.py)
+ - EB_Mesa (easybuild.easyblocks.mesa @ %(topdir)s/m/mesa.py)
- EB_OpenBLAS (easybuild.easyblocks.openblas @ %(topdir)s/o/openblas.py)
- EB_OpenMPI (easybuild.easyblocks.openmpi @ %(topdir)s/o/openmpi.py)
+ - EB_OpenSSL_wrapper (easybuild.easyblocks.openssl_wrapper @ %(topdir)s/o/openssl_wrapper.py)
+ - EB_Perl (easybuild.easyblocks.perl @ %(topdir)s/p/perl.py)
+ - EB_Python (easybuild.easyblocks.python @ %(topdir)s/p/python.py)
- EB_ScaLAPACK (easybuild.easyblocks.scalapack @ %(topdir)s/s/scalapack.py)
- EB_toy_buggy (easybuild.easyblocks.toy_buggy @ %(topdir)s/t/toy_buggy.py)
+ - EB_XCrySDen (easybuild.easyblocks.xcrysden @ %(topdir)s/x/xcrysden.py)
- ExtensionEasyBlock (easybuild.framework.extensioneasyblock )
- DummyExtension (easybuild.easyblocks.generic.dummyextension @ %(topdir)s/generic/dummyextension.py)
+ - CustomDummyExtension (easybuild.easyblocks.generic.customdummyextension @ %(topdir)s/generic/customdummyextension.py)
+ - ChildCustomDummyExtension (easybuild.easyblocks.generic.childcustomdummyextension @ %(topdir)s/generic/childcustomdummyextension.py)
+ - DeprecatedDummyExtension (easybuild.easyblocks.generic.deprecateddummyextension @ %(topdir)s/generic/deprecateddummyextension.py)
+ - ChildDeprecatedDummyExtension (easybuild.easyblocks.generic.childdeprecateddummyextension @ %(topdir)s/generic/childdeprecateddummyextension.py)
- EB_toy (easybuild.easyblocks.toy @ %(topdir)s/t/toy.py)
+ - EB_toy_deprecated (easybuild.easyblocks.toy_deprecated @ %(topdir)s/t/toy_deprecated.py)
- EB_toy_eula (easybuild.easyblocks.toy_eula @ %(topdir)s/t/toy_eula.py)
- EB_toytoy (easybuild.easyblocks.toytoy @ %(topdir)s/t/toytoy.py)
- Toy_Extension (easybuild.easyblocks.generic.toy_extension @ %(topdir)s/generic/toy_extension.py)
+ - MesonNinja (easybuild.easyblocks.generic.mesonninja @ %(topdir)s/generic/mesonninja.py)
- ModuleRC (easybuild.easyblocks.generic.modulerc @ %(topdir)s/generic/modulerc.py)
+ - PerlBundle (easybuild.easyblocks.generic.perlbundle @ %(topdir)s/generic/perlbundle.py)
- PythonBundle (easybuild.easyblocks.generic.pythonbundle @ %(topdir)s/generic/pythonbundle.py)
+ - PythonPackage (easybuild.easyblocks.generic.pythonpackage @ %(topdir)s/generic/pythonpackage.py)
+ - Tarball (easybuild.easyblocks.generic.tarball @ %(topdir)s/generic/tarball.py)
- Toolchain (easybuild.easyblocks.generic.toolchain @ %(topdir)s/generic/toolchain.py)
- **Extension** (easybuild.framework.extension)
- ExtensionEasyBlock (easybuild.framework.extensioneasyblock )
- DummyExtension (easybuild.easyblocks.generic.dummyextension @ %(topdir)s/generic/dummyextension.py)
+ - CustomDummyExtension (easybuild.easyblocks.generic.customdummyextension @ %(topdir)s/generic/customdummyextension.py)
+ - ChildCustomDummyExtension (easybuild.easyblocks.generic.childcustomdummyextension @ %(topdir)s/generic/childcustomdummyextension.py)
+ - DeprecatedDummyExtension (easybuild.easyblocks.generic.deprecateddummyextension @ %(topdir)s/generic/deprecateddummyextension.py)
+ - ChildDeprecatedDummyExtension (easybuild.easyblocks.generic.childdeprecateddummyextension @ %(topdir)s/generic/childdeprecateddummyextension.py)
- EB_toy (easybuild.easyblocks.toy @ %(topdir)s/t/toy.py)
+ - EB_toy_deprecated (easybuild.easyblocks.toy_deprecated @ %(topdir)s/t/toy_deprecated.py)
- EB_toy_eula (easybuild.easyblocks.toy_eula @ %(topdir)s/t/toy_eula.py)
- EB_toytoy (easybuild.easyblocks.toytoy @ %(topdir)s/t/toytoy.py)
- - Toy_Extension (easybuild.easyblocks.generic.toy_extension @ %(topdir)s/generic/toy_extension.py)"""
+ - Toy_Extension (easybuild.easyblocks.generic.toy_extension @ %(topdir)s/generic/toy_extension.py)""" # noqa
LIST_SOFTWARE_SIMPLE_TXT = """
* GCC
-* gzip"""
+* gzip""" # noqa
GCC_DESCR = "The GNU Compiler Collection includes front ends for C, C++, Objective-C, Fortran, Java, and Ada, "
GCC_DESCR += "as well as libraries for these languages (libstdc++, libgcj,...)."
@@ -291,7 +494,7 @@
* gzip v1.4: GCC/4.6.3, system
* gzip v1.5: foss/2018a, intel/2018a
-""" % {'gcc_descr': GCC_DESCR, 'gzip_descr': GZIP_DESCR}
+""" % {'gcc_descr': GCC_DESCR, 'gzip_descr': GZIP_DESCR} # noqa
LIST_SOFTWARE_SIMPLE_RST = """List of supported software
==========================
@@ -307,7 +510,7 @@
---
* GCC
-* gzip"""
+* gzip""" # noqa
LIST_SOFTWARE_DETAILED_RST = """List of supported software
==========================
@@ -357,7 +560,7 @@
``1.4`` ``GCC/4.6.3``, ``system``
``1.5`` ``foss/2018a``, ``intel/2018a``
======= ===============================
-""" % {'gcc_descr': GCC_DESCR, 'gzip_descr': GZIP_DESCR}
+""" % {'gcc_descr': GCC_DESCR, 'gzip_descr': GZIP_DESCR} # noqa
LIST_SOFTWARE_SIMPLE_MD = """# List of supported software
@@ -369,7 +572,7 @@
## G
* GCC
-* gzip"""
+* gzip""" # noqa
LIST_SOFTWARE_DETAILED_MD = """# List of supported software
@@ -403,7 +606,7 @@
version|toolchain
-------|-------------------------------
``1.4``|``GCC/4.6.3``, ``system``
-``1.5``|``foss/2018a``, ``intel/2018a``""" % {'gcc_descr': GCC_DESCR, 'gzip_descr': GZIP_DESCR}
+``1.5``|``foss/2018a``, ``intel/2018a``""" % {'gcc_descr': GCC_DESCR, 'gzip_descr': GZIP_DESCR} # noqa
LIST_SOFTWARE_SIMPLE_MD = """# List of supported software
@@ -415,7 +618,7 @@
## G
* GCC
-* gzip"""
+* gzip""" # noqa
LIST_SOFTWARE_DETAILED_MD = """# List of supported software
@@ -449,7 +652,7 @@
version|toolchain
-------|-------------------------------
``1.4``|``GCC/4.6.3``, ``system``
-``1.5``|``foss/2018a``, ``intel/2018a``""" % {'gcc_descr': GCC_DESCR, 'gzip_descr': GZIP_DESCR}
+``1.5``|``foss/2018a``, ``intel/2018a``""" % {'gcc_descr': GCC_DESCR, 'gzip_descr': GZIP_DESCR} # noqa
LIST_SOFTWARE_SIMPLE_JSON = """[
{
@@ -458,7 +661,7 @@
{
"name": "gzip"
}
-]"""
+]""" # noqa
LIST_SOFTWARE_DETAILED_JSON = """[
{
@@ -501,7 +704,7 @@
"version": "1.5",
"versionsuffix": ""
}
-]""" % {'gcc_descr': GCC_DESCR, 'gzip_descr': GZIP_DESCR}
+]""" % {'gcc_descr': GCC_DESCR, 'gzip_descr': GZIP_DESCR} # noqa
class DocsTest(EnhancedTestCase):
@@ -513,14 +716,33 @@ def test_get_easyblock_classes(self):
# result should correspond with test easyblocks in test/framework/sandbox/easybuild/easyblocks/generic
eb_classes = get_easyblock_classes('easybuild.easyblocks.generic')
eb_names = [x.__name__ for x in eb_classes]
- expected = ['ConfigureMake', 'DummyExtension', 'MakeCp', 'ModuleRC',
- 'PythonBundle', 'Toolchain', 'Toy_Extension', 'bar']
+ expected = [
+ 'Bundle',
+ 'CMakeMake',
+ 'ChildCustomDummyExtension',
+ 'ChildDeprecatedDummyExtension',
+ 'CmdCp',
+ 'ConfigureMake',
+ 'CustomDummyExtension',
+ 'DeprecatedDummyExtension',
+ 'DummyExtension',
+ 'MakeCp',
+ 'MesonNinja',
+ 'ModuleRC',
+ 'PerlBundle',
+ 'PythonBundle',
+ 'PythonPackage',
+ 'Tarball',
+ 'Toolchain',
+ 'Toy_Extension',
+ 'bar',
+ ]
self.assertEqual(sorted(eb_names), expected)
def test_gen_easyblocks_overview(self):
""" Test gen_easyblocks_overview_* functions """
gen_easyblocks_pkg = 'easybuild.easyblocks.generic'
- modules = import_available_modules(gen_easyblocks_pkg)
+ names = [eb_class.__name__ for eb_class in get_easyblock_classes(gen_easyblocks_pkg)]
common_params = {
'ConfigureMake': ['configopts', 'buildopts', 'installopts'],
}
@@ -564,15 +786,9 @@ def test_gen_easyblocks_overview(self):
])
self.assertIn(check_configuremake, ebdoc)
- names = []
- for mod in modules:
- for name, _ in inspect.getmembers(mod, inspect.isclass):
- eb_class = getattr(mod, name)
- # skip imported classes that are not easyblocks
- if eb_class.__module__.startswith(gen_easyblocks_pkg):
- self.assertIn(name, ebdoc)
- names.append(name)
+ for name in names:
+ self.assertIn(name, ebdoc)
toc = [":ref:`" + n + "`" for n in sorted(set(names))]
pattern = " - ".join(toc)
@@ -610,17 +826,11 @@ def test_gen_easyblocks_overview(self):
])
self.assertIn(check_configuremake, ebdoc)
- names = []
- for mod in modules:
- for name, _ in inspect.getmembers(mod, inspect.isclass):
- eb_class = getattr(mod, name)
- # skip imported classes that are not easyblocks
- if eb_class.__module__.startswith(gen_easyblocks_pkg):
- self.assertIn(name, ebdoc)
- names.append(name)
+ for name in names:
+ self.assertIn(name, ebdoc)
- toc = ["\\[" + n + "\\]\\(#" + n.lower() + "\\)" for n in sorted(set(names))]
+ toc = ["\\[" + n + "\\]\\(#" + n.lower() + "\\)" for n in sorted(names)]
pattern = " - ".join(toc)
regex = re.compile(pattern)
self.assertTrue(re.search(regex, ebdoc), "Pattern %s found in %s" % (regex.pattern, ebdoc))
diff --git a/test/framework/easyblock.py b/test/framework/easyblock.py
index 19218eca6a..de3b3e5198 100644
--- a/test/framework/easyblock.py
+++ b/test/framework/easyblock.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -28,6 +28,7 @@
@author: Jens Timmerman (Ghent University)
@author: Kenneth Hoste (Ghent University)
@author: Maxime Boissonneault (Compute Canada)
+@author: Jan Andre Reuter (Juelich Supercomputing Centre)
"""
import os
import re
@@ -35,6 +36,7 @@
import sys
import tempfile
from inspect import cleandoc
+from test.framework.github import requires_github_access
from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered, init_config
from unittest import TextTestRunner
@@ -48,12 +50,12 @@
from easybuild.tools import LooseVersion, config
from easybuild.tools.build_log import EasyBuildError
from easybuild.tools.config import get_module_syntax, update_build_option
+from easybuild.tools.environment import modify_env
from easybuild.tools.filetools import change_dir, copy_dir, copy_file, mkdir, read_file, remove_dir, remove_file
-from easybuild.tools.filetools import verify_checksum, write_file
+from easybuild.tools.filetools import symlink, verify_checksum, write_file
from easybuild.tools.module_generator import module_generator
-from easybuild.tools.modules import EnvironmentModules, Lmod, reset_module_caches
+from easybuild.tools.modules import EnvironmentModules, Lmod, ModEnvVarType, reset_module_caches
from easybuild.tools.version import get_git_revision, this_is_easybuild
-from easybuild.tools.py2vs3 import string_type
class EasyBlockTest(EnhancedTestCase):
@@ -195,18 +197,17 @@ def test_load_module(self):
self.mock_stdout(True)
eb.prepare_step(start_dir=False)
stderr = self.get_stderr()
- stdout = self.get_stdout()
self.mock_stderr(False)
self.mock_stdout(False)
- self.assertFalse(stdout)
self.assertTrue(stderr.strip().startswith("WARNING: Long $TMPDIR path may cause problems with OpenMPI 2.x"))
# we expect $TMPDIR to be tweaked by the prepare step (OpenMPI 2.x doesn't like long $TMPDIR values)
tweaked_tmpdir = os.environ.get('TMPDIR')
self.assertNotEqual(tweaked_tmpdir, orig_tmpdir)
- eb.make_module_step()
- eb.load_module()
+ with self.mocked_stdout_stderr():
+ eb.make_module_step()
+ eb.load_module()
# $TMPDIR does *not* get reset to original value after loading of module
# (which involves resetting the environment before loading the module)
@@ -286,8 +287,11 @@ def test_make_module_extend_modpath(self):
txt = eb.make_module_extend_modpath()
if module_syntax == 'Tcl':
regexs = [r'^module use ".*/modules/funky/Compiler/pi/3.14/%s"$' % c for c in modclasses]
- home = r'\[if { \[info exists ::env\(HOME\)\] } { concat \$::env\(HOME\) } '
- home += r'else { concat "HOME_NOT_DEFINED" } \]'
+ if self.modtool.supports_tcl_getenv:
+ home = r'\[getenv HOME "HOME_NOT_DEFINED"\]'
+ else:
+ home = r'\[if { \[info exists ::env\(HOME\)\] } { concat \$::env\(HOME\) } '
+ home += r'else { concat "HOME_NOT_DEFINED" } \]'
fj_usermodsdir = 'file join "%s" "funky" "Compiler/pi/3.14"' % usermodsdir
regexs.extend([
# extension for user modules is guarded
@@ -312,7 +316,7 @@ def test_make_module_extend_modpath(self):
regex = re.compile(regex, re.M)
self.assertTrue(regex.search(txt), "Pattern '%s' found in: %s" % (regex.pattern, txt))
- # Repeat this but using an alternate envvars (instead of $HOME)
+ # Repeat this but using an alternative envvars (instead of $HOME)
list_of_envvars = ['SITE_INSTALLS', 'USER_INSTALLS']
build_options = {
@@ -329,9 +333,12 @@ def test_make_module_extend_modpath(self):
for envvar in list_of_envvars:
if module_syntax == 'Tcl':
regexs = [r'^module use ".*/modules/funky/Compiler/pi/3.14/%s"$' % c for c in modclasses]
- module_envvar = r'\[if \{ \[info exists ::env\(%s\)\] \} ' % envvar
- module_envvar += r'\{ concat \$::env\(%s\) \} ' % envvar
- module_envvar += r'else { concat "%s" } \]' % (envvar + '_NOT_DEFINED')
+ if self.modtool.supports_tcl_getenv:
+ module_envvar = r'\[getenv %s "%s"]' % (envvar, envvar + '_NOT_DEFINED')
+ else:
+ module_envvar = r'\[if \{ \[info exists ::env\(%s\)\] \} ' % envvar
+ module_envvar += r'\{ concat \$::env\(%s\) \} ' % envvar
+ module_envvar += r'else { concat "%s" } \]' % (envvar + '_NOT_DEFINED')
fj_usermodsdir = 'file join "%s" "funky" "Compiler/pi/3.14"' % usermodsdir
regexs.extend([
# extension for user modules is guarded
@@ -429,12 +436,13 @@ def test_make_module_req(self):
# create fake directories and files that should be guessed
os.makedirs(eb.installdir)
+ for path in ('bin', ('bin', 'testdir'), 'sbin', 'share', ('share', 'man'), 'lib', 'lib64'):
+ path_components = (path, ) if isinstance(path, str) else path
+ os.mkdir(os.path.join(eb.installdir, *path_components))
+
write_file(os.path.join(eb.installdir, 'foo.jar'), 'foo.jar')
write_file(os.path.join(eb.installdir, 'bla.jar'), 'bla.jar')
- for path in ('bin', ('bin', 'testdir'), 'sbin', 'share', ('share', 'man'), 'lib', 'lib64'):
- if isinstance(path, string_type):
- path = (path, )
- os.mkdir(os.path.join(eb.installdir, *path))
+ write_file(os.path.join(eb.installdir, 'share', 'man', 'pi'), 'Man page')
# this is not a path that should be picked up
os.mkdir(os.path.join(eb.installdir, 'CPATH'))
@@ -514,8 +522,18 @@ def test_make_module_req(self):
self.assertEqual(len(re.findall(r'^prepend_path\("%s", pathJoin\(root, "lib"\)\)$' % var,
guess, re.M)), 1)
- # check for behavior when a string value is used as dict value by make_module_req_guesses
- eb.make_module_req_guess = lambda: {'PATH': 'bin'}
+ # nuke default module load environment
+ default_mod_load_vars = [
+ 'ACLOCAL_PATH', 'CLASSPATH', 'CMAKE_PREFIX_PATH', 'CMAKE_LIBRARY_PATH', 'CPATH', 'GI_TYPELIB_PATH',
+ 'LD_LIBRARY_PATH', 'LIBRARY_PATH', 'MANPATH', 'PATH', 'PKG_CONFIG_PATH', 'XDG_DATA_DIRS',
+ ]
+ for env_var in default_mod_load_vars:
+ eb.module_load_environment.remove(env_var)
+
+ self.assertEqual(len(eb.module_load_environment.vars), 0)
+
+ # check for behavior when a string value is used as value of module_load_environment
+ eb.module_load_environment.PATH = 'bin'
with eb.module_generator.start_module_creation():
txt = eb.make_module_req()
if get_module_syntax() == 'Tcl':
@@ -527,7 +545,7 @@ def test_make_module_req(self):
# check for correct behaviour if empty string is specified as one of the values
# prepend-path statements should be included for both the 'bin' subdir and the install root
- eb.make_module_req_guess = lambda: {'PATH': ['bin', '']}
+ eb.module_load_environment.PATH = ['bin', '']
with eb.module_generator.start_module_creation():
txt = eb.make_module_req()
if get_module_syntax() == 'Tcl':
@@ -540,7 +558,7 @@ def test_make_module_req(self):
self.fail("Unknown module syntax: %s" % get_module_syntax())
# check for correct order of prepend statements when providing a list (and that no duplicates are allowed)
- eb.make_module_req_guess = lambda: {'LD_LIBRARY_PATH': ['lib/pathC', 'lib/pathA', 'lib/pathB', 'lib/pathA']}
+ eb.module_load_environment.LD_LIBRARY_PATH = ['lib/pathC', 'lib/pathA', 'lib/pathB', 'lib/pathA']
for path in ['pathA', 'pathB', 'pathC']:
os.mkdir(os.path.join(eb.installdir, 'lib', path))
write_file(os.path.join(eb.installdir, 'lib', path, 'libfoo.so'), 'test')
@@ -568,12 +586,14 @@ def test_make_module_req(self):
# If PATH or LD_LIBRARY_PATH contain only folders, do not add an entry
sub_lib_path = os.path.join('lib', 'path_folders')
sub_path_path = os.path.join('bin', 'path_folders')
- eb.make_module_req_guess = lambda: {'LD_LIBRARY_PATH': sub_lib_path, 'PATH': sub_path_path}
+ eb.module_load_environment.LD_LIBRARY_PATH = sub_lib_path
+ eb.module_load_environment.PATH = sub_path_path
for path in (sub_lib_path, sub_path_path):
full_path = os.path.join(eb.installdir, path, 'subpath')
os.makedirs(full_path)
write_file(os.path.join(full_path, 'any.file'), 'test')
- txt = eb.make_module_req()
+ with eb.module_generator.start_module_creation():
+ txt = eb.make_module_req()
if get_module_syntax() == 'Tcl':
self.assertFalse(re.search(r"prepend-path\s+LD_LIBRARY_PATH\s+\$%s\n" % sub_lib_path,
txt, re.M))
@@ -584,6 +604,231 @@ def test_make_module_req(self):
txt, re.M))
self.assertFalse(re.search(r'prepend_path\("PATH", pathJoin\(root, "%s"\)\)\n' % sub_path_path, txt, re.M))
+ # Module load environement may contain non-path variables
+ # TODO: remove whenever this is properly supported, in the meantime check warning
+ eb.module_load_environment.NONPATH = {'contents': 'non_path', 'var_type': "STRING"}
+ eb.module_load_environment.PATH = ['bin']
+ with self.mocked_stdout_stderr():
+ txt = eb.make_module_req()
+
+ self.assertEqual(list(eb.module_load_environment), ['PATH', 'LD_LIBRARY_PATH', 'NONPATH'])
+
+ if get_module_syntax() == 'Tcl':
+ self.assertTrue(re.match(r"^\nprepend-path\s+PATH\s+\$root/bin\n$", txt, re.M))
+ self.assertFalse(re.match(r"^\nprepend-path\s+NONPATH\s+\$root/non_path\n$", txt, re.M))
+ elif get_module_syntax() == 'Lua':
+ self.assertTrue(re.match(r'^\nprepend_path\("PATH", pathJoin\(root, "bin"\)\)\n$', txt, re.M))
+ self.assertFalse(re.match(r'^\nprepend_path\("NONPATH", pathJoin\(root, "non_path"\)\)\n$', txt, re.M))
+ else:
+ self.fail("Unknown module syntax: %s" % get_module_syntax())
+
+ logtxt = read_file(eb.logfile)
+ self.assertTrue(re.search(r"WARNING Non-path variables found in module load env.*NONPATH", logtxt, re.M))
+
+ eb.module_load_environment.remove('NONPATH')
+
+ # make sure that entries that symlink to another directory are retained;
+ # the test case inspired by the directory structure for old imkl versions (like 2020.4)
+ remove_dir(eb.installdir)
+ # lib/ symlinked to libraries/
+ real_libdir = os.path.join(eb.installdir, 'libraries')
+ mkdir(real_libdir, parents=True)
+ symlink(real_libdir, os.path.join(eb.installdir, 'lib'))
+ # lib/intel64/ symlinked to lib/intel64_lin/
+ mkdir(os.path.join(eb.installdir, 'lib', 'intel64_lin'), parents=True)
+ symlink(os.path.join(eb.installdir, 'lib', 'intel64_lin'), os.path.join(eb.installdir, 'lib', 'intel64'))
+ # library file present in lib/intel64
+ write_file(os.path.join(eb.installdir, 'lib', 'intel64', 'libfoo.so'), 'libfoo.so')
+ # lib64/ symlinked to lib/
+ symlink(os.path.join(eb.installdir, 'lib'), os.path.join(eb.installdir, 'lib64'))
+
+ eb.module_load_environment.LD_LIBRARY_PATH = [os.path.join('lib', 'intel64')]
+ eb.module_load_environment.LIBRARY_PATH = eb.module_load_environment.LD_LIBRARY_PATH
+ with eb.module_generator.start_module_creation():
+ txt = eb.make_module_req()
+
+ if get_module_syntax() == 'Tcl':
+ self.assertTrue(re.search(r"^prepend-path\s+LD_LIBRARY_PATH\s+\$root/libraries/intel64_lin$", txt, re.M))
+ self.assertTrue(re.search(r"^prepend-path\s+LIBRARY_PATH\s+\$root/libraries/intel64_lin\n$", txt, re.M))
+ elif get_module_syntax() == 'Lua':
+ self.assertTrue(re.search(r'^prepend_path\("LD_LIBRARY_PATH", pathJoin\(root, "libraries/intel64_lin"\)\)$',
+ txt, re.M))
+ self.assertTrue(re.search(r'^prepend_path\("LIBRARY_PATH", pathJoin\(root, "libraries/intel64_lin"\)\)$',
+ txt, re.M))
+ else:
+ self.fail("Unknown module syntax: %s" % get_module_syntax())
+
+ # test absolute paths
+ eb.module_load_environment.PATH = ['/bin']
+
+ with eb.module_generator.start_module_creation():
+ txt = eb.make_module_req()
+
+ if get_module_syntax() == 'Tcl':
+ self.assertTrue(re.search(r"^prepend-path\s+PATH\s+/bin$", txt, re.M))
+ elif get_module_syntax() == 'Lua':
+ self.assertTrue(re.search(r'^prepend_path\("PATH", "/bin"\)$', txt, re.M))
+ else:
+ self.fail("Unknown module syntax: %s" % get_module_syntax())
+
+ # make sure that relative entries that symlink to directories outside of install dir trigger an error
+ symlink("/bin", os.path.join(eb.installdir, 'bin'))
+ eb.module_load_environment.PATH = ['bin']
+
+ with eb.module_generator.start_module_creation():
+ err_regex = "Expansion of search path glob.*pointing outside of install directory.*"
+ self.assertErrorRegex(EasyBuildError, err_regex, eb.make_module_req)
+
+ # Test modextrapaths: with absolute + empty paths, appending and custom delimiters
+ remove_dir(eb.installdir)
+ self.contents += '\n'.join([
+ "",
+ "modextrapaths = {",
+ " 'PATH': [''],",
+ " 'TEST_VAR': ['foo', 'baz', '/bin'],",
+ " 'TEST_VAR_CUSTOM': {'paths': ['foo', 'baz'], 'delimiter': ';', 'prepend': False},",
+ " 'LD_LIBRARY_PATH': 'foo',",
+ " MODULE_LOAD_ENV_HEADERS: ['include/foo', 'include/bar'],",
+ "}",
+ ])
+ self.writeEC()
+ ec = EasyConfig(self.eb_file)
+ eb = EasyBlock(ec)
+ eb.installdir = config.install_path()
+
+ # populate install dir
+ mkdir(os.path.join(eb.installdir, 'lib'), parents=True)
+ write_file(os.path.join(eb.installdir, 'lib', 'libfoo.so'), 'install lib')
+ mkdir(os.path.join(eb.installdir, 'foo'), parents=True)
+ write_file(os.path.join(eb.installdir, 'foo', 'bar'), 'install file')
+ mkdir(os.path.join(eb.installdir, 'baz'), parents=True)
+ mkdir(os.path.join(eb.installdir, 'include', 'foo'), parents=True)
+ write_file(os.path.join(eb.installdir, 'include', 'foo', 'foo.h'), 'header file')
+ mkdir(os.path.join(eb.installdir, 'include', 'bar'), parents=True)
+
+ with eb.module_generator.start_module_creation():
+ txt = eb.make_module_req()
+
+ expected_patterns = [
+ r"^append[-_]path.*TEST_VAR_CUSTOM.*root.*foo.*",
+ r"^prepend[-_]path.*CPATH.*root.*include.*",
+ r"^prepend[-_]path.*CPATH.*root.*include/foo.*",
+ r"^prepend[-_]path.*LD_LIBRARY_PATH.*root.*lib",
+ r"^prepend[-_]path.*LD_LIBRARY_PATH.*root.*foo",
+ r"^prepend[-_]path.*TEST_VAR.*root.*foo",
+ r"^prepend[-_]path.*TEST_VAR.*/bin",
+ ]
+ if get_module_syntax() == 'Tcl':
+ expected_patterns.append(r"^prepend-path\s+PATH\s+\$root$")
+ elif get_module_syntax() == 'Lua':
+ expected_patterns.append(r'^prepend_path\("PATH", root\)$')
+ else:
+ self.fail("Unknown module syntax: %s" % get_module_syntax())
+
+ for pattern in expected_patterns:
+ self.assertTrue(re.search(pattern, txt, re.M), "Pattern '%s' found in: %s" % (pattern, txt))
+
+ non_expected_patterns = [
+ r"^append[-_]path.*TEST_VAR_APPEND.*root.*baz",
+ r"^prepend[-_]path.*CPATH.*root.*include/bar.*",
+ r"^prepend[-_]path.*TEST_VAR.*root.*baz",
+ ]
+ for pattern in non_expected_patterns:
+ self.assertFalse(re.search(pattern, txt, re.M), "Pattern '%s' found in: %s" % (pattern, txt))
+
+ # cleanup
+ eb.close_log()
+ os.remove(eb.logfile)
+
+ def test_module_search_path_headers(self):
+ """Test functionality of module-search-path-headers option"""
+ sp_headers_mode = {
+ "cpath": ["CPATH"],
+ "include_paths": ["C_INCLUDE_PATH", "CPLUS_INCLUDE_PATH", "OBJC_INCLUDE_PATH"],
+ }
+
+ self.contents = '\n'.join([
+ 'easyblock = "ConfigureMake"',
+ 'name = "pi"',
+ 'version = "3.14"',
+ 'homepage = "http://example.com"',
+ 'description = "test easyconfig"',
+ 'toolchain = SYSTEM',
+ ])
+ self.writeEC()
+
+ for build_opt, sp_headers in sp_headers_mode.items():
+ update_build_option('module_search_path_headers', build_opt)
+ eb = EasyBlock(EasyConfig(self.eb_file))
+ eb.installdir = config.install_path()
+ try:
+ os.makedirs(os.path.join(eb.installdir, 'include'))
+ write_file(os.path.join(eb.installdir, 'include', 'header.h'), 'dummy header file')
+ except FileExistsError:
+ pass
+
+ with eb.module_generator.start_module_creation():
+ guess = eb.make_module_req()
+
+ if not sp_headers:
+ # none option adds nothing to module file
+ if get_module_syntax() == 'Tcl':
+ tcl_ref_pattern = r"^prepend-path\s+CPATH\s+\$root/include$"
+ self.assertFalse(re.search(tcl_ref_pattern, guess, re.M))
+ elif get_module_syntax() == 'Lua':
+ lua_ref_pattern = r'^prepend_path\("CPATH", pathJoin\(root, "include"\)\)$'
+ self.assertFalse(re.search(lua_ref_pattern, guess, re.M))
+ else:
+ for env_var in sp_headers:
+ if get_module_syntax() == 'Tcl':
+ tcl_ref_pattern = rf"^prepend-path\s+{env_var}\s+\$root/include$"
+ self.assertTrue(re.search(tcl_ref_pattern, guess, re.M))
+ elif get_module_syntax() == 'Lua':
+ lua_ref_pattern = rf'^prepend_path\("{env_var}", pathJoin\(root, "include"\)\)$'
+ self.assertTrue(re.search(lua_ref_pattern, guess, re.M))
+
+ # test with easyconfig parameter
+ for ec_param, sp_headers in sp_headers_mode.items():
+ self.contents += f'\nmodule_search_path_headers = "{ec_param}"'
+ self.writeEC()
+ eb = EasyBlock(EasyConfig(self.eb_file))
+ eb.installdir = config.install_path()
+ try:
+ os.makedirs(os.path.join(eb.installdir, 'include'))
+ write_file(os.path.join(eb.installdir, 'include', 'header.h'), 'dummy header file')
+ except FileExistsError:
+ pass
+
+ for build_opt in sp_headers_mode:
+ update_build_option('module_search_path_headers', build_opt)
+ with eb.module_generator.start_module_creation():
+ guess = eb.make_module_req()
+ if not sp_headers:
+ # none option adds nothing to module file
+ if get_module_syntax() == 'Tcl':
+ tcl_ref_pattern = r"^prepend-path\s+CPATH\s+\$root/include$"
+ self.assertFalse(re.search(tcl_ref_pattern, guess, re.M))
+ elif get_module_syntax() == 'Lua':
+ lua_ref_pattern = r'^prepend_path\("CPATH", pathJoin\(root, "include"\)\)$'
+ self.assertFalse(re.search(lua_ref_pattern, guess, re.M))
+ else:
+ for env_var in sp_headers:
+ if get_module_syntax() == 'Tcl':
+ tcl_ref_pattern = rf"^prepend-path\s+{env_var}\s+\$root/include$"
+ self.assertTrue(re.search(tcl_ref_pattern, guess, re.M))
+ elif get_module_syntax() == 'Lua':
+ lua_ref_pattern = rf'^prepend_path\("{env_var}", pathJoin\(root, "include"\)\)$'
+ self.assertTrue(re.search(lua_ref_pattern, guess, re.M))
+
+ # test wrong easyconfig parameter
+ self.contents += '\nmodule_search_path_headers = "WRONG_OPT"'
+ self.writeEC()
+ ec = EasyConfig(self.eb_file)
+
+ error_pattern = "Unknown value selected for option module-search-path-headers"
+ with eb.module_generator.start_module_creation():
+ self.assertErrorRegex(EasyBuildError, error_pattern, EasyBlock, ec)
+
# cleanup
eb.close_log()
os.remove(eb.logfile)
@@ -641,49 +886,6 @@ def test_make_module_extra(self):
self.assertTrue(expected_alt.match(alttxt),
"Pattern %s found in %s" % (expected_alt.pattern, alttxt))
- installver = '3.14-gompi-2018a'
-
- # also check how absolute paths specified in modexself.contents = '\n'.join([
- self.contents += "\nmodextrapaths = {'TEST_PATH_VAR': ['foo', '/test/absolute/path', 'bar']}"
- self.contents += "\nmodextrapaths_append = {'TEST_PATH_VAR_APPEND': ['foo', '/test/absolute/path', 'bar']}"
- self.writeEC()
- ec = EasyConfig(self.eb_file)
- eb = EasyBlock(ec)
- eb.installdir = os.path.join(config.install_path(), 'pi', installver)
- eb.check_readiness_step()
-
- # absolute paths are not allowed by default
- error_pattern = "Absolute path .* passed to update_paths which only expects relative paths"
- self.assertErrorRegex(EasyBuildError, error_pattern, eb.make_module_step)
-
- # allow use of absolute paths, and verify contents of module
- self.contents += "\nallow_prepend_abs_path = True"
- self.contents += "\nallow_append_abs_path = True"
- self.writeEC()
- ec = EasyConfig(self.eb_file)
- eb = EasyBlock(ec)
- eb.installdir = os.path.join(config.install_path(), 'pi', installver)
- eb.check_readiness_step()
-
- modrootpath = eb.make_module_step()
-
- modpath = os.path.join(modrootpath, 'pi', installver)
- if get_module_syntax() == 'Lua':
- modpath += '.lua'
-
- self.assertExists(modpath)
- txt = read_file(modpath)
- patterns = [
- r"^prepend[-_]path.*TEST_PATH_VAR.*root.*foo",
- r"^prepend[-_]path.*TEST_PATH_VAR.*/test/absolute/path",
- r"^prepend[-_]path.*TEST_PATH_VAR.*root.*bar",
- r"^append[-_]path.*TEST_PATH_VAR_APPEND.*root.*foo",
- r"^append[-_]path.*TEST_PATH_VAR_APPEND.*/test/absolute/path",
- r"^append[-_]path.*TEST_PATH_VAR_APPEND.*root.*bar",
- ]
- for pattern in patterns:
- self.assertTrue(re.search(pattern, txt, re.M), "Pattern '%s' found in: %s" % (pattern, txt))
-
def test_make_module_deppaths(self):
"""Test for make_module_deppaths"""
init_config(build_options={'silent': True})
@@ -704,9 +906,10 @@ def test_make_module_deppaths(self):
eb = EasyBlock(EasyConfig(self.eb_file))
eb.installdir = os.path.join(config.install_path(), 'pi', '3.14')
- eb.check_readiness_step()
- eb.make_builddir()
- eb.prepare_step()
+ with self.mocked_stdout_stderr():
+ eb.check_readiness_step()
+ eb.make_builddir()
+ eb.prepare_step()
if get_module_syntax() == 'Tcl':
use_load = '\n'.join([
@@ -724,7 +927,8 @@ def test_make_module_deppaths(self):
self.fail("Unknown module syntax: %s" % get_module_syntax())
expected = use_load
- self.assertEqual(eb.make_module_deppaths().strip(), expected)
+ with self.mocked_stdout_stderr():
+ self.assertEqual(eb.make_module_deppaths().strip(), expected)
def test_make_module_dep(self):
"""Test for make_module_dep"""
@@ -746,26 +950,32 @@ def test_make_module_dep(self):
eb = EasyBlock(EasyConfig(self.eb_file))
eb.installdir = os.path.join(config.install_path(), 'pi', '3.14')
- eb.check_readiness_step()
- eb.make_builddir()
- eb.prepare_step()
+ with self.mocked_stdout_stderr():
+ eb.check_readiness_step()
+ eb.make_builddir()
+ eb.prepare_step()
if get_module_syntax() == 'Tcl':
- tc_load = '\n'.join([
- "if { ![ is-loaded gompi/2018a ] } {",
- " module load gompi/2018a",
- "}",
- ])
- fftw_load = '\n'.join([
- "if { ![ is-loaded FFTW/3.3.7-gompi-2018a ] } {",
- " module load FFTW/3.3.7-gompi-2018a",
- "}",
- ])
- lapack_load = '\n'.join([
- "if { ![ is-loaded OpenBLAS/0.2.20-GCC-6.4.0-2.28 ] } {",
- " module load OpenBLAS/0.2.20-GCC-6.4.0-2.28",
- "}",
- ])
+ if self.modtool.supports_safe_auto_load:
+ tc_load = "module load gompi/2018a"
+ fftw_load = "module load FFTW/3.3.7-gompi-2018a"
+ lapack_load = "module load OpenBLAS/0.2.20-GCC-6.4.0-2.28"
+ else:
+ tc_load = '\n'.join([
+ "if { ![ is-loaded gompi/2018a ] } {",
+ " module load gompi/2018a",
+ "}",
+ ])
+ fftw_load = '\n'.join([
+ "if { ![ is-loaded FFTW/3.3.7-gompi-2018a ] } {",
+ " module load FFTW/3.3.7-gompi-2018a",
+ "}",
+ ])
+ lapack_load = '\n'.join([
+ "if { ![ is-loaded OpenBLAS/0.2.20-GCC-6.4.0-2.28 ] } {",
+ " module load OpenBLAS/0.2.20-GCC-6.4.0-2.28",
+ "}",
+ ])
elif get_module_syntax() == 'Lua':
tc_load = '\n'.join([
'if not ( isloaded("gompi/2018a") ) then',
@@ -786,7 +996,8 @@ def test_make_module_dep(self):
self.fail("Unknown module syntax: %s" % get_module_syntax())
expected = tc_load + '\n\n' + fftw_load + '\n\n' + lapack_load
- self.assertEqual(eb.make_module_dep().strip(), expected)
+ with self.mocked_stdout_stderr():
+ self.assertEqual(eb.make_module_dep().strip(), expected)
# provide swap info for FFTW to trigger an extra 'unload FFTW'
unload_info = {
@@ -794,12 +1005,18 @@ def test_make_module_dep(self):
}
if get_module_syntax() == 'Tcl':
- fftw_load = '\n'.join([
- "if { ![ is-loaded FFTW/3.3.7-gompi-2018a ] } {",
- " module unload FFTW",
- " module load FFTW/3.3.7-gompi-2018a",
- "}",
- ])
+ if self.modtool.supports_safe_auto_load:
+ fftw_load = '\n'.join([
+ "module unload FFTW",
+ "module load FFTW/3.3.7-gompi-2018a",
+ ])
+ else:
+ fftw_load = '\n'.join([
+ "if { ![ is-loaded FFTW/3.3.7-gompi-2018a ] } {",
+ " module unload FFTW",
+ " module load FFTW/3.3.7-gompi-2018a",
+ "}",
+ ])
elif get_module_syntax() == 'Lua':
fftw_load = '\n'.join([
'if not ( isloaded("FFTW/3.3.7-gompi-2018a") ) then',
@@ -810,7 +1027,8 @@ def test_make_module_dep(self):
else:
self.fail("Unknown module syntax: %s" % get_module_syntax())
expected = tc_load + '\n\n' + fftw_load + '\n\n' + lapack_load
- self.assertEqual(eb.make_module_dep(unload_info=unload_info).strip(), expected)
+ with self.mocked_stdout_stderr():
+ self.assertEqual(eb.make_module_dep(unload_info=unload_info).strip(), expected)
def test_make_module_dep_hmns(self):
"""Test for make_module_dep under HMNS"""
@@ -842,17 +1060,19 @@ def test_make_module_dep_hmns(self):
eb = EasyBlock(EasyConfig(self.eb_file))
eb.installdir = os.path.join(config.install_path(), 'pi', '3.14')
- eb.check_readiness_step()
- eb.make_builddir()
- eb.prepare_step()
+ with self.mocked_stdout_stderr():
+ eb.check_readiness_step()
+ eb.make_builddir()
+ eb.prepare_step()
# GCC, OpenMPI and hwloc modules should *not* be included in loads for dependencies
- mod_dep_txt = eb.make_module_dep()
+ with self.mocked_stdout_stderr():
+ mod_dep_txt = eb.make_module_dep()
for mod in ['GCC/6.4.0-2.28', 'OpenMPI/2.1.2']:
- regex = re.compile('load.*%s' % mod)
+ regex = re.compile('(load|depends[-_]on).*%s' % mod)
self.assertFalse(regex.search(mod_dep_txt), "Pattern '%s' found in: %s" % (regex.pattern, mod_dep_txt))
- regex = re.compile('load.*FFTW/3.3.7')
+ regex = re.compile('(load|depends[-_]on).*FFTW/3.3.7')
self.assertTrue(regex.search(mod_dep_txt), "Pattern '%s' found in: %s" % (regex.pattern, mod_dep_txt))
def test_make_module_dep_of_dep_hmns(self):
@@ -994,6 +1214,60 @@ def test_handle_iterate_opts(self):
self.assertEqual(eb.cfg.iterating, False)
self.assertEqual(eb.cfg['configopts'], ["--opt1 --anotheropt", "--opt2", "--opt3 --optbis"])
+ def test_post_processing_step(self):
+ """Test post_processing_step and deprecated post_install_step."""
+ init_config(build_options={'silent': True})
+
+ test_ecs_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'easyconfigs', 'test_ecs')
+ toy_ec_fn = os.path.join(test_ecs_dir, 't', 'toy', 'toy-0.0.eb')
+
+ # these imports only work here, since EB_toy is a test easyblock
+ from easybuild.easyblocks.toy import EB_toy
+ from easybuild.easyblocks.toy_deprecated import EB_toy_deprecated
+
+ cwd = os.getcwd()
+ toy_ec = EasyConfig(toy_ec_fn)
+ eb = EB_toy_deprecated(toy_ec)
+ eb.silent = True
+ depr_msg = r"EasyBlock.post_install_step\(\) is deprecated, use EasyBlock.post_processing_step\(\) instead"
+ expected_error = r"DEPRECATED \(since v6.0\).*" + depr_msg
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, expected_error, eb.run_all_steps, True)
+
+ change_dir(cwd)
+ toy_ec = EasyConfig(toy_ec_fn)
+ eb = EB_toy(toy_ec)
+ eb.silent = True
+ with self.mocked_stdout_stderr() as (_, stderr):
+ eb.run_all_steps(True)
+ # no deprecation warning
+ stderr = stderr.getvalue()
+ self.assertFalse(stderr)
+
+ libtoy_post_a = os.path.join(eb.installdir, 'lib', 'libtoy_post.a')
+ self.assertExists(libtoy_post_a)
+
+ # check again with toy easyblock that still uses post_install_step,
+ # to verify that the expected file is being created when deprecated functionality is allow
+ remove_file(libtoy_post_a)
+ modify_env(os.environ, self.orig_environ, verbose=False)
+ change_dir(cwd)
+
+ self.allow_deprecated_behaviour()
+ toy_ec = EasyConfig(toy_ec_fn)
+ eb = EB_toy_deprecated(toy_ec)
+ eb.silent = True
+ with self.mocked_stdout_stderr() as (stdout, stderr):
+ eb.run_all_steps(True)
+
+ regex = re.compile(depr_msg, re.M)
+ stdout = stdout.getvalue()
+ self.assertTrue("This step is deprecated.\n" in stdout)
+ stderr = stderr.getvalue()
+ self.assertTrue(regex.search(stderr), f"Pattern {regex.pattern} found in: {stderr}")
+
+ self.assertExists(libtoy_post_a)
+
def test_extensions_step(self):
"""Test the extensions_step"""
init_config(build_options={'silent': True})
@@ -1031,6 +1305,64 @@ def test_extensions_step(self):
eb.close_log()
os.remove(eb.logfile)
+ def test_extensions_step_deprecations(self):
+ """Test extension install with deprecated substeps."""
+ install_substeps = ["pre_install_extension", "install_extension", "post_install_extension"]
+
+ test_ec = os.path.join(self.test_prefix, 'test.eb')
+ test_ec_txt = '\n'.join([
+ 'easyblock = "ConfigureMake"',
+ 'name = "pi"',
+ 'version = "3.14"',
+ 'homepage = "http://example.com"',
+ 'description = "test easyconfig"',
+ 'toolchain = SYSTEM',
+ 'exts_defaultclass = "DummyExtension"',
+ 'exts_list = ["ext1"]',
+ 'exts_list = [',
+ ' "dummy_ext",',
+ ' ("custom_ext", "0.0", {"easyblock": "CustomDummyExtension"}),',
+ ' ("deprec_ext", "0.0", {"easyblock": "DeprecatedDummyExtension"}),',
+ ' ("childcustom_ext", "0.0", {"easyblock": "ChildCustomDummyExtension"}),',
+ ' ("childdeprec_ext", "0.0", {"easyblock": "ChildDeprecatedDummyExtension"}),',
+ ']',
+ ])
+ write_file(test_ec, test_ec_txt)
+ ec = process_easyconfig(test_ec)[0]
+ eb = get_easyblock_instance(ec)
+ eb.prepare_for_extensions()
+ eb.init_ext_instances()
+
+ # Default DummyExtension without deprecated or custom install substeps
+ ext = eb.ext_instances[0]
+ self.assertEqual(ext.__class__.__name__, "DummyExtension")
+ for substep in install_substeps:
+ self.assertEqual(ext.install_extension_substep(substep), None)
+ # CustomDummyExtension
+ ext = eb.ext_instances[1]
+ self.assertEqual(ext.__class__.__name__, "CustomDummyExtension")
+ for substep in install_substeps:
+ expected_return = f"Extension installed with custom {substep}()"
+ self.assertEqual(ext.install_extension_substep(substep), expected_return)
+ # DeprecatedDummyExtension
+ ext = eb.ext_instances[2]
+ self.assertEqual(ext.__class__.__name__, "DeprecatedDummyExtension")
+ for substep in install_substeps:
+ expected_error = rf"DEPRECATED \(since v6.0\).*use {substep}\(\) instead.*"
+ self.assertErrorRegex(EasyBuildError, expected_error, ext.install_extension_substep, substep)
+ # ChildCustomDummyExtension
+ ext = eb.ext_instances[3]
+ self.assertEqual(ext.__class__.__name__, "ChildCustomDummyExtension")
+ for substep in install_substeps:
+ expected_return = f"Extension installed with custom {substep}()"
+ self.assertEqual(ext.install_extension_substep(substep), expected_return)
+ # ChildDeprecatedDummyExtension
+ ext = eb.ext_instances[4]
+ self.assertEqual(ext.__class__.__name__, "ChildDeprecatedDummyExtension")
+ for substep in install_substeps:
+ expected_error = rf"DEPRECATED \(since v6.0\).*use {substep}\(\) instead.*"
+ self.assertErrorRegex(EasyBuildError, expected_error, ext.install_extension_substep, substep)
+
def test_init_extensions(self):
"""Test creating extension instances."""
@@ -1086,7 +1418,7 @@ def test_extension_source_tmpl(self):
eb = EasyBlock(EasyConfig(self.eb_file))
error_pattern = r"source_tmpl value must be a string! "
- error_pattern += r"\(found value of type 'list'\): \['bar-0\.0\.tar\.gz'\]"
+ error_pattern += r"\(found value of type 'list'\): \['%\(name\)s-%\(version\)s.tar.gz'\]"
self.assertErrorRegex(EasyBuildError, error_pattern, eb.fetch_step)
self.contents = self.contents.replace("'source_tmpl': [SOURCE_TAR_GZ]", "'source_tmpl': SOURCE_TAR_GZ")
@@ -1175,10 +1507,11 @@ def test_make_module_step(self):
modextravars['TEST_PUSHENV'] = {'value': '123', 'pushenv': True}
modextrapaths = {
- 'PATH': ('xbin', 'pibin'),
+ 'PATH': ('', 'xbin', 'pibin'),
'CPATH': 'pi/include',
+ 'TCLLIBPATH': {'paths': 'pi', 'delimiter': ' '},
+ 'APPEND_PATH': {'paths': 'pi', 'prepend': False},
}
- modextrapaths_append = {'APPEND_PATH': 'append_path'}
self.contents = '\n'.join([
'easyblock = "ConfigureMake"',
'name = "%s"' % name,
@@ -1192,7 +1525,6 @@ def test_make_module_step(self):
"hiddendependencies = [('test', '1.2.3'), ('OpenMPI', '2.1.2-GCC-6.4.0-2.28')]",
"modextravars = %s" % str(modextravars),
"modextrapaths = %s" % str(modextrapaths),
- "modextrapaths_append = %s" % str(modextrapaths_append),
])
# test if module is generated correctly
@@ -1200,15 +1532,20 @@ def test_make_module_step(self):
ec = EasyConfig(self.eb_file)
eb = EasyBlock(ec)
eb.installdir = os.path.join(config.install_path(), 'pi', '3.14')
- eb.check_readiness_step()
- eb.make_builddir()
- eb.prepare_step()
+ with self.mocked_stdout_stderr():
+ eb.check_readiness_step()
+ eb.make_builddir()
+ eb.prepare_step()
- # Create a dummy file in bin to test if the duplicate entry of modextrapaths is ignored
- os.makedirs(os.path.join(eb.installdir, 'bin'))
- write_file(os.path.join(eb.installdir, 'bin', 'dummy_exe'), 'hello')
+ # populate install dir
+ for bin_dir in ('bin', 'xbin', 'pibin'):
+ mkdir(os.path.join(eb.installdir, bin_dir), parents=True)
+ write_file(os.path.join(eb.installdir, bin_dir, 'dummy.exe'), 'hello')
+ mkdir(os.path.join(eb.installdir, 'pi', 'include'), parents=True)
+ write_file(os.path.join(eb.installdir, 'pi', 'include', 'dummy.h'), 'hello')
- modpath = os.path.join(eb.make_module_step(), name, version)
+ with self.mocked_stdout_stderr():
+ modpath = os.path.join(eb.make_module_step(), name, version)
if get_module_syntax() == 'Lua':
modpath += '.lua'
self.assertExists(modpath)
@@ -1249,55 +1586,66 @@ def test_make_module_step(self):
self.assertTrue(regex.search(txt), "Pattern %s found in %s" % (regex.pattern, txt))
for (key, vals) in modextrapaths.items():
- if isinstance(vals, string_type):
- vals = [vals]
- for val in vals:
+ placement = 'prepend'
+ delim_tcl = ''
+ delim_lua = ''
+ if isinstance(vals, dict):
+ paths = vals['paths']
+ if isinstance(paths, str):
+ paths = [paths]
+ if 'delimiter' in vals:
+ delim_tcl = fr'+-d\s+"{vals["delimiter"]}"\s'
+ delim_lua = fr', "{vals["delimiter"]}"'
+ if 'prepend' in vals:
+ placement = 'prepend' if vals['prepend'] else 'append'
+ elif isinstance(vals, str):
+ paths = [vals]
+ else:
+ paths = vals
+
+ for val in paths:
if get_module_syntax() == 'Tcl':
- regex = re.compile(r'^prepend-path\s+%s\s+\$root/%s$' % (key, val), re.M)
+ if val == '':
+ full_val = r'\$root'
+ else:
+ full_val = fr'\$root/{val}'
+ regex = re.compile(fr'^{placement}-path\s{delim_tcl}+{key}\s+{full_val}$', re.M)
elif get_module_syntax() == 'Lua':
- regex = re.compile(r'^prepend_path\("%s", pathJoin\(root, "%s"\)\)$' % (key, val), re.M)
+ if val == '':
+ full_val = 'root'
+ else:
+ full_val = fr'pathJoin\(root, "{val}"\)'
+ regex = re.compile(fr'^{placement}_path\("{key}", {full_val}{delim_lua}\)$', re.M)
else:
- self.fail("Unknown module syntax: %s" % get_module_syntax())
- self.assertTrue(regex.search(txt), "Pattern %s found in %s" % (regex.pattern, txt))
+ self.fail(f"Unknown module syntax: {get_module_syntax()}")
+ self.assertTrue(regex.search(txt), f"Pattern {regex.pattern} found in {txt}")
# Check for duplicates
num_prepends = len(regex.findall(txt))
- self.assertEqual(num_prepends, 1, "Expected exactly 1 %s command in %s" % (regex.pattern, txt))
-
- for (key, vals) in modextrapaths_append.items():
- if isinstance(vals, string_type):
- vals = [vals]
- for val in vals:
- if get_module_syntax() == 'Tcl':
- regex = re.compile(r'^append-path\s+%s\s+\$root/%s$' % (key, val), re.M)
- elif get_module_syntax() == 'Lua':
- regex = re.compile(r'^append_path\("%s", pathJoin\(root, "%s"\)\)$' % (key, val), re.M)
- else:
- self.fail("Unknown module syntax: %s" % get_module_syntax())
- self.assertTrue(regex.search(txt), "Pattern %s found in %s" % (regex.pattern, txt))
+ self.assertEqual(num_prepends, 1, f"Expected exactly 1 {regex.pattern} command in {txt}")
for (name, ver) in [('GCC', '6.4.0-2.28')]:
if get_module_syntax() == 'Tcl':
- regex = re.compile(r'^\s*module load %s\s*$' % os.path.join(name, ver), re.M)
+ regex = re.compile(r'^\s*(module load|depends-on) %s\s*$' % os.path.join(name, ver), re.M)
elif get_module_syntax() == 'Lua':
- regex = re.compile(r'^\s*load\("%s"\)$' % os.path.join(name, ver), re.M)
+ regex = re.compile(r'^\s*(load|depends_on)\("%s"\)$' % os.path.join(name, ver), re.M)
else:
self.fail("Unknown module syntax: %s" % get_module_syntax())
self.assertTrue(regex.search(txt), "Pattern %s found in %s" % (regex.pattern, txt))
for (name, ver) in [('test', '1.2.3')]:
if get_module_syntax() == 'Tcl':
- regex = re.compile(r'^\s*module load %s/.%s\s*$' % (name, ver), re.M)
+ regex = re.compile(r'^\s*(module load|depends-on) %s/.%s\s*$' % (name, ver), re.M)
elif get_module_syntax() == 'Lua':
- regex = re.compile(r'^\s*load\("%s/.%s"\)$' % (name, ver), re.M)
+ regex = re.compile(r'^\s*(load|depends_on)\("%s/.%s"\)$' % (name, ver), re.M)
else:
self.fail("Unknown module syntax: %s" % get_module_syntax())
self.assertTrue(regex.search(txt), "Pattern %s found in %s" % (regex.pattern, txt))
for (name, ver) in [('OpenMPI', '2.1.2-GCC-6.4.0-2.28')]:
if get_module_syntax() == 'Tcl':
- regex = re.compile(r'^\s*module load %s/.?%s\s*$' % (name, ver), re.M)
+ regex = re.compile(r'^\s*(module load|depends-on) %s/.?%s\s*$' % (name, ver), re.M)
elif get_module_syntax() == 'Lua':
- regex = re.compile(r'^\s*load\("%s/.?%s"\)$' % (name, ver), re.M)
+ regex = re.compile(r'^\s*(load|depends_on)\("%s/.?%s"\)$' % (name, ver), re.M)
else:
self.fail("Unknown module syntax: %s" % get_module_syntax())
self.assertFalse(regex.search(txt), "Pattern '%s' *not* found in %s" % (regex.pattern, txt))
@@ -1324,7 +1672,8 @@ def test_make_module_step(self):
eb = EasyBlock(ec)
eb.installdir = os.path.join(config.install_path(), 'pi', '3.14')
eb.check_readiness_step()
- self.assertErrorRegex(EasyBuildError, error_pattern, eb.make_module_step)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, eb.make_module_step)
def test_gen_dirs(self):
"""Test methods that generate/set build/install directory names."""
@@ -1390,13 +1739,15 @@ def test_make_builddir(self):
eb.gen_builddir()
# by default, make_builddir will re-create the build directory (i.e. remove existing & re-create)
- eb.make_builddir()
+ with self.mocked_stdout_stderr():
+ eb.make_builddir()
builddir = eb.builddir
testfile = os.path.join(builddir, 'test123', 'foobar.txt')
write_file(testfile, 'test123')
self.assertExists(testfile)
- eb.make_builddir()
+ with self.mocked_stdout_stderr():
+ eb.make_builddir()
self.assertEqual(builddir, eb.builddir)
# file is gone because directory was removed and re-created
self.assertNotExists(testfile)
@@ -1406,7 +1757,8 @@ def test_make_builddir(self):
# make sure that build directory does *not* get re-created when we're building in installation directory
# and we're iterating over a list of (pre)config/build/installopts
eb.build_in_installdir = True
- eb.make_builddir()
+ with self.mocked_stdout_stderr():
+ eb.make_builddir()
# also need to create install directory since build dir == install dir
eb.make_installdir()
builddir = eb.builddir
@@ -1418,7 +1770,8 @@ def test_make_builddir(self):
# with iteration count > 0, build directory is not re-created because of build-in-installdir
eb.iter_idx = 1
- eb.make_builddir()
+ with self.mocked_stdout_stderr():
+ eb.make_builddir()
eb.make_installdir()
self.assertEqual(builddir, eb.builddir)
self.assertExists(testfile)
@@ -1427,7 +1780,8 @@ def test_make_builddir(self):
# resetting iteration index to 0 results in re-creating build directory
eb.iter_idx = 0
- eb.make_builddir()
+ with self.mocked_stdout_stderr():
+ eb.make_builddir()
eb.make_installdir()
self.assertEqual(builddir, eb.builddir)
self.assertNotExists(testfile)
@@ -1457,14 +1811,14 @@ def test_fetch_sources(self):
toy_source = os.path.join(testdir, 'sandbox', 'sources', 'toy', 'toy-0.0.tar.gz')
- eb.fetch_sources()
+ with self.mocked_stdout_stderr():
+ eb.fetch_sources()
self.assertEqual(len(eb.src), 1)
self.assertTrue(os.path.samefile(eb.src[0]['path'], toy_source))
self.assertEqual(eb.src[0]['name'], 'toy-0.0.tar.gz')
self.assertEqual(eb.src[0]['cmd'], None)
- self.assertEqual(len(eb.src[0]['checksum']), 7)
- self.assertEqual(eb.src[0]['checksum'][0], 'be662daa971a640e40be5c804d9d7d10')
- self.assertEqual(eb.src[0]['checksum'][1], '44332000aa33b99ad1e00cbd1a7da769220d74647060a10e807b916d73ea27bc')
+ self.assertEqual(len(eb.src[0]['checksum']), 2)
+ self.assertEqual(eb.src[0]['checksum'][0], '44332000aa33b99ad1e00cbd1a7da769220d74647060a10e807b916d73ea27bc')
# reconfigure EasyBuild so we can check 'downloaded' sources
os.environ['EASYBUILD_SOURCEPATH'] = self.test_prefix
@@ -1494,7 +1848,8 @@ def test_fetch_sources(self):
'extract_cmd': "tar xfz %s",
},
]
- eb.fetch_sources(sources, checksums=[])
+ with self.mocked_stdout_stderr():
+ eb.fetch_sources(sources, checksums=[])
toy_source_dir = os.path.join(self.test_prefix, 't', 'toy')
expected_sources = ['toy-0.0-extra.txt', 'toy-0.0_gzip.patch.gz', 'toy-0.0-renamed.tar.gz']
@@ -1527,15 +1882,54 @@ def test_fetch_sources(self):
error_pattern = "Found one or more unexpected keys in 'sources' specification: {'nosuchkey': 'foobar'}"
self.assertErrorRegex(EasyBuildError, error_pattern, eb.fetch_sources, sources, checksums=[])
+ @requires_github_access()
+ def test_fetch_sources_git(self):
+ """Test fetch_sources method from git repo."""
+
+ testdir = os.path.abspath(os.path.dirname(__file__))
+ ec = process_easyconfig(os.path.join(testdir, 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb'))[0]
+ eb = get_easyblock_instance(ec)
+ eb.src = []
+ sources = [
+ {
+ 'filename': 'testrepository.tar.xz',
+ 'git_config': {
+ 'repo_name': 'testrepository',
+ 'url': 'https://github.com/easybuilders',
+ 'tag': 'branch_tag_for_test',
+ }
+ }
+ ]
+ checksums = ["00000000"]
+
+ if sys.version_info[0] >= 3 and sys.version_info[1] < 9:
+ self.allow_deprecated_behaviour()
+
+ with self.mocked_stdout_stderr():
+ eb.fetch_sources(sources, checksums=checksums)
+
+ if sys.version_info[0] >= 3 and sys.version_info[1] < 9:
+ self.disallow_deprecated_behaviour()
+
+ self.assertEqual(len(eb.src), 1)
+ self.assertEqual(eb.src[0]['name'], "testrepository.tar.xz")
+ self.assertExists(eb.src[0]['path'])
+ self.assertEqual(eb.src[0]['cmd'], None)
+
+ reference_checksum = "00000000"
+ if sys.version_info[0] >= 3 and sys.version_info[1] < 9:
+ # checksums of tarballs made by EB cannot be reliably checked prior to Python 3.9
+ # due to changes introduced in python/cpython#90021
+ reference_checksum = None
+
+ self.assertEqual(eb.src[0]['checksum'], reference_checksum)
+
+ # cleanup
+ remove_file(eb.src[0]['path'])
+
def test_download_instructions(self):
"""Test use of download_instructions easyconfig parameter."""
- # skip test when using Python 2, since it somehow fails then,
- # cfr. https://github.com/easybuilders/easybuild-framework/pull/4333
- if sys.version_info[0] == 2:
- print("Skipping test_download_instructions because Python 2.x is being used")
- return
-
orig_test_ec = '\n'.join([
"easyblock = 'ConfigureMake'",
"name = 'software_with_missing_sources'",
@@ -1556,7 +1950,8 @@ def test_download_instructions(self):
common_error_pattern = "^Couldn't find file software_with_missing_sources-0.0.tar.gz anywhere"
error_pattern = common_error_pattern + ", and downloading it didn't work either"
- self.assertErrorRegex(EasyBuildError, error_pattern, eb.fetch_step)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, eb.fetch_step)
download_instructions = "download_instructions = 'Manual download from example.com required'"
sources = "sources = [SOURCE_TAR_GZ]"
@@ -1569,11 +1964,9 @@ def test_download_instructions(self):
self.mock_stdout(True)
self.assertErrorRegex(EasyBuildError, error_pattern, eb.fetch_step)
stderr = self.get_stderr().strip()
- stdout = self.get_stdout().strip()
self.mock_stderr(False)
- self.mock_stdout(False)
- self.assertEqual(stderr, "Download instructions:\n\nManual download from example.com required")
- self.assertEqual(stdout, '')
+ self.assertIn("Download instructions:\n\n Manual download from example.com required", stderr)
+ self.assertIn("Make the files available in the active source path", stderr)
# create dummy source file
write_file(os.path.join(os.path.dirname(self.eb_file), 'software_with_missing_sources-0.0.tar.gz'), '')
@@ -1585,11 +1978,10 @@ def test_download_instructions(self):
self.mock_stdout(True)
self.assertErrorRegex(EasyBuildError, error_pattern, eb.fetch_step)
stderr = self.get_stderr().strip()
- stdout = self.get_stdout().strip()
self.mock_stderr(False)
self.mock_stdout(False)
- self.assertEqual(stderr, "Download instructions:\n\nManual download from example.com required")
- self.assertEqual(stdout, '')
+ self.assertIn("Download instructions:\n\n Manual download from example.com required", stderr)
+ self.assertIn("Make the files available in the active source path", stderr)
# wipe top-level download instructions, try again
self.contents = self.contents.replace(download_instructions, '')
@@ -1601,10 +1993,8 @@ def test_download_instructions(self):
self.mock_stdout(True)
self.assertErrorRegex(EasyBuildError, error_pattern, eb.fetch_step)
stderr = self.get_stderr().strip()
- stdout = self.get_stdout().strip()
self.mock_stderr(False)
self.mock_stdout(False)
- self.assertEqual(stdout, '')
# inject download instructions for extension
download_instructions = ' ' * 8 + "'download_instructions': "
@@ -1618,11 +2008,10 @@ def test_download_instructions(self):
self.mock_stdout(True)
self.assertErrorRegex(EasyBuildError, error_pattern, eb.fetch_step)
stderr = self.get_stderr().strip()
- stdout = self.get_stdout().strip()
self.mock_stderr(False)
self.mock_stdout(False)
- self.assertEqual(stderr, "Download instructions:\n\nExtension sources must be downloaded via example.com")
- self.assertEqual(stdout, '')
+ self.assertIn("Download instructions:\n\n Extension sources must be downloaded via example.com", stderr)
+ self.assertIn("Make the files available in the active source path", stderr)
# download instructions should also be printed if 'source_tmpl' is used to specify extension sources
self.contents = self.contents.replace(sources, "'source_tmpl': SOURCE_TAR_GZ,")
@@ -1633,11 +2022,10 @@ def test_download_instructions(self):
self.mock_stdout(True)
self.assertErrorRegex(EasyBuildError, error_pattern, eb.fetch_step)
stderr = self.get_stderr().strip()
- stdout = self.get_stdout().strip()
self.mock_stderr(False)
self.mock_stdout(False)
- self.assertEqual(stderr, "Download instructions:\n\nExtension sources must be downloaded via example.com")
- self.assertEqual(stdout, '')
+ self.assertIn("Download instructions:\n\n Extension sources must be downloaded via example.com", stderr)
+ self.assertIn("Make the files available in the active source path", stderr)
# create dummy source file for extension
write_file(os.path.join(os.path.dirname(self.eb_file), 'ext_with_missing_sources-0.0.tar.gz'), '')
@@ -1647,11 +2035,9 @@ def test_download_instructions(self):
self.mock_stdout(True)
eb.fetch_step()
stderr = self.get_stderr().strip()
- stdout = self.get_stdout().strip()
self.mock_stderr(False)
self.mock_stdout(False)
self.assertEqual(stderr, '')
- self.assertEqual(stdout, '')
def test_fetch_patches(self):
"""Test fetch_patches method."""
@@ -1720,41 +2106,50 @@ def test_obtain_file(self):
# 'downloading' a file to (first) sourcepath works
init_config(args=["--sourcepath=%s:/no/such/dir:%s" % (tmpdir, testdir)])
shutil.copy2(toy_tarball_path, tmpdir_subdir)
- res = eb.obtain_file(toy_tarball, urls=['file://%s' % tmpdir_subdir])
+ with self.mocked_stdout_stderr():
+ res = eb.obtain_file(toy_tarball, urls=['file://%s' % tmpdir_subdir])
self.assertEqual(res, os.path.join(tmpdir, 't', 'toy', toy_tarball))
# test no_download option
urls = ['file://%s' % tmpdir_subdir]
- error_pattern = "Couldn't find file toy-0.0.tar.gz anywhere, and downloading it is disabled"
- self.assertErrorRegex(EasyBuildError, error_pattern, eb.obtain_file,
- toy_tarball, urls=urls, alt_location='alt_toy', no_download=True)
+ error_pattern = "Couldn't find file 'toy-0.0.tar.gz' anywhere, and downloading it is disabled"
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, eb.obtain_file,
+ toy_tarball, urls=urls, alt_location='alt_toy', no_download=True)
# 'downloading' a file to (first) alternative sourcepath works
- res = eb.obtain_file(toy_tarball, urls=urls, alt_location='alt_toy')
+ with self.mocked_stdout_stderr():
+ res = eb.obtain_file(toy_tarball, urls=urls, alt_location='alt_toy')
self.assertEqual(res, os.path.join(tmpdir, 'a', 'alt_toy', toy_tarball))
# make sure that directory in which easyconfig file is located is *ignored* when alt_location is used
dummy_toy_tar_gz = os.path.join(os.path.dirname(test_ec), 'toy-0.0.tar.gz')
write_file(dummy_toy_tar_gz, '')
- res = eb.obtain_file(toy_tarball, urls=['file://%s' % tmpdir_subdir])
+ with self.mocked_stdout_stderr():
+ res = eb.obtain_file(toy_tarball, urls=['file://%s' % tmpdir_subdir])
self.assertEqual(res, dummy_toy_tar_gz)
- res = eb.obtain_file(toy_tarball, urls=['file://%s' % tmpdir_subdir], alt_location='alt_toy')
+ with self.mocked_stdout_stderr():
+ res = eb.obtain_file(toy_tarball, urls=['file://%s' % tmpdir_subdir], alt_location='alt_toy')
self.assertEqual(res, os.path.join(tmpdir, 'a', 'alt_toy', toy_tarball))
remove_file(dummy_toy_tar_gz)
# finding a file in sourcepath works
init_config(args=["--sourcepath=%s:/no/such/dir:%s" % (sandbox_sources, tmpdir)])
- res = eb.obtain_file(toy_tarball)
+ with self.mocked_stdout_stderr():
+ res = eb.obtain_file(toy_tarball)
self.assertEqual(res, toy_tarball_path)
- # finding a file in the alternate location works
- res = eb.obtain_file(toy_tarball, alt_location='alt_toy')
+ # finding a file in the alternative location works
+ with self.mocked_stdout_stderr():
+ res = eb.obtain_file(toy_tarball, alt_location='alt_toy')
self.assertEqual(res, alt_toy_tarball_path)
# sourcepath has preference over downloading
- res = eb.obtain_file(toy_tarball, urls=['file://%s' % tmpdir_subdir])
+ with self.mocked_stdout_stderr():
+ res = eb.obtain_file(toy_tarball, urls=['file://%s' % tmpdir_subdir])
self.assertEqual(res, toy_tarball_path)
- res = eb.obtain_file(toy_tarball, urls=['file://%s' % tmpdir_subdir], alt_location='alt_toy')
+ with self.mocked_stdout_stderr():
+ res = eb.obtain_file(toy_tarball, urls=['file://%s' % tmpdir_subdir], alt_location='alt_toy')
self.assertEqual(res, alt_toy_tarball_path)
init_config(args=["--sourcepath=%s:%s" % (tmpdir, sandbox_sources)])
@@ -1769,10 +2164,8 @@ def test_obtain_file(self):
self.mock_stdout(True)
res = eb.obtain_file(toy_tarball, urls=['file://%s' % tmpdir_subdir], force_download=True)
stderr = self.get_stderr()
- stdout = self.get_stdout()
self.mock_stderr(False)
self.mock_stdout(False)
- self.assertEqual(stdout, '')
msg = "WARNING: Found file toy-0.0.tar.gz at %s, but re-downloading it anyway..." % toy_tarball_path
self.assertEqual(stderr.strip(), msg)
@@ -1783,13 +2176,15 @@ def test_obtain_file(self):
# obtain_file yields error for non-existing files
fn = 'thisisclearlyanonexistingfile'
error_regex = "Couldn't find file %s anywhere, and downloading it didn't work either" % fn
- self.assertErrorRegex(EasyBuildError, error_regex, eb.obtain_file, fn, urls=['file://%s' % tmpdir_subdir])
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_regex, eb.obtain_file, fn, urls=['file://%s' % tmpdir_subdir])
# also test triggering error when downloading from a URL that includes URL-encoded characters
# cfr. https://github.com/easybuilders/easybuild-framework/pull/4005
url = 'file://%s' % os.path.dirname(tmpdir_subdir)
url += '%2F' + os.path.basename(tmpdir_subdir)
- self.assertErrorRegex(EasyBuildError, error_regex, eb.obtain_file, fn, urls=[url])
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_regex, eb.obtain_file, fn, urls=[url])
# file specifications via URL also work, are downloaded to (first) sourcepath
init_config(args=["--sourcepath=%s:/no/such/dir:%s" % (tmpdir, sandbox_sources)])
@@ -1801,7 +2196,8 @@ def test_obtain_file(self):
fn = os.path.basename(file_url)
res = None
try:
- res = eb.obtain_file(file_url)
+ with self.mocked_stdout_stderr():
+ res = eb.obtain_file(file_url)
except EasyBuildError as err:
# if this fails, it should be because there's no online access
download_fail_regex = re.compile('socket error')
@@ -1843,7 +2239,8 @@ def test_fallback_source_url(self):
ec = process_easyconfig(udunits_ec)[0]
eb = EasyBlock(ec['ec'])
- eb.fetch_step()
+ with self.mocked_stdout_stderr():
+ eb.fetch_step()
expected_path = os.path.join(self.test_prefix, 'u', 'UDUNITS', 'udunits-2.2.26.tar.gz')
self.assertTrue(os.path.samefile(eb.src[0]['path'], expected_path))
@@ -1917,7 +2314,8 @@ def test_obtain_file_extension(self):
toy_ec = process_easyconfig(toy_ec_file)[0]
toy_eb = EasyBlock(toy_ec['ec'])
- toy_eb.fetch_step()
+ with self.mocked_stdout_stderr():
+ toy_eb.fetch_step()
test_ext = toy_eb.exts[-1]
test_ext_src_fn = os.path.basename(test_ext['src'])
@@ -1990,8 +2388,9 @@ def test_exclude_path_to_top_of_module_tree(self):
for ec_file, modfile_path, excluded_deps in tests:
ec = EasyConfig(os.path.join(test_ecs_path, ec_file))
eb = EasyBlock(ec)
- eb.toolchain.prepare()
- modpath = eb.make_module_step()
+ with self.mocked_stdout_stderr():
+ eb.toolchain.prepare()
+ modpath = eb.make_module_step()
modfile_path = os.path.join(modpath, modfile_path)
modtxt = read_file(modfile_path)
@@ -2032,16 +2431,18 @@ def test_patch_step(self):
# test applying patches without sources
ec['ec']['sources'] = []
eb = EasyBlock(ec['ec'])
- eb.fetch_step()
- eb.extract_step()
- self.assertErrorRegex(EasyBuildError, '.*', eb.patch_step)
+ with self.mocked_stdout_stderr():
+ eb.fetch_step()
+ eb.extract_step()
+ self.assertErrorRegex(EasyBuildError, '.*', eb.patch_step)
# test actual patching of unpacked sources
ec['ec']['sources'] = orig_sources
eb = EasyBlock(ec['ec'])
- eb.fetch_step()
- eb.extract_step()
- eb.patch_step()
+ with self.mocked_stdout_stderr():
+ eb.fetch_step()
+ eb.extract_step()
+ eb.patch_step()
# verify that patches were applied
toydir = os.path.join(eb.builddir, 'toy-0.0')
self.assertEqual(sorted(os.listdir(toydir)), ['toy-extra.txt', 'toy.source'])
@@ -2051,9 +2452,10 @@ def test_patch_step(self):
# check again with backup of patched files enabled
update_build_option('backup_patched_files', True)
eb = EasyBlock(ec['ec'])
- eb.fetch_step()
- eb.extract_step()
- eb.patch_step()
+ with self.mocked_stdout_stderr():
+ eb.fetch_step()
+ eb.extract_step()
+ eb.patch_step()
# verify that patches were applied
toydir = os.path.join(eb.builddir, 'toy-0.0')
self.assertEqual(sorted(os.listdir(toydir)), ['toy-extra.txt', 'toy.source', 'toy.source.orig'])
@@ -2086,8 +2488,9 @@ def test_extensions_sanity_check(self):
eb.silent = True
error_pattern = r"Sanity check failed: extensions sanity check failed for 1 extensions: toy\n"
error_pattern += r"failing sanity check for 'toy' extension: "
- error_pattern += r'command "thisshouldfail" failed; output:\n/bin/bash:.* thisshouldfail: command not found'
- self.assertErrorRegex(EasyBuildError, error_pattern, eb.run_all_steps, True)
+ error_pattern += r'command "thisshouldfail" failed; output:\n.* thisshouldfail: command not found'
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, eb.run_all_steps, True)
# purposely put sanity check command in place that breaks the build,
# to check whether sanity check is only run once;
@@ -2096,7 +2499,8 @@ def test_extensions_sanity_check(self):
toy_ec.update('sanity_check_commands', [("%(installdir)s/bin/toy && rm %(installdir)s/bin/toy", '')])
eb = EB_toy(toy_ec)
eb.silent = True
- eb.run_all_steps(True)
+ with self.mocked_stdout_stderr():
+ eb.run_all_steps(True)
def test_parallel(self):
"""Test defining of parallelism."""
@@ -2106,56 +2510,204 @@ def test_parallel(self):
handle, toy_ec1 = tempfile.mkstemp(prefix='easyblock_test_file_', suffix='.eb')
os.close(handle)
- write_file(toy_ec1, toytxt + "\nparallel = 123")
+ write_file(toy_ec1, toytxt + "\nparallel = 13")
handle, toy_ec2 = tempfile.mkstemp(prefix='easyblock_test_file_', suffix='.eb')
os.close(handle)
- write_file(toy_ec2, toytxt + "\nparallel = 123\nmaxparallel = 67")
+ write_file(toy_ec2, toytxt + "\nparallel = 12\nmaxparallel = 6")
handle, toy_ec3 = tempfile.mkstemp(prefix='easyblock_test_file_', suffix='.eb')
os.close(handle)
write_file(toy_ec3, toytxt + "\nparallel = False")
+ handle, toy_ec4 = tempfile.mkstemp(prefix='easyblock_test_file_', suffix='.eb')
+ os.close(handle)
+ write_file(toy_ec4, toytxt + "\nmaxparallel = 6")
+
+ handle, toy_ec5 = tempfile.mkstemp(prefix='easyblock_test_file_', suffix='.eb')
+ os.close(handle)
+ write_file(toy_ec5, toytxt + "\nmaxparallel = False")
+
# default: parallelism is derived from # available cores + ulimit
- test_eb = EasyBlock(EasyConfig(toy_ec))
- test_eb.check_readiness_step()
- self.assertTrue(isinstance(test_eb.cfg['parallel'], int) and test_eb.cfg['parallel'] > 0)
-
- # only 'parallel' easyconfig parameter specified (no 'parallel' build option)
- test_eb = EasyBlock(EasyConfig(toy_ec1))
- test_eb.check_readiness_step()
- self.assertEqual(test_eb.cfg['parallel'], 123)
-
- # both 'parallel' and 'maxparallel' easyconfig parameters specified (no 'parallel' build option)
- test_eb = EasyBlock(EasyConfig(toy_ec2))
- test_eb.check_readiness_step()
- self.assertEqual(test_eb.cfg['parallel'], 67)
-
- # make sure 'parallel = False' is not overriden (no 'parallel' build option)
- test_eb = EasyBlock(EasyConfig(toy_ec3))
- test_eb.check_readiness_step()
- self.assertEqual(test_eb.cfg['parallel'], False)
-
- # only 'parallel' build option specified
- init_config(build_options={'parallel': '97', 'validate': False})
- test_eb = EasyBlock(EasyConfig(toy_ec))
- test_eb.check_readiness_step()
- self.assertEqual(test_eb.cfg['parallel'], 97)
-
- # both 'parallel' build option and easyconfig parameter specified (no 'maxparallel')
- test_eb = EasyBlock(EasyConfig(toy_ec1))
- test_eb.check_readiness_step()
- self.assertEqual(test_eb.cfg['parallel'], 97)
-
- # both 'parallel' and 'maxparallel' easyconfig parameters specified + 'parallel' build option
- test_eb = EasyBlock(EasyConfig(toy_ec2))
- test_eb.check_readiness_step()
- self.assertEqual(test_eb.cfg['parallel'], 67)
-
- # make sure 'parallel = False' is not overriden (with 'parallel' build option)
- test_eb = EasyBlock(EasyConfig(toy_ec3))
- test_eb.check_readiness_step()
- self.assertEqual(test_eb.cfg['parallel'], 0)
+ # Note that --max-parallel has a default of 16, so we need a lower auto_parallel value here
+ auto_parallel = 16 - 4 # Using + 3 below which must still be less
+ st.det_parallelism._default_parallelism = auto_parallel
+
+ # 'parallel' build option NOT specified
+ test_cases = {
+ '': auto_parallel,
+ 'parallel = False': 1,
+ 'parallel = 1': 1,
+ 'parallel = 6': 6,
+ f'parallel = {auto_parallel + 3}': auto_parallel + 3, # Setting parallel disables auto-detection
+ 'maxparallel = False': 1,
+ 'maxparallel = 1': 1,
+ 'maxparallel = 6': 6,
+ f'maxparallel = {auto_parallel + 3}': auto_parallel,
+ 'parallel = 8\nmaxparallel = 6': 6,
+ 'parallel = 8\nmaxparallel = 9': 8,
+ 'parallel = False\nmaxparallel = 6': 1,
+ 'parallel = 8\nmaxparallel = False': 1,
+ }
+
+ for txt, expected in test_cases.items():
+ with self.subTest(ec_params=txt):
+ self.contents = toytxt + '\n' + txt
+ self.writeEC()
+ with self.temporarily_allow_deprecated_behaviour(), self.mocked_stdout_stderr():
+ test_eb = EasyBlock(EasyConfig(self.eb_file))
+ test_eb.post_init()
+ self.assertEqual(test_eb.cfg.parallel, expected)
+ with self.temporarily_allow_deprecated_behaviour(), self.mocked_stdout_stderr():
+ self.assertEqual(test_eb.cfg['parallel'], expected)
+
+ # 'parallel' build option specified
+ buildopt_parallel = 11
+ # When build option is given the auto-parallelism is ignored. Verify by setting it very low
+ st.det_parallelism._default_parallelism = 2
+ init_config(build_options={
+ 'parallel': str(buildopt_parallel),
+ 'validate': False,
+ })
+
+ test_cases = {
+ '': buildopt_parallel,
+ 'parallel = False': 1,
+ 'parallel = 1': 1,
+ 'parallel = 6': 6,
+ f'parallel = {buildopt_parallel + 2}': buildopt_parallel,
+ 'maxparallel = False': 1,
+ 'maxparallel = 1': 1,
+ 'maxparallel = 6': 6,
+ f'maxparallel = {buildopt_parallel + 2}': buildopt_parallel,
+ 'parallel = 8\nmaxparallel = 6': 6,
+ 'parallel = 8\nmaxparallel = 9': 8,
+ 'parallel = False\nmaxparallel = 6': 1,
+ 'parallel = 8\nmaxparallel = False': 1,
+ }
+
+ for txt, expected in test_cases.items():
+ with self.subTest(ec_params=txt):
+ self.contents = toytxt + '\n' + txt
+ self.writeEC()
+ with self.temporarily_allow_deprecated_behaviour(), self.mocked_stdout_stderr():
+ test_eb = EasyBlock(EasyConfig(self.eb_file))
+ test_eb.post_init()
+ self.assertEqual(test_eb.cfg.parallel, expected)
+ with self.temporarily_allow_deprecated_behaviour(), self.mocked_stdout_stderr():
+ self.assertEqual(test_eb.cfg['parallel'], expected)
+
+ # re-check when --max-parallel is used instead
+ buildopt_max_parallel = 8
+ st.det_parallelism._default_parallelism = 16
+ init_config(build_options={
+ 'max_parallel': buildopt_max_parallel,
+ 'validate': False,
+ })
+
+ test_cases = {
+ '': buildopt_max_parallel,
+ 'parallel = False': 1,
+ 'parallel = 1': 1,
+ 'parallel = 6': 6,
+ # --max-parallel value limits max. parallelism, so only 8 cores will be used when 'parallel = 10' is used
+ f'parallel = {buildopt_max_parallel + 2}': buildopt_max_parallel,
+ 'maxparallel = False': 1,
+ 'maxparallel = 1': 1,
+ 'maxparallel = 6': 6,
+ # minimum of 'maxparallel' easyconfig parameter and --max-parallel configuration option is used
+ f'maxparallel = {buildopt_max_parallel + 2}': buildopt_max_parallel,
+ 'parallel = 8\nmaxparallel = 6': 6,
+ 'parallel = 8\nmaxparallel = 9': 8,
+ 'parallel = False\nmaxparallel = 6': 1,
+ 'parallel = 8\nmaxparallel = False': 1,
+ }
+
+ for txt, expected in test_cases.items():
+ with self.subTest(ec_params=txt):
+ self.contents = toytxt + '\n' + txt
+ self.writeEC()
+ with self.temporarily_allow_deprecated_behaviour(), self.mocked_stdout_stderr():
+ test_eb = EasyBlock(EasyConfig(self.eb_file))
+ test_eb.post_init()
+ self.assertEqual(test_eb.cfg.parallel, expected)
+ with self.temporarily_allow_deprecated_behaviour(), self.mocked_stdout_stderr():
+ self.assertEqual(test_eb.cfg['parallel'], expected)
+
+ # re-check when both --max-parallel and --parallel are used (--max-parallel wins)
+ init_config(build_options={
+ 'max_parallel': buildopt_max_parallel,
+ 'parallel': buildopt_parallel,
+ 'validate': False,
+ })
+
+ for txt, expected in test_cases.items():
+ with self.subTest(ec_params=txt):
+ self.contents = toytxt + '\n' + txt
+ self.writeEC()
+ with self.temporarily_allow_deprecated_behaviour(), self.mocked_stdout_stderr():
+ test_eb = EasyBlock(EasyConfig(self.eb_file))
+ test_eb.post_init()
+ self.assertEqual(test_eb.cfg.parallel, expected)
+ with self.temporarily_allow_deprecated_behaviour(), self.mocked_stdout_stderr():
+ self.assertEqual(test_eb.cfg['parallel'], expected)
+
+ # Template updated correctly
+ self.contents = toytxt + '\nmaxparallel=2'
+ self.writeEC()
+ test_eb = EasyBlock(EasyConfig(self.eb_file))
+ test_eb.post_init()
+
+ test_eb.cfg['buildopts'] = '-j %(parallel)s'
+ self.assertEqual(test_eb.cfg['buildopts'], '-j 2')
+ # Might be done in an easyblock step
+ test_eb.cfg.parallel = 42
+ self.assertEqual(test_eb.cfg['buildopts'], '-j 42')
+ # Unaffected by build settings
+ test_eb.cfg.parallel = 421337
+ self.assertEqual(test_eb.cfg['buildopts'], '-j 421337')
+ # False is equal to 1
+ test_eb.cfg.parallel = False
+ self.assertEqual(test_eb.cfg['buildopts'], '-j 1')
+
+ # Legacy behavior. To be removed after deprecation of the parallel EC parameter
+ self.contents = toytxt + '\nmaxparallel=99'
+ self.writeEC()
+ with self.temporarily_allow_deprecated_behaviour(), self.mocked_stdout_stderr():
+ test_eb = EasyBlock(EasyConfig(self.eb_file))
+ parallel = buildopt_max_parallel - 2
+ test_eb.cfg['parallel'] = parallel # Old Easyblocks might change that before the ready step
+ test_eb.post_init()
+ self.assertEqual(test_eb.cfg.parallel, parallel)
+ self.assertEqual(test_eb.cfg['parallel'], parallel)
+ # Afterwards it also gets reflected directly ignoring maxparallel
+ parallel = buildopt_max_parallel * 3
+ test_eb.cfg['parallel'] = parallel
+ self.assertEqual(test_eb.cfg.parallel, parallel)
+ self.assertEqual(test_eb.cfg['parallel'], parallel)
+
+ # Reset mocked value
+ del st.det_parallelism._default_parallelism
+
+ def test_keepsymlinks(self):
+ """Test keepsymlinks parameter (default: True)."""
+ topdir = os.path.abspath(os.path.dirname(__file__))
+ toy_ec = os.path.join(topdir, 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb')
+ toytxt = read_file(toy_ec)
+
+ test_cases = {
+ '': True,
+ 'keepsymlinks = False': False,
+ 'keepsymlinks = True': True,
+ }
+
+ for txt, expected in test_cases.items():
+ with self.subTest(ec_params=txt):
+ self.contents = toytxt + '\n' + txt
+ self.writeEC()
+ test_eb = EasyBlock(EasyConfig(self.eb_file))
+ test_eb.post_init()
+ self.assertEqual(test_eb.cfg['keepsymlinks'], expected)
def test_guess_start_dir(self):
"""Test guessing the start dir."""
@@ -2172,7 +2724,8 @@ def check_start_dir(expected_start_dir):
eb = EasyBlock(ec['ec'])
eb.silent = True
eb.cfg['stop'] = 'patch'
- eb.run_all_steps(False)
+ with self.mocked_stdout_stderr():
+ eb.run_all_steps(False)
eb.guess_start_dir()
abs_expected_start_dir = os.path.join(eb.builddir, expected_start_dir)
self.assertTrue(os.path.samefile(eb.cfg['start_dir'], abs_expected_start_dir))
@@ -2213,7 +2766,7 @@ def check_ext_start_dir(expected_start_dir, unpack_src=True, parent_startdir=Non
eb.extensions_step(fetch=True, install=False)
# extract sources of the extension
ext = eb.ext_instances[-1]
- ext.run(unpack_src=unpack_src)
+ ext.install_extension(unpack_src=unpack_src)
if expected_start_dir is None:
self.assertIsNone(ext.start_dir)
@@ -2342,14 +2895,16 @@ def test_prepare_step_load_tc_deps_modules(self):
os.environ['EBROOTHWLOC'] = self.test_prefix
# loaded of modules for toolchain + dependencies can be disabled via load_tc_deps_modules=False
- eb.prepare_step(load_tc_deps_modules=False)
+ with self.mocked_stdout_stderr():
+ eb.prepare_step(load_tc_deps_modules=False)
self.assertEqual(self.modtool.list(), [])
del os.environ['EBROOTGCC']
del os.environ['EBROOTHWLOC']
# modules for toolchain + dependencies are still loaded by default
- eb.prepare_step()
+ with self.mocked_stdout_stderr():
+ eb.prepare_step()
loaded_modules = self.modtool.list()
self.assertEqual(len(loaded_modules), 2)
self.assertEqual(loaded_modules[0]['mod_name'], 'GCC/6.4.0-2.28')
@@ -2388,7 +2943,8 @@ def test_prepare_step_hmns(self):
eb = EasyBlock(test_ec['ec'])
mkdir(os.path.join(self.test_buildpath, 'toy', '0.0', 'system-system'), parents=True)
- eb.prepare_step()
+ with self.mocked_stdout_stderr():
+ eb.prepare_step()
loaded_modules = self.modtool.list()
self.assertEqual(len(loaded_modules), 1)
@@ -2404,7 +2960,8 @@ def test_prepare_step_cuda_cache(self):
ec = process_easyconfig(toy_ec)[0]
eb = EasyBlock(ec['ec'])
eb.silent = True
- eb.make_builddir()
+ with self.mocked_stdout_stderr():
+ eb.make_builddir()
eb.prepare_step(start_dir=False)
logtxt = read_file(eb.logfile)
@@ -2419,10 +2976,12 @@ def test_prepare_step_cuda_cache(self):
ec = process_easyconfig(test_ec)[0]
eb = EasyBlock(ec['ec'])
eb.silent = True
- eb.make_builddir()
+ with self.mocked_stdout_stderr():
+ eb.make_builddir()
write_file(eb.logfile, '')
- eb.prepare_step(start_dir=False)
+ with self.mocked_stdout_stderr():
+ eb.prepare_step(start_dir=False)
logtxt = read_file(eb.logfile)
self.assertNotIn('Disabling CUDA PTX cache', logtxt)
self.assertIn('Enabling CUDA PTX cache', logtxt)
@@ -2430,7 +2989,8 @@ def test_prepare_step_cuda_cache(self):
init_config(build_options={'cuda_cache_maxsize': 0}) # Disable
write_file(eb.logfile, '')
- eb.prepare_step(start_dir=False)
+ with self.mocked_stdout_stderr():
+ eb.prepare_step(start_dir=False)
logtxt = read_file(eb.logfile)
self.assertIn('Disabling CUDA PTX cache', logtxt)
self.assertNotIn('Enabling CUDA PTX cache', logtxt)
@@ -2440,7 +3000,8 @@ def test_prepare_step_cuda_cache(self):
cuda_cache_dir = os.path.join(self.test_prefix, 'custom-cuda-cache')
init_config(build_options={'cuda_cache_maxsize': 1234, 'cuda_cache_dir': cuda_cache_dir})
write_file(eb.logfile, '')
- eb.prepare_step(start_dir=False)
+ with self.mocked_stdout_stderr():
+ eb.prepare_step(start_dir=False)
logtxt = read_file(eb.logfile)
self.assertNotIn('Disabling CUDA PTX cache', logtxt)
self.assertIn('Enabling CUDA PTX cache', logtxt)
@@ -2485,8 +3046,8 @@ def test_checksum_step(self):
copy_file(toy_ec, self.test_prefix)
toy_ec = os.path.join(self.test_prefix, os.path.basename(toy_ec))
ectxt = read_file(toy_ec)
- # replace MD5 checksum for toy-0.0.tar.gz
- ectxt = ectxt.replace('be662daa971a640e40be5c804d9d7d10', '00112233445566778899aabbccddeeff')
+ # replace SHA256 checksum for toy-0.0.tar.gz
+ ectxt = ectxt.replace('44332000aa33b99ad1e00cbd1a7da769220d74647060a10e807b916d73ea27bc', '76543210' * 8)
# replace SHA256 checksums for source of bar extension
ectxt = ectxt.replace('f3676716b610545a4e8035087f5be0a0248adee0abb3930d3edb76d498ae91e7', '01234567' * 8)
write_file(toy_ec, ectxt)
@@ -2501,13 +3062,6 @@ def test_checksum_step(self):
error_msg = "Checksum verification for extension source bar-0.0.tar.gz failed"
self.assertErrorRegex(EasyBuildError, error_msg, eb.collect_exts_file_info)
- # also check with deprecated fetch_extension_sources method
- self.allow_deprecated_behaviour()
- self.mock_stderr(True)
- self.assertErrorRegex(EasyBuildError, error_msg, eb.fetch_extension_sources)
- self.mock_stderr(False)
- self.disallow_deprecated_behaviour()
-
# create test easyconfig from which checksums have been stripped
test_ec = os.path.join(self.test_prefix, 'test.eb')
ectxt = read_file(toy_ec)
@@ -2519,8 +3073,8 @@ def test_checksum_step(self):
# make sure that test easyconfig file indeed doesn't contain any checksums (either top-level or for extensions)
self.assertEqual(ec_json['ec']['checksums'], [])
- for ext in ec_json['ec']['exts_list']:
- if isinstance(ext, string_type):
+ for ext in ec_json['ec'].get_ref('exts_list'):
+ if isinstance(ext, str):
continue
elif isinstance(ext, tuple):
self.assertEqual(ext[2].get('checksums', []), [])
@@ -2555,17 +3109,10 @@ def test_checksum_step(self):
error_msg = "Checksum verification for .*/toy-0.0.tar.gz using .* failed"
self.assertErrorRegex(EasyBuildError, error_msg, eb.checksum_step)
- # also check verification of checksums for extensions, which is part of fetch_extension_sources
+ # also check verification of checksums for extensions, which is part of collect_exts_file_info
error_msg = "Checksum verification for extension source bar-0.0.tar.gz failed"
self.assertErrorRegex(EasyBuildError, error_msg, eb.collect_exts_file_info)
- # also check with deprecated fetch_extension_sources method
- self.allow_deprecated_behaviour()
- self.mock_stderr(True)
- self.assertErrorRegex(EasyBuildError, error_msg, eb.fetch_extension_sources)
- self.mock_stderr(False)
- self.disallow_deprecated_behaviour()
-
# if --ignore-checksums is enabled, faulty checksums are reported but otherwise ignored (no error)
build_options = {
'ignore_checksums': True,
@@ -2590,20 +3137,8 @@ def test_checksum_step(self):
self.mock_stderr(False)
self.mock_stdout(False)
self.assertEqual(stdout, '')
- self.assertEqual(stderr.strip(), "WARNING: Ignoring failing checksum verification for bar-0.0.tar.gz")
-
- # also check with deprecated fetch_extension_sources method
- self.allow_deprecated_behaviour()
- self.mock_stderr(True)
- self.mock_stdout(True)
- eb.fetch_extension_sources()
- stderr = self.get_stderr()
- stdout = self.get_stdout()
- self.mock_stderr(False)
- self.mock_stdout(False)
- self.assertEqual(stdout, '')
- self.assertTrue(stderr.strip().endswith("WARNING: Ignoring failing checksum verification for bar-0.0.tar.gz"))
- self.disallow_deprecated_behaviour()
+ self.assertEqual(stderr.strip(), "WARNING: Ignoring failing checksum verification for bar-0.0.tar.gz\n\n\n"
+ "WARNING: Ignoring failing checksum verification for toy-0.0.tar.gz")
def test_check_checksums(self):
"""Test for check_checksums_for and check_checksums methods."""
@@ -2659,10 +3194,10 @@ def run_checks():
# no checksum issues
self.assertEqual(eb.check_checksums(), [])
- # tuple of two alternate SHA256 checksums: OK
+ # tuple of two alternative SHA256 checksums: OK
eb.cfg['checksums'] = [
(
- # two alternate checksums for toy-0.0.tar.gz
+ # two alternative checksums for toy-0.0.tar.gz
'a2848f34fcd5d6cf47def00461fcb528a0484d8edef8208d6d2e2909dc61d9cd',
'44332000aa33b99ad1e00cbd1a7da769220d74647060a10e807b916d73ea27bc',
),
@@ -2713,6 +3248,20 @@ def run_checks():
eb.json_checksums = None
self.assertEqual(eb.check_checksums(), [])
+ # more checks for check_checksums_for method, which also takes regular dict as input
+ self.assertEqual(eb.check_checksums_for({}), [])
+ expected = "Checksums missing for one or more sources/patches in test.eb: "
+ expected += "found 1 sources + 0 patches vs 0 checksums"
+ self.assertEqual(eb.check_checksums_for({'sources': ['test.tar.gz']}), [expected])
+
+ # example from QuantumESPRESSO easyconfig, template used in extract_cmd should not cause trouble
+ eb.cfg['sources'] = [{
+ 'filename': 'q-e-qe-%(version)s.tar.gz',
+ 'extract_cmd': 'mkdir -p %(builddir)s/qe-%(version)s && tar xzvf %s --strip-components=1 -C $_',
+ 'source_urls': ['https://gitlab.com/QEF/q-e/-/archive/qe-%(version)s'],
+ }]
+ res = eb.check_checksums_for(eb.cfg)
+
def test_this_is_easybuild(self):
"""Test 'this_is_easybuild' function (and get_git_revision function used by it)."""
# make sure both return a non-Unicode string
@@ -2771,7 +3320,8 @@ def test_stale_module_caches(self):
# installing both one.eb and two.eb in one go should work
# this verifies whether the "module show" cache is cleared in between builds,
# since one/1.0 is required for ec2, and the underlying one/1.0.2 is installed via ec1 in the same session
- self.eb_main([ec1, ec2], raise_error=True, do_build=True, verbose=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main([ec1, ec2], raise_error=True, do_build=True, verbose=True)
def test_avail_easyblocks(self):
"""Test avail_easyblocks function."""
@@ -2895,6 +3445,175 @@ def run_sanity_check_step(sanity_check_paths, enhance_sanity_check):
run_sanity_check_step({}, False)
run_sanity_check_step({}, True)
+ def test_create_easyblock_without_logfile(self):
+ """
+ Test creating an EasyBlock without a logfile.
+ This represents scenarios found in Bundle and QuantumESPRESSO, where an EasyBlock is
+ created within another EasyBlock.
+ """
+ self.contents = '\n'.join([
+ 'easyblock = "ConfigureMake"',
+ 'name = "pi"',
+ 'version = "3.14"',
+ 'homepage = "http://example.com"',
+ 'description = "test easyconfig"',
+ 'toolchain = SYSTEM',
+ ])
+ self.writeEC()
+ # Ensure that the default case works as expected
+ eb = EasyBlock(EasyConfig(self.eb_file))
+ self.assertNotEqual(eb.log, None)
+ self.assertNotEqual(eb.logfile, None)
+ # Get reference to the actual log instance and ensure that it works
+ # This is NOT eb.log, which represents a separate logger with a separate name.
+ file_log = fancylogger.getLogger(name=None)
+ self.assertNotEqual(getattr(file_log, 'logtofile_%s' % eb.logfile), False)
+
+ # Now, create another EasyBlock by passing logfile from first EasyBlock.
+ eb_external_logfile = EasyBlock(EasyConfig(self.eb_file), logfile=eb.logfile)
+ self.assertNotEqual(eb_external_logfile.log, None)
+ self.assertTrue(eb_external_logfile.external_logfile)
+ self.assertEqual(eb_external_logfile.logfile, eb.logfile)
+ # Try to log something in it.
+ eb_external_logfile.log.info("Test message")
+
+ # Try to close EasyBlock with external logfile. This should not affect the logger.
+ eb_external_logfile.close_log()
+ self.assertNotEqual(getattr(file_log, 'logtofile_%s' % eb.logfile), False)
+ # Then close the log from creating EasyBlock. This should work as expected.
+ eb.close_log()
+ self.assertEqual(getattr(file_log, 'logtofile_%s' % eb.logfile), False)
+
+ os.remove(eb.logfile)
+
+ def test_expand_module_search_path(self):
+ """Testcase for expand_module_search_path"""
+ top_dir = os.path.abspath(os.path.dirname(__file__))
+ toy_ec = os.path.join(top_dir, 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb')
+ eb = EasyBlock(EasyConfig(toy_ec))
+ eb.installdir = config.install_path()
+ test_emsp = eb.expand_module_search_path # shortcut
+
+ # create test directories and files
+ os.makedirs(eb.installdir)
+ test_directories = (
+ 'empty_dir',
+ 'dir_empty_subdir',
+ ('dir_empty_subdir', 'empty_subdir'),
+ 'dir_with_file',
+ 'dir_full_subdirs',
+ ('dir_full_subdirs', 'subdir1'),
+ ('dir_full_subdirs', 'subdir2'),
+ )
+ for path in test_directories:
+ path_components = (path, ) if isinstance(path, str) else path
+ os.mkdir(os.path.join(eb.installdir, *path_components))
+
+ write_file(os.path.join(eb.installdir, 'dir_with_file', 'file.txt'), 'test file')
+ write_file(os.path.join(eb.installdir, 'dir_full_subdirs', 'subdir1', 'file11.txt'), 'test file 1.1')
+ write_file(os.path.join(eb.installdir, 'dir_full_subdirs', 'subdir1', 'file12.txt'), 'test file 1.2')
+ write_file(os.path.join(eb.installdir, 'dir_full_subdirs', 'subdir2', 'file21.txt'), 'test file 2.1')
+
+ self.assertEqual(test_emsp("nonexistent", ModEnvVarType.PATH), [])
+ self.assertEqual(test_emsp("nonexistent", ModEnvVarType.PATH_WITH_FILES), [])
+ self.assertEqual(test_emsp("nonexistent", ModEnvVarType.PATH_WITH_TOP_FILES), [])
+ self.assertEqual(test_emsp("empty_dir", ModEnvVarType.PATH), ["empty_dir"])
+ self.assertEqual(test_emsp("empty_dir", ModEnvVarType.PATH_WITH_FILES), [])
+ self.assertEqual(test_emsp("empty_dir", ModEnvVarType.PATH_WITH_TOP_FILES), [])
+ self.assertEqual(test_emsp("dir_empty_subdir", ModEnvVarType.PATH), ["dir_empty_subdir"])
+ self.assertEqual(test_emsp("dir_empty_subdir", ModEnvVarType.PATH_WITH_FILES), [])
+ self.assertEqual(test_emsp("dir_empty_subdir", ModEnvVarType.PATH_WITH_TOP_FILES), [])
+ self.assertEqual(test_emsp("dir_with_file", ModEnvVarType.PATH), ["dir_with_file"])
+ self.assertEqual(test_emsp("dir_with_file", ModEnvVarType.PATH_WITH_FILES), ["dir_with_file"])
+ self.assertEqual(test_emsp("dir_with_file", ModEnvVarType.PATH_WITH_TOP_FILES), ["dir_with_file"])
+ self.assertEqual(test_emsp("dir_full_subdirs", ModEnvVarType.PATH), ["dir_full_subdirs"])
+ self.assertEqual(test_emsp("dir_full_subdirs", ModEnvVarType.PATH_WITH_FILES), ["dir_full_subdirs"])
+ self.assertEqual(test_emsp("dir_full_subdirs", ModEnvVarType.PATH_WITH_TOP_FILES), [])
+
+ # test globs
+ ref_expanded_paths = ["dir_empty_subdir/empty_subdir"]
+ self.assertEqual(test_emsp("dir_empty_subdir/*", ModEnvVarType.PATH), ref_expanded_paths)
+ self.assertEqual(test_emsp("dir_empty_subdir/*", ModEnvVarType.PATH_WITH_FILES), [])
+ self.assertEqual(test_emsp("dir_empty_subdir/*", ModEnvVarType.PATH_WITH_TOP_FILES), [])
+ ref_expanded_paths = ["dir_full_subdirs/subdir1", "dir_full_subdirs/subdir2"]
+ self.assertEqual(sorted(test_emsp("dir_full_subdirs/*", ModEnvVarType.PATH)), ref_expanded_paths)
+ self.assertEqual(sorted(test_emsp("dir_full_subdirs/*", ModEnvVarType.PATH_WITH_FILES)), ref_expanded_paths)
+ self.assertEqual(sorted(test_emsp("dir_full_subdirs/*", ModEnvVarType.PATH_WITH_TOP_FILES)), ref_expanded_paths)
+ ref_expanded_paths = ["dir_full_subdirs/subdir2/file21.txt"]
+ self.assertEqual(test_emsp("dir_full_subdirs/subdir2/*", ModEnvVarType.PATH), ref_expanded_paths)
+ self.assertEqual(test_emsp("dir_full_subdirs/subdir2/*", ModEnvVarType.PATH_WITH_FILES), ref_expanded_paths)
+ self.assertEqual(test_emsp("dir_full_subdirs/subdir2/*", ModEnvVarType.PATH_WITH_TOP_FILES), ref_expanded_paths)
+ self.assertEqual(test_emsp("nonexistent/*", True), [])
+ self.assertEqual(test_emsp("nonexistent/*", ModEnvVarType.PATH), [])
+ self.assertEqual(test_emsp("nonexistent/*", ModEnvVarType.PATH_WITH_FILES), [])
+ self.assertEqual(test_emsp("nonexistent/*", ModEnvVarType.PATH_WITH_TOP_FILES), [])
+
+ # test just one lib directory
+ os.mkdir(os.path.join(eb.installdir, "lib"))
+ self.assertEqual(test_emsp("lib", ModEnvVarType.PATH), ["lib"])
+ self.assertEqual(test_emsp("lib", ModEnvVarType.PATH_WITH_FILES), [])
+ self.assertEqual(test_emsp("lib", ModEnvVarType.PATH_WITH_TOP_FILES), [])
+ write_file(os.path.join(eb.installdir, "lib", "libtest.so"), "not actually a lib")
+ self.assertEqual(test_emsp("lib", ModEnvVarType.PATH), ["lib"])
+ self.assertEqual(test_emsp("lib", ModEnvVarType.PATH_WITH_FILES), ["lib"])
+ self.assertEqual(test_emsp("lib", ModEnvVarType.PATH_WITH_TOP_FILES), ["lib"])
+
+ # test both lib and lib64 directories
+ os.mkdir(os.path.join(eb.installdir, "lib64"))
+ self.assertEqual(sorted(test_emsp("lib*", ModEnvVarType.PATH)), ["lib", "lib64"])
+ self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH_WITH_FILES), ["lib"])
+ self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH_WITH_TOP_FILES), ["lib"])
+ write_file(os.path.join(eb.installdir, "lib64", "libtest.so"), "not actually a lib")
+ self.assertEqual(sorted(test_emsp("lib*", ModEnvVarType.PATH)), ["lib", "lib64"])
+ self.assertEqual(sorted(test_emsp("lib*", ModEnvVarType.PATH_WITH_FILES)), ["lib", "lib64"])
+ self.assertEqual(sorted(test_emsp("lib*", ModEnvVarType.PATH_WITH_TOP_FILES)), ["lib", "lib64"])
+
+ # test lib64 symlinked to lib
+ remove_dir(os.path.join(eb.installdir, "lib64"))
+ os.symlink("lib", os.path.join(eb.installdir, "lib64"))
+ self.assertEqual(test_emsp("lib", ModEnvVarType.PATH), ["lib"])
+ self.assertEqual(test_emsp("lib", ModEnvVarType.PATH_WITH_FILES), ["lib"])
+ self.assertEqual(test_emsp("lib", ModEnvVarType.PATH_WITH_TOP_FILES), ["lib"])
+ self.assertEqual(test_emsp("lib64", ModEnvVarType.PATH), ["lib"])
+ self.assertEqual(test_emsp("lib64", ModEnvVarType.PATH_WITH_FILES), ["lib"])
+ self.assertEqual(test_emsp("lib64", ModEnvVarType.PATH_WITH_TOP_FILES), ["lib"])
+ self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH), ["lib", "lib"])
+ self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH_WITH_FILES), ["lib", "lib"])
+ self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH_WITH_TOP_FILES), ["lib", "lib"])
+
+ # test lib symlinked to lib64
+ remove_dir(os.path.join(eb.installdir, "lib"))
+ remove_file(os.path.join(eb.installdir, "lib64"))
+ os.mkdir(os.path.join(eb.installdir, "lib64"))
+ write_file(os.path.join(eb.installdir, "lib64", "libtest.so"), "not actually a lib")
+ os.symlink("lib64", os.path.join(eb.installdir, "lib"))
+ self.assertEqual(test_emsp("lib", ModEnvVarType.PATH), ["lib64"])
+ self.assertEqual(test_emsp("lib", ModEnvVarType.PATH_WITH_FILES), ["lib64"])
+ self.assertEqual(test_emsp("lib", ModEnvVarType.PATH_WITH_TOP_FILES), ["lib64"])
+ self.assertEqual(test_emsp("lib64", ModEnvVarType.PATH), ["lib64"])
+ self.assertEqual(test_emsp("lib64", ModEnvVarType.PATH_WITH_FILES), ["lib64"])
+ self.assertEqual(test_emsp("lib64", ModEnvVarType.PATH_WITH_TOP_FILES), ["lib64"])
+ self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH), ["lib64", "lib64"])
+ self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH_WITH_FILES), ["lib64", "lib64"])
+ self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH_WITH_TOP_FILES), ["lib64", "lib64"])
+
+ # test both lib and lib64 symlinked to some other folder
+ remove_dir(os.path.join(eb.installdir, "lib64"))
+ remove_file(os.path.join(eb.installdir, "lib"))
+ os.mkdir(os.path.join(eb.installdir, "some_dir"))
+ write_file(os.path.join(eb.installdir, "some_dir", "libtest.so"), "not actually a lib")
+ os.symlink("some_dir", os.path.join(eb.installdir, "lib"))
+ os.symlink("some_dir", os.path.join(eb.installdir, "lib64"))
+ self.assertEqual(test_emsp("lib", ModEnvVarType.PATH), ["some_dir"])
+ self.assertEqual(test_emsp("lib", ModEnvVarType.PATH_WITH_FILES), ["some_dir"])
+ self.assertEqual(test_emsp("lib", ModEnvVarType.PATH_WITH_TOP_FILES), ["some_dir"])
+ self.assertEqual(test_emsp("lib64", ModEnvVarType.PATH), ["some_dir"])
+ self.assertEqual(test_emsp("lib64", ModEnvVarType.PATH_WITH_FILES), ["some_dir"])
+ self.assertEqual(test_emsp("lib64", ModEnvVarType.PATH_WITH_TOP_FILES), ["some_dir"])
+ self.assertEqual(sorted(test_emsp("lib*", ModEnvVarType.PATH)), ["some_dir", "some_dir"])
+ self.assertEqual(sorted(test_emsp("lib*", ModEnvVarType.PATH_WITH_FILES)), ["some_dir", "some_dir"])
+ self.assertEqual(sorted(test_emsp("lib*", ModEnvVarType.PATH_WITH_TOP_FILES)), ["some_dir", "some_dir"])
+
def suite():
""" return all the tests in this file """
diff --git a/test/framework/easyconfig.py b/test/framework/easyconfig.py
index c43c84be0e..21a6066557 100644
--- a/test/framework/easyconfig.py
+++ b/test/framework/easyconfig.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -51,7 +51,7 @@
from easybuild.framework.easyconfig.constants import EXTERNAL_MODULE_MARKER
from easybuild.framework.easyconfig.easyconfig import ActiveMNS, EasyConfig, create_paths, copy_easyconfigs
from easybuild.framework.easyconfig.easyconfig import det_subtoolchain_version, fix_deprecated_easyconfigs
-from easybuild.framework.easyconfig.easyconfig import is_generic_easyblock, get_easyblock_class, get_module_path
+from easybuild.framework.easyconfig.easyconfig import get_easyblock_class, get_module_path
from easybuild.framework.easyconfig.easyconfig import letter_dir_for, process_easyconfig, resolve_template
from easybuild.framework.easyconfig.easyconfig import triage_easyconfig_params, verify_easyconfig_filename
from easybuild.framework.easyconfig.licenses import License, LicenseGPLv3
@@ -73,7 +73,6 @@
from easybuild.tools.module_naming_scheme.toolchain import det_toolchain_compilers, det_toolchain_mpi
from easybuild.tools.module_naming_scheme.utilities import det_full_ec_version
from easybuild.tools.options import parse_external_modules_metadata
-from easybuild.tools.py2vs3 import reload
from easybuild.tools.robot import det_robot_path, resolve_dependencies
from easybuild.tools.systemtools import AARCH64, KNOWN_ARCH_CONSTANTS, POWER, X86_64
from easybuild.tools.systemtools import get_cpu_architecture, get_shared_lib_ext, get_os_name, get_os_version
@@ -86,11 +85,7 @@
try:
import pycodestyle # noqa
except ImportError:
- try:
- import pep8 # noqa
- except ImportError:
- pass
-
+ pass
EXPECTED_DOTTXT_TOY_DEPS = """digraph graphname {
toy;
@@ -120,6 +115,11 @@ def setUp(self):
github_token = gh.fetch_github_token(GITHUB_TEST_ACCOUNT)
self.skip_github_tests = github_token is None and os.getenv('FORCE_EB_GITHUB_TESTS') is None
+ self.orig_easyconfig_DEPRECATED_EASYCONFIG_PARAMETERS = easyconfig.easyconfig.DEPRECATED_EASYCONFIG_PARAMETERS
+ self.orig_easyconfig_DEPRECATED_EASYCONFIG_TEMPLATES = easyconfig.easyconfig.DEPRECATED_EASYCONFIG_TEMPLATES
+ self.orig_easyconfig_ALTERNATIVE_EASYCONFIG_PARAMETERS = easyconfig.easyconfig.ALTERNATIVE_EASYCONFIG_PARAMETERS
+ self.orig_easyconfig_ALTERNATIVE_EASYCONFIG_TEMPLATES = easyconfig.easyconfig.ALTERNATIVE_EASYCONFIG_TEMPLATES
+
def prep(self):
"""Prepare for test."""
# (re)cleanup last test file
@@ -133,10 +133,17 @@ def prep(self):
def tearDown(self):
""" make sure to remove the temporary file """
st.get_cpu_architecture = self.orig_get_cpu_architecture
+
super(EasyConfigTest, self).tearDown()
if os.path.exists(self.eb_file):
os.remove(self.eb_file)
+ # restore orignal values of DEPRECATED_EASYCONFIG_TEMPLATES & co in easyconfig.templates
+ easyconfig.easyconfig.DEPRECATED_EASYCONFIG_PARAMETERS = self.orig_easyconfig_DEPRECATED_EASYCONFIG_PARAMETERS
+ easyconfig.easyconfig.DEPRECATED_EASYCONFIG_TEMPLATES = self.orig_easyconfig_DEPRECATED_EASYCONFIG_TEMPLATES
+ easyconfig.easyconfig.ALTERNATIVE_EASYCONFIG_PARAMETERS = self.orig_easyconfig_ALTERNATIVE_EASYCONFIG_PARAMETERS
+ easyconfig.easyconfig.ALTERNATIVE_EASYCONFIG_TEMPLATES = self.orig_easyconfig_ALTERNATIVE_EASYCONFIG_TEMPLATES
+
def test_empty(self):
""" empty files should not parse! """
self.assertErrorRegex(EasyBuildError, "expected a valid path", EasyConfig, "")
@@ -487,8 +494,11 @@ def test_exts_list(self):
"checksums": [
# SHA256 checksum for source (gzip-1.4.eb)
"6a5abcab719cefa95dca4af0db0d2a9d205d68f775a33b452ec0f2b75b6a3a45",
- # SHA256 checksum for 'patch' (toy-0.0.eb)
- "2d964e0e8f05a7cce0dd83a3e68c9737da14b87b61b8b8b0291d58d4c8d1031c",
+ # SHA256 checksum for 'patch' (toy-0.0.eb);
+ # using dict value with key that has a template value,
+ # to make sure that works as expected...
+ {"toy-0.%(version_minor)s.eb":
+ "177b34bcdfa1abde96f30354848a01894ebc9c24913bc5145306cd30f78fc8ad"},
],
}),
# Can use templates in name and version
@@ -512,7 +522,8 @@ def test_exts_list(self):
self.assertEqual(exts_sources[1]['version'], '2.0')
self.assertEqual(exts_sources[1]['options'], {
'checksums': ['6a5abcab719cefa95dca4af0db0d2a9d205d68f775a33b452ec0f2b75b6a3a45',
- '2d964e0e8f05a7cce0dd83a3e68c9737da14b87b61b8b8b0291d58d4c8d1031c'],
+ {'toy-0.%(version_minor)s.eb':
+ '177b34bcdfa1abde96f30354848a01894ebc9c24913bc5145306cd30f78fc8ad'}],
'patches': [('toy-0.0.eb', '.')],
'source_tmpl': 'gzip-1.4.eb',
'source_urls': [('http://example.com', 'suffix')],
@@ -522,9 +533,11 @@ def test_exts_list(self):
self.assertEqual(exts_sources[3]['name'], 'ext-pi')
self.assertEqual(exts_sources[3]['version'], '3.0')
- modfile = os.path.join(eb.make_module_step(), 'PI', '3.14' + eb.module_generator.MODULE_FILE_EXTENSION)
+ with self.mocked_stdout_stderr():
+ modfile = os.path.join(eb.make_module_step(), 'PI', '3.14' + eb.module_generator.MODULE_FILE_EXTENSION)
modtxt = read_file(modfile)
- regex = re.compile('EBEXTSLISTPI.*ext1-1.0,ext2-2.0')
+ # verify that templates used for extensions are resolved as they should
+ regex = re.compile('EBEXTSLISTPI.*"ext1-1.0,ext2-2.0,ext-PI-3.14,ext-pi-3.0')
self.assertTrue(regex.search(modtxt), "Pattern '%s' found in: %s" % (regex.pattern, modtxt))
def test_extensions_templates(self):
@@ -568,7 +581,8 @@ def test_extensions_templates(self):
self.prep()
ec = EasyConfig(self.eb_file)
eb = EasyBlock(ec)
- eb.fetch_step()
+ with self.mocked_stdout_stderr():
+ eb.fetch_step()
# inject OS dependency that can not be fullfilled,
# to check whether OS deps are validated again for each extension (they shouldn't be);
@@ -577,7 +591,8 @@ def test_extensions_templates(self):
eb.cfg.rawtxt += "\nosdependencies = ['this_os_dep_does_not_exist']"
# run extensions step to install 'toy' extension
- eb.extensions_step()
+ with self.mocked_stdout_stderr():
+ eb.extensions_step()
# check whether template values were resolved correctly in Extension instances that were created/used
toy_ext = eb.ext_instances[0]
@@ -660,8 +675,8 @@ def test_tweaking(self):
'version = "3.14"',
'toolchain = {"name": "GCC", "version": "4.6.3"}',
'patches = %s',
- 'parallel = 1',
- 'keepsymlinks = True',
+ 'maxparallel = 1',
+ 'keepsymlinks = False',
]) % str(patches)
self.prep()
@@ -679,11 +694,11 @@ def test_tweaking(self):
'versionsuffix': versuff,
'toolchain_version': tcver,
'patches': new_patches,
- 'keepsymlinks': 'True', # Don't change this
+ 'keepsymlinks': 'False', # Don't change this
# It should be possible to overwrite values with True/False/None as they often have special meaning
'runtest': 'False',
'hidden': 'True',
- 'parallel': 'None', # Good example: parallel=None means "Auto detect"
+ 'maxparallel': 'None', # Good example: maxparallel=None means "unlimitted"
# Adding new options (added only by easyblock) should also be possible
# and in case the string "True/False/None" is really wanted it is possible to quote it first
'test_none': '"False"',
@@ -700,7 +715,7 @@ def test_tweaking(self):
self.assertEqual(eb['patches'], new_patches)
self.assertIs(eb['runtest'], False)
self.assertIs(eb['hidden'], True)
- self.assertIsNone(eb['parallel'])
+ self.assertIsNone(eb['maxparallel'])
self.assertEqual(eb['test_none'], 'False')
self.assertEqual(eb['test_bool'], 'True')
self.assertEqual(eb['test_123'], 'None')
@@ -737,6 +752,32 @@ def test_tweaking(self):
# cleanup
os.remove(tweaked_fn)
+ def test_parse_easyconfig(self):
+ """Test parse_easyconfig function"""
+ self.contents = textwrap.dedent("""
+ easyblock = "ConfigureMake"
+ name = "PI"
+ version = "3.14"
+ homepage = "http://example.com"
+ description = "test easyconfig"
+ toolchain = SYSTEM
+ """)
+ self.prep()
+ ecs, gen_ecs = parse_easyconfigs([(self.eb_file, False)])
+ self.assertEqual(len(ecs), 1)
+ self.assertEqual(ecs[0]['spec'], self.eb_file)
+ self.assertIsInstance(ecs[0]['ec'], EasyConfig)
+ self.assertFalse(gen_ecs)
+ # Passing the same EC multiple times is ignored
+ ecs, gen_ecs = parse_easyconfigs([(self.eb_file, False), (self.eb_file, False)])
+ self.assertEqual(len(ecs), 1)
+ # Similar for symlinks
+ linked_ec = os.path.join(self.test_prefix, 'linked.eb')
+ os.symlink(self.eb_file, linked_ec)
+ ecs, gen_ecs = parse_easyconfigs([(self.eb_file, False), (linked_ec, False)])
+ self.assertEqual(len(ecs), 1)
+ self.assertEqual(ecs[0]['spec'], self.eb_file)
+
def test_alt_easyconfig_paths(self):
"""Test alt_easyconfig_paths function that collects list of additional paths for easyconfig files."""
@@ -790,7 +831,8 @@ def test_tweak_multiple_tcs(self):
untweaked_openmpi_2 = os.path.join(test_easyconfigs, 'o', 'OpenMPI', 'OpenMPI-3.1.1-GCC-7.3.0-2.30.eb')
easyconfigs, _ = parse_easyconfigs([(untweaked_openmpi_1, False), (untweaked_openmpi_2, False)])
tweak_specs = {'moduleclass': 'debugger'}
- easyconfigs = tweak(easyconfigs, tweak_specs, self.modtool, targetdirs=tweaked_ecs_paths)
+ easyconfigs, tweak_map = tweak(easyconfigs, tweak_specs, self.modtool, targetdirs=tweaked_ecs_paths,
+ return_map=True)
# Check that all expected tweaked easyconfigs exists
tweaked_openmpi_1 = os.path.join(tweaked_ecs_paths[0], os.path.basename(untweaked_openmpi_1))
tweaked_openmpi_2 = os.path.join(tweaked_ecs_paths[0], os.path.basename(untweaked_openmpi_2))
@@ -802,6 +844,7 @@ def test_tweak_multiple_tcs(self):
"Tweaked value not found in " + tweaked_openmpi_content_1)
self.assertTrue('moduleclass = "debugger"' in tweaked_openmpi_content_2,
"Tweaked value not found in " + tweaked_openmpi_content_2)
+ self.assertEqual(tweak_map, {tweaked_openmpi_1: untweaked_openmpi_1, tweaked_openmpi_2: untweaked_openmpi_2})
def test_installversion(self):
"""Test generation of install version."""
@@ -1189,7 +1232,6 @@ def test_templating_constants(self):
'R: %%(rver)s, %%(rmajver)s, %%(rminver)s, %%(rshortver)s',
]),
'modextrapaths = {"PI_MOD_NAME": "%%(module_name)s"}',
- 'modextrapaths_append = {"PATH_APPEND": "appended_path"}',
'license_file = HOME + "/licenses/PI/license.txt"',
"github_account = 'easybuilders'",
]) % inp
@@ -1198,9 +1240,11 @@ def test_templating_constants(self):
ec.validate()
# temporarily disable templating, just so we can check later whether it's *still* disabled
+ self.assertTrue(ec.templating_enabled)
with ec.disable_templating():
ec.generate_template_values()
- self.assertFalse(ec.enable_templating)
+ self.assertFalse(ec.templating_enabled)
+ self.assertTrue(ec.templating_enabled)
self.assertEqual(ec['description'], "test easyconfig PI")
self.assertEqual(ec['sources'][0], 'PI-3.04.tar.gz')
@@ -1230,7 +1274,6 @@ def test_templating_constants(self):
self.assertEqual(ec['modloadmsg'], expected)
self.assertEqual(ec['modunloadmsg'], expected)
self.assertEqual(ec['modextrapaths'], {'PI_MOD_NAME': 'PI/3.04-Python-2.7.10'})
- self.assertEqual(ec['modextrapaths_append'], {'PATH_APPEND': 'appended_path'})
self.assertEqual(ec['license_file'], os.path.join(os.environ['HOME'], 'licenses', 'PI', 'license.txt'))
# test the escaping insanity here (ie all the crap we allow in easyconfigs)
@@ -1260,6 +1303,56 @@ def test_templating_constants(self):
ec = EasyConfig(test_ec)
self.assertEqual(ec['sanity_check_commands'], ['mpiexec -np 1 -- toy'])
+ def test_template_constant_import(self):
+ """Test importing template constants works"""
+ from easybuild.framework.easyconfig.templates import GITHUB_SOURCE, GNU_SOURCE, SHLIB_EXT
+ from easybuild.framework.easyconfig.templates import TEMPLATE_CONSTANTS
+ self.assertEqual(GITHUB_SOURCE, TEMPLATE_CONSTANTS['GITHUB_SOURCE'][0])
+ self.assertEqual(GNU_SOURCE, TEMPLATE_CONSTANTS['GNU_SOURCE'][0])
+ self.assertEqual(SHLIB_EXT, get_shared_lib_ext())
+
+ def test_ec_method_resolve_template(self):
+ """Test the `resolve_template` method of easyconfig instances."""
+ # don't use any escaping insanity here, since it is templated itself
+ self.contents = textwrap.dedent("""
+ easyblock = "ConfigureMake"
+ name = "PI"
+ version = "3.14"
+ homepage = "http://example.com"
+ description = "test easyconfig %(name)s version %(version_major)s"
+ toolchain = SYSTEM
+ installopts = "PREFIX=%(installdir)s"
+ """)
+ self.prep()
+ ec = EasyConfig(self.eb_file, validate=False)
+
+ # We can resolve anything with values from the EC
+ self.assertEqual(ec.resolve_template('%(namelower)s %(version_major)s begins with %(nameletterlower)s'),
+ 'pi 3 begins with p')
+
+ # `resolve_template` does basically the same resolving any value on acccess
+ description = ec.get('description', resolve=False)
+ self.assertIn('%', description, 'Description needs a template for the next test')
+ self.assertEqual(ec.resolve_template(description), ec['description'])
+
+ val = "PREFIX=%(installdir)s"
+
+ # by default unresolved template value triggers an error being raised
+ error_pattern = "Failed to resolve all templates"
+ self.assertErrorRegex(EasyBuildError, error_pattern, ec.resolve_template, val)
+ self.assertErrorRegex(EasyBuildError, error_pattern, ec.get, 'installopts')
+
+ # this can be (temporarily) disabled
+ with ec.allow_unresolved_templates():
+ self.assertFalse(ec.expect_resolved_template_values)
+ self.assertEqual(ec.resolve_template(val), val)
+ self.assertEqual(ec['installopts'], val)
+
+ # Enforced again
+ self.assertTrue(ec.expect_resolved_template_values)
+ self.assertErrorRegex(EasyBuildError, error_pattern, ec.resolve_template, val)
+ self.assertErrorRegex(EasyBuildError, error_pattern, ec.get, 'installopts')
+
def test_templating_cuda_toolchain(self):
"""Test templates via toolchain component, like setting %(cudaver)s with fosscuda toolchain."""
@@ -1367,7 +1460,7 @@ def test_templating_doc(self):
# expected length: 1 per constant and 2 extra per constantgroup (title + empty line in between)
temps = [
easyconfig.templates.TEMPLATE_NAMES_EASYCONFIG,
- easyconfig.templates.TEMPLATE_SOFTWARE_VERSIONS * 3,
+ list(easyconfig.templates.TEMPLATE_SOFTWARE_VERSIONS.keys()) * 3,
easyconfig.templates.TEMPLATE_NAMES_CONFIG,
easyconfig.templates.TEMPLATE_NAMES_LOWER,
easyconfig.templates.TEMPLATE_NAMES_EASYBLOCK_RUN_STEP,
@@ -1423,6 +1516,30 @@ def test_start_dir_template(self):
self.assertIn('start_dir in extension configure is %s &&' % ext_start_dir, logtxt)
self.assertIn('start_dir in extension build is %s &&' % ext_start_dir, logtxt)
+ def test_rpath_template(self):
+ """Test the %(rpath)s template"""
+ test_easyconfigs = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'easyconfigs', 'test_ecs')
+ toy_ec = os.path.join(test_easyconfigs, 't', 'toy', 'toy-0.0.eb')
+
+ test_ec = os.path.join(self.test_prefix, 'test.eb')
+ test_ec_txt = read_file(toy_ec)
+ test_ec_txt += "configopts = '--with-rpath=%(rpath_enabled)s'"
+ write_file(test_ec, test_ec_txt)
+
+ ec = EasyConfig(test_ec)
+ expected = '--with-rpath=true' if get_os_name() == 'Linux' else '--with-rpath=false'
+ self.assertEqual(ec['configopts'], expected)
+
+ # force True
+ update_build_option('rpath', True)
+ ec = EasyConfig(test_ec)
+ self.assertEqual(ec['configopts'], "--with-rpath=true")
+
+ # force False
+ update_build_option('rpath', False)
+ ec = EasyConfig(test_ec)
+ self.assertEqual(ec['configopts'], "--with-rpath=false")
+
def test_sysroot_template(self):
"""Test the %(sysroot)s template"""
@@ -1480,6 +1597,47 @@ def test_software_commit_template(self):
self.assertEqual(ec['buildopts'], "--some-opt=%s" % software_commit)
self.assertEqual(ec['installopts'], "--some-opt=%s" % software_commit)
+ def test_template_deprecation_and_alternative(self):
+ """Test deprecation of (and alternative) templates"""
+
+ self.prep()
+
+ template_test_deprecations = {
+ 'builddir': ('depr_build_dir', '1000000000'),
+ 'cudaver': ('depr_cuda_ver', '1000000000'),
+ 'start_dir': ('depr_start_dir', '1000000000'),
+ }
+ easyconfig.easyconfig.DEPRECATED_EASYCONFIG_TEMPLATES = template_test_deprecations
+
+ template_test_alternatives = {
+ 'installdir': 'alt_install_dir',
+ 'version_major_minor': 'alt_ver_maj_min',
+ }
+ easyconfig.easyconfig.ALTERNATIVE_EASYCONFIG_TEMPLATES = template_test_alternatives
+
+ tmpl_str = ("cd %(start_dir)s && make %(namelower)s -Dbuild=%(builddir)s --with-cuda='%(cudaver)s'"
+ " && echo %(alt_install_dir)s %(version_major_minor)s")
+ tmpl_dict = {
+ 'depr_build_dir': '/example/build_dir',
+ 'depr_cuda_ver': '12.1.1',
+ 'installdir': '/example/installdir',
+ 'start_dir': '/example/build_dir/start_dir',
+ 'alt_ver_maj_min': '1.2',
+ 'namelower': 'foo',
+ }
+
+ with self.mocked_stdout_stderr() as (_, stderr):
+ res = resolve_template(tmpl_str, tmpl_dict)
+ stderr = stderr.getvalue()
+
+ for tmpl in [*template_test_deprecations.keys(), *template_test_alternatives.keys()]:
+ self.assertNotIn("%(" + tmpl + ")s", res)
+
+ for old, (new, ver) in template_test_deprecations.items():
+ depr_str = (f"WARNING: Deprecated functionality, will no longer work in EasyBuild v{ver}: "
+ f"Easyconfig template '{old}' is deprecated, use '{new}' instead")
+ self.assertIn(depr_str, stderr)
+
def test_constant_doc(self):
"""test constant documentation"""
doc = avail_easyconfig_constants()
@@ -1587,9 +1745,10 @@ def test_buildininstalldir(self):
self.prep()
ec = EasyConfig(self.eb_file)
eb = EasyBlock(ec)
- eb.post_init()
- eb.make_builddir()
- eb.make_installdir()
+ with self.mocked_stdout_stderr():
+ eb.post_init()
+ eb.make_builddir()
+ eb.make_installdir()
self.assertEqual(eb.builddir, eb.installdir)
self.assertTrue(os.path.isdir(eb.builddir))
@@ -1671,17 +1830,6 @@ def test_get_easyblock_class(self):
self.assertErrorRegex(EasyBuildError, "neither name nor easyblock were specified", get_easyblock_class, None)
self.assertEqual(get_easyblock_class(None, error_on_missing_easyblock=False), None)
- # also test deprecated default_fallback named argument
- self.assertErrorRegex(EasyBuildError, "DEPRECATED", get_easyblock_class, None, name='gzip',
- default_fallback=False)
-
- orig_value = easybuild.tools.build_log.CURRENT_VERSION
- easybuild.tools.build_log.CURRENT_VERSION = '3.9'
- self.mock_stderr(True)
- self.assertEqual(get_easyblock_class(None, name='gzip', default_fallback=False), None)
- self.mock_stderr(False)
- easybuild.tools.build_log.CURRENT_VERSION = orig_value
-
def test_letter_dir(self):
"""Test letter_dir_for function."""
test_cases = {
@@ -1809,27 +1957,70 @@ def foo(key):
self.assertErrorRegex(EasyBuildError, error_regex, foo, key)
+ def test_alternative_easyconfig_parameters(self):
+ """Test handling of alternative easyconfig parameters."""
+
+ test_ecs_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'easyconfigs', 'test_ecs')
+ toy_ec = os.path.join(test_ecs_dir, 't', 'toy', 'toy-0.0.eb')
+
+ test_ec_txt = read_file(toy_ec)
+ test_ec_txt = test_ec_txt.replace('postinstallcmds', 'post_install_cmds')
+ test_ec_txt = test_ec_txt.replace('moduleclass', 'env_mod_class')
+
+ test_ec = os.path.join(self.test_prefix, 'test.eb')
+ write_file(test_ec, test_ec_txt)
+
+ # post_install_cmds is not accepted unless it's registered as an alternative easyconfig parameter
+ easyconfig.easyconfig.ALTERNATIVE_EASYCONFIG_PARAMETERS = {}
+ self.assertErrorRegex(EasyBuildError, "post_install_cmds -> postinstallcmds", EasyConfig, test_ec)
+
+ easyconfig.easyconfig.ALTERNATIVE_EASYCONFIG_PARAMETERS = {
+ 'env_mod_class': 'moduleclass',
+ 'post_install_cmds': 'postinstallcmds',
+ }
+ ec = EasyConfig(test_ec)
+
+ expected = 'tools'
+ self.assertEqual(ec['moduleclass'], expected)
+ self.assertEqual(ec['env_mod_class'], expected)
+
+ expected = ['echo TOY > %(installdir)s/README']
+ with ec.disable_templating():
+ self.assertEqual(ec['postinstallcmds'], expected)
+ self.assertEqual(ec['post_install_cmds'], expected)
+
+ # test setting of easyconfig parameter with original & alternative name
+ ec['moduleclass'] = 'test1'
+ self.assertEqual(ec['moduleclass'], 'test1')
+ self.assertEqual(ec['env_mod_class'], 'test1')
+ ec.update('moduleclass', 'test2')
+ self.assertEqual(ec['moduleclass'], 'test1 test2 ')
+ self.assertEqual(ec['env_mod_class'], 'test1 test2 ')
+
+ ec['env_mod_class'] = 'test3'
+ self.assertEqual(ec['moduleclass'], 'test3')
+ self.assertEqual(ec['env_mod_class'], 'test3')
+ ec.update('env_mod_class', 'test4')
+ self.assertEqual(ec['moduleclass'], 'test3 test4 ')
+ self.assertEqual(ec['env_mod_class'], 'test3 test4 ')
+
def test_deprecated_easyconfig_parameters(self):
- """Test handling of replaced easyconfig parameters."""
- os.environ.pop('EASYBUILD_DEPRECATED')
- easybuild.tools.build_log.CURRENT_VERSION = self.orig_current_version
+ """Test handling of deprecated easyconfig parameters."""
+ self.allow_deprecated_behaviour()
init_config()
test_ecs_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'easyconfigs', 'test_ecs')
ec = EasyConfig(os.path.join(test_ecs_dir, 't', 'toy', 'toy-0.0.eb'))
- orig_deprecated_parameters = copy.deepcopy(easyconfig.parser.DEPRECATED_PARAMETERS)
- easyconfig.parser.DEPRECATED_PARAMETERS.update({
+ easyconfig.easyconfig.DEPRECATED_EASYCONFIG_PARAMETERS = {
'foobar': ('barfoo', '0.0'), # deprecated since forever
- 'foobarbarfoo': ('barfoofoobar', '1000000000'), # won't be actually deprecated for a while
- })
-
- # copy classes before reloading, so we can restore them (other isinstance checks fail)
- orig_EasyConfig = copy.deepcopy(easyconfig.easyconfig.EasyConfig)
- orig_ActiveMNS = copy.deepcopy(easyconfig.easyconfig.ActiveMNS)
- reload(easyconfig.parser)
+ # won't be actually deprecated for a while;
+ # note that we should map foobarbarfoo to a valid easyconfig parameter here,
+ # or we'll hit errors when parsing an easyconfig file that uses it
+ 'foobarbarfoo': ('required_linked_shared_libs', '1000000000'),
+ }
- for key, (newkey, depr_ver) in easyconfig.parser.DEPRECATED_PARAMETERS.items():
+ for key, (newkey, depr_ver) in easyconfig.easyconfig.DEPRECATED_EASYCONFIG_PARAMETERS.items():
if LooseVersion(depr_ver) <= easybuild.tools.build_log.CURRENT_VERSION:
# deprecation error
error_regex = "DEPRECATED.*since v%s.*'%s' is deprecated.*use '%s' instead" % (depr_ver, key, newkey)
@@ -1842,18 +2033,42 @@ def foo(key):
self.assertErrorRegex(EasyBuildError, error_regex, foo, key)
else:
# only deprecation warning, but key is replaced when getting/setting
- ec[key] = 'test123'
- self.assertEqual(ec[newkey], 'test123')
- self.assertEqual(ec[key], 'test123')
- ec[newkey] = '123test'
- self.assertEqual(ec[newkey], '123test')
- self.assertEqual(ec[key], '123test')
-
- easyconfig.parser.DEPRECATED_PARAMETERS = orig_deprecated_parameters
- reload(easyconfig.parser)
- reload(easyconfig.easyconfig)
- easyconfig.easyconfig.EasyConfig = orig_EasyConfig
- easyconfig.easyconfig.ActiveMNS = orig_ActiveMNS
+ with self.mocked_stdout_stderr():
+ ec[key] = 'test123'
+ self.assertEqual(ec[newkey], 'test123')
+ self.assertEqual(ec[key], 'test123')
+ ec[newkey] = '123test'
+ self.assertEqual(ec[newkey], '123test')
+ self.assertEqual(ec[key], '123test')
+
+ variables = {
+ 'name': 'example',
+ 'version': '1.2.3',
+ 'foobar': 'foobar',
+ 'local_var': 'test',
+ }
+ ec = {
+ 'name': None,
+ 'version': None,
+ 'homepage': None,
+ 'toolchain': None,
+ }
+ ec_params, unknown_keys = triage_easyconfig_params(variables, ec)
+ # deprecated easyconfig parameter 'foobar' is retained as easyconfig parameter;
+ # only local_var is not retained, since that's a local variable
+ self.assertEqual(unknown_keys, [])
+ expected = {'name': 'example', 'version': '1.2.3', 'foobar': 'foobar'}
+ self.assertEqual(ec_params, expected)
+
+ # try parsing an easyconfig file that defines a deprecated easyconfig parameter
+ toy_ec = os.path.join(test_ecs_dir, 't', 'toy', 'toy-0.0.eb')
+ test_ec = os.path.join(self.test_prefix, 'test.eb')
+ write_file(test_ec, read_file(toy_ec))
+ write_file(test_ec, "\nfoobarbarfoo = 'foobarbarfoo'", append=True)
+
+ with self.mocked_stdout_stderr():
+ ec = EasyConfig(test_ec)
+ self.assertEqual(ec['required_linked_shared_libs'], 'foobarbarfoo')
def test_unknown_easyconfig_parameter(self):
"""Check behaviour when unknown easyconfig parameters are used."""
@@ -2115,7 +2330,8 @@ def test_external_dependencies(self):
os.environ['PI_PREFIX'] = '/test/prefix/PI'
os.environ['TEST_INC'] = '/test/prefix/test/include'
ec.toolchain.dry_run = True
- ec.toolchain.prepare(deps=ec.dependencies(), silent=True)
+ with self.mocked_stdout_stderr():
+ ec.toolchain.prepare(deps=ec.dependencies(), silent=True)
self.assertEqual(os.environ.get('EBROOTBAR'), '/foo/bar')
self.assertEqual(os.environ.get('EBROOTFOO'), '/foo/bar')
@@ -2178,8 +2394,8 @@ def test_external_dependencies_templates(self):
'pyshortver': '3.6',
'pyver': '3.6.5',
}
- for key in expected_template_values:
- self.assertEqual(ec.template_values[key], expected_template_values[key])
+ for key, expected in expected_template_values.items():
+ self.assertEqual(ec.template_values[key], expected)
self.assertEqual(ec['versionsuffix'], '-Python-3.6.5-Perl-5.30')
@@ -2255,8 +2471,8 @@ def test_quote_str(self):
'foo\\bar': '"foo\\bar"',
}
- for t in teststrings:
- self.assertEqual(quote_str(t), teststrings[t])
+ for t, expected in teststrings.items():
+ self.assertEqual(quote_str(t), expected)
# test escape_newline
self.assertEqual(quote_str("foo\nbar", escape_newline=False), '"foo\nbar"')
@@ -2354,11 +2570,11 @@ def test_dump(self):
test_ec = os.path.join(self.test_prefix, 'test.eb')
ec = EasyConfig(os.path.join(test_ecs_dir, ecfile))
- ec.enable_templating = False
- ecdict = ec.asdict()
- ec.dump(test_ec)
- # dict representation of EasyConfig instance should not change after dump
- self.assertEqual(ecdict, ec.asdict())
+ with ec.disable_templating():
+ ecdict = ec.asdict()
+ ec.dump(test_ec)
+ # dict representation of EasyConfig instance should not change after dump
+ self.assertEqual(ecdict, ec.asdict())
ectxt = read_file(test_ec)
patterns = [
@@ -2373,7 +2589,6 @@ def test_dump(self):
# parse result again
dumped_ec = EasyConfig(test_ec)
- dumped_ec.enable_templating = False
# check that selected parameters still have the same value
params = [
@@ -2382,9 +2597,10 @@ def test_dump(self):
'dependencies', # checking this is important w.r.t. filtered hidden dependencies being restored in dump
'exts_list', # exts_lists (in Python easyconfig) use another layer of templating so shouldn't change
]
- for param in params:
- if param in ec:
- self.assertEqual(ec[param], dumped_ec[param])
+ with ec.disable_templating(), dumped_ec.disable_templating():
+ for param in params:
+ if param in ec:
+ self.assertEqual(ec[param], dumped_ec[param])
ec_txt = textwrap.dedent("""
easyblock = 'EB_toy'
@@ -2538,8 +2754,8 @@ def test_dump_autopep8(self):
def test_dump_extra(self):
"""Test EasyConfig's dump() method for files containing extra values"""
- if not ('pycodestyle' in sys.modules or 'pep8' in sys.modules):
- print("Skipping test_dump_extra (no pycodestyle or pep8 available)")
+ if 'pycodestyle' not in sys.modules:
+ print("Skipping test_dump_extra pycodestyle is not available")
return
rawtxt = '\n'.join([
@@ -2581,8 +2797,8 @@ def test_dump_extra(self):
def test_dump_template(self):
""" Test EasyConfig's dump() method for files containing templates"""
- if not ('pycodestyle' in sys.modules or 'pep8' in sys.modules):
- print("Skipping test_dump_template (no pycodestyle or pep8 available)")
+ if 'pycodestyle' not in sys.modules:
+ print("Skipping test_dump_template pycodestyle is not available")
return
rawtxt = '\n'.join([
@@ -2635,7 +2851,7 @@ def test_dump_template(self):
ec.dump(testec)
ectxt = read_file(testec)
- self.assertTrue(ec.enable_templating) # templating should still be enabled after calling dump()
+ self.assertTrue(ec.templating_enabled) # templating should still be enabled after calling dump()
patterns = [
r"easyblock = 'EB_foo'",
@@ -2670,8 +2886,8 @@ def test_dump_template(self):
def test_dump_comments(self):
""" Test dump() method for files containing comments """
- if not ('pycodestyle' in sys.modules or 'pep8' in sys.modules):
- print("Skipping test_dump_comments (no pycodestyle or pep8 available)")
+ if 'pycodestyle' not in sys.modules:
+ print("Skipping test_dump_comments pycodestyle is not available")
return
rawtxt = '\n'.join([
@@ -3365,6 +3581,8 @@ def test_template_constant_dict(self):
arch_regex = re.compile('^[a-z0-9_]+$')
+ rpath = 'true' if get_os_name() == 'Linux' else 'false'
+
expected = {
'bitbucket_account': 'gzip',
'github_account': 'gzip',
@@ -3373,7 +3591,7 @@ def test_template_constant_dict(self):
'namelower': 'gzip',
'nameletter': 'g',
'nameletterlower': 'g',
- 'parallel': None,
+ 'rpath_enabled': rpath,
'software_commit': '',
'sysroot': '',
'toolchain_name': 'foss',
@@ -3400,17 +3618,17 @@ def test_template_constant_dict(self):
except AttributeError:
pass # Ignore if not present
orig_get_avail_core_count = st.get_avail_core_count
- st.get_avail_core_count = lambda: 42
+ st.get_avail_core_count = lambda: 12
# also check template values after running check_readiness_step (which runs set_parallel)
eb = EasyBlock(ec)
- eb.check_readiness_step()
+ with self.mocked_stdout_stderr():
+ eb.check_readiness_step()
st.get_avail_core_count = orig_get_avail_core_count
res = template_constant_dict(ec)
res.pop('arch')
- expected['parallel'] = 42
self.assertEqual(res, expected)
toy_ec = os.path.join(test_ecs_dir, 't', 'toy', 'toy-0.0-deps.eb')
@@ -3452,11 +3670,11 @@ def test_template_constant_dict(self):
'toolchain_name': 'system',
'toolchain_version': 'system',
'nameletterlower': 't',
- 'parallel': None,
'pymajver': '3',
'pyminver': '7',
'pyshortver': '3.7',
'pyver': '3.7.2',
+ 'rpath_enabled': rpath,
'software_commit': '',
'sysroot': '',
'version': '0.01',
@@ -3487,7 +3705,7 @@ def test_template_constant_dict(self):
ec = EasyConfigParser(filename=test_ec).get_config_dict()
expected['module_name'] = None
- for key in ('bitbucket_account', 'github_account', 'parallel', 'versionprefix'):
+ for key in ('bitbucket_account', 'github_account', 'versionprefix'):
del expected[key]
dep_names = [x[0] for x in ec['dependencies']]
@@ -3523,6 +3741,7 @@ def test_template_constant_dict(self):
'namelower': 'foo',
'nameletter': 'f',
'nameletterlower': 'f',
+ 'rpath_enabled': rpath,
'software_commit': '',
'sysroot': '',
'version': '1.2.3',
@@ -3599,6 +3818,7 @@ def test_hidden_toolchain(self):
args = [
ec_file,
'--dry-run',
+ '--robot',
]
outtxt = self.eb_main(args, raise_error=True)
self.assertTrue(re.search(r'module: GCC/\.4\.9\.2', outtxt))
@@ -3716,7 +3936,7 @@ def test_resolve_template(self):
# On unknown values the value is returned unchanged
for value in ('%(invalid)s', '%(name)s %(invalid)s', '%%%(invalid)s', '% %(invalid)s', '%s %(invalid)s'):
- self.assertEqual(resolve_template(value, tmpl_dict), value)
+ self.assertEqual(resolve_template(value, tmpl_dict, expect_resolved=False), value)
def test_det_subtoolchain_version(self):
"""Test det_subtoolchain_version function"""
@@ -3754,19 +3974,6 @@ def test_det_subtoolchain_version(self):
for subtoolchain_name in subtoolchains[current_tc['name']]]
self.assertEqual(versions, [None, ''])
- # --add-dummy-to-minimal-toolchains is still supported, but deprecated
- self.allow_deprecated_behaviour()
- init_config(build_options={'add_system_to_minimal_toolchains': False, 'add_dummy_to_minimal_toolchains': True})
- self.mock_stderr(True)
- versions = [det_subtoolchain_version(current_tc, subtoolchain_name, optional_toolchains, cands)
- for subtoolchain_name in subtoolchains[current_tc['name']]]
- stderr = self.get_stderr()
- self.mock_stderr(False)
- self.assertEqual(versions, [None, ''])
- depr_msg = "WARNING: Deprecated functionality, will no longer work in v5.0: "
- depr_msg += "Use --add-system-to-minimal-toolchains instead of --add-dummy-to-minimal-toolchains"
- self.assertIn(depr_msg, stderr)
-
# and GCCcore if existing too
init_config(build_options={'add_system_to_minimal_toolchains': True})
current_tc = {'name': 'GCC', 'version': '4.9.3-2.25'}
@@ -3929,22 +4136,6 @@ def test_get_paths_for(self):
if env_eb_script_path:
os.environ['EB_SCRIPT_PATH'] = env_eb_script_path
- def test_is_generic_easyblock(self):
- """Test for is_generic_easyblock function."""
-
- # is_generic_easyblock in easyconfig.py is deprecated, moved to filetools.py
- self.allow_deprecated_behaviour()
-
- self.mock_stderr(True)
-
- for name in ['Binary', 'ConfigureMake', 'CMakeMake', 'PythonPackage', 'JAR']:
- self.assertTrue(is_generic_easyblock(name))
-
- for name in ['EB_bzip2', 'EB_DL_underscore_POLY_underscore_Classic', 'EB_GCC', 'EB_WRF_minus_Fire']:
- self.assertFalse(is_generic_easyblock(name))
-
- self.mock_stderr(False)
-
def test_get_module_path(self):
"""Test get_module_path function."""
self.assertEqual(get_module_path('EB_bzip2', generic=False), 'easybuild.easyblocks.bzip2')
@@ -4308,9 +4499,6 @@ def test_fix_deprecated_easyconfigs(self):
"""Test fix_deprecated_easyconfigs function."""
test_ecs_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'easyconfigs', 'test_ecs')
toy_ec = os.path.join(test_ecs_dir, 't', 'toy', 'toy-0.0.eb')
- gzip_ec = os.path.join(test_ecs_dir, 'g', 'gzip', 'gzip-1.4.eb')
-
- gzip_ec_txt = read_file(gzip_ec)
toy_ec_txt = read_file(toy_ec)
test_ec = os.path.join(self.test_prefix, 'test.eb')
@@ -4327,115 +4515,42 @@ def test_fix_deprecated_easyconfigs(self):
regex = re.compile(r'^(toolchain\s*=.*)$', re.M)
test_ectxt = regex.sub(r'\1\n\nsome_list = [x + "1" for x in ["one", "two", "three"]]', test_ectxt)
- # test fixing the use of 'dummy' toolchain to SYSTEM
- tc_regex = re.compile('^toolchain = .*', re.M)
- tc_strs = [
- "{'name': 'dummy', 'version': 'dummy'}",
- "{'name': 'dummy', 'version': ''}",
- "{'name': 'dummy', 'version': '1.2.3'}",
- "{'version': '', 'name': 'dummy'}",
- "{'version': 'dummy', 'name': 'dummy'}",
- ]
-
unknown_params_error_pattern = "Use of 2 unknown easyconfig parameters detected in test.eb: foo, some_list"
- for tc_str in tc_strs:
- # first check if names of local variables get fixed if 'dummy' toolchain is not used
- init_config(build_options={'local_var_naming_check': 'error', 'silent': True})
-
- write_file(test_ec, test_ectxt)
- self.assertErrorRegex(EasyBuildError, unknown_params_error_pattern, EasyConfig, test_ec)
-
- self.mock_stderr(True)
- self.mock_stdout(True)
- fix_deprecated_easyconfigs([test_ec])
- stderr, stdout = self.get_stderr(), self.get_stdout()
- self.mock_stderr(False)
- self.mock_stdout(False)
- self.assertFalse(stderr)
- self.assertIn("test.eb... FIXED!", stdout)
-
- # parsing now works
- ec = EasyConfig(test_ec)
-
- # cleanup
- remove_file(glob.glob(os.path.join(test_ec + '.orig*'))[0])
-
- # now inject use of 'dummy' toolchain
- write_file(test_ec, tc_regex.sub("toolchain = %s" % tc_str, test_ectxt))
-
- test_ec_txt = read_file(test_ec)
- regex = re.compile("^toolchain = {.*'name': 'dummy'.*$", re.M)
- self.assertTrue(regex.search(test_ec_txt), "Pattern '%s' found in: %s" % (regex.pattern, test_ec_txt))
-
- # mimic default behaviour where only warnings are being printed;
- # use of dummy toolchain or local variables not following recommended naming scheme is not fatal by default
- init_config(build_options={'local_var_naming_check': 'warn', 'silent': False})
- self.mock_stderr(True)
- self.mock_stdout(True)
- ec = EasyConfig(test_ec)
- stderr, stdout = self.get_stderr(), self.get_stdout()
- self.mock_stderr(False)
- self.mock_stdout(False)
-
- self.assertFalse(stdout)
+ # check if names of local variables get fixed
+ init_config(build_options={'local_var_naming_check': 'error', 'silent': True})
- warnings = [
- "WARNING: Use of 2 unknown easyconfig parameters detected in test.eb: foo, some_list",
- "Use of 'dummy' toolchain is deprecated, use 'system' toolchain instead",
- ]
- for warning in warnings:
- self.assertIn(warning, stderr)
-
- init_config(build_options={'local_var_naming_check': 'error', 'silent': True})
-
- # easyconfig doesn't parse because of local variables with name other than 'local_*'
- self.assertErrorRegex(EasyBuildError, unknown_params_error_pattern, EasyConfig, test_ec)
-
- self.mock_stderr(True)
- self.mock_stdout(True)
- fix_deprecated_easyconfigs([toy_ec, test_ec, gzip_ec])
- stderr, stdout = self.get_stderr(), self.get_stdout()
- self.mock_stderr(False)
- self.mock_stdout(False)
-
- ectxt = read_file(test_ec)
- self.assertFalse(regex.search(ectxt), "Pattern '%s' *not* found in: %s" % (regex.pattern, ectxt))
- regex = re.compile("^toolchain = SYSTEM$", re.M)
- self.assertTrue(regex.search(ectxt), "Pattern '%s' found in: %s" % (regex.pattern, ectxt))
-
- self.assertEqual(gzip_ec_txt, read_file(gzip_ec))
- self.assertEqual(toy_ec_txt, read_file(toy_ec))
- self.assertTrue(test_ec_txt != read_file(test_ec))
-
- # original easyconfig is backed up automatically
- test_ecs = sorted([f for f in os.listdir(self.test_prefix) if f.startswith('test.eb')])
- self.assertEqual(len(test_ecs), 2)
- backup_test_ec = os.path.join(self.test_prefix, test_ecs[1])
- self.assertEqual(test_ec_txt, read_file(backup_test_ec))
+ write_file(test_ec, test_ectxt)
+ self.assertErrorRegex(EasyBuildError, unknown_params_error_pattern, EasyConfig, test_ec)
- remove_file(backup_test_ec)
+ self.mock_stderr(True)
+ self.mock_stdout(True)
+ fix_deprecated_easyconfigs([test_ec])
+ stderr, stdout = self.get_stderr(), self.get_stdout()
+ self.mock_stderr(False)
+ self.mock_stdout(False)
+ self.assertFalse(stderr)
+ self.assertIn("test.eb... FIXED!", stdout)
- # parsing works now, toolchain is replaced with system toolchain
- ec = EasyConfig(test_ec)
- self.assertEqual(ec['toolchain'], {'name': 'system', 'version': 'system'})
- self.assertEqual(ec['configopts'], "--foobar --barfoo --barfoobaz")
+ # parsing now works
+ ec = EasyConfig(test_ec)
+ self.assertEqual(ec['configopts'], "--foobar --barfoo --barfoobaz")
+ self.assertFalse(stderr)
+ stdout = stdout.split('\n')
+ self.assertEqual(len(stdout), 6)
+ patterns = [
+ r"^\* \[1/1\] fixing .*/test.eb\.\.\. FIXED!$",
+ r"^\s*\(changes made in place, original copied to .*/test.eb.orig_[0-9_]+\)$",
+ r'^$',
+ r"^All done! Fixed 1 easyconfigs \(out of 1 found\).$",
+ r'^$',
+ r'^$',
+ ]
+ for idx, pattern in enumerate(patterns):
+ self.assertTrue(re.match(pattern, stdout[idx]), "Pattern '%s' matches '%s'" % (pattern, stdout[idx]))
- self.assertFalse(stderr)
- stdout = stdout.split('\n')
- self.assertEqual(len(stdout), 8)
- patterns = [
- r"^\* \[1/3\] fixing .*/t/toy/toy-0.0.eb\.\.\. \(no changes made\)$",
- r"^\* \[2/3\] fixing .*/test.eb\.\.\. FIXED!$",
- r"^\s*\(changes made in place, original copied to .*/test.eb.orig_[0-9_]+\)$",
- r"^\* \[3/3\] fixing .*/g/gzip/gzip-1.4.eb\.\.\. \(no changes made\)$",
- r'^$',
- r"^All done! Fixed 1 easyconfigs \(out of 3 found\).$",
- r'^$',
- r'^$',
- ]
- for idx, pattern in enumerate(patterns):
- self.assertTrue(re.match(pattern, stdout[idx]), "Pattern '%s' matches '%s'" % (pattern, stdout[idx]))
+ # cleanup
+ remove_file(glob.glob(os.path.join(test_ec + '.orig*'))[0])
def test_parse_list_comprehension_scope(self):
"""Test parsing of an easyconfig file that uses a local variable in list comprehension."""
@@ -4495,15 +4610,19 @@ def test_triage_easyconfig_params(self):
self.assertEqual(sorted(unknown_keys), ['bleh', 'foobar'])
# check behaviour when easyconfig parameters that use a name indicating a local variable were defined
- ec.update({
+ local_vars = {
'x': None,
'local_foo': None,
'_foo': None,
'_': None,
- })
+ }
+ ec.update(local_vars)
error = "Found 4 easyconfig parameters that are considered local variables: _, _foo, local_foo, x"
self.assertErrorRegex(EasyBuildError, error, triage_easyconfig_params, variables, ec)
+ for key in local_vars:
+ del ec[key]
+
def test_local_vars_detection(self):
"""Test detection of using unknown easyconfig parameters that are likely local variables."""
@@ -4645,31 +4764,39 @@ def test_cuda_compute_capabilities(self):
description = 'test'
toolchain = SYSTEM
cuda_compute_capabilities = ['5.1', '7.0', '7.1']
- installopts = '%(cuda_compute_capabilities)s'
- preinstallopts = 'period="%(cuda_cc_space_sep)s" noperiod="%(cuda_cc_space_sep_no_period)s"'
- prebuildopts = '%(cuda_cc_semicolon_sep)s'
- configopts = 'comma="%(cuda_sm_comma_sep)s" space="%(cuda_sm_space_sep)s"'
preconfigopts = 'CUDAARCHS="%(cuda_cc_cmake)s"'
+ configopts = 'comma="%(cuda_sm_comma_sep)s" space="%(cuda_sm_space_sep)s"'
+ prebuildopts = '%(cuda_cc_semicolon_sep)s'
+ buildopts = ('comma="%(cuda_int_comma_sep)s" space="%(cuda_int_space_sep)s" '
+ 'semi="%(cuda_int_semicolon_sep)s"')
+ preinstallopts = 'period="%(cuda_cc_space_sep)s" noperiod="%(cuda_cc_space_sep_no_period)s"'
+ installopts = '%(cuda_compute_capabilities)s'
""")
self.prep()
ec = EasyConfig(self.eb_file)
- self.assertEqual(ec['installopts'], '5.1,7.0,7.1')
- self.assertEqual(ec['preinstallopts'], 'period="5.1 7.0 7.1" noperiod="51 70 71"')
- self.assertEqual(ec['prebuildopts'], '5.1;7.0;7.1')
+ self.assertEqual(ec['preconfigopts'], 'CUDAARCHS="51;70;71"')
self.assertEqual(ec['configopts'], 'comma="sm_51,sm_70,sm_71" '
'space="sm_51 sm_70 sm_71"')
- self.assertEqual(ec['preconfigopts'], 'CUDAARCHS="51;70;71"')
+ self.assertEqual(ec['prebuildopts'], '5.1;7.0;7.1')
+ self.assertEqual(ec['buildopts'], 'comma="51,70,71" '
+ 'space="51 70 71" '
+ 'semi="51;70;71"')
+ self.assertEqual(ec['preinstallopts'], 'period="5.1 7.0 7.1" noperiod="51 70 71"')
+ self.assertEqual(ec['installopts'], '5.1,7.0,7.1')
# build options overwrite it
init_config(build_options={'cuda_compute_capabilities': ['4.2', '6.3']})
ec = EasyConfig(self.eb_file)
- self.assertEqual(ec['installopts'], '4.2,6.3')
- self.assertEqual(ec['preinstallopts'], 'period="4.2 6.3" noperiod="42 63"')
- self.assertEqual(ec['prebuildopts'], '4.2;6.3')
+ self.assertEqual(ec['preconfigopts'], 'CUDAARCHS="42;63"')
self.assertEqual(ec['configopts'], 'comma="sm_42,sm_63" '
'space="sm_42 sm_63"')
- self.assertEqual(ec['preconfigopts'], 'CUDAARCHS="42;63"')
+ self.assertEqual(ec['buildopts'], 'comma="42,63" '
+ 'space="42 63" '
+ 'semi="42;63"')
+ self.assertEqual(ec['prebuildopts'], '4.2;6.3')
+ self.assertEqual(ec['preinstallopts'], 'period="4.2 6.3" noperiod="42 63"')
+ self.assertEqual(ec['installopts'], '4.2,6.3')
def test_det_copy_ec_specs(self):
"""Test det_copy_ec_specs function."""
@@ -4697,60 +4824,51 @@ def test_det_copy_ec_specs(self):
return
# use fixed PR (speeds up the test due to caching in fetch_files_from_pr;
- # see https://github.com/easybuilders/easybuild-easyconfigs/pull/8007
- from_pr = 8007
- arrow_ec_fn = 'Arrow-0.7.1-intel-2017b-Python-3.6.3.eb'
- bat_ec_fn = 'bat-0.3.3-intel-2017b-Python-3.6.3.eb'
- bat_patch_fn = 'bat-0.3.3-fix-pyspark.patch'
+ # see https://github.com/easybuilders/easybuild-easyconfigs/pull/22345
+ from_pr = 22345
+ ec_fn = 'QuantumESPRESSO-7.4-foss-2024a.eb'
+ patch_fn = 'QuantumESPRESSO-7.4-parallel-symmetrization.patch'
pr_files = [
- arrow_ec_fn,
- bat_ec_fn,
- bat_patch_fn,
+ ec_fn,
+ patch_fn,
]
# if no paths are specified, default is to copy all files touched by PR to current working directory
paths, target_path = det_copy_ec_specs([], from_pr)
- self.assertEqual(len(paths), 3)
+ self.assertEqual(len(paths), 2)
filenames = sorted([os.path.basename(x) for x in paths])
self.assertEqual(filenames, sorted(pr_files))
self.assertTrue(os.path.samefile(target_path, cwd))
# last argument is used as target directory,
# unless it corresponds to a file touched by PR
- args = [bat_ec_fn, 'target_dir']
+ args = [ec_fn, 'target_dir']
paths, target_path = det_copy_ec_specs(args, from_pr)
self.assertEqual(len(paths), 1)
- self.assertEqual(os.path.basename(paths[0]), bat_ec_fn)
+ self.assertEqual(os.path.basename(paths[0]), ec_fn)
self.assertEqual(target_path, 'target_dir')
- args = [bat_ec_fn]
+ args = [ec_fn]
paths, target_path = det_copy_ec_specs(args, from_pr)
self.assertEqual(len(paths), 1)
- self.assertEqual(os.path.basename(paths[0]), bat_ec_fn)
+ self.assertEqual(os.path.basename(paths[0]), ec_fn)
self.assertTrue(os.path.samefile(target_path, cwd))
- args = [arrow_ec_fn, bat_ec_fn]
+ args = [ec_fn, patch_fn]
paths, target_path = det_copy_ec_specs(args, from_pr)
self.assertEqual(len(paths), 2)
- self.assertEqual(os.path.basename(paths[0]), arrow_ec_fn)
- self.assertEqual(os.path.basename(paths[1]), bat_ec_fn)
- self.assertTrue(os.path.samefile(target_path, cwd))
-
- args = [bat_ec_fn, bat_patch_fn]
- paths, target_path = det_copy_ec_specs(args, from_pr)
- self.assertEqual(len(paths), 2)
- self.assertEqual(os.path.basename(paths[0]), bat_ec_fn)
- self.assertEqual(os.path.basename(paths[1]), bat_patch_fn)
+ self.assertEqual(os.path.basename(paths[0]), ec_fn)
+ self.assertEqual(os.path.basename(paths[1]), patch_fn)
self.assertTrue(os.path.samefile(target_path, cwd))
# also test with combination of local files and files from PR
- args = [arrow_ec_fn, 'test.eb', 'test.patch', bat_patch_fn]
+ args = [ec_fn, 'test.eb', 'test.patch', patch_fn]
paths, target_path = det_copy_ec_specs(args, from_pr)
self.assertEqual(len(paths), 4)
- self.assertEqual(os.path.basename(paths[0]), arrow_ec_fn)
+ self.assertEqual(os.path.basename(paths[0]), ec_fn)
self.assertEqual(paths[1], 'test.eb')
self.assertEqual(paths[2], 'test.patch')
- self.assertEqual(os.path.basename(paths[3]), bat_patch_fn)
+ self.assertEqual(os.path.basename(paths[3]), patch_fn)
self.assertTrue(os.path.samefile(target_path, cwd))
def test_recursive_module_unload(self):
@@ -4759,6 +4877,10 @@ def test_recursive_module_unload(self):
toy_ec = os.path.join(test_ecs_dir, 'f', 'foss', 'foss-2018a.eb')
test_ec = os.path.join(self.test_prefix, 'test.eb')
test_ec_txt = read_file(toy_ec)
+
+ # this test only makes sense if depends_on is not used
+ self.allow_deprecated_behaviour()
+ test_ec_txt += '\nmodule_depends_on = False'
write_file(test_ec, test_ec_txt)
test_module = os.path.join(self.test_installpath, 'modules', 'all', 'foss', '2018a')
@@ -4769,7 +4891,10 @@ def test_recursive_module_unload(self):
recursive_unload_pat = r'if mode\(\) == "unload" or not \( isloaded\("%(mod)s"\) \) then\n'
recursive_unload_pat += r'\s*load\("%(mod)s"\)'
else:
- guarded_load_pat = r'if { \!\[ is-loaded %(mod)s \] } {\n\s*module load %(mod)s'
+ if self.modtool.supports_safe_auto_load:
+ guarded_load_pat = r'\nmodule load %(mod)s'
+ else:
+ guarded_load_pat = r'if { \!\[ is-loaded %(mod)s \] } {\n\s*module load %(mod)s'
recursive_unload_pat = r'if { \[ module-info mode remove \] \|\| \!\[ is-loaded %(mod)s \] } {\n'
recursive_unload_pat += r'\s*module load %(mod)s'
@@ -4784,8 +4909,9 @@ def test_recursive_module_unload(self):
self.assertFalse(ec['recursive_module_unload'])
eb = EasyBlock(ec)
eb.builddir = self.test_prefix
- eb.prepare_step()
- eb.make_module_step()
+ with self.mocked_stdout_stderr():
+ eb.prepare_step()
+ eb.make_module_step()
modtxt = read_file(test_module)
fail_msg = "Pattern '%s' should be found in: %s" % (guarded_load_regex.pattern, modtxt)
self.assertTrue(guarded_load_regex.search(modtxt), fail_msg)
@@ -4797,43 +4923,62 @@ def test_recursive_module_unload(self):
# recursive_module_unload easyconfig parameter is honored
test_ec_bis = os.path.join(self.test_prefix, 'test_bis.eb')
test_ec_bis_txt = read_file(toy_ec) + '\nrecursive_module_unload = True'
+ # this test only makes sense if depends_on is not used
+ test_ec_bis_txt += '\nmodule_depends_on = False'
write_file(test_ec_bis, test_ec_bis_txt)
ec_bis = EasyConfig(test_ec_bis)
self.assertTrue(ec_bis['recursive_module_unload'])
eb_bis = EasyBlock(ec_bis)
eb_bis.builddir = self.test_prefix
- eb_bis.prepare_step()
- eb_bis.make_module_step()
+ with self.mocked_stdout_stderr():
+ eb_bis.prepare_step()
+ eb_bis.make_module_step()
modtxt = read_file(test_module)
- fail_msg = "Pattern '%s' should not be found in: %s" % (guarded_load_regex.pattern, modtxt)
- self.assertFalse(guarded_load_regex.search(modtxt), fail_msg)
- fail_msg = "Pattern '%s' should be found in: %s" % (recursive_unload_regex.pattern, modtxt)
- self.assertTrue(recursive_unload_regex.search(modtxt), fail_msg)
+ if self.modtool.supports_safe_auto_load:
+ fail_msg = "Pattern '%s' should be found in: %s" % (guarded_load_regex.pattern, modtxt)
+ self.assertTrue(guarded_load_regex.search(modtxt), fail_msg)
+ fail_msg = "Pattern '%s' should not be found in: %s" % (recursive_unload_regex.pattern, modtxt)
+ self.assertFalse(recursive_unload_regex.search(modtxt), fail_msg)
+ else:
+ fail_msg = "Pattern '%s' should not be found in: %s" % (guarded_load_regex.pattern, modtxt)
+ self.assertFalse(guarded_load_regex.search(modtxt), fail_msg)
+ fail_msg = "Pattern '%s' should be found in: %s" % (recursive_unload_regex.pattern, modtxt)
+ self.assertTrue(recursive_unload_regex.search(modtxt), fail_msg)
# recursive_mod_unload build option is honored
update_build_option('recursive_mod_unload', True)
eb = EasyBlock(ec)
eb.builddir = self.test_prefix
- eb.prepare_step()
- eb.make_module_step()
+ with self.mocked_stdout_stderr():
+ eb.prepare_step()
+ eb.make_module_step()
modtxt = read_file(test_module)
- fail_msg = "Pattern '%s' should not be found in: %s" % (guarded_load_regex.pattern, modtxt)
- self.assertFalse(guarded_load_regex.search(modtxt), fail_msg)
- fail_msg = "Pattern '%s' should be found in: %s" % (recursive_unload_regex.pattern, modtxt)
- self.assertTrue(recursive_unload_regex.search(modtxt), fail_msg)
+ if self.modtool.supports_safe_auto_load:
+ fail_msg = "Pattern '%s' should be found in: %s" % (guarded_load_regex.pattern, modtxt)
+ self.assertTrue(guarded_load_regex.search(modtxt), fail_msg)
+ fail_msg = "Pattern '%s' should not be found in: %s" % (recursive_unload_regex.pattern, modtxt)
+ self.assertFalse(recursive_unload_regex.search(modtxt), fail_msg)
+ else:
+ fail_msg = "Pattern '%s' should not be found in: %s" % (guarded_load_regex.pattern, modtxt)
+ self.assertFalse(guarded_load_regex.search(modtxt), fail_msg)
+ fail_msg = "Pattern '%s' should be found in: %s" % (recursive_unload_regex.pattern, modtxt)
+ self.assertTrue(recursive_unload_regex.search(modtxt), fail_msg)
# disabling via easyconfig parameter works even when recursive_mod_unload build option is enabled
self.assertTrue(build_option('recursive_mod_unload'))
test_ec_bis = os.path.join(self.test_prefix, 'test_bis.eb')
test_ec_bis_txt = read_file(toy_ec) + '\nrecursive_module_unload = False'
+ # this test only makes sense if depends_on is not used
+ test_ec_bis_txt += '\nmodule_depends_on = False'
write_file(test_ec_bis, test_ec_bis_txt)
ec_bis = EasyConfig(test_ec_bis)
self.assertEqual(ec_bis['recursive_module_unload'], False)
eb_bis = EasyBlock(ec_bis)
eb_bis.builddir = self.test_prefix
- eb_bis.prepare_step()
- eb_bis.make_module_step()
+ with self.mocked_stdout_stderr():
+ eb_bis.prepare_step()
+ eb_bis.make_module_step()
modtxt = read_file(test_module)
fail_msg = "Pattern '%s' should be found in: %s" % (guarded_load_regex.pattern, modtxt)
self.assertTrue(guarded_load_regex.search(modtxt), fail_msg)
@@ -4928,6 +5073,9 @@ def test_get_cuda_cc_template_value(self):
'cuda_compute_capabilities': '6.5,7.0',
'cuda_cc_space_sep': '6.5 7.0',
'cuda_cc_semicolon_sep': '6.5;7.0',
+ 'cuda_int_comma_sep': '65,70',
+ 'cuda_int_space_sep': '65 70',
+ 'cuda_int_semicolon_sep': '65;70',
'cuda_sm_comma_sep': 'sm_65,sm_70',
'cuda_sm_space_sep': 'sm_65 sm_70',
}
@@ -4937,8 +5085,8 @@ def test_get_cuda_cc_template_value(self):
update_build_option('cuda_compute_capabilities', ['6.5', '7.0'])
ec = EasyConfig(self.eb_file)
- for key in cuda_template_values:
- self.assertEqual(ec.get_cuda_cc_template_value(key), cuda_template_values[key])
+ for key, expected in cuda_template_values.items():
+ self.assertEqual(ec.get_cuda_cc_template_value(key), expected)
update_build_option('cuda_compute_capabilities', None)
ec = EasyConfig(self.eb_file)
@@ -4950,8 +5098,8 @@ def test_get_cuda_cc_template_value(self):
self.prep()
ec = EasyConfig(self.eb_file)
- for key in cuda_template_values:
- self.assertEqual(ec.get_cuda_cc_template_value(key), cuda_template_values[key])
+ for key, expected in cuda_template_values.items():
+ self.assertEqual(ec.get_cuda_cc_template_value(key), expected)
def test_count_files(self):
"""Tests for EasyConfig.count_files method."""
@@ -4982,7 +5130,7 @@ def test_count_files(self):
toy_exts_ec = EasyConfig(toy_exts)
self.assertEqual(len(toy_exts_ec['sources']), 1)
self.assertEqual(len(toy_exts_ec['patches']), 1)
- self.assertEqual(len(toy_exts_ec['exts_list']), 4)
+ self.assertEqual(len(toy_exts_ec.get_ref('exts_list')), 4)
self.assertEqual(toy_exts_ec.count_files(), 7)
test_ec = os.path.join(self.test_prefix, 'test.eb')
@@ -4993,7 +5141,7 @@ def test_count_files(self):
' ("test-ext-one", "0.0", {',
' "sources": ["test-ext-one-0.0-part1.tgz", "test-ext-one-0.0-part2.zip"],',
# if both 'sources' and 'source_tmpl' are specified, 'source_tmpl' is ignored,
- # see EasyBlock.fetch_extension_sources, so it should be too when counting files
+ # see EasyBlock.collect_exts_file_info, so it should be too when counting files
' "source_tmpl": "test-ext-one-%(version)s.tar.gz",',
' }),',
' ("test-ext-two", "0.0", {',
@@ -5061,6 +5209,43 @@ def test_easyconfigs_caches(self):
regex = re.compile(r"libtoy/0\.0 is already installed", re.M)
self.assertTrue(regex.search(stdout), "Pattern '%s' should be found in: %s" % (regex.pattern, stdout))
+ def test_templates(self):
+ """
+ Test use of template values like %(version)s
+ """
+ test_ecs_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'easyconfigs', 'test_ecs')
+ toy_ec = os.path.join(test_ecs_dir, 't', 'toy', 'toy-0.0.eb')
+
+ test_ec_txt = read_file(toy_ec)
+ test_ec_txt += '\ndescription = "name: %(name)s, version: %(version)s"'
+
+ test_ec = os.path.join(self.test_prefix, 'test.eb')
+ write_file(test_ec, test_ec_txt)
+ ec = EasyConfig(test_ec)
+
+ # get_ref provides access to non-templated raw value
+ self.assertEqual(ec.get_ref('description'), "name: %(name)s, version: %(version)s")
+ self.assertEqual(ec['description'], "name: toy, version: 0.0")
+
+ # error when using wrong template value or using template value that can not be resolved yet too early
+ test_ec_txt += '\ndescription = "name: %(name)s, version: %(version)s, pyshortver: %(pyshortver)s"'
+ write_file(test_ec, test_ec_txt)
+ ec = EasyConfig(test_ec)
+
+ self.assertEqual(ec.get_ref('description'), "name: %(name)s, version: %(version)s, pyshortver: %(pyshortver)s")
+ error_pattern = r"Failed to resolve all templates in.* %\(pyshortver\)s.* using template dictionary:"
+ self.assertErrorRegex(EasyBuildError, error_pattern, ec.__getitem__, 'description')
+
+ # EasyBuild can be configured to allow unresolved templates
+ update_build_option('allow_unresolved_templates', True)
+ self.assertEqual(ec.get_ref('description'), "name: %(name)s, version: %(version)s, pyshortver: %(pyshortver)s")
+ with self.mocked_stdout_stderr() as (stdout, stderr):
+ self.assertEqual(ec['description'], "name: %(name)s, version: %(version)s, pyshortver: %(pyshortver)s")
+
+ self.assertFalse(stdout.getvalue())
+ regex = re.compile(r"WARNING: Failed to resolve all templates.* %\(pyshortver\)s", re.M)
+ self.assertRegex(stderr.getvalue(), regex)
+
def suite():
""" returns all the testcases in this module """
diff --git a/test/framework/easyconfigformat.py b/test/framework/easyconfigformat.py
index 7b84a2c867..537b4c1977 100644
--- a/test/framework/easyconfigformat.py
+++ b/test/framework/easyconfigformat.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/easyconfigparser.py b/test/framework/easyconfigparser.py
index fa943df938..4fbe6676a0 100644
--- a/test/framework/easyconfigparser.py
+++ b/test/framework/easyconfigparser.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -39,7 +39,6 @@
from easybuild.framework.easyconfig.parser import EasyConfigParser
from easybuild.tools.build_log import EasyBuildError
from easybuild.tools.filetools import read_file
-from easybuild.tools.py2vs3 import string_type
TESTDIRBASE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'easyconfigs')
@@ -198,11 +197,11 @@ def test_easyconfig_constants(self):
# make sure both keys and values are of appropriate types
for constant_name in constants:
- self.assertIsInstance(constant_name, string_type, "Constant name %s is a string" % constant_name)
+ self.assertIsInstance(constant_name, str, "Constant name %s is a string" % constant_name)
val = constants[constant_name]
fail_msg = "The constant %s should have an acceptable type, found %s (%s)" % (constant_name,
type(val), str(val))
- self.assertIsInstance(val, (string_type, dict, tuple), fail_msg)
+ self.assertIsInstance(val, (str, dict, tuple), fail_msg)
# check a couple of randomly picked constant values
self.assertEqual(constants['SOURCE_TAR_GZ'], '%(name)s-%(version)s.tar.gz')
diff --git a/test/framework/easyconfigs/test_ecs/p/Python/Python-2.7.10-intel-2018a.eb b/test/framework/easyconfigs/test_ecs/p/Python/Python-2.7.10-intel-2018a.eb
index b2195b5ce0..a4fba84756 100644
--- a/test/framework/easyconfigs/test_ecs/p/Python/Python-2.7.10-intel-2018a.eb
+++ b/test/framework/easyconfigs/test_ecs/p/Python/Python-2.7.10-intel-2018a.eb
@@ -29,8 +29,7 @@ dependencies = [
# it's nice to have an up to date openssl for security reasons
]
-# zlib is only included here for the sake of testing parsing of .yeb easyconfigs!
-osdependencies = ['zlib', ('openssl-devel', 'libssl-dev', 'libopenssl-devel')]
+osdependencies = [('openssl-devel', 'libssl-dev', 'libopenssl-devel')]
# order is important!
# package versions updated May 28th 2015
diff --git a/test/framework/easyconfigs/test_ecs/p/Python/Python-2.7.15.eb b/test/framework/easyconfigs/test_ecs/p/Python/Python-2.7.15.eb
new file mode 100644
index 0000000000..b9f408e3ef
--- /dev/null
+++ b/test/framework/easyconfigs/test_ecs/p/Python/Python-2.7.15.eb
@@ -0,0 +1,23 @@
+easyblock = 'ConfigureMake'
+
+name = 'Python'
+version = '2.7.15'
+
+homepage = 'http://python.org/'
+description = """Python is a programming language that lets you work more quickly and integrate your systems
+more effectively."""
+
+toolchain = SYSTEM
+
+source_urls = ['http://www.python.org/ftp/%(namelower)s/%(version)s/']
+sources = [SOURCE_TGZ]
+
+# This just serves to have a Python as a dependency to test e.g. the Python version templates
+# So all dependencies and extensions are removed
+dependencies = []
+
+osdependencies = []
+
+exts_list = []
+
+moduleclass = 'lang'
diff --git a/test/framework/easyconfigs/test_ecs/p/Python/Python-3.7.2.eb b/test/framework/easyconfigs/test_ecs/p/Python/Python-3.7.2.eb
new file mode 100644
index 0000000000..a61cabbc32
--- /dev/null
+++ b/test/framework/easyconfigs/test_ecs/p/Python/Python-3.7.2.eb
@@ -0,0 +1,23 @@
+easyblock = 'ConfigureMake'
+
+name = 'Python'
+version = '3.7.2'
+
+homepage = 'http://python.org/'
+description = """Python is a programming language that lets you work more quickly and integrate your systems
+more effectively."""
+
+toolchain = SYSTEM
+
+source_urls = ['http://www.python.org/ftp/%(namelower)s/%(version)s/']
+sources = [SOURCE_TGZ]
+
+# This just serves to have a Python as a dependency to test e.g. the Python version templates
+# So all dependencies and extensions are removed
+dependencies = []
+
+osdependencies = []
+
+exts_list = []
+
+moduleclass = 'lang'
diff --git a/test/framework/easyconfigs/test_ecs/s/SQLite/SQLite-3.8.10.2-GCC-6.4.0-2.28.eb b/test/framework/easyconfigs/test_ecs/s/SQLite/SQLite-3.8.10.2-GCC-6.4.0-2.28.eb
index c272f8dbbd..c6df4d42c6 100644
--- a/test/framework/easyconfigs/test_ecs/s/SQLite/SQLite-3.8.10.2-GCC-6.4.0-2.28.eb
+++ b/test/framework/easyconfigs/test_ecs/s/SQLite/SQLite-3.8.10.2-GCC-6.4.0-2.28.eb
@@ -31,7 +31,7 @@ dependencies = [
# ('Tcl', '8.6.4'),
]
-parallel = 1
+maxparallel = 1
sanity_check_paths = {
'files': ['bin/sqlite3', 'include/sqlite3ext.h', 'include/sqlite3.h', 'lib/libsqlite3.a', 'lib/libsqlite3.so'],
diff --git a/test/framework/easyconfigs/test_ecs/s/SQLite/SQLite-3.8.10.2-foss-2018a.eb b/test/framework/easyconfigs/test_ecs/s/SQLite/SQLite-3.8.10.2-foss-2018a.eb
index 593ca3018a..937553eaf3 100644
--- a/test/framework/easyconfigs/test_ecs/s/SQLite/SQLite-3.8.10.2-foss-2018a.eb
+++ b/test/framework/easyconfigs/test_ecs/s/SQLite/SQLite-3.8.10.2-foss-2018a.eb
@@ -31,7 +31,7 @@ dependencies = [
# ('Tcl', '8.6.4'),
]
-parallel = 1
+maxparallel = 1
sanity_check_paths = {
'files': ['bin/sqlite3', 'include/sqlite3ext.h', 'include/sqlite3.h', 'lib/libsqlite3.a', 'lib/libsqlite3.so'],
diff --git a/test/framework/easyconfigs/test_ecs/s/SQLite/SQLite-3.8.10.2-gompi-2018a.eb b/test/framework/easyconfigs/test_ecs/s/SQLite/SQLite-3.8.10.2-gompi-2018a.eb
index 5463f54352..b0be557296 100644
--- a/test/framework/easyconfigs/test_ecs/s/SQLite/SQLite-3.8.10.2-gompi-2018a.eb
+++ b/test/framework/easyconfigs/test_ecs/s/SQLite/SQLite-3.8.10.2-gompi-2018a.eb
@@ -31,7 +31,7 @@ dependencies = [
# ('Tcl', '8.6.4'),
]
-parallel = 1
+maxparallel = 1
sanity_check_paths = {
'files': ['bin/sqlite3', 'include/sqlite3ext.h', 'include/sqlite3.h', 'lib/libsqlite3.a', 'lib/libsqlite3.so'],
diff --git a/test/framework/easyconfigs/test_ecs/s/ScaLAPACK/ScaLAPACK-2.0.2-gompi-2018a-OpenBLAS-0.2.20-broken.eb b/test/framework/easyconfigs/test_ecs/s/ScaLAPACK/ScaLAPACK-2.0.2-gompi-2018a-OpenBLAS-0.2.20-broken.eb
index 6b3ce0d492..e85f325169 100644
--- a/test/framework/easyconfigs/test_ecs/s/ScaLAPACK/ScaLAPACK-2.0.2-gompi-2018a-OpenBLAS-0.2.20-broken.eb
+++ b/test/framework/easyconfigs/test_ecs/s/ScaLAPACK/ScaLAPACK-2.0.2-gompi-2018a-OpenBLAS-0.2.20-broken.eb
@@ -23,6 +23,6 @@ builddependencies = [('CMake','2.8.10')]
dependencies = [(local_blaslib, local_blasver, '')]
# parallel build tends to fail, so disabling it
-parallel = 1
+maxparallel = 1
moduleclass = 'numlib'
diff --git a/test/framework/easyconfigs/test_ecs/s/ScaLAPACK/ScaLAPACK-2.0.2-gompi-2018a-OpenBLAS-0.2.20.eb b/test/framework/easyconfigs/test_ecs/s/ScaLAPACK/ScaLAPACK-2.0.2-gompi-2018a-OpenBLAS-0.2.20.eb
index bb6a33480d..4a2c2bcaf2 100644
--- a/test/framework/easyconfigs/test_ecs/s/ScaLAPACK/ScaLAPACK-2.0.2-gompi-2018a-OpenBLAS-0.2.20.eb
+++ b/test/framework/easyconfigs/test_ecs/s/ScaLAPACK/ScaLAPACK-2.0.2-gompi-2018a-OpenBLAS-0.2.20.eb
@@ -22,6 +22,6 @@ versionsuffix = "-%s-%s" % (local_blaslib, local_blasver)
dependencies = [(local_blaslib, local_blasver, '')]
# parallel build tends to fail, so disabling it
-parallel = 1
+maxparallel = 1
moduleclass = 'numlib'
diff --git a/test/framework/easyconfigs/test_ecs/s/ScaLAPACK/ScaLAPACK-2.0.2-gompic-2018a-OpenBLAS-0.2.20.eb b/test/framework/easyconfigs/test_ecs/s/ScaLAPACK/ScaLAPACK-2.0.2-gompic-2018a-OpenBLAS-0.2.20.eb
index d8c8855873..b3db5dbbf0 100644
--- a/test/framework/easyconfigs/test_ecs/s/ScaLAPACK/ScaLAPACK-2.0.2-gompic-2018a-OpenBLAS-0.2.20.eb
+++ b/test/framework/easyconfigs/test_ecs/s/ScaLAPACK/ScaLAPACK-2.0.2-gompic-2018a-OpenBLAS-0.2.20.eb
@@ -22,6 +22,6 @@ versionsuffix = "-%s-%s" % (local_blaslib, local_blasver)
dependencies = [(local_blaslib, local_blasver)]
# parallel build tends to fail, so disabling it
-parallel = 1
+maxparallel = 1
moduleclass = 'numlib'
diff --git a/test/framework/easyconfigs/test_ecs/t/toy/toy-0.0-deps.eb b/test/framework/easyconfigs/test_ecs/t/toy/toy-0.0-deps.eb
index 4ae349d0a0..1e3f34a777 100644
--- a/test/framework/easyconfigs/test_ecs/t/toy/toy-0.0-deps.eb
+++ b/test/framework/easyconfigs/test_ecs/t/toy/toy-0.0-deps.eb
@@ -9,13 +9,10 @@ toolchain = SYSTEM
sources = [SOURCE_TAR_GZ]
checksums = [[
- 'be662daa971a640e40be5c804d9d7d10', # default (MD5)
'44332000aa33b99ad1e00cbd1a7da769220d74647060a10e807b916d73ea27bc', # default (SHA256)
- ('adler32', '0x998410035'),
- ('crc32', '0x1553842328'),
- ('md5', 'be662daa971a640e40be5c804d9d7d10'),
- ('sha1', 'f618096c52244539d0e89867405f573fdb0b55b0'),
- ('size', 273),
+ ('sha512',
+ '3c9dc629e1f2fd01a15c68f9f2a328b5da045c2ec1a189dc72d7195642f32e0'
+ 'ff59275aba5fa2a78e84417c7645d0ca5d06aff39e688a8936061ed5c4c600708'),
]]
patches = ['toy-0.0_fix-silly-typo-in-printf-statement.patch']
diff --git a/test/framework/easyconfigs/test_ecs/t/toy/toy-0.0-gompi-2018a-test.eb b/test/framework/easyconfigs/test_ecs/t/toy/toy-0.0-gompi-2018a-test.eb
index ced8241a45..8d5b7b7f99 100644
--- a/test/framework/easyconfigs/test_ecs/t/toy/toy-0.0-gompi-2018a-test.eb
+++ b/test/framework/easyconfigs/test_ecs/t/toy/toy-0.0-gompi-2018a-test.eb
@@ -10,13 +10,10 @@ toolchainopts = {'pic': True, 'opt': True, 'optarch': True}
sources = [SOURCE_TAR_GZ]
checksums = [[
- 'be662daa971a640e40be5c804d9d7d10', # default (MD5)
'44332000aa33b99ad1e00cbd1a7da769220d74647060a10e807b916d73ea27bc', # default (SHA256)
- ('adler32', '0x998410035'),
- ('crc32', '0x1553842328'),
- ('md5', 'be662daa971a640e40be5c804d9d7d10'),
- ('sha1', 'f618096c52244539d0e89867405f573fdb0b55b0'),
- ('size', 273),
+ ('sha512',
+ '3c9dc629e1f2fd01a15c68f9f2a328b5da045c2ec1a189dc72d7195642f32e0'
+ 'ff59275aba5fa2a78e84417c7645d0ca5d06aff39e688a8936061ed5c4c600708'),
{SOURCE_TAR_GZ: '44332000aa33b99ad1e00cbd1a7da769220d74647060a10e807b916d73ea27bc',
'bar.tgz': '33ac60685a3e29538db5094259ea85c15906cbd0f74368733f4111eab6187c8f'},
]]
@@ -26,10 +23,14 @@ exts_default_options = {
'source_urls': ['http://example.com/%(name)s'],
}
+local_bar_buildopts = " && gcc bar.c -o anotherbar && "
+# used to check whether $TOY_LIBS_PATH is defined even when 'lib' subdirectory doesn't exist yet
+local_bar_buildopts += 'echo "TOY_EXAMPLES=$TOY_EXAMPLES" > %(installdir)s/toy_libs_path.txt'
+
exts_list = [
'ulimit', # extension that is part of "standard library"
('bar', '0.0', {
- 'buildopts': " && gcc bar.c -o anotherbar",
+ 'buildopts': local_bar_buildopts,
'checksums': ['f3676716b610545a4e8035087f5be0a0248adee0abb3930d3edb76d498ae91e7'], # checksum for
# custom extension filter to verify use of stdin value being passed to filter command
'exts_filter': ("cat | grep '^bar$'", '%(name)s'),
@@ -41,7 +42,7 @@ exts_list = [
'unknowneasyconfigparameterthatshouldbeignored': 'foo',
# set boolean value (different from default value) to trigger (now fixed) bug with --inject-checksums
# cfr. https://github.com/easybuilders/easybuild-framework/pull/3034
- 'keepsymlinks': True,
+ 'keepsymlinks': False,
}),
('barbar', '1.2', {
'start_dir': 'src',
@@ -58,6 +59,8 @@ sanity_check_paths = {
'dirs': [],
}
+modextrapaths = {'TOY_EXAMPLES': 'examples'}
+
postinstallcmds = ["echo TOY > %(installdir)s/README"]
moduleclass = 'tools'
diff --git a/test/framework/easyconfigs/test_ecs/t/toy/toy-0.0-gompi-2018a.eb b/test/framework/easyconfigs/test_ecs/t/toy/toy-0.0-gompi-2018a.eb
index 925432d02a..c8d0504764 100644
--- a/test/framework/easyconfigs/test_ecs/t/toy/toy-0.0-gompi-2018a.eb
+++ b/test/framework/easyconfigs/test_ecs/t/toy/toy-0.0-gompi-2018a.eb
@@ -9,13 +9,10 @@ toolchainopts = {'pic': True, 'opt': True, 'optarch': True}
sources = [SOURCE_TAR_GZ]
checksums = [[
- 'be662daa971a640e40be5c804d9d7d10', # default (MD5)
'44332000aa33b99ad1e00cbd1a7da769220d74647060a10e807b916d73ea27bc', # default (SHA256)
- ('adler32', '0x998410035'),
- ('crc32', '0x1553842328'),
- ('md5', 'be662daa971a640e40be5c804d9d7d10'),
- ('sha1', 'f618096c52244539d0e89867405f573fdb0b55b0'),
- ('size', 273),
+ ('sha512',
+ '3c9dc629e1f2fd01a15c68f9f2a328b5da045c2ec1a189dc72d7195642f32e0'
+ 'ff59275aba5fa2a78e84417c7645d0ca5d06aff39e688a8936061ed5c4c600708'),
]]
patches = [
'toy-0.0_fix-silly-typo-in-printf-statement.patch',
diff --git a/test/framework/easyconfigs/test_ecs/t/toy/toy-0.0-multiple.eb b/test/framework/easyconfigs/test_ecs/t/toy/toy-0.0-multiple.eb
index 68ece2259f..e02e21f7ae 100644
--- a/test/framework/easyconfigs/test_ecs/t/toy/toy-0.0-multiple.eb
+++ b/test/framework/easyconfigs/test_ecs/t/toy/toy-0.0-multiple.eb
@@ -11,8 +11,8 @@ toolchain = SYSTEM
sources = [SOURCE_TAR_GZ]
patches = ['toy-0.0_fix-silly-typo-in-printf-statement.patch']
checksums = [
- ('adler32', '0x998410035'),
- 'a99f2a72cee1689a2f7e3ace0356efb1',
+ '44332000aa33b99ad1e00cbd1a7da769220d74647060a10e807b916d73ea27bc',
+ '81a3accc894592152f81814fbf133d39afad52885ab52c25018722c7bda92487',
]
moduleclass = 'tools'
diff --git a/test/framework/easyconfigs/test_ecs/t/toy/toy-0.0-test.eb b/test/framework/easyconfigs/test_ecs/t/toy/toy-0.0-test.eb
index 90cc7429d3..286cdf7f6c 100644
--- a/test/framework/easyconfigs/test_ecs/t/toy/toy-0.0-test.eb
+++ b/test/framework/easyconfigs/test_ecs/t/toy/toy-0.0-test.eb
@@ -9,13 +9,10 @@ toolchain = SYSTEM
sources = [SOURCE_TAR_GZ]
checksums = [[
- 'be662daa971a640e40be5c804d9d7d10', # default (MD5)
'44332000aa33b99ad1e00cbd1a7da769220d74647060a10e807b916d73ea27bc', # default (SHA256)
- ('adler32', '0x998410035'),
- ('crc32', '0x1553842328'),
- ('md5', 'be662daa971a640e40be5c804d9d7d10'),
- ('sha1', 'f618096c52244539d0e89867405f573fdb0b55b0'),
- ('size', 273),
+ ('sha512',
+ '3c9dc629e1f2fd01a15c68f9f2a328b5da045c2ec1a189dc72d7195642f32e0'
+ 'ff59275aba5fa2a78e84417c7645d0ca5d06aff39e688a8936061ed5c4c600708'),
]]
patches = [
'toy-0.0_fix-silly-typo-in-printf-statement.patch',
diff --git a/test/framework/easyconfigs/test_ecs/t/toy/toy-0.0.eb b/test/framework/easyconfigs/test_ecs/t/toy/toy-0.0.eb
index c2a88616b1..0e087cec7c 100644
--- a/test/framework/easyconfigs/test_ecs/t/toy/toy-0.0.eb
+++ b/test/framework/easyconfigs/test_ecs/t/toy/toy-0.0.eb
@@ -8,13 +8,10 @@ toolchain = SYSTEM
sources = [SOURCE_TAR_GZ]
checksums = [[
- 'be662daa971a640e40be5c804d9d7d10', # default (MD5)
'44332000aa33b99ad1e00cbd1a7da769220d74647060a10e807b916d73ea27bc', # default (SHA256)
- ('adler32', '0x998410035'),
- ('crc32', '0x1553842328'),
- ('md5', 'be662daa971a640e40be5c804d9d7d10'),
- ('sha1', 'f618096c52244539d0e89867405f573fdb0b55b0'),
- ('size', 273),
+ ('sha512',
+ '3c9dc629e1f2fd01a15c68f9f2a328b5da045c2ec1a189dc72d7195642f32e0'
+ 'ff59275aba5fa2a78e84417c7645d0ca5d06aff39e688a8936061ed5c4c600708'),
]]
patches = [
'toy-0.0_fix-silly-typo-in-printf-statement.patch',
diff --git a/test/framework/easyconfigs/v2.0/toy-with-sections.eb b/test/framework/easyconfigs/v2.0/toy-with-sections.eb
index 34b9af0dcd..a1a508bcf0 100644
--- a/test/framework/easyconfigs/v2.0/toy-with-sections.eb
+++ b/test/framework/easyconfigs/v2.0/toy-with-sections.eb
@@ -16,7 +16,6 @@ software_license_urls = ['https://github.com/easybuilders/easybuild/wiki/License
sources = ['%(name)s-0.0.tar.gz'] # purposely fixed to 0.0
checksums = [
- 'be662daa971a640e40be5c804d9d7d10', # MD5
'44332000aa33b99ad1e00cbd1a7da769220d74647060a10e807b916d73ea27bc', # SHA256
]
diff --git a/test/framework/easyconfigs/v2.0/toy.eb b/test/framework/easyconfigs/v2.0/toy.eb
index a1cdfaf6d8..3c5a000f1b 100644
--- a/test/framework/easyconfigs/v2.0/toy.eb
+++ b/test/framework/easyconfigs/v2.0/toy.eb
@@ -16,7 +16,6 @@ software_license_urls = ['https://github.com/easybuilders/easybuild/wiki/License
sources = ['%(name)s-0.0.tar.gz'] # purposely fixed to 0.0
checksums = [
- 'be662daa971a640e40be5c804d9d7d10', # MD5
'44332000aa33b99ad1e00cbd1a7da769220d74647060a10e807b916d73ea27bc', # SHA256
]
diff --git a/test/framework/easyconfigs/yeb/CrayCCE-5.1.29.yeb b/test/framework/easyconfigs/yeb/CrayCCE-5.1.29.yeb
deleted file mode 100644
index 7244848a78..0000000000
--- a/test/framework/easyconfigs/yeb/CrayCCE-5.1.29.yeb
+++ /dev/null
@@ -1,18 +0,0 @@
-easyblock: Toolchain
-
-name: CrayCCE
-version: 5.1.29
-
-homepage: (none)
-description: Toolchain using Cray compiler wrapper, using PrgEnv-cray module.
-
-toolchain: system, system
-
-dependencies:
- # also loads cray-libsci
- - name: PrgEnv-cray/5.1.29
- external_module: true
- - name: fftw/3.3.4.0
- external_module: true
-
-moduleclass: toolchain
diff --git a/test/framework/easyconfigs/yeb/Python-2.7.10-intel-2018a.yeb b/test/framework/easyconfigs/yeb/Python-2.7.10-intel-2018a.yeb
deleted file mode 100644
index 527f7bf8a5..0000000000
--- a/test/framework/easyconfigs/yeb/Python-2.7.10-intel-2018a.yeb
+++ /dev/null
@@ -1,131 +0,0 @@
-_internal_variables_:
- - &numpyversion 1.9.2
- - &scipyversion 0.15.1
-
-easyblock: ConfigureMake
-
-name: Python
-version: 2.7.10
-
-homepage: http://python.org/
-description: |
- Python is a programming language that lets you work more quickly and integrate your systems
- more effectively.
-
-toolchain: {name: intel, version: 2018a}
-toolchainopts: {pic: True, opt: True, optarch: True}
-
-source_urls: ['http://www.python.org/ftp/python/%(version)s/']
-sources: [*SOURCE_TGZ]
-
-# python needs bzip2 to build the bz2 package
-# commented out for testing to avoid having to add them all - dependencies are tested in other files
-dependencies: [
-# [bzip2, 1.0.6],
-# [zlib, 1.2.8],
-# [libreadline, '6.3'],
-# [ncurses, '5.9'],
-# [SQLite, 3.8.10.2],
-# [Tk, 8.6.4, -no-X11],
-# [OpenSSL, 1.0.1m], # OS dependency should be preferred if the os version is more recent then this version, its
-# nice to have an up to date openssl for security reasons
-]
-
-# zlib is only included here for the sake of testing parsing of .yeb easyconfigs!
-osdependencies: [zlib, [openssl-devel, libssl-dev, libopenssl-devel]]
-
-# order is important!
-# package versions updated May 28th 2015
-exts_list: [
- [setuptools, '16.0', {
- source_urls: ["https://pypi.python.org/packages/source/s/setuptools/"],
- }],
- [pip, 7.0.1, {
- source_urls: ["https://pypi.python.org/packages/source/p/pip/"],
- }],
- [nose, 1.3.6, {
- source_urls: ["https://pypi.python.org/packages/source/n/nose/"],
- }],
- [numpy, *numpyversion, {
- source_urls: [
- [!join ["http://sourceforge.net/projects/numpy/files/NumPy/", *numpyversion], download]
- ],
- patches: [
- numpy-1.8.0-mkl.patch, # % numpyversion,
- ],
- }],
- [scipy, *scipyversion, {
- source_urls: [
- [!join ["http://sourceforge.net/projects/scipy/files/scipy/", *scipyversion], download]],
- }],
- [blist, 1.3.6, {
- source_urls: ["https://pypi.python.org/packages/source/b/blist/"],
- }],
- [mpi4py, 1.3.1, {
- source_urls: ["http://bitbucket.org/mpi4py/mpi4py/downloads/"],
- }],
- [paycheck, 1.0.2, {
- source_urls: ["https://pypi.python.org/packages/source/p/paycheck/"],
- }],
- [argparse, 1.3.0, {
- source_urls: ["https://pypi.python.org/packages/source/a/argparse/"],
- }],
- [pbr, 1.0.1, {
- source_urls: ["https://pypi.python.org/packages/source/p/pbr/"],
- }],
- [lockfile, 0.10.2, {
- source_urls: ["https://pypi.python.org/packages/source/l/lockfile/"],
- }],
- [Cython, '0.22', {
- source_urls: ["http://www.cython.org/release/"],
- }],
- [six, 1.9.0, {
- source_urls: ["https://pypi.python.org/packages/source/s/six/"],
- }],
- [dateutil, 2.4.2, {
- source_tmpl: python-%(name)s-%(version)s.tar.gz,
- source_urls: ["https://pypi.python.org/packages/source/p/python-dateutil/"],
- }],
- [deap, 1.0.2, {
- source_tmpl: "%(name)s-%(version)s.post2.tar.gz",
- source_urls: ["https://pypi.python.org/packages/source/d/deap/"],
- }],
- [decorator, 3.4.2, {
- source_urls: ["https://pypi.python.org/packages/source/d/decorator/"],
- }],
- [arff, 2.0.2, {
- source_tmpl: liac-%(name)s-%(version)s.zip,
- source_urls: ["https://pypi.python.org/packages/source/l/liac-arff/"],
- }],
- [pycrypto, 2.6.1, {
- modulename: Crypto,
- source_urls: ["http://ftp.dlitz.net/pub/dlitz/crypto/pycrypto/"],
- }],
- [ecdsa, '0.13', {
- source_urls: ["https://pypi.python.org/packages/source/e/ecdsa/"],
- }],
- [paramiko, 1.15.2, {
- source_urls: ["https://pypi.python.org/packages/source/p/paramiko/"],
- }],
- [pyparsing, 2.0.3, {
- source_urls: ["https://pypi.python.org/packages/source/p/pyparsing/"],
- }],
- [netifaces, 0.10.4, {
- source_urls: ["https://pypi.python.org/packages/source/n/netifaces"],
- }],
- [netaddr, 0.7.14, {
- source_urls: ["https://pypi.python.org/packages/source/n/netaddr"],
- }],
- [mock, 1.0.1, {
- source_urls: ["https://pypi.python.org/packages/source/m/mock"],
- }],
- [pytz, '2015.4', {
- source_urls: ["https://pypi.python.org/packages/source/p/pytz"],
- }],
- [pandas, 0.16.1, {
- source_urls: ["https://pypi.python.org/packages/source/p/pandas"],
- }],
-]
-
-moduleclass: lang
-
diff --git a/test/framework/easyconfigs/yeb/SQLite-3.8.10.2-foss-2018a.yeb b/test/framework/easyconfigs/yeb/SQLite-3.8.10.2-foss-2018a.yeb
deleted file mode 100644
index a9d0afea39..0000000000
--- a/test/framework/easyconfigs/yeb/SQLite-3.8.10.2-foss-2018a.yeb
+++ /dev/null
@@ -1,32 +0,0 @@
-_internal_variables_:
- - &versionstr 3081002
-
-easyblock: ConfigureMake
-
-name: SQLite
-version: 3.8.10.2
-
-homepage: http://www.sqlite.org/
-# quotes on description to escape special character :
-description: "SQLite: SQL Database Engine in a C Library"
-
-toolchain: foss, 2018a
-
-# eg. http://www.sqlite.org/2014/sqlite-autoconf-3080600.tar.gz
-source_urls: ["http://www.sqlite.org/2015/"]
-sources: [!join [sqlite-autoconf-, *versionstr, .tar.gz]]
-
-# commented out for testing to avoid having to add them all - dependencies are tested in other files
-dependencies: [
-# [libreadline, '6.3'],
-# [Tcl, 8.6.4],
-]
-
-parallel: 1
-
-sanity_check_paths: {
- files: [bin/sqlite3, include/sqlite3ext.h, include/sqlite3.h, lib/libsqlite3.a, lib/libsqlite3.so],
- dirs: [lib/pkgconfig],
-}
-
-moduleclass: devel
diff --git a/test/framework/easyconfigs/yeb/bzip-bad-toolchain.yeb b/test/framework/easyconfigs/yeb/bzip-bad-toolchain.yeb
deleted file mode 100644
index f9c0f7f074..0000000000
--- a/test/framework/easyconfigs/yeb/bzip-bad-toolchain.yeb
+++ /dev/null
@@ -1,26 +0,0 @@
-%YAML 1.2
----
-# not really (there's an EB_bzip2 easyblock), but fine for use in unit tests
-easyblock: ConfigureMake
-
-name: bzip2
-version: 1.0.6
-
-homepage: 'http://www.bzip.org/'
-# preserve newlines using '|' to ensure match with description from .eb easyconfig
-description: |
- bzip2 is a freely available, patent free, high-quality data compressor. It typically
- compresses files to within 10% to 15% of the best available techniques (the PPM family of statistical
- compressors), whilst being around twice as fast at compression and six times faster at decompression.
-
-# bad toolchain with four parameters
-toolchain: GCC, 4.9, False, 2
-toolchainopts: {pic: True}
-
-sources:
- # SOURCE_TAR_GZ is a known constant, hence the *
- - *SOURCE_TAR_GZ
-source_urls:
- - http://www.bzip.org/%(version)s
-
-moduleclass: tools
diff --git a/test/framework/easyconfigs/yeb/bzip2-1.0.6-GCC-4.9.2.yeb b/test/framework/easyconfigs/yeb/bzip2-1.0.6-GCC-4.9.2.yeb
deleted file mode 100644
index a2fd43d756..0000000000
--- a/test/framework/easyconfigs/yeb/bzip2-1.0.6-GCC-4.9.2.yeb
+++ /dev/null
@@ -1,28 +0,0 @@
-%YAML 1.2
----
-# not really (there's an EB_bzip2 easyblock), but fine for use in unit tests
-easyblock: ConfigureMake
-
-name: bzip2
-version: 1.0.6
-
-homepage: 'http://www.bzip.org/'
-# preserve newlines using '|' to ensure match with description from .eb easyconfig
-description: |
- bzip2 is a freely available, patent free, high-quality data compressor. It typically
- compresses files to within 10% to 15% of the best available techniques (the PPM family of statistical
- compressors), whilst being around twice as fast at compression and six times faster at decompression.
-
-toolchain: [GCC, 4.9.2]
-toolchainopts: {pic: True}
-
-sources:
- # SOURCE_TAR_GZ is a known constant, hence the *
- - *SOURCE_TAR_GZ
-source_urls:
- - http://www.bzip.org/%(version)s
-
-builddependencies:
- - gzip: 1.6
-
-moduleclass: tools
diff --git a/test/framework/easyconfigs/yeb/foss-2018a.yeb b/test/framework/easyconfigs/yeb/foss-2018a.yeb
deleted file mode 100644
index 99de832ac9..0000000000
--- a/test/framework/easyconfigs/yeb/foss-2018a.yeb
+++ /dev/null
@@ -1,42 +0,0 @@
-_internal_variables_:
- - &version 2018a
-
- - &comp_name GCC
- - &comp_version 6.4.0-2.28
- - &comp [*comp_name, *comp_version]
-
- - &blaslib OpenBLAS
- - &blasver 0.2.20
- - &blas !join [*blaslib, -, *blasver]
-
- - &comp_mpi_tc [gompi, *version]
-
-
-easyblock: Toolchain
-
-name: foss
-version: *version
-
-homepage: (none)
-description: |
- GNU Compiler Collection (GCC) based compiler toolchain, including
- OpenMPI for MPI support, OpenBLAS (BLAS and LAPACK support), FFTW and ScaLAPACK.
-
-toolchain: {name: system, version: system}
-
-# compiler toolchain dependencies
-# we need GCC and OpenMPI as explicit dependencies instead of gompi toolchain
-# because of toolchain preperation functions
-dependencies:
- - *comp_name: *comp_version
- - OpenMPI: 2.1.2
- toolchain: *comp
- - *blaslib: *blasver
- toolchain: *comp
- - FFTW: 3.3.7
- toolchain: *comp_mpi_tc
- - ScaLAPACK: 2.0.2
- versionsuffix: !join [-, *blas]
- toolchain: *comp_mpi_tc
-
-moduleclass: toolchain
diff --git a/test/framework/easyconfigs/yeb/gzip-1.6-GCC-4.9.2.yeb b/test/framework/easyconfigs/yeb/gzip-1.6-GCC-4.9.2.yeb
deleted file mode 100644
index da64d1518d..0000000000
--- a/test/framework/easyconfigs/yeb/gzip-1.6-GCC-4.9.2.yeb
+++ /dev/null
@@ -1,25 +0,0 @@
-%YAML 1.2
----
-easyblock: ConfigureMake
-
-name: gzip
-version: 1.6
-
-homepage: 'http://www.gnu.org/software/gzip/'
-description:
- gzip (GNU zip) is a popular data compression program
- as a replacement for compress
-
-toolchain: {name: GCC, version: 4.9.2}
-
-# http://ftp.gnu.org/gnu/gzip/gzip-1.6.tar.gz
-source_urls: [*GNU_SOURCE]
-sources: ['%(name)s-%(version)s.tar.gz']
-
-# make sure the gzip, gunzip and compress binaries are available after installation
-sanity_check_paths: {
- files: [bin/gunzip, bin/gzip, bin/uncompress],
- dirs: [],
-}
-
-moduleclass: tools
diff --git a/test/framework/easyconfigs/yeb/intel-2018a.yeb b/test/framework/easyconfigs/yeb/intel-2018a.yeb
deleted file mode 100644
index c1ec0daf29..0000000000
--- a/test/framework/easyconfigs/yeb/intel-2018a.yeb
+++ /dev/null
@@ -1,24 +0,0 @@
-_internal_variables_:
- - &compver 2018.1.163
-
-easyblock: Toolchain
-
-name: intel
-version: 2018a
-
-homepage: http://software.intel.com/en-us/intel-cluster-toolkit-compiler/
-description:
- Intel Cluster Toolkit Compiler Edition provides Intel C/C++ and Fortran compilers,
- Intel MPI & Intel MKL.
-
-toolchain: {name: system, version: system}
-
-# fake intel toolchain easyconfig, no dependencies (good enough for testing)
-dependencies: [
- # [icc, *compver],
- # [ifort, *compver],
- # [impi, 2018.1.163],
- # [imkl, 2018.1.163]
-]
-
-moduleclass: toolchain
diff --git a/test/framework/easyconfigs/yeb/toy-0.0.yeb b/test/framework/easyconfigs/yeb/toy-0.0.yeb
deleted file mode 100644
index 2f5265ea6d..0000000000
--- a/test/framework/easyconfigs/yeb/toy-0.0.yeb
+++ /dev/null
@@ -1,38 +0,0 @@
-%YAML 1.2
----
-
-name: toy
-version: 0.0
-
-homepage: 'https://easybuilders.github.io/easybuild'
-description: "Toy C program, 100% toy."
-
-toolchain: system, system
-
-sources:
- - *SOURCE_TAR_GZ
-
-checksums: [[
- 'be662daa971a640e40be5c804d9d7d10', # default [MD5]
- '44332000aa33b99ad1e00cbd1a7da769220d74647060a10e807b916d73ea27bc', # default (SHA256)
- ['adler32', '0x998410035'],
- ['crc32', '0x1553842328'],
- ['md5', 'be662daa971a640e40be5c804d9d7d10'],
- ['sha1', 'f618096c52244539d0e89867405f573fdb0b55b0'],
- ['size', 273],
-]]
-
-patches: [
- 'toy-0.0_fix-silly-typo-in-printf-statement.patch',
- ['toy-extra.txt', 'toy-0.0'],
-]
-
-sanity_check_paths: {
- files: [['bin/yot', 'bin/toy']],
- dirs: ['bin'],
-}
-
-postinstallcmds: ["echo TOY > %(installdir)s/README"]
-
-moduleclass: tools
-# trailing comment, leave this here, it may trigger bugs with extract_comments()
diff --git a/test/framework/easyconfigversion.py b/test/framework/easyconfigversion.py
index 4da54b450a..300cf6aff2 100644
--- a/test/framework/easyconfigversion.py
+++ b/test/framework/easyconfigversion.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2014-2024 Ghent University
+# Copyright 2014-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/easystack.py b/test/framework/easystack.py
index a51059d280..900ed43afd 100644
--- a/test/framework/easystack.py
+++ b/test/framework/easystack.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -155,7 +155,10 @@ def test_easystack_restore_env_after_each_build(self):
'--easystack',
test_es_path
]
+ self.mock_stdout(True)
+ stdout = self.eb_main(args, do_build=True, raise_error=True)
stdout = self.eb_main(args, do_build=True, raise_error=True, reset_env=False, redo_init_config=False)
+ self.mock_stdout(False)
regex = re.compile(r"WARNING Loaded modules detected: \[.*gompi/2018.*\]\n")
self.assertFalse(regex.search(stdout), "Pattern '%s' should not be found in: %s" % (regex.pattern, stdout))
diff --git a/test/framework/ebconfigobj.py b/test/framework/ebconfigobj.py
index 169fe988a2..1b858c0471 100644
--- a/test/framework/ebconfigobj.py
+++ b/test/framework/ebconfigobj.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2014-2024 Ghent University
+# Copyright 2014-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -116,6 +116,23 @@ def test_squash_simple(self):
res = cov.squash(version, tc['name'], tc['version'])
self.assertEqual(res, {}) # very simple
+ # Ensure that a version of '0' with trailing '.0's is matched against '0.0' but not anything higher
+ # This is for testing the DEFAULT_UNDEFINED_VERSION detection
+ for num_zeroes in range(1, 6):
+ tc = tc_first
+ zero_version = '.'.join(['0'] * num_zeroes)
+ txt = [
+ '[SUPPORTED]',
+ 'versions = ' + zero_version,
+ 'toolchains = ' + tc_tmpl % tc,
+ '[DEFAULT]',
+ 'y=a',
+ ]
+ co = ConfigObj(txt)
+ cov = EBConfigObj(co)
+ self.assertEqual(cov.squash('0.0', tc['name'], tc['version']), {'y': 'a'})
+ self.assertEqual(cov.squash('0.1', tc['name'], tc['version']), {})
+
def test_squash_invalid(self):
"""Try to squash invalid files. Should trigger error"""
tc_first = {'version': '10', 'name': self.tc_first}
@@ -123,8 +140,8 @@ def test_squash_invalid(self):
tc_tmpl = '%(name)s == %(version)s'
- default_version = '1.0'
- all_wrong_versions = [default_version, '>= 0.0', '< 1.0']
+ default_version = '1.1'
+ all_wrong_versions = [default_version, '>= 0.0', '< 1.1']
# all txt should have default version and first toolchain unmodified
diff --git a/test/framework/environment.py b/test/framework/environment.py
index f184c6b9c7..75919f6a13 100644
--- a/test/framework/environment.py
+++ b/test/framework/environment.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2015-2024 Ghent University
+# Copyright 2015-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/filetools.py b/test/framework/filetools.py
index f6dd6fd7ac..fe37a07820 100644
--- a/test/framework/filetools.py
+++ b/test/framework/filetools.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -41,18 +41,19 @@
import stat
import sys
import tempfile
+import textwrap
import time
+import types
+from io import StringIO
from test.framework.github import requires_github_access
from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered, init_config
from unittest import TextTestRunner
-from easybuild.tools import run
+from urllib import request
import easybuild.tools.filetools as ft
-import easybuild.tools.py2vs3 as py2vs3
from easybuild.tools.build_log import EasyBuildError
-from easybuild.tools.config import IGNORE, ERROR, build_option, update_build_option
+from easybuild.tools.config import IGNORE, ERROR, WARN, build_option, update_build_option
from easybuild.tools.multidiff import multidiff
-from easybuild.tools.py2vs3 import StringIO, std_urllib
-from easybuild.tools.run import run_cmd
+from easybuild.tools.run import run_shell_cmd
from easybuild.tools.systemtools import LINUX, get_os_type
@@ -292,10 +293,6 @@ def test_checksums(self):
ft.write_file(fp, "easybuild\n")
known_checksums = {
- 'adler32': '0x379257805',
- 'crc32': '0x1457143216',
- 'md5': '7167b64b1ca062b9674ffef46f9325db',
- 'sha1': 'db05b79e09a4cc67e9dd30b313b5488813db3190',
'sha256': '1c49562c4b404f3120a3fa0926c8d09c99ef80e470f7de03ffdfa14047960ea5',
'sha512': '7610f6ce5e91e56e350d25c917490e4815f7986469fafa41056698aec256733e'
'b7297da8b547d5e74b851d7c4e475900cec4744df0f887ae5c05bf1757c224b4',
@@ -322,15 +319,13 @@ def test_checksums(self):
ft._log.setLevel(old_log_level)
- # default checksum type is MD5
- self.assertEqual(ft.compute_checksum(fp), known_checksums['md5'])
+ # default checksum type is SHA256
+ self.assertEqual(ft.compute_checksum(fp), known_checksums['sha256'])
- # both MD5 and SHA256 checksums can be verified without specifying type
- self.assertTrue(ft.verify_checksum(fp, known_checksums['md5']))
+ # SHA256 checksums can be verified without specifying type
self.assertTrue(ft.verify_checksum(fp, known_checksums['sha256']))
- # providing non-matching MD5 and SHA256 checksums results in failed verification
- self.assertFalse(ft.verify_checksum(fp, '1c49562c4b404f3120a3fa0926c8d09c'))
+ # providing non-matching SHA256 checksums results in failed verification
self.assertFalse(ft.verify_checksum(fp, '7167b64b1ca062b9674ffef46f9325db7167b64b1ca062b9674ffef46f9325db'))
# checksum of length 32 is assumed to be MD5, length 64 to be SHA256, other lengths not allowed
@@ -344,25 +339,14 @@ def test_checksums(self):
for checksum_type, checksum in broken_checksums.items():
self.assertFalse(ft.compute_checksum(fp, checksum_type=checksum_type) == checksum)
self.assertFalse(ft.verify_checksum(fp, (checksum_type, checksum)))
- # md5 is default
- self.assertFalse(ft.compute_checksum(fp) == broken_checksums['md5'])
- self.assertFalse(ft.verify_checksum(fp, broken_checksums['md5']))
+ # sha256 is default
+ self.assertFalse(ft.compute_checksum(fp) == broken_checksums['sha256'])
self.assertFalse(ft.verify_checksum(fp, broken_checksums['sha256']))
# test specify alternative checksums
alt_checksums = ('7167b64b1ca062b9674ffef46f9325db7167b64b1ca062b9674ffef46f9325db', known_checksums['sha256'])
self.assertTrue(ft.verify_checksum(fp, alt_checksums))
- alt_checksums = ('fecf50db81148786647312bbd3b5c740', '2c829facaba19c0fcd81f9ce96bef712',
- '840078aeb4b5d69506e7c8edae1e1b89', known_checksums['md5'])
- self.assertTrue(ft.verify_checksum(fp, alt_checksums))
-
- alt_checksums = ('840078aeb4b5d69506e7c8edae1e1b89', known_checksums['md5'], '2c829facaba19c0fcd81f9ce96bef712')
- self.assertTrue(ft.verify_checksum(fp, alt_checksums))
-
- alt_checksums = (known_checksums['md5'], '840078aeb4b5d69506e7c8edae1e1b89', '2c829facaba19c0fcd81f9ce96bef712')
- self.assertTrue(ft.verify_checksum(fp, alt_checksums))
-
alt_checksums = (known_checksums['sha256'],)
self.assertTrue(ft.verify_checksum(fp, alt_checksums))
@@ -372,6 +356,8 @@ def test_checksums(self):
# Check dictionary
alt_checksums = (known_checksums['sha256'],)
self.assertTrue(ft.verify_checksum(fp, {os.path.basename(fp): known_checksums['sha256']}))
+ # None is accepted
+ self.assertTrue(ft.verify_checksum(fp, {os.path.basename(fp): None}))
faulty_dict = {'wrong-name': known_checksums['sha256']}
self.assertErrorRegex(EasyBuildError,
"Missing checksum for " + os.path.basename(fp) + " in .*wrong-name.*",
@@ -384,16 +370,86 @@ def test_checksums(self):
init_config(build_options=build_options)
self.assertErrorRegex(EasyBuildError, "Missing checksum for", ft.verify_checksum, fp, None)
- self.assertTrue(ft.verify_checksum(fp, known_checksums['md5']))
self.assertTrue(ft.verify_checksum(fp, known_checksums['sha256']))
# Test dictionary-type checksums
- for checksum in [known_checksums[x] for x in ('md5', 'sha256')]:
+ self.assertErrorRegex(EasyBuildError, "Missing checksum for", ft.verify_checksum,
+ fp, {os.path.basename(fp): None})
+ for checksum in [known_checksums[x] for x in ['sha256']]:
dict_checksum = {os.path.basename(fp): checksum, 'foo': 'baa'}
self.assertTrue(ft.verify_checksum(fp, dict_checksum))
del dict_checksum[os.path.basename(fp)]
self.assertErrorRegex(EasyBuildError, "Missing checksum for", ft.verify_checksum, fp, dict_checksum)
+ def test_deprecated_checksums(self):
+ """Test checksum functionality."""
+
+ fp = os.path.join(self.test_prefix, 'test.txt')
+ ft.write_file(fp, "easybuild\n")
+
+ known_checksums = {
+ 'adler32': '0x379257805',
+ 'crc32': '0x1457143216',
+ 'md5': '7167b64b1ca062b9674ffef46f9325db',
+ 'sha1': 'db05b79e09a4cc67e9dd30b313b5488813db3190',
+ }
+
+ self.allow_deprecated_behaviour()
+ self.mock_stderr(True) # just to capture deprecation warning
+
+ # make sure checksums computation/verification is correct
+ for checksum_type, checksum in known_checksums.items():
+ self.assertEqual(ft.compute_checksum(fp, checksum_type=checksum_type), checksum)
+ self.assertTrue(ft.verify_checksum(fp, (checksum_type, checksum)))
+
+ # MD5 checksums can be verified without specifying type
+ self.assertTrue(ft.verify_checksum(fp, known_checksums['md5']))
+
+ # providing non-matching MD5 checksums results in failed verification
+ self.assertFalse(ft.verify_checksum(fp, '1c49562c4b404f3120a3fa0926c8d09c'))
+
+ # checksum of length 32 is assumed to be MD5, length 64 to be SHA256, other lengths not allowed
+ # checksum of length other than 32/64 yields an error
+ error_pattern = r"Length of checksum '.*' \(\d+\) does not match with either MD5 \(32\) or SHA256 \(64\)"
+ for checksum in ['tooshort', 'inbetween32and64charactersisnotgoodeither', known_checksums['md5'] + 'foo']:
+ self.assertErrorRegex(EasyBuildError, error_pattern, ft.verify_checksum, fp, checksum)
+
+ # make sure faulty checksums are reported
+ broken_checksums = {typ: (val[:-3] + 'foo') for typ, val in known_checksums.items()}
+ for checksum_type, checksum in broken_checksums.items():
+ self.assertFalse(ft.compute_checksum(fp, checksum_type=checksum_type) == checksum)
+ self.assertFalse(ft.verify_checksum(fp, (checksum_type, checksum)))
+ self.assertFalse(ft.verify_checksum(fp, broken_checksums['md5']))
+
+ # test specify alternative checksums
+ alt_checksums = ('fecf50db81148786647312bbd3b5c740', '2c829facaba19c0fcd81f9ce96bef712',
+ '840078aeb4b5d69506e7c8edae1e1b89', known_checksums['md5'])
+ self.assertTrue(ft.verify_checksum(fp, alt_checksums))
+
+ alt_checksums = ('840078aeb4b5d69506e7c8edae1e1b89', known_checksums['md5'], '2c829facaba19c0fcd81f9ce96bef712')
+ self.assertTrue(ft.verify_checksum(fp, alt_checksums))
+
+ alt_checksums = (known_checksums['md5'], '840078aeb4b5d69506e7c8edae1e1b89', '2c829facaba19c0fcd81f9ce96bef712')
+ self.assertTrue(ft.verify_checksum(fp, alt_checksums))
+
+ # check whether missing checksums are enforced
+ build_options = {
+ 'enforce_checksums': True,
+ }
+ init_config(build_options=build_options)
+
+ self.assertErrorRegex(EasyBuildError, "Missing checksum for", ft.verify_checksum, fp, None)
+ self.assertTrue(ft.verify_checksum(fp, known_checksums['md5']))
+
+ # Test dictionary-type checksums
+ for checksum in [known_checksums[x] for x in ['md5']]:
+ dict_checksum = {os.path.basename(fp): checksum, 'foo': 'baa'}
+ self.assertTrue(ft.verify_checksum(fp, dict_checksum))
+ del dict_checksum[os.path.basename(fp)]
+ self.assertErrorRegex(EasyBuildError, "Missing checksum for", ft.verify_checksum, fp, dict_checksum)
+
+ self.mock_stderr(False)
+
def test_common_path_prefix(self):
"""Test get common path prefix for a list of paths."""
self.assertEqual(ft.det_common_path_prefix(['/foo/bar/foo', '/foo/bar/baz', '/foo/bar/bar']), '/foo/bar')
@@ -420,6 +476,43 @@ def test_normalize_path(self):
self.assertEqual(ft.normalize_path('/././foo//bar/././baz/'), '/foo/bar/baz')
self.assertEqual(ft.normalize_path('//././foo//bar/././baz/'), '//foo/bar/baz')
+ def test_is_parent_path(self):
+ """Test is_parent_path"""
+ self.assertTrue(ft.is_parent_path('/foo/bar', '/foo/bar/test0'))
+ self.assertTrue(ft.is_parent_path('/foo/bar', '/foo/bar/test0/test1'))
+ self.assertTrue(ft.is_parent_path('/foo/bar', '/foo/bar'))
+ self.assertFalse(ft.is_parent_path('/foo/bar/test0', '/foo/bar'))
+ self.assertFalse(ft.is_parent_path('/foo/bar', '/foo/test'))
+
+ # Check that trailing slashes are ignored
+ self.assertTrue(ft.is_parent_path('/foo/bar/', '/foo/bar'))
+ self.assertTrue(ft.is_parent_path('/foo/bar', '/foo/bar/'))
+ self.assertTrue(ft.is_parent_path('/foo/bar/', '/foo/bar/'))
+
+ # Check that is also accepts relative paths
+ self.assertTrue(ft.is_parent_path('foo/bar', 'foo/bar/test0'))
+ self.assertTrue(ft.is_parent_path('foo/bar', 'foo/bar/test0/test1'))
+ self.assertTrue(ft.is_parent_path('foo/bar', 'foo/bar'))
+ self.assertFalse(ft.is_parent_path('foo/bar/test0', 'foo/bar'))
+ self.assertFalse(ft.is_parent_path('foo/bar', 'foo/test'))
+
+ # Check that relative paths are accounted
+ self.assertTrue(ft.is_parent_path('foo/../baz', 'bar/../baz'))
+
+ # Check that symbolic links are accounted
+ ft.mkdir(os.path.join(self.test_prefix, 'base'))
+ ft.mkdir(os.path.join(self.test_prefix, 'base', 'concrete'))
+ ft.symlink(
+ os.path.join(self.test_prefix, 'base', 'concrete'),
+ os.path.join(self.test_prefix, 'base', 'link')
+ )
+ self.assertTrue(
+ ft.is_parent_path(
+ os.path.join(self.test_prefix, 'base', 'link'),
+ os.path.join(self.test_prefix, 'base', 'concrete', 'file')
+ )
+ )
+
def test_det_file_size(self):
"""Test det_file_size function."""
@@ -435,7 +528,7 @@ def test_det_file_size(self):
# also try with actual HTTP header
try:
- fh = std_urllib.urlopen(test_url)
+ fh = request.urlopen(test_url)
self.assertEqual(ft.det_file_size(fh.info()), expected_size)
fh.close()
@@ -447,7 +540,7 @@ def test_det_file_size(self):
res.close()
except ImportError:
pass
- except std_urllib.URLError:
+ except request.URLError:
print("Skipping online test for det_file_size (working offline)")
def test_download_file(self):
@@ -458,29 +551,34 @@ def test_download_file(self):
test_dir = os.path.abspath(os.path.dirname(__file__))
toy_source_dir = os.path.join(test_dir, 'sandbox', 'sources', 'toy')
source_url = 'file://%s/%s' % (toy_source_dir, fn)
- res = ft.download_file(fn, source_url, target_location)
+ with self.mocked_stdout_stderr():
+ res = ft.download_file(fn, source_url, target_location)
self.assertEqual(res, target_location, "'download' of local file works")
downloads = glob.glob(target_location + '*')
self.assertEqual(len(downloads), 1)
# non-existing files result in None return value
- self.assertEqual(ft.download_file(fn, 'file://%s/nosuchfile' % test_dir, target_location), None)
+ with self.mocked_stdout_stderr():
+ self.assertEqual(ft.download_file(fn, 'file://%s/nosuchfile' % test_dir, target_location), None)
# install broken proxy handler for opening local files
# this should make urlopen use this broken proxy for downloading from a file:// URL
- proxy_handler = std_urllib.ProxyHandler({'file': 'file://%s/nosuchfile' % test_dir})
- std_urllib.install_opener(std_urllib.build_opener(proxy_handler))
+ proxy_handler = request.ProxyHandler({'file': 'file://%s/nosuchfile' % test_dir})
+ request.install_opener(request.build_opener(proxy_handler))
# downloading over a broken proxy results in None return value (failed download)
# this tests whether proxies are taken into account by download_file
- self.assertEqual(ft.download_file(fn, source_url, target_location), None, "download over broken proxy fails")
+ with self.mocked_stdout_stderr():
+ self.assertEqual(ft.download_file(fn, source_url, target_location), None,
+ "download over broken proxy fails")
# modify existing download so we can verify re-download
ft.write_file(target_location, '')
# restore a working file handler, and retest download of local file
- std_urllib.install_opener(std_urllib.build_opener(std_urllib.FileHandler()))
- res = ft.download_file(fn, source_url, target_location)
+ request.install_opener(request.build_opener(request.FileHandler()))
+ with self.mocked_stdout_stderr():
+ res = ft.download_file(fn, source_url, target_location)
self.assertEqual(res, target_location, "'download' of local file works after removing broken proxy")
# existing file was re-downloaded, so a backup should have been created of the existing file
@@ -496,12 +594,21 @@ def test_download_file(self):
target_location = os.path.join(self.test_prefix, 'jenkins_robots.txt')
url = 'https://raw.githubusercontent.com/easybuilders/easybuild-framework/master/README.rst'
try:
- std_urllib.urlopen(url)
- res = ft.download_file(fn, url, target_location)
+ request.urlopen(url)
+ with self.mocked_stdout_stderr():
+ res = ft.download_file(fn, url, target_location)
self.assertEqual(res, target_location, "download with specified timeout works")
- except std_urllib.URLError:
+ except request.URLError:
print("Skipping timeout test in test_download_file (working offline)")
+ # check whether disabling trace output works
+ target_location = os.path.join(self.test_prefix, 'test.txt')
+ with self.mocked_stdout_stderr():
+ ft.download_file(fn, source_url, target_location, forced=True, trace=False)
+ stdout = self.get_stdout()
+ self.assertEqual(stdout, '')
+ ft.remove_file(target_location)
+
# also test behaviour of download_file under --dry-run
build_options = {
'extended_dry_run': True,
@@ -520,9 +627,10 @@ def test_download_file(self):
self.assertEqual(path, target_location)
self.assertNotExists(target_location)
- self.assertTrue(re.match("^file written: .*/foo$", txt))
+ self.assertTrue(re.match("file written: .*/foo", txt))
- ft.download_file(fn, source_url, target_location, forced=True)
+ with self.mocked_stdout_stderr():
+ ft.download_file(fn, source_url, target_location, forced=True)
self.assertExists(target_location)
self.assertTrue(os.path.samefile(path, target_location))
@@ -542,7 +650,8 @@ def fake_urllib_open(*args, **kwargs):
# if requests is available, file is downloaded
if ft.HAVE_REQUESTS:
- res = ft.download_file(fn, url, target)
+ with self.mocked_stdout_stderr():
+ res = ft.download_file(fn, url, target)
self.assertTrue(res and os.path.exists(res))
self.assertIn("https://easybuild.io", ft.read_file(res))
@@ -558,7 +667,8 @@ def fake_urllib_open(*args, **kwargs):
# if requests is available, file is downloaded
if ft.HAVE_REQUESTS:
- res = ft.download_file(fn, url, target)
+ with self.mocked_stdout_stderr():
+ res = ft.download_file(fn, url, target)
self.assertTrue(res and os.path.exists(res))
self.assertIn("https://easybuild.io", ft.read_file(res))
@@ -592,18 +702,22 @@ def fake_urllib_open(url, *args, **kwargs):
target_path = os.path.join(self.test_prefix, fn)
# first try without allowing insecure downloads (default)
- res = ft.download_file(fn, url, target_path)
+ with self.mocked_stdout_stderr():
+ res = ft.download_file(fn, url, target_path)
self.assertEqual(res, None)
update_build_option('insecure_download', True)
+ self.mock_stdout(True)
self.mock_stderr(True)
res = ft.download_file(fn, url, target_path)
stderr = self.get_stderr()
+ self.mock_stdout(False)
self.mock_stderr(False)
self.assertIn("WARNING: Not checking server certificates while downloading toy-0.0.eb", stderr)
self.assertExists(res)
- self.assertTrue(ft.read_file(res).startswith("name = 'toy'"))
+ with self.mocked_stdout_stderr():
+ self.assertTrue(ft.read_file(res).startswith("name = 'toy'"))
# also test insecure download via requests fallback
if ft.HAVE_REQUESTS:
@@ -629,14 +743,17 @@ def fake_requests_get(url, *args, **kwargs):
ft.requests.get = fake_requests_get
update_build_option('insecure_download', False)
- res = ft.download_file(fn, url, target_path)
+ with self.mocked_stdout_stderr():
+ res = ft.download_file(fn, url, target_path)
self.assertEqual(res, None)
update_build_option('insecure_download', True)
self.mock_stderr(True)
+ self.mock_stdout(True)
res = ft.download_file(fn, url, target_path)
stderr = self.get_stderr()
self.mock_stderr(False)
+ self.mock_stdout(False)
self.assertIn("WARNING: Not checking server certificates while downloading README.rst", stderr)
self.assertExists(res)
@@ -918,7 +1035,7 @@ def test_is_binary(self):
self.assertTrue(ft.is_binary(b'\00'))
self.assertTrue(ft.is_binary(b"File is binary when it includes \00 somewhere"))
- self.assertTrue(ft.is_binary(ft.read_file('/bin/ls', mode='rb')))
+ self.assertTrue(ft.is_binary(ft.read_file('/bin/bash', mode='rb')))
def test_det_patched_files(self):
"""Test det_patched_files function."""
@@ -1157,9 +1274,9 @@ def test_multidiff(self):
self.assertTrue(lines[8].startswith(expected))
# no postinstallcmds in toy-0.0-deps.eb
- expected = "29 %s+ postinstallcmds = " % green
+ expected = "26 %s+ postinstallcmds = " % green
self.assertTrue(any(line.startswith(expected) for line in lines))
- expected = "30 %s+%s (1/2) toy-0.0" % (green, endcol)
+ expected = "27 %s+%s (1/2) toy-0.0" % (green, endcol)
self.assertTrue(any(line.startswith(expected) for line in lines), "Found '%s' in: %s" % (expected, lines))
self.assertEqual(lines[-1], "=====")
@@ -1178,9 +1295,9 @@ def test_multidiff(self):
self.assertTrue(lines[8].startswith(expected))
# no postinstallcmds in toy-0.0-deps.eb
- expected = "29 + postinstallcmds = "
+ expected = "26 + postinstallcmds = "
self.assertTrue(any(line.startswith(expected) for line in lines), "Found '%s' in: %s" % (expected, lines))
- expected = "30 + (1/2) toy-0.0-"
+ expected = "27 + (1/2) toy-0.0-"
self.assertTrue(any(line.startswith(expected) for line in lines), "Found '%s' in: %s" % (expected, lines))
self.assertEqual(lines[-1], "=====")
@@ -1436,27 +1553,38 @@ def test_apply_regex_substitutions(self):
# passing empty list of substitions is a no-op
ft.write_file(testfile, testtxt)
- ft.apply_regex_substitutions(testfile, [], on_missing_match=run.IGNORE)
+ ft.apply_regex_substitutions(testfile, [], on_missing_match=IGNORE)
new_testtxt = ft.read_file(testfile)
self.assertEqual(new_testtxt, testtxt)
# Check handling of on_missing_match
ft.write_file(testfile, testtxt)
regex_subs_no_match = [('Not there', 'Not used')]
- error_pat = 'Nothing found to replace in %s' % testfile
+ error_pat = "Nothing found to replace 'Not there' in %s" % testfile
# Error
self.assertErrorRegex(EasyBuildError, error_pat, ft.apply_regex_substitutions, testfile, regex_subs_no_match,
- on_missing_match=run.ERROR)
+ on_missing_match=ERROR)
+ # First matches, but 2nd not
+ regex_subs_part_match = [regex_subs[0], ('Not there', 'Not used')]
+ self.assertErrorRegex(EasyBuildError, error_pat, ft.apply_regex_substitutions, testfile, regex_subs_part_match,
+ on_missing_match=ERROR, match_all=True)
+ # First matched so OK with match_all
+ ft.apply_regex_substitutions(testfile, regex_subs_part_match,
+ on_missing_match=ERROR, match_all=False)
# Warn
with self.log_to_testlogfile():
- ft.apply_regex_substitutions(testfile, regex_subs_no_match, on_missing_match=run.WARN)
+ ft.apply_regex_substitutions(testfile, regex_subs_no_match, on_missing_match=WARN)
+ logtxt = ft.read_file(self.logfile)
+ self.assertIn('WARNING ' + error_pat, logtxt)
+ with self.log_to_testlogfile():
+ ft.apply_regex_substitutions(testfile, regex_subs_part_match, on_missing_match=WARN, match_all=True)
logtxt = ft.read_file(self.logfile)
self.assertIn('WARNING ' + error_pat, logtxt)
# Ignore
with self.log_to_testlogfile():
- ft.apply_regex_substitutions(testfile, regex_subs_no_match, on_missing_match=run.IGNORE)
+ ft.apply_regex_substitutions(testfile, regex_subs_no_match, on_missing_match=IGNORE)
logtxt = ft.read_file(self.logfile)
self.assertIn('INFO ' + error_pat, logtxt)
@@ -1465,6 +1593,24 @@ def test_apply_regex_substitutions(self):
path = os.path.join(self.test_prefix, 'nosuchfile.txt')
self.assertErrorRegex(EasyBuildError, error_pat, ft.apply_regex_substitutions, path, regex_subs)
+ # Replace multi-line strings
+ testtxt = "This si wrong\nBut mkae right\nLeave this!"
+ expected_testtxt = 'This is wrong.\nBut make right\nLeave this!'
+ ft.write_file(testfile, testtxt)
+ repl = ('This si( .*)\n(.*)mkae right$', 'This is wrong.\nBut make right')
+ ft.apply_regex_substitutions(testfile, [repl], backup=False, on_missing_match=ERROR, single_line=False)
+ new_testtxt = ft.read_file(testfile)
+ self.assertEqual(new_testtxt, expected_testtxt)
+ # Supports capture groups
+ ft.write_file(testfile, testtxt)
+ repls = [
+ ('This si( .*)\n(.*)mkae right$', r'This is\1.\n\2make right'),
+ ('Lea(ve)', r'Do \g<0>\1'), # Reference to full match
+ ]
+ ft.apply_regex_substitutions(testfile, repls, backup=False, on_missing_match=ERROR, single_line=False)
+ new_testtxt = ft.read_file(testfile)
+ self.assertEqual(new_testtxt, expected_testtxt.replace('Leave', 'Do Leaveve'))
+
# make sure apply_regex_substitutions can patch files that include UTF-8 characters
testtxt = b"foo \xe2\x80\x93 bar" # This is an UTF-8 "-"
ft.write_file(testfile, testtxt)
@@ -1485,34 +1631,32 @@ def test_apply_regex_substitutions(self):
# also test apply_regex_substitutions with a *list* of paths
# cfr. https://github.com/easybuilders/easybuild-framework/issues/3493
+ # and a compiled regex
test_dir = os.path.join(self.test_prefix, 'test_dir')
test_file1 = os.path.join(test_dir, 'one.txt')
test_file2 = os.path.join(test_dir, 'two.txt')
ft.write_file(test_file1, "Donald is an elephant")
ft.write_file(test_file2, "2 + 2 = 5")
regexs = [
- ('Donald', 'Dumbo'),
+ (re.compile('donald', re.I), 'Dumbo'), # Only matches if this is used as-is
('= 5', '= 4'),
]
ft.apply_regex_substitutions([test_file1, test_file2], regexs)
# also check dry run mode
init_config(build_options={'extended_dry_run': True})
- self.mock_stderr(True)
- self.mock_stdout(True)
- ft.apply_regex_substitutions([test_file1, test_file2], regexs)
- stderr, stdout = self.get_stderr(), self.get_stdout()
- self.mock_stderr(False)
- self.mock_stdout(False)
+ with self.mocked_stdout_stderr():
+ ft.apply_regex_substitutions([test_file1, test_file2], regexs)
+ stderr, stdout = self.get_stderr(), self.get_stdout()
self.assertFalse(stderr)
- regex = re.compile('\n'.join([
+ regex = '\n'.join([
r"applying regex substitutions to file\(s\): .*/test_dir/one.txt, .*/test_dir/two.txt",
- r" \* regex pattern 'Donald', replacement string 'Dumbo'",
+ r" \* regex pattern 'donald', replacement string 'Dumbo'",
r" \* regex pattern '= 5', replacement string '= 4'",
'',
- ]))
- self.assertTrue(regex.search(stdout), "Pattern '%s' should be found in: %s" % (regex.pattern, stdout))
+ ])
+ self.assertTrue(re.search(regex, stdout), "Pattern '%s' should be found in: %s" % (regex, stdout))
def test_find_flexlm_license(self):
"""Test find_flexlm_license function."""
@@ -1612,7 +1756,8 @@ def test_is_alt_pypi_url(self):
def test_pypi_source_urls(self):
"""Test pypi_source_urls() function."""
- res = ft.pypi_source_urls('easybuild')
+ with self.mocked_stdout_stderr():
+ res = ft.pypi_source_urls('easybuild')
eb340_url = 'https://pypi.python.org/packages/'
eb340_url += '93/41/574d01f352671fbc8589a436167e15a7f3e27ac0aa635d208eb29ee8fd4e/'
eb340_url += 'easybuild-3.4.0.tar.gz#sha256=d870b27211f2224aab89bfd3279834ffb89ff00ad849a0dc2bf5cc1691efa9d2'
@@ -1631,7 +1776,8 @@ def test_pypi_source_urls(self):
# check for Python package that has yanked releases,
# see https://github.com/easybuilders/easybuild-framework/issues/3301
- res = ft.pypi_source_urls('ipython')
+ with self.mocked_stdout_stderr():
+ res = ft.pypi_source_urls('ipython')
self.assertTrue(isinstance(res, list) and res)
prefix = 'https://pypi.python.org/packages'
for entry in res:
@@ -1642,21 +1788,25 @@ def test_derive_alt_pypi_url(self):
"""Test derive_alt_pypi_url() function."""
url = 'https://pypi.python.org/packages/source/e/easybuild/easybuild-2.7.0.tar.gz'
alturl = url.replace('source/e/easybuild', '5b/03/e135b19fadeb9b1ccb45eac9f60ca2dc3afe72d099f6bd84e03cb131f9bf')
- self.assertEqual(ft.derive_alt_pypi_url(url), alturl)
+ with self.mocked_stdout_stderr():
+ self.assertEqual(ft.derive_alt_pypi_url(url), alturl)
# test case to ensure that '.' characters in filename are escaped using '\.'
# if not, the alternative URL for tornado-4.5b1.tar.gz is found...
url = 'https://pypi.python.org/packages/source/t/tornado/tornado-4.5.1.tar.gz'
alturl = url.replace('source/t/tornado', 'df/42/a180ee540e12e2ec1007ac82a42b09dd92e5461e09c98bf465e98646d187')
- self.assertEqual(ft.derive_alt_pypi_url(url), alturl)
+ with self.mocked_stdout_stderr():
+ self.assertEqual(ft.derive_alt_pypi_url(url), alturl)
# no crash on non-existing version
url = 'https://pypi.python.org/packages/source/e/easybuild/easybuild-0.0.0.tar.gz'
- self.assertEqual(ft.derive_alt_pypi_url(url), None)
+ with self.mocked_stdout_stderr():
+ self.assertEqual(ft.derive_alt_pypi_url(url), None)
# no crash on non-existing package
url = 'https://pypi.python.org/packages/source/n/nosuchpackageonpypiever/nosuchpackageonpypiever-0.0.0.tar.gz'
- self.assertEqual(ft.derive_alt_pypi_url(url), None)
+ with self.mocked_stdout_stderr():
+ self.assertEqual(ft.derive_alt_pypi_url(url), None)
def test_create_patch_info(self):
"""Test create_patch_info function."""
@@ -1675,25 +1825,15 @@ def test_create_patch_info(self):
self.assertEqual(ft.create_patch_info({'name': 'foo.txt', 'copy': 'subdir', 'alt_location': 'alt'}),
{'name': 'foo.txt', 'copy': 'subdir', 'alt_location': 'alt'})
- self.allow_deprecated_behaviour()
- self.mock_stderr(True)
- self.assertEqual(ft.create_patch_info('foo.txt'), {'name': 'foo.txt'})
- stderr = self.get_stderr()
- self.mock_stderr(False)
- self.disallow_deprecated_behaviour()
- expected_warning = "Use of patch file with filename that doesn't end with correct extension: foo.txt "
- expected_warning += "(should be any of: .patch, .patch.bz2, .patch.gz, .patch.xz)"
- fail_msg = "Warning '%s' should appear in stderr output: %s" % (expected_warning, stderr)
- self.assertIn(expected_warning, stderr, fail_msg)
-
- # deprecation warning is treated as an error in context of unit test suite
- expected_error = expected_warning.replace('(', '\\(').replace(')', '\\)')
+ expected_error = r"Wrong patch spec \(foo.txt\), extension type should be any of .patch, .patch.bz2, "
+ expected_error += ".patch.gz, .patch.xz."
self.assertErrorRegex(EasyBuildError, expected_error, ft.create_patch_info, 'foo.txt')
# faulty input
error_msg = "Wrong patch spec"
self.assertErrorRegex(EasyBuildError, error_msg, ft.create_patch_info, None)
self.assertErrorRegex(EasyBuildError, error_msg, ft.create_patch_info, {'copy': 'subdir'})
+ self.assertErrorRegex(EasyBuildError, error_msg, ft.create_patch_info, {'name': 'foo.txt'})
self.assertErrorRegex(EasyBuildError, error_msg, ft.create_patch_info, {'name': 'foo.txt', 'random': 'key'})
self.assertErrorRegex(EasyBuildError, error_msg, ft.create_patch_info,
{'name': 'foo.txt', 'copy': 'subdir', 'sourcepath': 'subdir'})
@@ -1707,7 +1847,8 @@ def test_apply_patch(self):
""" Test apply_patch """
testdir = os.path.dirname(os.path.abspath(__file__))
toy_tar_gz = os.path.join(testdir, 'sandbox', 'sources', 'toy', 'toy-0.0.tar.gz')
- path = ft.extract_file(toy_tar_gz, self.test_prefix, change_into_dir=False)
+ with self.mocked_stdout_stderr():
+ path = ft.extract_file(toy_tar_gz, self.test_prefix, change_into_dir=False)
toy_patch_fn = 'toy-0.0_fix-silly-typo-in-printf-statement.patch'
toy_patch = os.path.join(testdir, 'sandbox', 'sources', 'toy', toy_patch_fn)
@@ -1729,7 +1870,8 @@ def test_apply_patch(self):
# This patch is dependent on the previous one
toy_patch_gz = os.path.join(testdir, 'sandbox', 'sources', 'toy', 'toy-0.0_gzip.patch.gz')
- self.assertTrue(ft.apply_patch(toy_patch_gz, path))
+ with self.mocked_stdout_stderr():
+ self.assertTrue(ft.apply_patch(toy_patch_gz, path))
patched_gz = ft.read_file(os.path.join(path, 'toy-0.0', 'toy.source'))
pattern = "I'm a toy, and very very proud of it"
self.assertIn(pattern, patched_gz)
@@ -1761,10 +1903,9 @@ def test_apply_patch(self):
self.assertEqual(ft.read_file(os.path.join(target_dir, 'subdir', 'target.txt')), '123')
# cleanup and re-extract toy source tarball
- ft.remove_dir(self.test_prefix)
- ft.mkdir(self.test_prefix)
ft.change_dir(self.test_prefix)
- path = ft.extract_file(toy_tar_gz, self.test_prefix, change_into_dir=False)
+ with self.mocked_stdout_stderr():
+ path = ft.extract_file(toy_tar_gz, self.test_prefix, change_into_dir=False)
# test applying of patch with git
toy_source_path = os.path.join(self.test_prefix, 'toy-0.0', 'toy.source')
@@ -1789,20 +1930,11 @@ def test_apply_patch(self):
self.assertEqual(ft.read_file(new_file_path), "This is a new file\n")
# cleanup & restore
- ft.remove_dir(path)
- path = ft.extract_file(toy_tar_gz, self.test_prefix, change_into_dir=False)
+ with self.mocked_stdout_stderr():
+ path = ft.extract_file(toy_tar_gz, self.test_prefix, change_into_dir=False)
self.assertNotIn("I'm a toy, and very proud of it", ft.read_file(toy_source_path))
- # mock stderr to catch deprecation warning caused by setting 'use_git_am'
- self.allow_deprecated_behaviour()
- self.mock_stderr(True)
- ft.apply_patch(toy_patch, self.test_prefix, use_git_am=True)
- stderr = self.get_stderr()
- self.mock_stderr(False)
- self.assertIn("I'm a toy, and very proud of it", ft.read_file(toy_source_path))
- self.assertIn("'use_git_am' named argument in apply_patch function has been renamed to 'use_git'", stderr)
-
def test_copy_file(self):
"""Test copy_file function."""
testdir = os.path.dirname(os.path.abspath(__file__))
@@ -1932,13 +2064,14 @@ def test_copy_file_xattr(self):
cmd = "xattr -w foo bar %s" % special_file
if cmd:
- (_, ec) = run_cmd(cmd, simple=False, log_all=False, log_ok=False)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd, fail_on_error=False)
# need to make file read-only after setting extended attribute
ft.adjust_permissions(special_file, stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH, add=False)
# only proceed if setting extended attribute worked
- if ec == 0:
+ if res.exit_code == 0:
target = os.path.join(self.test_prefix, 'copy.txt')
ft.copy_file(special_file, target)
self.assertTrue(os.path.exists(target))
@@ -1952,9 +2085,10 @@ def test_copy_file_xattr(self):
cmd = "attr -g foo %s" % target
else:
cmd = "xattr -l %s" % target
- (out, ec) = run_cmd(cmd, simple=False, log_all=False, log_ok=False)
- self.assertEqual(ec, 0)
- self.assertTrue(out.endswith('\nbar\n'))
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd, fail_on_error=False)
+ self.assertEqual(res.exit_code, 0)
+ self.assertTrue(res.output.endswith('\nbar\n'))
def test_copy_files(self):
"""Test copy_files function."""
@@ -2283,6 +2417,19 @@ def test_copy(self):
self.assertTrue(os.path.isfile(os.path.join(self.test_prefix, 'GCC-4.6.3.eb')))
self.assertEqual(txt, '')
+ def test_get_cwd(self):
+ """Test get_cwd"""
+ toy_dir = os.path.join(self.test_prefix, "test_get_cwd_dir")
+ os.mkdir(toy_dir)
+ os.chdir(toy_dir)
+
+ self.assertTrue(os.path.samefile(ft.get_cwd(), toy_dir))
+
+ os.rmdir(toy_dir)
+ self.assertErrorRegex(EasyBuildError, ft.CWD_NOTFOUND_ERROR, ft.get_cwd)
+
+ self.assertEqual(ft.get_cwd(must_exist=False), None)
+
def test_change_dir(self):
"""Test change_dir"""
@@ -2316,7 +2463,8 @@ def test_extract_file(self):
toy_tarball = os.path.join(testdir, 'sandbox', 'sources', 'toy', 'toy-0.0.tar.gz')
self.assertNotExists(os.path.join(self.test_prefix, 'toy-0.0', 'toy.source'))
- path = ft.extract_file(toy_tarball, self.test_prefix, change_into_dir=False)
+ with self.mocked_stdout_stderr():
+ path = ft.extract_file(toy_tarball, self.test_prefix, change_into_dir=False)
self.assertExists(os.path.join(self.test_prefix, 'toy-0.0', 'toy.source'))
self.assertTrue(os.path.samefile(path, self.test_prefix))
# still in same directory as before if change_into_dir is set to False
@@ -2326,7 +2474,8 @@ def test_extract_file(self):
toy_tarball_renamed = os.path.join(self.test_prefix, 'toy_tarball')
shutil.copyfile(toy_tarball, toy_tarball_renamed)
- path = ft.extract_file(toy_tarball_renamed, self.test_prefix, cmd="tar xfvz %s", change_into_dir=False)
+ with self.mocked_stdout_stderr():
+ path = ft.extract_file(toy_tarball_renamed, self.test_prefix, cmd="tar xfvz %s", change_into_dir=False)
self.assertTrue(os.path.samefile(os.getcwd(), cwd))
self.assertExists(os.path.join(self.test_prefix, 'toy-0.0', 'toy.source'))
self.assertTrue(os.path.samefile(path, self.test_prefix))
@@ -2347,9 +2496,10 @@ def test_extract_file(self):
self.assertTrue(os.path.samefile(path, self.test_prefix))
self.assertNotExists(os.path.join(self.test_prefix, 'toy-0.0'))
- self.assertTrue(re.search('running command "tar xzf .*/toy-0.0.tar.gz"', txt))
+ self.assertTrue(re.search('running shell command "tar xzf .*/toy-0.0.tar.gz"', txt))
- path = ft.extract_file(toy_tarball, self.test_prefix, forced=True, change_into_dir=False)
+ with self.mocked_stdout_stderr():
+ path = ft.extract_file(toy_tarball, self.test_prefix, forced=True, change_into_dir=False)
self.assertExists(os.path.join(self.test_prefix, 'toy-0.0', 'toy.source'))
self.assertTrue(os.path.samefile(path, self.test_prefix))
self.assertTrue(os.path.samefile(os.getcwd(), cwd))
@@ -2359,37 +2509,26 @@ def test_extract_file(self):
ft.remove_dir(os.path.join(self.test_prefix, 'toy-0.0'))
- # a deprecation warning is printed (which is an error in this context)
- # if the 'change_into_dir' named argument was left unspecified
- error_pattern = "extract_file function was called without specifying value for change_into_dir"
- self.assertErrorRegex(EasyBuildError, error_pattern, ft.extract_file, toy_tarball, self.test_prefix)
- self.allow_deprecated_behaviour()
-
- # make sure we're not in self.test_prefix now (checks below assumes so)
- self.assertFalse(os.path.samefile(os.getcwd(), self.test_prefix))
-
- # by default, extract_file changes to directory in which source file was unpacked
- self.mock_stderr(True)
- path = ft.extract_file(toy_tarball, self.test_prefix)
- stderr = self.get_stderr().strip()
- self.mock_stderr(False)
- self.assertTrue(os.path.samefile(path, self.test_prefix))
- self.assertTrue(os.path.samefile(os.getcwd(), self.test_prefix))
- regex = re.compile("^WARNING: .*extract_file function was called without specifying value for change_into_dir")
- self.assertTrue(regex.search(stderr), "Pattern '%s' found in: %s" % (regex.pattern, stderr))
-
ft.change_dir(cwd)
self.assertFalse(os.path.samefile(os.getcwd(), self.test_prefix))
- # no deprecation warning when change_into_dir is set to True
- self.mock_stderr(True)
- path = ft.extract_file(toy_tarball, self.test_prefix, change_into_dir=True)
- stderr = self.get_stderr().strip()
- self.mock_stderr(False)
+ with self.mocked_stdout_stderr():
+ path = ft.extract_file(toy_tarball, self.test_prefix, change_into_dir=True)
+ stdout = self.get_stdout()
+ stderr = self.get_stderr()
self.assertTrue(os.path.samefile(path, self.test_prefix))
self.assertTrue(os.path.samefile(os.getcwd(), self.test_prefix))
self.assertFalse(stderr)
+ self.assertTrue("running shell command" in stdout)
+
+ # check whether disabling trace output works
+ with self.mocked_stdout_stderr():
+ path = ft.extract_file(toy_tarball, self.test_prefix, change_into_dir=True, trace=False)
+ stdout = self.get_stdout()
+ stderr = self.get_stderr()
+ self.assertFalse(stderr)
+ self.assertFalse(stdout)
def test_remove(self):
"""Test remove_file, remove_dir and join remove functions."""
@@ -2474,7 +2613,7 @@ def test_index_functions(self):
# test with specified path with and without trailing '/'s
for path in [test_ecs, test_ecs + '/', test_ecs + '//']:
index = ft.create_index(path)
- self.assertEqual(len(index), 92)
+ self.assertEqual(len(index), 94)
expected = [
os.path.join('b', 'bzip2', 'bzip2-1.0.6-GCC-4.9.2.eb'),
@@ -2488,12 +2627,13 @@ def test_index_functions(self):
self.assertTrue(fp.endswith('.eb') or os.path.basename(fp) == 'checksums.json')
# set up some files to create actual index file for
- ft.copy_dir(os.path.join(test_ecs, 'g'), os.path.join(self.test_prefix, 'g'))
+ ecs_dir = os.path.join(self.test_prefix, 'easyconfigs')
+ ft.copy_dir(os.path.join(test_ecs, 'g'), ecs_dir)
# test dump_index function
- index_fp = ft.dump_index(self.test_prefix)
+ index_fp = ft.dump_index(ecs_dir)
self.assertExists(index_fp)
- self.assertTrue(os.path.samefile(self.test_prefix, os.path.dirname(index_fp)))
+ self.assertTrue(os.path.samefile(ecs_dir, os.path.dirname(index_fp)))
datestamp_pattern = r"[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+"
expected_header = [
@@ -2501,9 +2641,9 @@ def test_index_functions(self):
"# valid until: " + datestamp_pattern,
]
expected = [
- os.path.join('g', 'gzip', 'gzip-1.4.eb'),
- os.path.join('g', 'GCC', 'GCC-7.3.0-2.30.eb'),
- os.path.join('g', 'gompic', 'gompic-2018a.eb'),
+ os.path.join('gzip', 'gzip-1.4.eb'),
+ os.path.join('GCC', 'GCC-7.3.0-2.30.eb'),
+ os.path.join('gompic', 'gompic-2018a.eb'),
]
index_txt = ft.read_file(index_fp)
for fn in expected_header + expected:
@@ -2513,28 +2653,28 @@ def test_index_functions(self):
# test load_index function
self.mock_stderr(True)
self.mock_stdout(True)
- index = ft.load_index(self.test_prefix)
+ index = ft.load_index(ecs_dir)
stderr = self.get_stderr()
stdout = self.get_stdout()
self.mock_stderr(False)
self.mock_stdout(False)
self.assertFalse(stderr)
- regex = re.compile(r"^== found valid index for %s, so using it\.\.\.$" % self.test_prefix)
+ regex = re.compile(r"^== found valid index for %s, so using it\.\.\.$" % ecs_dir)
self.assertTrue(regex.match(stdout.strip()), "Pattern '%s' matches with: %s" % (regex.pattern, stdout))
- self.assertEqual(len(index), 26)
+ self.assertEqual(len(index), 25)
for fn in expected:
self.assertIn(fn, index)
# dump_index will not overwrite existing index without force
error_pattern = "File exists, not overwriting it without --force"
- self.assertErrorRegex(EasyBuildError, error_pattern, ft.dump_index, self.test_prefix)
+ self.assertErrorRegex(EasyBuildError, error_pattern, ft.dump_index, ecs_dir)
ft.remove_file(index_fp)
# test creating index file that's infinitely valid
- index_fp = ft.dump_index(self.test_prefix, max_age_sec=0)
+ index_fp = ft.dump_index(ecs_dir, max_age_sec=0)
index_txt = ft.read_file(index_fp)
expected_header[1] = r"# valid until: 9999-12-31 23:59:59\.9+"
for fn in expected_header + expected:
@@ -2543,40 +2683,40 @@ def test_index_functions(self):
self.mock_stderr(True)
self.mock_stdout(True)
- index = ft.load_index(self.test_prefix)
+ index = ft.load_index(ecs_dir)
stderr = self.get_stderr()
stdout = self.get_stdout()
self.mock_stderr(False)
self.mock_stdout(False)
self.assertFalse(stderr)
- regex = re.compile(r"^== found valid index for %s, so using it\.\.\.$" % self.test_prefix)
+ regex = re.compile(r"^== found valid index for %s, so using it\.\.\.$" % ecs_dir)
self.assertTrue(regex.match(stdout.strip()), "Pattern '%s' matches with: %s" % (regex.pattern, stdout))
- self.assertEqual(len(index), 26)
+ self.assertEqual(len(index), 25)
for fn in expected:
self.assertIn(fn, index)
ft.remove_file(index_fp)
# test creating index file that's only valid for a (very) short amount of time
- index_fp = ft.dump_index(self.test_prefix, max_age_sec=1)
+ index_fp = ft.dump_index(ecs_dir, max_age_sec=1)
time.sleep(3)
self.mock_stderr(True)
self.mock_stdout(True)
- index = ft.load_index(self.test_prefix)
+ index = ft.load_index(ecs_dir)
stderr = self.get_stderr()
stdout = self.get_stdout()
self.mock_stderr(False)
self.mock_stdout(False)
self.assertIsNone(index)
self.assertFalse(stdout)
- regex = re.compile(r"WARNING: Index for %s is no longer valid \(too old\), so ignoring it" % self.test_prefix)
+ regex = re.compile(r"WARNING: Index for %s is no longer valid \(too old\), so ignoring it" % ecs_dir)
self.assertTrue(regex.search(stderr), "Pattern '%s' found in: %s" % (regex.pattern, stderr))
# check whether load_index takes into account --ignore-index
init_config(build_options={'ignore_index': True})
- self.assertEqual(ft.load_index(self.test_prefix), None)
+ self.assertEqual(ft.load_index(ecs_dir), None)
def test_search_file(self):
"""Test search_file function."""
@@ -2833,7 +2973,7 @@ def test_diff_files(self):
self.assertTrue(regex.search(res), "Pattern '%s' found in: %s" % (regex.pattern, res))
@requires_github_access()
- def test_get_source_tarball_from_git(self):
+ def test_github_get_source_tarball_from_git(self):
"""Test get_source_tarball_from_git function."""
target_dir = os.path.join(self.test_prefix, 'target')
@@ -2848,7 +2988,7 @@ def test_get_source_tarball_from_git(self):
def run_check():
"""Helper function to run get_source_tarball_from_git & check dry run output"""
with self.mocked_stdout_stderr():
- res = ft.get_source_tarball_from_git('test.tar.gz', target_dir, git_config)
+ res = ft.get_source_tarball_from_git('test', target_dir, git_config)
stdout = self.get_stdout()
stderr = self.get_stderr()
self.assertEqual(stderr, '')
@@ -2856,113 +2996,142 @@ def run_check():
self.assertTrue(regex.search(stdout), "Pattern '%s' found in: %s" % (regex.pattern, stdout))
self.assertEqual(os.path.dirname(res), target_dir)
- self.assertEqual(os.path.basename(res), 'test.tar.gz')
+ self.assertEqual(os.path.basename(res), 'test.tar.xz')
git_config = {
'repo_name': 'testrepository',
'url': 'git@github.com:easybuilders',
'tag': 'tag_for_tests',
}
- git_repo = {'git_repo': 'git@github.com:easybuilders/testrepository.git'} # Just to make the below shorter
+ string_args = {
+ 'git_repo': 'git@github.com:easybuilders/testrepository.git',
+ 'git_clone_cmd': 'git clone --no-checkout',
+ 'test_prefix': self.test_prefix,
+ }
+
expected = '\n'.join([
- r' running command "git clone --depth 1 --branch tag_for_tests %(git_repo)s"',
- r" \(in .*/tmp.*\)",
- r' running command "tar cfvz .*/target/test.tar.gz --exclude .git testrepository"',
+ r' running shell command "{git_clone_cmd} {git_repo}"',
r" \(in .*/tmp.*\)",
- ]) % git_repo
+ r' running shell command "git checkout refs/tags/tag_for_tests"',
+ r" \(in .*/{repo_name}\)",
+ r"Archiving '.*/{repo_name}' into '{test_prefix}/target/test.tar.xz'...",
+ ]).format(**string_args, repo_name='testrepository')
run_check()
git_config['clone_into'] = 'test123'
expected = '\n'.join([
- r' running command "git clone --depth 1 --branch tag_for_tests %(git_repo)s test123"',
+ r' running shell command "{git_clone_cmd} {git_repo} test123"',
r" \(in .*/tmp.*\)",
- r' running command "tar cfvz .*/target/test.tar.gz --exclude .git test123"',
- r" \(in .*/tmp.*\)",
- ]) % git_repo
+ r' running shell command "git checkout refs/tags/tag_for_tests"',
+ r" \(in .*/{repo_name}\)",
+ r"Archiving '.*/{repo_name}' into '{test_prefix}/target/test.tar.xz'...",
+ ]).format(**string_args, repo_name='test123')
run_check()
del git_config['clone_into']
git_config['recursive'] = True
expected = '\n'.join([
- r' running command "git clone --depth 1 --branch tag_for_tests --recursive %(git_repo)s"',
- r" \(in .*/tmp.*\)",
- r' running command "tar cfvz .*/target/test.tar.gz --exclude .git testrepository"',
+ r' running shell command "{git_clone_cmd} {git_repo}"',
r" \(in .*/tmp.*\)",
- ]) % git_repo
+ r' running shell command "git checkout refs/tags/tag_for_tests"',
+ r" \(in .*/{repo_name}\)",
+ r' running shell command "git submodule update --init --recursive"',
+ r" \(in .*/{repo_name}\)",
+ r"Archiving '.*/{repo_name}' into '{test_prefix}/target/test.tar.xz'...",
+ ]).format(**string_args, repo_name='testrepository')
run_check()
git_config['recurse_submodules'] = ['!vcflib', '!sdsl-lite']
expected = '\n'.join([
- ' running command "git clone --depth 1 --branch tag_for_tests --recursive'
- + ' --recurse-submodules=\'!vcflib\' --recurse-submodules=\'!sdsl-lite\' %(git_repo)s"',
+ r' running shell command "{git_clone_cmd} {git_repo}"',
r" \(in .*/tmp.*\)",
- r' running command "tar cfvz .*/target/test.tar.gz --exclude .git testrepository"',
- r" \(in .*/tmp.*\)",
- ]) % git_repo
+ r' running shell command "git checkout refs/tags/tag_for_tests"',
+ r" \(in .*/{repo_name}\)",
+ r' running shell command "git submodule update --init --recursive -- \':!vcflib\' \':!sdsl-lite\'"',
+ r" \(in .*/{repo_name}\)",
+ r"Archiving '.*/{repo_name}' into '{test_prefix}/target/test.tar.xz'...",
+ ]).format(**string_args, repo_name='testrepository')
run_check()
git_config['extra_config_params'] = [
'submodule."fastahack".active=false',
'submodule."sha1".active=false',
]
+ git_cmd_extra = 'git -c submodule."fastahack".active=false -c submodule."sha1".active=false'
expected = '\n'.join([
- ' running command "git -c submodule."fastahack".active=false -c submodule."sha1".active=false'
- + ' clone --depth 1 --branch tag_for_tests --recursive'
- + ' --recurse-submodules=\'!vcflib\' --recurse-submodules=\'!sdsl-lite\' %(git_repo)s"',
- r" \(in .*/tmp.*\)",
- r' running command "tar cfvz .*/target/test.tar.gz --exclude .git testrepository"',
+ r' running shell command "{git_cmd_extra} clone --no-checkout {git_repo}"',
r" \(in .*/tmp.*\)",
- ]) % git_repo
+ r' running shell command "{git_cmd_extra} checkout refs/tags/tag_for_tests"',
+ r" \(in .*/{repo_name}\)",
+ r' running shell command "{git_cmd_extra} submodule update --init --recursive --'
+ + ' \':!vcflib\' \':!sdsl-lite\'"',
+ r" \(in .*/{repo_name}\)",
+ r"Archiving '.*/{repo_name}' into '{test_prefix}/target/test.tar.xz'...",
+ ]).format(**string_args, repo_name='testrepository', git_cmd_extra=git_cmd_extra)
run_check()
del git_config['recurse_submodules']
del git_config['extra_config_params']
- git_config['keep_git_dir'] = True
- expected = '\n'.join([
- r' running command "git clone --branch tag_for_tests --recursive %(git_repo)s"',
- r" \(in .*/tmp.*\)",
- r' running command "tar cfvz .*/target/test.tar.gz testrepository"',
- r" \(in .*/tmp.*\)",
- ]) % git_repo
- run_check()
- del git_config['keep_git_dir']
-
del git_config['tag']
git_config['commit'] = '8456f86'
expected = '\n'.join([
- r' running command "git clone --no-checkout %(git_repo)s"',
+ r' running shell command "git clone --no-checkout {git_repo}"',
r" \(in .*/tmp.*\)",
- r' running command "git checkout 8456f86 && git submodule update --init --recursive"',
- r" \(in testrepository\)",
- r' running command "tar cfvz .*/target/test.tar.gz --exclude .git testrepository"',
- r" \(in .*/tmp.*\)",
- ]) % git_repo
+ r' running shell command "git checkout 8456f86"',
+ r" \(in .*/{repo_name}\)",
+ r' running shell command "git submodule update --init --recursive"',
+ r" \(in .*/{repo_name}\)",
+ r"Archiving '.*/{repo_name}' into '{test_prefix}/target/test.tar.xz'...",
+ ]).format(**string_args, repo_name='testrepository')
run_check()
git_config['recurse_submodules'] = ['!vcflib', '!sdsl-lite']
expected = '\n'.join([
- r' running command "git clone --no-checkout %(git_repo)s"',
- r" \(in .*/tmp.*\)",
- ' running command "git checkout 8456f86 && git submodule update --init --recursive'
- + ' --recurse-submodules=\'!vcflib\' --recurse-submodules=\'!sdsl-lite\'"',
- r" \(in testrepository\)",
- r' running command "tar cfvz .*/target/test.tar.gz --exclude .git testrepository"',
+ r' running shell command "git clone --no-checkout {git_repo}"',
r" \(in .*/tmp.*\)",
- ]) % git_repo
+ r' running shell command "git checkout 8456f86"',
+ r" \(in .*/{repo_name}\)",
+ r' running shell command "git submodule update --init --recursive -- \':!vcflib\' \':!sdsl-lite\'"',
+ r" \(in .*/{repo_name}\)",
+ r"Archiving '.*/{repo_name}' into '{test_prefix}/target/test.tar.xz'...",
+ ]).format(**string_args, repo_name='testrepository')
run_check()
del git_config['recursive']
del git_config['recurse_submodules']
expected = '\n'.join([
- r' running command "git clone --no-checkout %(git_repo)s"',
- r" \(in .*/tmp.*\)",
- r' running command "git checkout 8456f86"',
- r" \(in testrepository\)",
- r' running command "tar cfvz .*/target/test.tar.gz --exclude .git testrepository"',
- r" \(in .*/tmp.*\)",
- ]) % git_repo
+ r' running shell command "git clone --no-checkout {git_repo}"',
+ r" \(in .*\)",
+ r' running shell command "git checkout 8456f86"',
+ r" \(in .*/{repo_name}\)",
+ r"Archiving '.*/{repo_name}' into '{test_prefix}/target/test.tar.xz'...",
+ ]).format(**string_args, repo_name='testrepository')
run_check()
+ # tarball formats that are not reproducible
+ bad_filenames = ['test.tar.gz', 'test.tar.bz2']
+ # tarball formats that are reproducible
+ good_filenames = ['test.tar', 'test.tar.xz']
+ # extensionsless filenames get a default archive compression of XZ
+ noext_filename = ['test']
+ for test_filename in bad_filenames + good_filenames + noext_filename:
+ with self.mocked_stdout_stderr():
+ res = ft.get_source_tarball_from_git(test_filename, target_dir, git_config)
+ stderr = self.get_stderr()
+
+ regex = re.compile("Can not create reproducible archive.*")
+ if test_filename in bad_filenames:
+ self.assertTrue(regex.search(stderr), f"Pattern '{regex.pattern}' found in: {stderr}")
+ else:
+ self.assertFalse(regex.search(stderr), f"Pattern '{regex.pattern}' found in: {stderr}")
+
+ ref_filename = f"{test_filename}.tar.xz" if test_filename in noext_filename else test_filename
+ self.assertTrue(res.endswith(ref_filename))
+
+ # non-tarball formats are not supported
+ with self.mocked_stdout_stderr():
+ self.assertRaises(EasyBuildError, ft.get_source_tarball_from_git, 'test.zip', target_dir, git_config)
+
# Test with real data.
init_config()
git_config = {
@@ -2972,48 +3141,109 @@ def run_check():
}
try:
- res = ft.get_source_tarball_from_git('test.tar.gz', target_dir, git_config)
+ res = ft.get_source_tarball_from_git('test', target_dir, git_config)
# (only) tarball is created in specified target dir
- test_file = os.path.join(target_dir, 'test.tar.gz')
+ test_file = os.path.join(target_dir, 'test.tar.xz')
self.assertEqual(res, test_file)
self.assertTrue(os.path.isfile(test_file))
- test_tar_gzs = [os.path.basename(test_file)]
- self.assertEqual(os.listdir(target_dir), ['test.tar.gz'])
+ test_tar_files = [os.path.basename(test_file)]
+ self.assertEqual(os.listdir(target_dir), ['test.tar.xz'])
# Check that we indeed downloaded the right tag
extracted_dir = tempfile.mkdtemp(prefix='extracted_dir')
- extracted_repo_dir = ft.extract_file(test_file, extracted_dir, change_into_dir=False)
+ with self.mocked_stdout_stderr():
+ extracted_repo_dir = ft.extract_file(test_file, extracted_dir, change_into_dir=False)
self.assertTrue(os.path.isfile(os.path.join(extracted_repo_dir, 'this-is-a-branch.txt')))
+ self.assertFalse(os.path.isdir(os.path.join(extracted_repo_dir, '.git')))
os.remove(test_file)
# use a tag that clashes with a branch name and make sure this is handled correctly
git_config['tag'] = 'tag_for_tests'
with self.mocked_stdout_stderr():
- res = ft.get_source_tarball_from_git('test.tar.gz', target_dir, git_config)
- stderr = self.get_stderr()
- self.assertIn('Tag tag_for_tests was not downloaded in the first try', stderr)
+ res = ft.get_source_tarball_from_git('test', target_dir, git_config)
self.assertEqual(res, test_file)
self.assertTrue(os.path.isfile(test_file))
# Check that we indeed downloaded the tag and not the branch
extracted_dir = tempfile.mkdtemp(prefix='extracted_dir')
- extracted_repo_dir = ft.extract_file(test_file, extracted_dir, change_into_dir=False)
+ with self.mocked_stdout_stderr():
+ extracted_repo_dir = ft.extract_file(test_file, extracted_dir, change_into_dir=False)
self.assertTrue(os.path.isfile(os.path.join(extracted_repo_dir, 'this-is-a-tag.txt')))
+ self.assertFalse(os.path.isdir(os.path.join(extracted_repo_dir, '.git')))
del git_config['tag']
git_config['commit'] = '90366ea'
- res = ft.get_source_tarball_from_git('test2.tar.gz', target_dir, git_config)
- test_file = os.path.join(target_dir, 'test2.tar.gz')
+ res = ft.get_source_tarball_from_git('test2', target_dir, git_config)
+ test_file = os.path.join(target_dir, 'test2.tar.xz')
self.assertEqual(res, test_file)
self.assertTrue(os.path.isfile(test_file))
- test_tar_gzs.append(os.path.basename(test_file))
- self.assertEqual(sorted(os.listdir(target_dir)), test_tar_gzs)
+ test_tar_files.append(os.path.basename(test_file))
+ self.assertCountEqual(sorted(os.listdir(target_dir)), test_tar_files)
+ extracted_dir = tempfile.mkdtemp(prefix='extracted_dir')
+ with self.mocked_stdout_stderr():
+ extracted_repo_dir = ft.extract_file(test_file, extracted_dir, change_into_dir=False)
+ self.assertTrue(os.path.isfile(os.path.join(extracted_repo_dir, 'README.md')))
+ self.assertFalse(os.path.isdir(os.path.join(extracted_repo_dir, '.git')))
git_config['keep_git_dir'] = True
- res = ft.get_source_tarball_from_git('test3.tar.gz', target_dir, git_config)
- test_file = os.path.join(target_dir, 'test3.tar.gz')
+ res = ft.get_source_tarball_from_git('test3', target_dir, git_config)
+ test_file = os.path.join(target_dir, 'test3.tar.xz')
+ self.assertEqual(res, test_file)
+ self.assertTrue(os.path.isfile(test_file))
+ test_tar_files.append(os.path.basename(test_file))
+ self.assertCountEqual(sorted(os.listdir(target_dir)), test_tar_files)
+ extracted_dir = tempfile.mkdtemp(prefix='extracted_dir')
+ with self.mocked_stdout_stderr():
+ extracted_repo_dir = ft.extract_file(test_file, extracted_dir, change_into_dir=False)
+ self.assertTrue(os.path.isfile(os.path.join(extracted_repo_dir, 'README.md')))
+ self.assertTrue(os.path.isdir(os.path.join(extracted_repo_dir, '.git')))
+
+ del git_config['keep_git_dir']
+ git_config['commit'] = '17a551c'
+ git_config['recursive'] = True
+ res = ft.get_source_tarball_from_git('test_recursive', target_dir, git_config)
+ test_file = os.path.join(target_dir, 'test_recursive.tar.xz')
+ self.assertEqual(res, test_file)
+ self.assertTrue(os.path.isfile(test_file))
+ test_tar_files.append(os.path.basename(test_file))
+ self.assertCountEqual(sorted(os.listdir(target_dir)), test_tar_files)
+ extracted_dir = tempfile.mkdtemp(prefix='extracted_dir')
+ with self.mocked_stdout_stderr():
+ extracted_repo_dir = ft.extract_file(test_file, extracted_dir, change_into_dir=False)
+ self.assertTrue(os.path.isfile(os.path.join(extracted_repo_dir, 'README.md')))
+ self.assertFalse(os.path.isdir(os.path.join(extracted_repo_dir, '.git')))
+ self.assertTrue(os.path.isdir(os.path.join(extracted_repo_dir, 'easybuilders.github.io')))
+ self.assertTrue(os.path.isfile(os.path.join(extracted_repo_dir, 'easybuilders.github.io', 'index.html')))
+
+ git_config['commit'] = '17a551c'
+ git_config['recurse_submodules'] = ['easybuilders.github.io']
+ res = ft.get_source_tarball_from_git('test_submodules', target_dir, git_config)
+ test_file = os.path.join(target_dir, 'test_submodules.tar.xz')
+ self.assertEqual(res, test_file)
+ self.assertTrue(os.path.isfile(test_file))
+ test_tar_files.append(os.path.basename(test_file))
+ self.assertCountEqual(sorted(os.listdir(target_dir)), test_tar_files)
+ extracted_dir = tempfile.mkdtemp(prefix='extracted_dir')
+ with self.mocked_stdout_stderr():
+ extracted_repo_dir = ft.extract_file(test_file, extracted_dir, change_into_dir=False)
+ self.assertTrue(os.path.isfile(os.path.join(extracted_repo_dir, 'README.md')))
+ self.assertFalse(os.path.isdir(os.path.join(extracted_repo_dir, '.git')))
+ self.assertTrue(os.path.isdir(os.path.join(extracted_repo_dir, 'easybuilders.github.io')))
+ self.assertTrue(os.path.isfile(os.path.join(extracted_repo_dir, 'easybuilders.github.io', 'index.html')))
+
+ git_config['commit'] = '17a551c'
+ git_config['recurse_submodules'] = ['!easybuilders.github.io']
+ res = ft.get_source_tarball_from_git('test_exclude_submodules', target_dir, git_config)
+ test_file = os.path.join(target_dir, 'test_exclude_submodules.tar.xz')
self.assertEqual(res, test_file)
self.assertTrue(os.path.isfile(test_file))
- test_tar_gzs.append(os.path.basename(test_file))
- self.assertEqual(sorted(os.listdir(target_dir)), test_tar_gzs)
+ test_tar_files.append(os.path.basename(test_file))
+ self.assertCountEqual(sorted(os.listdir(target_dir)), test_tar_files)
+ extracted_dir = tempfile.mkdtemp(prefix='extracted_dir')
+ with self.mocked_stdout_stderr():
+ extracted_repo_dir = ft.extract_file(test_file, extracted_dir, change_into_dir=False)
+ self.assertTrue(os.path.isfile(os.path.join(extracted_repo_dir, 'README.md')))
+ self.assertFalse(os.path.isdir(os.path.join(extracted_repo_dir, '.git')))
+ self.assertTrue(os.path.isdir(os.path.join(extracted_repo_dir, 'easybuilders.github.io')))
+ self.assertFalse(os.path.isfile(os.path.join(extracted_repo_dir, 'easybuilders.github.io', 'index.html')))
except EasyBuildError as err:
if "Network is down" in str(err):
@@ -3026,7 +3256,7 @@ def run_check():
'url': 'git@github.com:easybuilders',
'tag': 'tag_for_tests',
}
- args = ['test.tar.gz', self.test_prefix, git_config]
+ args = ['test', self.test_prefix, git_config]
for key in ['repo_name', 'url', 'tag']:
orig_value = git_config.pop(key)
@@ -3047,10 +3277,97 @@ def run_check():
self.assertErrorRegex(EasyBuildError, error_pattern, ft.get_source_tarball_from_git, *args)
del git_config['unknown']
- args[0] = 'test.txt'
- error_pattern = "git_config currently only supports filename ending in .tar.gz"
- self.assertErrorRegex(EasyBuildError, error_pattern, ft.get_source_tarball_from_git, *args)
- args[0] = 'test.tar.gz'
+ def test_make_archive(self):
+ """Test for make_archive method"""
+ # create fake directories and files to be archived
+ tmpdir = tempfile.mkdtemp()
+ tardir = os.path.join(tmpdir, "test_archive")
+ os.mkdir(tardir)
+ for path in ('bin', 'lib', 'include'):
+ os.mkdir(os.path.join(tardir, path))
+ ft.write_file(os.path.join(tardir, 'README'), 'Dummy readme')
+ ft.write_file(os.path.join(tardir, 'bin', 'executable'), 'Dummy binary')
+ ft.write_file(os.path.join(tardir, 'lib', 'lib.so'), 'Dummy library')
+ ft.write_file(os.path.join(tardir, 'include', 'header.h'), 'Dummy header')
+
+ # default behaviour
+ unreprod_txz = ft.make_archive(tardir, reproducible=False)
+ unreprod_txz_chksum = ft.compute_checksum(unreprod_txz, checksum_type="sha256")
+ self.assertEqual(unreprod_txz, "test_archive.tar.xz")
+ self.assertExists(unreprod_txz)
+ os.remove(unreprod_txz)
+ reprod_txz = ft.make_archive(tardir, reproducible=True)
+ reprod_txz_chksum = ft.compute_checksum(reprod_txz, checksum_type="sha256")
+ self.assertEqual(reprod_txz, "test_archive.tar.xz")
+ self.assertExists(reprod_txz)
+ os.remove(reprod_txz)
+ # custom filenames
+ custom_txz = ft.make_archive(tardir, archive_file="custom_name", reproducible=True)
+ custom_txz_chksum = ft.compute_checksum(custom_txz, checksum_type="sha256")
+ self.assertEqual(custom_txz, "custom_name.tar.xz")
+ self.assertExists(custom_txz)
+ os.remove(custom_txz)
+ customdir_txz = ft.make_archive(tardir, archive_file="custom_name", archive_dir=tmpdir, reproducible=True)
+ customdir_txz_chksum = ft.compute_checksum(customdir_txz, checksum_type="sha256")
+ self.assertEqual(customdir_txz, os.path.join(tmpdir, "custom_name.tar.xz"))
+ self.assertExists(customdir_txz)
+ os.remove(customdir_txz)
+ # custom .tar
+ reprod_tar = ft.make_archive(tardir, archive_file="custom_name.tar", reproducible=True)
+ reprod_tar_chksum = ft.compute_checksum(reprod_tar, checksum_type="sha256")
+ self.assertEqual(reprod_tar, "custom_name.tar")
+ self.assertExists(reprod_tar)
+ os.remove(reprod_tar)
+ unreprod_tar = ft.make_archive(tardir, archive_file="custom_name.tar", reproducible=False)
+ unreprod_tar_chksum = ft.compute_checksum(unreprod_tar, checksum_type="sha256")
+ self.assertEqual(unreprod_tar, "custom_name.tar")
+ self.assertExists(unreprod_tar)
+ os.remove(unreprod_tar)
+
+ # custom .tar.gz
+ self.mock_stdout(True)
+ self.mock_stderr(True)
+ custom_tgz = ft.make_archive(tardir, archive_file="custom_name.tar.gz", reproducible=True)
+ stderr = self.get_stderr()
+ self.mock_stdout(False)
+ self.mock_stderr(False)
+
+ warning_msg = "WARNING: Can not create reproducible archive due to unsupported file compression (gz)"
+ self.assertIn(warning_msg, stderr)
+
+ custom_tgz_chksum = ft.compute_checksum(custom_tgz, checksum_type="sha256")
+ self.assertEqual(custom_tgz, "custom_name.tar.gz")
+ self.assertExists(custom_tgz)
+ os.remove(custom_tgz)
+ self.mock_stdout(True)
+ self.mock_stderr(True)
+ custom_tgz = ft.make_archive(tardir, archive_file="custom_name.tar.gz", reproducible=False)
+ stderr = self.get_stderr()
+ self.mock_stdout(False)
+ self.mock_stderr(False)
+
+ self.assertNotIn(warning_msg, stderr)
+
+ custom_tgz_chksum = ft.compute_checksum(custom_tgz, checksum_type="sha256")
+ self.assertEqual(custom_tgz, "custom_name.tar.gz")
+ self.assertExists(custom_tgz)
+ os.remove(custom_tgz)
+
+ self.assertErrorRegex(EasyBuildError, "Unsupported archive format.*", ft.make_archive, tardir, "unknown.ext")
+
+ reference_checksum_txz = "ec0f91a462c2743b19b428f4c177d7109d2ccc018dcdedc12570d9d735d6fb1b"
+ reference_checksum_tar = "6e902e77925ab2faeef8377722434d4482f1fcc74af958c984c3f22509ae5084"
+
+ if sys.version_info[0] >= 3 and sys.version_info[1] >= 9:
+ # checksums of tarballs made by EB cannot be reliably checked prior to Python 3.9
+ # due to changes introduced in python/cpython#90021
+ self.assertNotEqual(unreprod_txz_chksum, reference_checksum_txz)
+ self.assertEqual(reprod_txz_chksum, reference_checksum_txz)
+ self.assertEqual(custom_txz_chksum, reference_checksum_txz)
+ self.assertEqual(customdir_txz_chksum, reference_checksum_txz)
+ self.assertNotEqual(unreprod_tar_chksum, reference_checksum_tar)
+ self.assertEqual(reprod_tar_chksum, reference_checksum_tar)
+ self.assertNotEqual(custom_tgz_chksum, reference_checksum_txz)
def test_is_sha256_checksum(self):
"""Test for is_sha256_checksum function."""
@@ -3142,6 +3459,18 @@ def test_is_generic_easyblock(self):
for name in ['EB_bzip2', 'EB_DL_underscore_POLY_underscore_Classic', 'EB_GCC', 'EB_WRF_minus_Fire']:
self.assertFalse(ft.is_generic_easyblock(name))
+ def test_load_source(self):
+ """Test for load_source function."""
+ txt = textwrap.dedent("""
+ def foobar():
+ pass
+ """)
+ fp = os.path.join(self.test_prefix, 'foobar.py')
+ ft.write_file(fp, txt)
+ foobar = ft.load_source('foobar', fp)
+ self.assertTrue(isinstance(foobar, types.ModuleType))
+ self.assertTrue(isinstance(foobar.foobar, types.FunctionType))
+
def test_get_easyblock_class_name(self):
"""Test for get_easyblock_class_name function."""
@@ -3511,16 +3840,29 @@ def test_set_gid_sticky_bits(self):
self.assertEqual(dir_perms & stat.S_ISGID, stat.S_ISGID)
self.assertEqual(dir_perms & stat.S_ISVTX, stat.S_ISVTX)
- def test_compat_makedirs(self):
- """Test compatibility layer for Python3 os.makedirs"""
- name = os.path.join(self.test_prefix, 'folder')
- self.assertNotExists(name)
- py2vs3.makedirs(name)
- self.assertExists(name)
- # exception is raised because file exists (OSError in Python 2, FileExistsError in Python 3)
- self.assertErrorRegex(Exception, '.*', py2vs3.makedirs, name)
- py2vs3.makedirs(name, exist_ok=True) # No error
- self.assertExists(name)
+ def test_get_first_non_existing_parent_path(self):
+ """Test get_first_non_existing_parent_path function."""
+ root_path = os.path.join(self.test_prefix, 'a')
+ target_path = os.path.join(self.test_prefix, 'a', 'b', 'c')
+ ft.mkdir(root_path, parents=True)
+ first_non_existing_parent = ft.get_first_non_existing_parent_path(target_path)
+ self.assertEqual(first_non_existing_parent, os.path.join(self.test_prefix, 'a', 'b'))
+ ft.remove_dir(root_path)
+
+ # Use a reflexive parent relation
+ root_path = os.path.join(self.test_prefix, 'a', 'b')
+ target_path = os.path.join(self.test_prefix, 'a', 'b', 'c')
+ ft.mkdir(root_path, parents=True)
+ first_non_existing_parent = ft.get_first_non_existing_parent_path(target_path)
+ self.assertEqual(first_non_existing_parent, os.path.join(self.test_prefix, 'a', 'b', 'c'))
+ ft.remove_dir(root_path)
+
+ root_path = os.path.join(self.test_prefix, 'a', 'b', 'c')
+ target_path = os.path.join(self.test_prefix, 'a', 'b', 'c')
+ ft.mkdir(root_path, parents=True)
+ first_non_existing_parent = ft.get_first_non_existing_parent_path(target_path)
+ self.assertEqual(first_non_existing_parent, None)
+ ft.remove_dir(root_path)
def test_create_unused_dir(self):
"""Test create_unused_dir function."""
@@ -3560,6 +3902,234 @@ def test_create_unused_dir(self):
self.assertEqual(path, os.path.join(self.test_prefix, 'file_0'))
self.assertExists(path)
+ def test_create_non_existing_paths(self):
+ """Test create_non_existing_paths function."""
+ test_root = os.path.join(self.test_prefix, 'test_create_non_existing_paths')
+
+ ft.mkdir(test_root)
+ requested_paths = [
+ os.path.join(test_root, 'folder_a'),
+ os.path.join(test_root, 'folder_b'),
+ ]
+ paths = ft.create_non_existing_paths(requested_paths)
+ self.assertEqual(paths, requested_paths)
+ self.assertAllExist(paths)
+ ft.remove_dir(test_root)
+
+ # Repeat with existing folder(s) should create new ones
+ ft.mkdir(test_root)
+ requested_paths = [
+ os.path.join(test_root, 'folder_a'),
+ os.path.join(test_root, 'folder_b'),
+ ]
+ for path in requested_paths:
+ ft.mkdir(path)
+ for i in range(10):
+ paths = ft.create_non_existing_paths(requested_paths)
+ self.assertEqual(paths, [f'{p}_{i}' for p in requested_paths])
+ self.assertAllExist(paths)
+ ft.remove_dir(test_root)
+
+ # Add a suffix in both directories if a suffix already exists
+ ft.mkdir(test_root)
+ requested_paths = [
+ os.path.join(test_root, 'existing_a'),
+ os.path.join(test_root, 'existing_b'),
+ ]
+ ft.mkdir(os.path.join(test_root, 'existing_b'))
+ paths = ft.create_non_existing_paths(requested_paths)
+ self.assertEqual(paths, [f'{p}_0' for p in requested_paths])
+ self.assertNotExists(os.path.join(test_root, 'existing_a'))
+ self.assertAllExist(paths)
+ ft.remove_dir(test_root)
+
+ # Skip suffix if a directory with the suffix already exists
+ ft.mkdir(test_root)
+ existing_suffix = 1
+ requested_paths = [
+ os.path.join(test_root, 'existing_suffix_a'),
+ os.path.join(test_root, 'existing_suffix_b'),
+ ]
+
+ ft.mkdir(os.path.join(test_root, f'existing_suffix_b_{existing_suffix}'))
+
+ def expected_suffix(n_calls_to_create_non_existing_paths):
+ if n_calls_to_create_non_existing_paths == 0:
+ return ""
+ new_suffix = n_calls_to_create_non_existing_paths - 1
+ if n_calls_to_create_non_existing_paths > existing_suffix:
+ new_suffix += 1
+ return f"_{new_suffix}"
+
+ for i in range(3):
+ paths = ft.create_non_existing_paths(requested_paths)
+ self.assertEqual(paths, [p + expected_suffix(i) for p in requested_paths])
+ self.assertAllExist(paths)
+ self.assertNotExists(os.path.join(test_root, f'existing_suffix_a_{existing_suffix}'))
+ self.assertExists(os.path.join(test_root, f'existing_suffix_b_{existing_suffix}'))
+
+ ft.remove_dir(test_root)
+
+ # Support creation of parent directories
+ ft.mkdir(test_root)
+ requested_paths = [os.path.join(test_root, 'parent_folder', 'folder')]
+ paths = ft.create_non_existing_paths(requested_paths)
+ self.assertEqual(paths, requested_paths)
+ self.assertAllExist(paths)
+ ft.remove_dir(test_root)
+
+ # Not influenced by similar folder
+ ft.mkdir(test_root)
+ requested_paths = [os.path.join(test_root, 'folder_a2')]
+ paths = ft.create_non_existing_paths(requested_paths)
+ self.assertEqual(paths, requested_paths)
+ self.assertAllExist(paths)
+ for i in range(10):
+ paths = ft.create_non_existing_paths(requested_paths)
+ self.assertEqual(paths, [f'{p}_{i}' for p in requested_paths])
+ self.assertAllExist(paths)
+ ft.remove_dir(test_root)
+
+ # Fail cleanly if passed a readonly folder
+ ft.mkdir(test_root)
+ readonly_dir = os.path.join(test_root, 'ro_folder')
+ ft.mkdir(readonly_dir)
+ old_perms = os.lstat(readonly_dir)[stat.ST_MODE]
+ ft.adjust_permissions(readonly_dir, stat.S_IREAD | stat.S_IEXEC, relative=False)
+ requested_path = [os.path.join(readonly_dir, 'new_folder')]
+ try:
+ self.assertErrorRegex(
+ EasyBuildError, "Failed to create directory",
+ ft.create_non_existing_paths, requested_path
+ )
+ finally:
+ ft.adjust_permissions(readonly_dir, old_perms, relative=False)
+ ft.remove_dir(test_root)
+
+ # Fail if the number of attempts to create the directory is exceeded
+ ft.mkdir(test_root)
+ requested_paths = [os.path.join(test_root, 'attempt')]
+ ft.mkdir(os.path.join(test_root, 'attempt'))
+ ft.mkdir(os.path.join(test_root, 'attempt_0'))
+ ft.mkdir(os.path.join(test_root, 'attempt_1'))
+ ft.mkdir(os.path.join(test_root, 'attempt_2'))
+ ft.mkdir(os.path.join(test_root, 'attempt_3'))
+ max_tries = 4
+ self.assertErrorRegex(
+ EasyBuildError,
+ rf"Exceeded maximum number of attempts \({max_tries}\) to generate non-existing paths",
+ ft.create_non_existing_paths,
+ requested_paths, max_tries=max_tries
+ )
+ ft.remove_dir(test_root)
+
+ # Ignore files same as folders. So first just create a file with no contents
+ ft.mkdir(test_root)
+ requested_path = os.path.join(test_root, 'file')
+ ft.write_file(requested_path, '')
+ paths = ft.create_non_existing_paths([requested_path])
+ self.assertEqual(paths, [requested_path + '_0'])
+ self.assertAllExist(paths)
+ ft.remove_dir(test_root)
+
+ # Deny creation of nested directories
+ requested_paths = [
+ os.path.join(test_root, 'foo/bar'),
+ os.path.join(test_root, 'foo/bar/baz'),
+ ]
+ self.assertErrorRegex(
+ EasyBuildError,
+ "Path '.*/foo/bar' is a parent path of '.*/foo/bar/baz'",
+ ft.create_non_existing_paths,
+ requested_paths
+ )
+ self.assertNotExists(test_root) # Fail early, do not create intermediate directories
+
+ requested_paths = [
+ os.path.join(test_root, 'foo/bar/baz'),
+ os.path.join(test_root, 'foo/bar'),
+ ]
+ self.assertErrorRegex(
+ EasyBuildError,
+ "Path '.*/foo/bar' is a parent path of '.*/foo/bar/baz'",
+ ft.create_non_existing_paths,
+ requested_paths
+ )
+ self.assertNotExists(test_root) # Fail early, do not create intermediate directories
+
+ requested_paths = [
+ os.path.join(test_root, 'foo/bar'),
+ os.path.join(test_root, 'foo/bar'),
+ ]
+ self.assertErrorRegex(
+ EasyBuildError,
+ "Path '.*/foo/bar' is a parent path of '.*/foo/bar'",
+ ft.create_non_existing_paths,
+ requested_paths
+ )
+ self.assertNotExists(test_root) # Fail early, do not create intermediate directories
+
+ # Allow creation of non-nested directories
+ ft.mkdir(test_root)
+ requested_paths = [
+ os.path.join(test_root, 'nested/foo/bar'),
+ os.path.join(test_root, 'nested/foo/baz'),
+ os.path.join(test_root, 'nested/buz'),
+ ]
+ paths = ft.create_non_existing_paths(requested_paths)
+ self.assertEqual(paths, requested_paths)
+ self.assertAllExist(paths)
+ ft.remove_dir(test_root)
+
+ # Test that permissions are set in single directories
+ ft.mkdir(test_root, set_gid=False, sticky=False)
+ init_config(build_options={'set_gid_bit': True, 'sticky_bit': True})
+ requested_path = os.path.join(test_root, 'directory')
+ paths = ft.create_non_existing_paths([requested_path])
+ self.assertEqual(len(paths), 1)
+ dir_perms = os.lstat(paths[0])[stat.ST_MODE]
+ self.assertEqual(dir_perms & stat.S_ISGID, stat.S_ISGID)
+ self.assertEqual(dir_perms & stat.S_ISVTX, stat.S_ISVTX)
+ init_config(build_options={'set_gid_bit': None, 'sticky_bit': None})
+ ft.remove_dir(test_root)
+
+ # Test that permissions are set correctly across a whole path
+ ft.mkdir(test_root, set_gid=False, sticky=False)
+ init_config(build_options={'set_gid_bit': True, 'sticky_bit': True})
+ requested_path = os.path.join(test_root, 'directory', 'subdirectory')
+ paths = ft.create_non_existing_paths([requested_path])
+ self.assertEqual(len(paths), 1)
+ tested_paths = [
+ os.path.join(test_root, 'directory'),
+ os.path.join(test_root, 'directory', 'subdirectory'),
+ ]
+ for path in tested_paths:
+ dir_perms = os.lstat(path)[stat.ST_MODE]
+ self.assertEqual(dir_perms & stat.S_ISGID, stat.S_ISGID, f"set_gid bit should be set for {path}")
+ self.assertEqual(dir_perms & stat.S_ISVTX, stat.S_ISVTX, f"sticky bit should be set for {path}")
+ init_config(build_options={'set_gid_bit': None, 'sticky_bit': None})
+ ft.remove_dir(test_root)
+
+ # Test that existing directory permissions are not modified
+ ft.mkdir(test_root)
+ init_config(build_options={'set_gid_bit': True, 'sticky_bit': True})
+ existing_parent = os.path.join(test_root, 'directory')
+ requested_path = os.path.join(existing_parent, 'subdirectory')
+
+ ft.mkdir(existing_parent, set_gid=False, sticky=False)
+ paths = ft.create_non_existing_paths([requested_path])
+ self.assertEqual(len(paths), 1)
+
+ dir_perms = os.lstat(paths[0])[stat.ST_MODE]
+ self.assertEqual(dir_perms & stat.S_ISGID, stat.S_ISGID, f"set_gid bit should be set for {path}")
+ self.assertEqual(dir_perms & stat.S_ISVTX, stat.S_ISVTX, f"sticky bit should be set for {path}")
+
+ dir_perms = os.lstat(existing_parent)[stat.ST_MODE]
+ self.assertEqual(dir_perms & stat.S_ISGID, 0, f"set_gid bit should not be set for {path}")
+ self.assertEqual(dir_perms & stat.S_ISVTX, 0, f"sticky bit should not be set for {path}")
+ init_config(build_options={'set_gid_bit': None, 'sticky_bit': None})
+ ft.remove_dir(test_root)
+
def suite():
""" returns all the testcases in this module """
diff --git a/test/framework/format_convert.py b/test/framework/format_convert.py
index e0add188b3..fc028f7299 100644
--- a/test/framework/format_convert.py
+++ b/test/framework/format_convert.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2014-2024 Ghent University
+# Copyright 2014-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/general.py b/test/framework/general.py
index 3837cecc87..e3b15129cc 100644
--- a/test/framework/general.py
+++ b/test/framework/general.py
@@ -1,5 +1,5 @@
##
-# Copyright 2015-2024 Ghent University
+# Copyright 2015-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/github.py b/test/framework/github.py
index d899ad06a1..ea862afb05 100644
--- a/test/framework/github.py
+++ b/test/framework/github.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -36,9 +36,11 @@
import sys
import textwrap
import unittest
+from string import ascii_letters
from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered, init_config
from time import gmtime
from unittest import TextTestRunner
+from urllib.request import HTTPError, URLError
import easybuild.tools.testing
from easybuild.base.rest import RestClient
@@ -53,7 +55,6 @@
from easybuild.tools.github import det_pr_title, fetch_easyconfigs_from_commit, fetch_files_from_commit
from easybuild.tools.github import is_patch_for, pick_default_branch
from easybuild.tools.testing import create_test_report, post_pr_test_report, session_state
-from easybuild.tools.py2vs3 import HTTPError, URLError, ascii_letters
import easybuild.tools.github as gh
try:
@@ -222,7 +223,8 @@ def test_github_read(self):
return
try:
- fp = self.ghfs.read("a_directory/a_file.txt", api=False)
+ with self.mocked_stdout_stderr():
+ fp = self.ghfs.read("a_directory/a_file.txt", api=False)
self.assertEqual(read_file(fp).strip(), "this is a line of text")
os.remove(fp)
except (IOError, OSError):
@@ -250,25 +252,21 @@ def test_github_add_pr_labels(self):
build_options['pr_target_repo'] = GITHUB_EASYCONFIGS_REPO
init_config(build_options=build_options)
- # PR #11262 includes easyconfigs that use 'dummy' toolchain,
- # so we need to allow triggering deprecated behaviour
- self.allow_deprecated_behaviour()
-
self.mock_stdout(True)
self.mock_stderr(True)
- gh.add_pr_labels(11262)
+ gh.add_pr_labels(22380)
stdout = self.get_stdout()
self.mock_stdout(False)
self.mock_stderr(False)
- self.assertIn("Could not determine any missing labels for PR #11262", stdout)
+ self.assertIn("Could not determine any missing labels for PR #22380", stdout)
self.mock_stdout(True)
self.mock_stderr(True)
- gh.add_pr_labels(8006) # closed, unmerged, unlabeled PR
+ gh.add_pr_labels(22088) # closed, unmerged, unlabeled PR
stdout = self.get_stdout()
self.mock_stdout(False)
self.mock_stderr(False)
- self.assertIn("PR #8006 should be labelled 'update'", stdout)
+ self.assertIn("Could not determine any missing labels for PR #22088", stdout)
def test_github_fetch_pr_data(self):
"""Test fetch_pr_data function."""
@@ -408,19 +406,21 @@ def test_github_fetch_easyblocks_from_pr(self):
'pr_target_account': gh.GITHUB_EB_MAIN,
})
+ # TODO: no 5.x PRs for new easyblocks
# PR with new easyblock plus non-easyblock file
- all_ebs_pr1964 = ['lammps.py']
+ # all_ebs_pr1964 = ['lammps.py']
# PR with changed easyblock
- all_ebs_pr1967 = ['siesta.py']
+ all_ebs_pr3631 = ['root.py']
# PR with more than one easyblock
- all_ebs_pr1949 = ['configuremake.py', 'rpackage.py']
+ all_ebs_pr3596 = ['wps.py', 'wrf.py']
- for pr, all_ebs in [(1964, all_ebs_pr1964), (1967, all_ebs_pr1967), (1949, all_ebs_pr1949)]:
+ for pr, all_ebs in [(3631, all_ebs_pr3631), (3596, all_ebs_pr3596)]:
try:
tmpdir = os.path.join(self.test_prefix, 'pr%s' % pr)
- eb_files = gh.fetch_easyblocks_from_pr(pr, path=tmpdir, github_user=GITHUB_TEST_ACCOUNT)
+ with self.mocked_stdout_stderr():
+ eb_files = gh.fetch_easyblocks_from_pr(pr, path=tmpdir, github_user=GITHUB_TEST_ACCOUNT)
self.assertEqual(sorted(all_ebs), sorted([os.path.basename(f) for f in eb_files]))
except URLError as err:
print("Ignoring URLError '%s' in test_fetch_easyblocks_from_pr" % err)
@@ -435,43 +435,30 @@ def test_github_fetch_easyconfigs_from_pr(self):
'pr_target_account': gh.GITHUB_EB_MAIN,
})
- # PR for rename of arrow to Arrow,
- # see https://github.com/easybuilders/easybuild-easyconfigs/pull/8007/files
- all_ecs_pr8007 = [
- 'Arrow-0.7.1-intel-2017b-Python-3.6.3.eb',
- 'bat-0.3.3-fix-pyspark.patch',
- 'bat-0.3.3-intel-2017b-Python-3.6.3.eb',
+ # PR for XCrySDen,
+ # see https://github.com/easybuilders/easybuild-easyconfigs/pull/22227/files
+ all_ecs_pr22227 = [
+ 'bwidget-1.10.1-GCCcore-13.3.0.eb',
+ 'quarto-1.5.57-x64.eb',
+ 'Sabre-2013-09-28-GCC-13.3.0.eb',
+ 'Togl-2.0-GCCcore-13.3.0.eb',
+ 'XCrySDen-1.6.2-foss-2024a.eb',
]
- # PR where also files are patched in test/
- # see https://github.com/easybuilders/easybuild-easyconfigs/pull/6587/files
- all_ecs_pr6587 = [
- 'WIEN2k-18.1-foss-2018a.eb',
- 'WIEN2k-18.1-gimkl-2017a.eb',
- 'WIEN2k-18.1-intel-2018a.eb',
- 'libxc-4.2.3-foss-2018a.eb',
- 'libxc-4.2.3-gimkl-2017a.eb',
- 'libxc-4.2.3-intel-2018a.eb',
+ # PR where only files are patched in test/
+ # see https://github.com/easybuilders/easybuild-easyconfigs/pull/22061/files
+ all_ecs_pr22061 = [
]
- # PR where files are renamed
- # see https://github.com/easybuilders/easybuild-easyconfigs/pull/7159/files
- all_ecs_pr7159 = [
- 'DOLFIN-2018.1.0.post1-foss-2018a-Python-3.6.4.eb',
- 'OpenFOAM-5.0-20180108-foss-2018a.eb',
- 'OpenFOAM-5.0-20180108-intel-2018a.eb',
- 'OpenFOAM-6-foss-2018b.eb',
- 'OpenFOAM-6-intel-2018a.eb',
- 'OpenFOAM-v1806-foss-2018b.eb',
- 'PETSc-3.9.3-foss-2018a.eb',
- 'SCOTCH-6.0.6-foss-2018a.eb',
- 'SCOTCH-6.0.6-foss-2018b.eb',
- 'SCOTCH-6.0.6-intel-2018a.eb',
- 'Trilinos-12.12.1-foss-2018a-Python-3.6.4.eb'
+ # PR where files are unarchived
+ # see https://github.com/easybuilders/easybuild-easyconfigs/pull/19834/files
+ all_ecs_pr19834 = [
+ 'Gblocks-0.91b.eb',
]
- for pr, all_ecs in [(8007, all_ecs_pr8007), (6587, all_ecs_pr6587), (7159, all_ecs_pr7159)]:
+ for pr, all_ecs in [(22227, all_ecs_pr22227), (22061, all_ecs_pr22061), (19834, all_ecs_pr19834)]:
try:
tmpdir = os.path.join(self.test_prefix, 'pr%s' % pr)
- ec_files = gh.fetch_easyconfigs_from_pr(pr, path=tmpdir, github_user=GITHUB_TEST_ACCOUNT)
+ with self.mocked_stdout_stderr():
+ ec_files = gh.fetch_easyconfigs_from_pr(pr, path=tmpdir, github_user=GITHUB_TEST_ACCOUNT)
self.assertEqual(sorted(all_ecs), sorted([os.path.basename(f) for f in ec_files]))
except URLError as err:
print("Ignoring URLError '%s' in test_fetch_easyconfigs_from_pr" % err)
@@ -490,35 +477,30 @@ def test_github_fetch_files_from_pr_cache(self):
gh.fetch_files_from_pr.clear_cache()
self.assertFalse(gh.fetch_files_from_pr._cache)
- pr7159_filenames = [
- 'DOLFIN-2018.1.0.post1-foss-2018a-Python-3.6.4.eb',
- 'OpenFOAM-5.0-20180108-foss-2018a.eb',
- 'OpenFOAM-5.0-20180108-intel-2018a.eb',
- 'OpenFOAM-6-foss-2018b.eb',
- 'OpenFOAM-6-intel-2018a.eb',
- 'OpenFOAM-v1806-foss-2018b.eb',
- 'PETSc-3.9.3-foss-2018a.eb',
- 'SCOTCH-6.0.6-foss-2018a.eb',
- 'SCOTCH-6.0.6-foss-2018b.eb',
- 'SCOTCH-6.0.6-intel-2018a.eb',
- 'Trilinos-12.12.1-foss-2018a-Python-3.6.4.eb'
+ pr22227_filenames = [
+ 'bwidget-1.10.1-GCCcore-13.3.0.eb',
+ 'quarto-1.5.57-x64.eb',
+ 'Sabre-2013-09-28-GCC-13.3.0.eb',
+ 'Togl-2.0-GCCcore-13.3.0.eb',
+ 'XCrySDen-1.6.2-foss-2024a.eb',
]
- pr7159_files = gh.fetch_easyconfigs_from_pr(7159, path=self.test_prefix, github_user=GITHUB_TEST_ACCOUNT)
- self.assertEqual(sorted(pr7159_filenames), sorted(os.path.basename(f) for f in pr7159_files))
+ with self.mocked_stdout_stderr():
+ pr22227_files = gh.fetch_easyconfigs_from_pr(22227, path=self.test_prefix, github_user=GITHUB_TEST_ACCOUNT)
+ self.assertEqual(sorted(pr22227_filenames), sorted(os.path.basename(f) for f in pr22227_files))
- # check that cache has been populated for PR 7159
+ # check that cache has been populated for PR 22227
self.assertEqual(len(gh.fetch_files_from_pr._cache.keys()), 1)
# github_account value is None (results in using default 'easybuilders')
- cache_key = (7159, None, 'easybuild-easyconfigs', self.test_prefix)
+ cache_key = (22227, None, 'easybuild-easyconfigs', self.test_prefix)
self.assertIn(cache_key, gh.fetch_files_from_pr._cache.keys())
cache_entry = gh.fetch_files_from_pr._cache[cache_key]
- self.assertEqual(sorted([os.path.basename(f) for f in cache_entry]), sorted(pr7159_filenames))
+ self.assertEqual(sorted([os.path.basename(f) for f in cache_entry]), sorted(pr22227_filenames))
# same query should return result from cache entry
- res = gh.fetch_easyconfigs_from_pr(7159, path=self.test_prefix, github_user=GITHUB_TEST_ACCOUNT)
- self.assertEqual(res, pr7159_files)
+ res = gh.fetch_easyconfigs_from_pr(22227, path=self.test_prefix, github_user=GITHUB_TEST_ACCOUNT)
+ self.assertEqual(res, pr22227_files)
# inject entry in cache and check result of matching query
pr_id = 12345
@@ -615,6 +597,7 @@ def test_github_download_repo(self):
return
cwd = os.getcwd()
+ self.mock_stdout(True)
# default: download tarball for master branch of easybuilders/easybuild-easyconfigs repo
path = gh.download_repo(path=self.test_prefix, github_user=GITHUB_TEST_ACCOUNT)
@@ -649,6 +632,7 @@ def test_github_download_repo(self):
self.assertIn('easybuild', os.listdir(repodir))
self.assertTrue(re.match('^[0-9a-f]{40}$', read_file(shafile)))
self.assertExists(os.path.join(repodir, 'easybuild', 'easyblocks', '__init__.py'))
+ self.mock_stdout(False)
def test_github_download_repo_commit(self):
"""Test downloading repo at specific commit (which does not require any GitHub token)"""
@@ -745,7 +729,8 @@ def test_github_find_easybuild_easyconfig(self):
if self.skip_github_tests:
print("Skipping test_find_easybuild_easyconfig, no GitHub token available?")
return
- path = gh.find_easybuild_easyconfig(github_user=GITHUB_TEST_ACCOUNT)
+ with self.mocked_stdout_stderr():
+ path = gh.find_easybuild_easyconfig(github_user=GITHUB_TEST_ACCOUNT)
expected = os.path.join('e', 'EasyBuild', r'EasyBuild-[1-9]+\.[0-9]+\.[0-9]+\.eb')
regex = re.compile(expected)
self.assertTrue(regex.search(path), "Pattern '%s' found in '%s'" % (regex.pattern, path))
@@ -773,15 +758,17 @@ def test_github_find_patches(self):
reg = re.compile(r'[1-9]+ of [1-9]+ easyconfigs checked')
self.assertTrue(re.search(reg, txt))
+ self.mock_stdout(True)
self.assertEqual(gh.find_software_name_for_patch('test.patch', []), None)
+ self.mock_stdout(False)
- # check behaviour of find_software_name_for_patch when non-UTF8 patch files are present (only with Python 3)
- if sys.version_info[0] >= 3:
- non_utf8_patch = os.path.join(self.test_prefix, 'problem.patch')
- with open(non_utf8_patch, 'wb') as fp:
- fp.write(bytes("+ ximage->byte_order=T1_byte_order; /* Set t1lib\xb4s byteorder */\n", 'iso_8859_1'))
+ non_utf8_patch = os.path.join(self.test_prefix, 'problem.patch')
+ with open(non_utf8_patch, 'wb') as fp:
+ fp.write(bytes("+ ximage->byte_order=T1_byte_order; /* Set t1lib\xb4s byteorder */\n", 'iso_8859_1'))
- self.assertEqual(gh.find_software_name_for_patch('test.patch', [self.test_prefix]), None)
+ self.mock_stdout(True)
+ self.assertEqual(gh.find_software_name_for_patch('test.patch', [self.test_prefix]), None)
+ self.mock_stdout(False)
def test_github_det_commit_status(self):
"""Test det_commit_status function."""
diff --git a/test/framework/hooks.py b/test/framework/hooks.py
index 5e810ac0f9..052df5ffdd 100644
--- a/test/framework/hooks.py
+++ b/test/framework/hooks.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2017-2024 Ghent University
+# Copyright 2017-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -29,6 +29,7 @@
"""
import os
import sys
+import textwrap
from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered, init_config
from unittest import TextTestRunner
@@ -79,6 +80,9 @@ def setUp(self):
'',
'def fail_hook(err):',
' print("EasyBuild FAIL: %s" % err)',
+ '',
+ 'def crash_hook(err):',
+ ' print("EasyBuild CRASHED, oh no! => %s" % err)',
])
write_file(self.test_hooks_pymod, test_hooks_pymod_txt)
@@ -97,8 +101,9 @@ def test_load_hooks(self):
hooks = load_hooks(self.test_hooks_pymod)
- self.assertEqual(len(hooks), 8)
+ self.assertEqual(len(hooks), 9)
expected = [
+ 'crash_hook',
'fail_hook',
'parse_hook',
'post_configure_hook',
@@ -140,6 +145,7 @@ def test_find_hook(self):
pre_single_extension_hook = [hooks[k] for k in hooks if k == 'pre_single_extension_hook'][0]
start_hook = [hooks[k] for k in hooks if k == 'start_hook'][0]
pre_run_shell_cmd_hook = [hooks[k] for k in hooks if k == 'pre_run_shell_cmd_hook'][0]
+ crash_hook = [hooks[k] for k in hooks if k == 'crash_hook'][0]
fail_hook = [hooks[k] for k in hooks if k == 'fail_hook'][0]
pre_build_and_install_loop_hook = [hooks[k] for k in hooks if k == 'pre_build_and_install_loop_hook'][0]
@@ -175,6 +181,10 @@ def test_find_hook(self):
self.assertEqual(find_hook('fail', hooks, pre_step_hook=True), None)
self.assertEqual(find_hook('fail', hooks, post_step_hook=True), None)
+ self.assertEqual(find_hook('crash', hooks), crash_hook)
+ self.assertEqual(find_hook('crash', hooks, pre_step_hook=True), None)
+ self.assertEqual(find_hook('crash', hooks, post_step_hook=True), None)
+
hook_name = 'build_and_install_loop'
self.assertEqual(find_hook(hook_name, hooks), None)
self.assertEqual(find_hook(hook_name, hooks, pre_step_hook=True), pre_build_and_install_loop_hook)
@@ -209,6 +219,7 @@ def run_hooks():
run_hook('single_extension', hooks, post_step_hook=True, args=[None])
run_hook('extensions', hooks, post_step_hook=True, args=[None])
run_hook('fail', hooks, args=[EasyBuildError('oops')])
+ run_hook('crash', hooks, args=[RuntimeError('boom!')])
stdout = self.get_stdout()
stderr = self.get_stderr()
self.mock_stdout(False)
@@ -244,6 +255,8 @@ def run_hooks():
"this is run before installing an extension",
"== Running fail hook...",
"EasyBuild FAIL: 'oops'",
+ "== Running crash hook...",
+ "EasyBuild CRASHED, oh no! => boom!",
]
expected_stdout = '\n'.join(expected_stdout_lines)
@@ -268,22 +281,24 @@ def test_verify_hooks(self):
self.assertEqual(verify_hooks(hooks), None)
test_broken_hooks_pymod = os.path.join(self.test_prefix, 'test_broken_hooks.py')
- test_hooks_txt = '\n'.join([
- '',
- 'def there_is_no_such_hook():',
- ' pass',
- 'def stat_hook(self):',
- ' pass',
- 'def post_source_hook(self):',
- ' pass',
- 'def install_hook(self):',
- ' pass',
- ])
+ test_hooks_txt = textwrap.dedent("""
+ def there_is_no_such_hook():
+ pass
+ def stat_hook(self):
+ pass
+ def post_source_hook(self):
+ pass
+ def post_extract_hook(self):
+ pass
+ def install_hook(self):
+ pass
+ """)
write_file(test_broken_hooks_pymod, test_hooks_txt)
error_msg_pattern = r"Found one or more unknown hooks:\n"
error_msg_pattern += r"\* install_hook \(did you mean 'pre_install_hook', or 'post_install_hook'\?\)\n"
+ error_msg_pattern += r"\* post_source_hook \(did you mean .*'\?\)\n"
error_msg_pattern += r"\* stat_hook \(did you mean 'start_hook'\?\)\n"
error_msg_pattern += r"\* there_is_no_such_hook\n\n"
error_msg_pattern += r"Run 'eb --avail-hooks' to get an overview of known hooks"
diff --git a/test/framework/include.py b/test/framework/include.py
index 09a5925a2f..ef20e682fd 100644
--- a/test/framework/include.py
+++ b/test/framework/include.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/lib.py b/test/framework/lib.py
index 7930e00567..ca95c0ab47 100644
--- a/test/framework/lib.py
+++ b/test/framework/lib.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2018-2024 Ghent University
+# Copyright 2018-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -43,7 +43,7 @@
from easybuild.tools.options import set_up_configuration
from easybuild.tools.filetools import mkdir
from easybuild.tools.modules import modules_tool
-from easybuild.tools.run import run_cmd
+from easybuild.tools.run import run_shell_cmd, run_cmd
class EasyBuildLibTest(TestCase):
@@ -67,31 +67,42 @@ def tearDown(self):
def configure(self):
"""Utility function to set up EasyBuild configuration."""
-
- # wipe BuildOption singleton instance, so it gets re-created when set_up_configuration is called
- if BuildOptions in BuildOptions._instances:
- del BuildOptions._instances[BuildOptions]
-
- self.assertNotIn(BuildOptions, BuildOptions._instances)
- set_up_configuration(silent=True)
- self.assertIn(BuildOptions, BuildOptions._instances)
+ set_up_configuration(silent=True, reconfigure=True)
def test_run_cmd(self):
"""Test use of run_cmd function in the context of using EasyBuild framework as a library."""
error_pattern = r"Undefined build option: .*"
error_pattern += r" Make sure you have set up the EasyBuild configuration using set_up_configuration\(\)"
- self.assertErrorRegex(EasyBuildError, error_pattern, run_cmd, "echo hello")
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, run_cmd, "echo hello")
self.configure()
# run_cmd works fine if set_up_configuration was called first
- (out, ec) = run_cmd("echo hello")
+ with self.mocked_stdout_stderr():
+ (out, ec) = run_cmd("echo hello")
self.assertEqual(ec, 0)
self.assertEqual(out, 'hello\n')
+ def test_run_shell_cmd(self):
+ """Test use of run_shell_cmd function in the context of using EasyBuild framework as a library."""
+
+ error_pattern = r"Undefined build option: .*"
+ error_pattern += r" Make sure you have set up the EasyBuild configuration using set_up_configuration\(\)"
+ self.assertErrorRegex(EasyBuildError, error_pattern, run_shell_cmd, "echo hello")
+
+ self.configure()
+
+ # runworks fine if set_up_configuration was called first
+ self.mock_stdout(True)
+ res = run_shell_cmd("echo hello")
+ self.mock_stdout(False)
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(res.output, 'hello\n')
+
def test_mkdir(self):
- """Test use of run_cmd function in the context of using EasyBuild framework as a library."""
+ """Test use of mkdir function in the context of using EasyBuild framework as a library."""
test_dir = os.path.join(self.tmpdir, 'test123')
diff --git a/test/framework/license.py b/test/framework/license.py
index 6dc79ac6a4..66e5938ab7 100644
--- a/test/framework/license.py
+++ b/test/framework/license.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -33,7 +33,6 @@
from unittest import TextTestRunner
from easybuild.framework.easyconfig.licenses import License, LicenseVeryRestrictive, what_licenses
-from easybuild.tools.py2vs3 import string_type
class LicenseTest(EnhancedTestCase):
@@ -62,7 +61,7 @@ def test_licenses(self):
"""Test format of available licenses."""
lics = what_licenses()
for lic in lics:
- self.assertIsInstance(lic, string_type)
+ self.assertIsInstance(lic, str)
self.assertTrue(lic.startswith('License'))
self.assertTrue(issubclass(lics[lic], License))
diff --git a/test/framework/module_generator.py b/test/framework/module_generator.py
index 075cc3adbd..c5fc23fe06 100644
--- a/test/framework/module_generator.py
+++ b/test/framework/module_generator.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -266,44 +266,57 @@ def test_load(self):
"""Test load part in generated module file."""
if self.MODULE_GENERATOR_CLASS == ModuleGeneratorTcl:
- # default: guarded module load (which implies no recursive unloading)
- expected = '\n'.join([
- '',
- "if { ![ is-loaded mod_name ] } {",
- " module load mod_name",
- "}",
- '',
- ])
+ if not self.modtool.supports_safe_auto_load:
+ # default: guarded module load (which implies no recursive unloading)
+ expected = '\n'.join([
+ '',
+ "if { ![ is-loaded mod_name ] } {",
+ " module load mod_name",
+ "}",
+ '',
+ ])
+ else:
+ expected = '\n'.join([
+ '',
+ "module load mod_name",
+ '',
+ ])
self.assertEqual(expected, self.modgen.load_module("mod_name"))
- # with recursive unloading: no if is-loaded guard
- expected = '\n'.join([
- '',
- "if { [ module-info mode remove ] || ![ is-loaded mod_name ] } {",
- " module load mod_name",
- "}",
- '',
- ])
+ if not self.modtool.supports_safe_auto_load:
+ # with recursive unloading: no if is-loaded guard
+ expected = '\n'.join([
+ '',
+ "if { [ module-info mode remove ] || ![ is-loaded mod_name ] } {",
+ " module load mod_name",
+ "}",
+ '',
+ ])
self.assertEqual(expected, self.modgen.load_module("mod_name", recursive_unload=True))
init_config(build_options={'recursive_mod_unload': True})
self.assertEqual(expected, self.modgen.load_module("mod_name"))
# Lmod 7.6+ depends-on
+
+ self.allow_deprecated_behaviour()
+
if self.modtool.supports_depends_on:
expected = '\n'.join([
'',
"depends-on mod_name",
'',
])
- self.assertEqual(expected, self.modgen.load_module("mod_name", depends_on=True))
+ with self.mocked_stdout_stderr():
+ txt = self.modgen.load_module("mod_name", depends_on=True)
+ self.assertEqual(expected, txt)
init_config(build_options={'mod_depends_on': 'True'})
self.assertEqual(expected, self.modgen.load_module("mod_name"))
else:
expected = "depends-on statements in generated module are not supported by modules tool"
- self.assertErrorRegex(EasyBuildError, expected, self.modgen.load_module, "mod_name", depends_on=True)
- init_config(build_options={'mod_depends_on': 'True'})
- self.assertErrorRegex(EasyBuildError, expected, self.modgen.load_module, "mod_name")
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, expected,
+ self.modgen.load_module, "mod_name", depends_on=True)
else:
# default: guarded module load (which implies no recursive unloading)
expected = '\n'.join([
@@ -330,20 +343,26 @@ def test_load(self):
self.assertEqual(expected, self.modgen.load_module("mod_name"))
# Lmod 7.6+ depends_on
+
+ self.allow_deprecated_behaviour()
+
if self.modtool.supports_depends_on:
expected = '\n'.join([
'',
'depends_on("mod_name")',
'',
])
- self.assertEqual(expected, self.modgen.load_module("mod_name", depends_on=True))
+ with self.mocked_stdout_stderr():
+ txt = self.modgen.load_module("mod_name", depends_on=True)
+
+ self.assertEqual(expected, txt)
init_config(build_options={'mod_depends_on': 'True'})
self.assertEqual(expected, self.modgen.load_module("mod_name"))
else:
expected = "depends_on statements in generated module are not supported by modules tool"
- self.assertErrorRegex(EasyBuildError, expected, self.modgen.load_module, "mod_name", depends_on=True)
- init_config(build_options={'mod_depends_on': 'True'})
- self.assertErrorRegex(EasyBuildError, expected, self.modgen.load_module, "mod_name")
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, expected,
+ self.modgen.load_module, "mod_name", depends_on=True)
def test_load_multi_deps(self):
"""Test generated load statement when multi_deps is involved."""
@@ -353,13 +372,24 @@ def test_load_multi_deps(self):
res = self.modgen.load_module('Python/3.7.4', multi_dep_mods=multi_dep_mods)
if self.MODULE_GENERATOR_CLASS == ModuleGeneratorTcl:
- expected = '\n'.join([
- '',
- "if { ![ is-loaded Python/3.7.4 ] && ![ is-loaded Python/2.7.16 ] } {",
- " module load Python/3.7.4",
- '}',
- '',
- ])
+ if not self.modtool.supports_safe_auto_load:
+ expected = '\n'.join([
+ '',
+ "if { ![ is-loaded Python/3.7.4 ] && ![ is-loaded Python/2.7.16 ] } {",
+ " module load Python/3.7.4",
+ '}',
+ '',
+ ])
+ else:
+ expected = '\n'.join([
+ '',
+ "if { [ module-info mode remove ] || [ is-loaded Python/2.7.16 ] } {",
+ " module load Python",
+ '} else {',
+ " module load Python/3.7.4",
+ '}',
+ '',
+ ])
else: # Lua syntax
expected = '\n'.join([
'',
@@ -371,8 +401,12 @@ def test_load_multi_deps(self):
self.assertEqual(expected, res)
if self.modtool.supports_depends_on:
+
+ self.allow_deprecated_behaviour()
+
# two versions with depends_on
- res = self.modgen.load_module('Python/3.7.4', multi_dep_mods=multi_dep_mods, depends_on=True)
+ with self.mocked_stdout_stderr():
+ res = self.modgen.load_module('Python/3.7.4', multi_dep_mods=multi_dep_mods, depends_on=True)
if self.MODULE_GENERATOR_CLASS == ModuleGeneratorTcl:
expected = '\n'.join([
@@ -396,19 +430,33 @@ def test_load_multi_deps(self):
])
self.assertEqual(expected, res)
+ self.disallow_deprecated_behaviour()
+
# now test with more than two versions...
multi_dep_mods = ['foo/1.2.3', 'foo/2.3.4', 'foo/3.4.5', 'foo/4.5.6']
res = self.modgen.load_module('foo/1.2.3', multi_dep_mods=multi_dep_mods)
if self.MODULE_GENERATOR_CLASS == ModuleGeneratorTcl:
- expected = '\n'.join([
- '',
- "if { ![ is-loaded foo/1.2.3 ] && ![ is-loaded foo/2.3.4 ] && " +
- "![ is-loaded foo/3.4.5 ] && ![ is-loaded foo/4.5.6 ] } {",
- " module load foo/1.2.3",
- '}',
- '',
- ])
+ if not self.modtool.supports_safe_auto_load:
+ expected = '\n'.join([
+ '',
+ "if { ![ is-loaded foo/1.2.3 ] && ![ is-loaded foo/2.3.4 ] && " +
+ "![ is-loaded foo/3.4.5 ] && ![ is-loaded foo/4.5.6 ] } {",
+ " module load foo/1.2.3",
+ '}',
+ '',
+ ])
+ else:
+ expected = '\n'.join([
+ '',
+ "if { [ module-info mode remove ] || [ is-loaded foo/2.3.4 ] || [ is-loaded foo/3.4.5 ] " +
+ "|| [ is-loaded foo/4.5.6 ] } {",
+ " module load foo",
+ "} else {",
+ " module load foo/1.2.3",
+ '}',
+ '',
+ ])
else: # Lua syntax
expected = '\n'.join([
'',
@@ -421,8 +469,12 @@ def test_load_multi_deps(self):
self.assertEqual(expected, res)
if self.modtool.supports_depends_on:
+
+ self.allow_deprecated_behaviour()
+
# more than two versions, with depends_on
- res = self.modgen.load_module('foo/1.2.3', multi_dep_mods=multi_dep_mods, depends_on=True)
+ with self.mocked_stdout_stderr():
+ res = self.modgen.load_module('foo/1.2.3', multi_dep_mods=multi_dep_mods, depends_on=True)
if self.MODULE_GENERATOR_CLASS == ModuleGeneratorTcl:
expected = '\n'.join([
@@ -448,18 +500,23 @@ def test_load_multi_deps(self):
])
self.assertEqual(expected, res)
+ self.disallow_deprecated_behaviour()
+
# what if we only list a single version?
# see https://github.com/easybuilders/easybuild-framework/issues/3080
res = self.modgen.load_module('one/1.0', multi_dep_mods=['one/1.0'])
if self.MODULE_GENERATOR_CLASS == ModuleGeneratorTcl:
- expected = '\n'.join([
- '',
- "if { ![ is-loaded one/1.0 ] } {",
- " module load one/1.0",
- '}',
- '',
- ])
+ if not self.modtool.supports_safe_auto_load:
+ expected = '\n'.join([
+ '',
+ "if { ![ is-loaded one/1.0 ] } {",
+ " module load one/1.0",
+ '}',
+ '',
+ ])
+ else:
+ expected = '\nmodule load one/1.0\n'
else: # Lua syntax
expected = '\n'.join([
'',
@@ -471,7 +528,11 @@ def test_load_multi_deps(self):
self.assertEqual(expected, res)
if self.modtool.supports_depends_on:
- res = self.modgen.load_module('one/1.0', multi_dep_mods=['one/1.0'], depends_on=True)
+
+ self.allow_deprecated_behaviour()
+
+ with self.mocked_stdout_stderr():
+ res = self.modgen.load_module('one/1.0', multi_dep_mods=['one/1.0'], depends_on=True)
if self.MODULE_GENERATOR_CLASS == ModuleGeneratorTcl:
expected = '\ndepends-on one/1.0\n'
@@ -528,10 +589,9 @@ def test_modulerc(self):
# loading of module with symbolic version works
self.modtool.load(['test/1.2.3'])
- # test/1.2.3.4.5 is actually loaded (rather than test/1.2.3)
+ # test/1.2.3.4.5 is actually loaded
res = self.modtool.list()
- self.assertEqual(len(res), 1)
- self.assertEqual(res[0]['mod_name'], 'test/1.2.3.4.5')
+ self.assertTrue(any(x['mod_name'] == 'test/1.2.3.4.5' for x in res))
# if same symbolic version is added again, nothing changes
self.modgen.modulerc(mod_ver_spec, filepath=modulerc_path)
@@ -650,7 +710,7 @@ def test_swap(self):
# create tiny test Tcl module to make sure that tested modules tools support single-argument swap
# see https://github.com/easybuilders/easybuild-framework/issues/3396;
- # this is known to fail with the ancient Tcl-only implementation of environment modules,
+ # this is known to fail with the ancient Tcl-only implementation of Environment Modules,
# but that's considered to be a non-issue (since this is mostly relevant for Cray systems,
# which are either using EnvironmentModulesC (3.2.10), EnvironmentModules (4.x) or Lmod...
if self.MODULE_GENERATOR_CLASS == ModuleGeneratorTcl and self.modtool.__class__ != EnvironmentModulesTcl:
@@ -721,13 +781,16 @@ def append_paths(*args, **kwargs):
# check for warning that is printed when same path is added multiple times
with self.modgen.start_module_creation():
self.modgen.append_paths('TEST', 'path1')
- self.mock_stderr(True)
- self.modgen.append_paths('TEST', 'path1')
- stderr = self.get_stderr()
- self.mock_stderr(False)
+ with self.mocked_stdout_stderr():
+ self.modgen.append_paths('TEST', 'path1')
+ stderr = self.get_stderr()
expected_warning = "\nWARNING: Suppressed adding the following path(s) to $TEST of the module "
expected_warning += "as they were already added: path1\n\n"
self.assertEqual(stderr, expected_warning)
+ with self.mocked_stdout_stderr():
+ self.modgen.append_paths('TEST', 'path1', warn_exists=False)
+ stderr = self.get_stderr()
+ self.assertEqual(stderr, '')
def test_module_extensions(self):
"""test the extensions() for extensions"""
@@ -821,14 +884,18 @@ def prepend_paths(*args, **kwargs):
# check for warning that is printed when same path is added multiple times
with self.modgen.start_module_creation():
self.modgen.prepend_paths('TEST', 'path1')
- self.mock_stderr(True)
- self.modgen.prepend_paths('TEST', 'path1')
- stderr = self.get_stderr()
- self.mock_stderr(False)
+ with self.mocked_stdout_stderr():
+ self.modgen.prepend_paths('TEST', 'path1')
+ stderr = self.get_stderr()
expected_warning = "\nWARNING: Suppressed adding the following path(s) to $TEST of the module "
expected_warning += "as they were already added: path1\n\n"
self.assertEqual(stderr, expected_warning)
+ with self.mocked_stdout_stderr():
+ self.modgen.prepend_paths('TEST', 'path1', warn_exists=False)
+ stderr = self.get_stderr()
+ self.assertEqual(stderr, '')
+
def test_det_user_modpath(self):
"""Test for generic det_user_modpath method."""
# None by default
@@ -844,7 +911,10 @@ def test_det_user_modpath(self):
init_config(build_options={'suffix_modules_path': ''})
user_modpath = 'my/{RUNTIME_ENV::TEST123}/modules'
if self.MODULE_GENERATOR_CLASS == ModuleGeneratorTcl:
- self.assertEqual(self.modgen.det_user_modpath(user_modpath), '"my" $::env(TEST123) "modules"')
+ if self.modtool.supports_tcl_getenv:
+ self.assertEqual(self.modgen.det_user_modpath(user_modpath), '"my" [getenv TEST123] "modules"')
+ else:
+ self.assertEqual(self.modgen.det_user_modpath(user_modpath), '"my" $::env(TEST123) "modules"')
else:
self.assertEqual(self.modgen.det_user_modpath(user_modpath), '"my", os.getenv("TEST123"), "modules"')
@@ -902,12 +972,20 @@ def test_getenv_cmd(self):
# otherwise we won't get the output produced by the test module file...
os.environ.pop('LMOD_QUIET', None)
- self.assertEqual('$::env(HOSTNAME)', self.modgen.getenv_cmd('HOSTNAME'))
- self.assertEqual('$::env(HOME)', self.modgen.getenv_cmd('HOME'))
+ if self.modtool.supports_tcl_getenv:
+ self.assertEqual('[getenv HOSTNAME]', self.modgen.getenv_cmd('HOSTNAME'))
+ self.assertEqual('[getenv HOME]', self.modgen.getenv_cmd('HOME'))
- expected = '[if { [info exists ::env(TEST)] } { concat $::env(TEST) } else { concat "foobar" } ]'
- getenv_txt = self.modgen.getenv_cmd('TEST', default='foobar')
- self.assertEqual(getenv_txt, expected)
+ expected = '[getenv TEST "foobar"]'
+ getenv_txt = self.modgen.getenv_cmd('TEST', default='foobar')
+ self.assertEqual(getenv_txt, expected)
+ else:
+ self.assertEqual('$::env(HOSTNAME)', self.modgen.getenv_cmd('HOSTNAME'))
+ self.assertEqual('$::env(HOME)', self.modgen.getenv_cmd('HOME'))
+
+ expected = '[if { [info exists ::env(TEST)] } { concat $::env(TEST) } else { concat "foobar" } ]'
+ getenv_txt = self.modgen.getenv_cmd('TEST', default='foobar')
+ self.assertEqual(getenv_txt, expected)
write_file(test_mod_file, '#%%Module\nputs stderr %s' % getenv_txt)
else:
@@ -1597,6 +1675,28 @@ def test_generated_module_file_swap(self):
# one/1.0 module was swapped for one/1.1
self.assertEqual(loaded_mods[-2]['mod_name'], 'one/1.1')
+ def test_check_group(self):
+ """Test check_group method."""
+ if self.MODULE_GENERATOR_CLASS == ModuleGeneratorTcl:
+ if self.modtool.supports_tcl_check_group:
+ expected = '\n'.join([
+ "if { ![ module-info usergroups group_name ] } {",
+ " error \"mesg\"",
+ "}",
+ '',
+ ])
+ self.assertEqual(expected, self.modgen.check_group("group_name", error_msg="mesg"))
+ else:
+ self.assertEqual('', self.modgen.check_group("group_name", error_msg="mesg"))
+ else:
+ expected = '\n'.join([
+ 'if not ( userInGroup("group_name") ) then',
+ ' LmodError("mesg")',
+ 'end',
+ '',
+ ])
+ self.assertEqual(expected, self.modgen.check_group("group_name", error_msg="mesg"))
+
class TclModuleGeneratorTest(ModuleGeneratorTest):
"""Test for module_generator module for Tcl syntax."""
diff --git a/test/framework/modules.py b/test/framework/modules.py
index 8ba4f4ac65..6080366fa6 100644
--- a/test/framework/modules.py
+++ b/test/framework/modules.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -42,7 +42,7 @@
import easybuild.tools.modules as mod
from easybuild.framework.easyblock import EasyBlock
from easybuild.framework.easyconfig.easyconfig import EasyConfig
-from easybuild.tools import StrictVersion
+from easybuild.tools import LooseVersion
from easybuild.tools.build_log import EasyBuildError
from easybuild.tools.environment import modify_env
from easybuild.tools.filetools import adjust_permissions, copy_file, copy_dir, mkdir
@@ -50,7 +50,7 @@
from easybuild.tools.modules import EnvironmentModules, EnvironmentModulesC, EnvironmentModulesTcl, Lmod, NoModulesTool
from easybuild.tools.modules import curr_module_paths, get_software_libdir, get_software_root, get_software_version
from easybuild.tools.modules import invalidate_module_caches_for, modules_tool, reset_module_caches
-from easybuild.tools.run import run_cmd
+from easybuild.tools.run import run_shell_cmd
from easybuild.tools.systemtools import get_shared_lib_ext
@@ -122,10 +122,12 @@ def test_run_module(self):
error_pattern = "Module command '.*thisdoesnotmakesense' failed with exit code [1-9]"
self.assertErrorRegex(EasyBuildError, error_pattern, self.modtool.run_module, 'thisdoesnotmakesense')
- # we need to use a different error pattern here with EnvironmentModulesC,
- # because a load of a non-existing module doesnt' trigger a non-zero exit code...
- # it will still fail though, just differently
- if isinstance(self.modtool, EnvironmentModulesC):
+ # we need to use a different error pattern here with EnvironmentModulesC and
+ # EnvironmentModules <5.5, because a load of a non-existing module doesnt' trigger a
+ # non-zero exit code. it will still fail though, just differently
+ version = LooseVersion(self.modtool.version)
+ if (isinstance(self.modtool, EnvironmentModulesC)
+ or (isinstance(self.modtool, EnvironmentModules) and version < '5.5')):
error_pattern = "Unable to locate a modulefile for 'nosuchmodule/1.2.3'"
else:
error_pattern = "Module command '.*load nosuchmodule/1.2.3' failed with exit code [1-9]"
@@ -191,6 +193,21 @@ def test_run_module(self):
regex = re.compile(r'^os\.environ\[', re.M)
self.assertFalse(regex.search(out), "Pattern '%s' should not be found in: %s" % (regex.pattern, out))
+ def test_list(self):
+ """
+ Test running 'module list' via ModulesTool instance.
+ """
+ # make very sure no modules are currently loaded
+ self.modtool.run_module('purge', '--force')
+
+ out = self.modtool.list()
+ self.assertEqual(out, [])
+
+ mods = ['GCC/7.3.0-2.30']
+ self.modtool.load(mods)
+ out = self.modtool.list()
+ self.assertEqual([x['mod_name'] for x in out], mods)
+
def test_avail(self):
"""Test if getting a (restricted) list of available modules works."""
self.init_testmods()
@@ -198,10 +215,8 @@ def test_avail(self):
# test modules include 3 GCC modules and one GCCcore module
ms = self.modtool.available('GCC')
expected = ['GCC/12.3.0', 'GCC/4.6.3', 'GCC/4.6.4', 'GCC/6.4.0-2.28', 'GCC/7.3.0-2.30']
- # Tcl-only modules tool does an exact match on module name, Lmod & Tcl/C do prefix matching
- # EnvironmentModules is a subclass of EnvironmentModulesTcl, but Modules 4+ behaves similarly to Tcl/C impl.,
- # so also append GCCcore/6.2.0 if we are an instance of EnvironmentModules
- if not isinstance(self.modtool, EnvironmentModulesTcl) or isinstance(self.modtool, EnvironmentModules):
+ # ancient Tcl-only Environment Modules tool does an exact match on module name, others do prefix matching
+ if not isinstance(self.modtool, EnvironmentModulesTcl):
expected.extend(['GCCcore/12.3.0', 'GCCcore/6.2.0'])
self.assertEqual(ms, expected)
@@ -211,15 +226,16 @@ def test_avail(self):
# all test modules are accounted for
ms = self.modtool.available()
+ version = LooseVersion(self.modtool.version)
- if isinstance(self.modtool, Lmod) and StrictVersion(self.modtool.version) >= StrictVersion('5.7.5'):
+ if isinstance(self.modtool, Lmod) and version >= '5.7.5' and not version.is_prerelease('5.7.5', ['rc']):
# with recent versions of Lmod, also the hidden modules are included in the output of 'avail'
self.assertEqual(len(ms), TEST_MODULES_COUNT + 3)
self.assertIn('bzip2/.1.0.6', ms)
self.assertIn('toy/.0.0-deps', ms)
self.assertIn('OpenMPI/.2.1.2-GCC-6.4.0-2.28', ms)
elif (isinstance(self.modtool, EnvironmentModules)
- and StrictVersion(self.modtool.version) >= StrictVersion('4.6.0')):
+ and version >= '4.6.0' and not version.is_prerelease('4.6.0', ['-beta'])):
# bzip2/.1.0.6 is not there, since that's a module file in Lua syntax
self.assertEqual(len(ms), TEST_MODULES_COUNT + 2)
self.assertIn('toy/.0.0-deps', ms)
@@ -299,7 +315,8 @@ def test_exist(self):
avail_mods = self.modtool.available()
self.assertIn('Java/1.8.0_181', avail_mods)
- if isinstance(self.modtool, Lmod) and StrictVersion(self.modtool.version) >= StrictVersion('7.0'):
+ version = LooseVersion(self.modtool.version)
+ if isinstance(self.modtool, Lmod) and version >= '7.0' and not version.is_prerelease('7.0', ['rc']):
self.assertIn('Java/1.8', avail_mods)
self.assertIn('Java/site_default', avail_mods)
self.assertIn('JavaAlias', avail_mods)
@@ -326,12 +343,12 @@ def test_exist(self):
easybuild.tools.modules.MODULE_SHOW_CACHE.clear()
self.assertEqual(self.modtool.exist(['Java/1.8', 'Java/1.8.0_181']), [True, True])
- # mimic more verbose stderr output produced by old Tmod version,
- # including a warning produced when multiple .modulerc files are being picked up
+ # mimic "module-*" output produced by EnvironmentModulesC or EnvironmentModulesTcl
+ # mimic warning produced by Environment Modules when a symbol is defined multiple times
# see https://github.com/easybuilders/easybuild-framework/issues/3376
ml_show_java18_stderr = '\n'.join([
"module-version Java/1.8.0_181 1.8",
- "WARNING: Duplicate version symbol '1.8' found",
+ "WARNING: Symbolic version 'Java/1.8' already defined",
"module-version Java/1.8.0_181 1.8",
"-------------------------------------------------------------------",
"/modulefiles/lang/Java/1.8.0_181:",
@@ -359,7 +376,7 @@ def test_exist(self):
self.assertEqual(self.modtool.exist(['Core/Java/1.8', 'Core/Java/site_default']), [True, True])
# also check with .modulerc.lua for Lmod 7.8 or newer
- if isinstance(self.modtool, Lmod) and StrictVersion(self.modtool.version) >= StrictVersion('7.8'):
+ if isinstance(self.modtool, Lmod) and version >= '7.8' and not version.is_prerelease('7.8', ['rc']):
shutil.move(os.path.join(self.test_prefix, 'Core', 'Java'), java_mod_dir)
reset_module_caches()
@@ -391,7 +408,7 @@ def test_exist(self):
self.assertEqual(self.modtool.exist(['Core/Java/site_default']), [True])
# Test alias in home directory .modulerc
- if isinstance(self.modtool, Lmod) and StrictVersion(self.modtool.version) >= StrictVersion('7.0'):
+ if isinstance(self.modtool, Lmod) and version >= '7.0' and not version.is_prerelease('7.0', ['rc']):
# Required or temporary HOME would be in MODULEPATH already
self.init_testmods()
# Sanity check: Module aliases don't exist yet
@@ -443,7 +460,7 @@ def test_load(self):
# if GCC is loaded again, $EBROOTGCC should be set again, and GCC should be listed last
self.modtool.load(['GCC/6.4.0-2.28'])
- # environment modules v4+ does not reload already loaded modules
+ # Environment Modules v4+ does not reload already loaded modules
if not isinstance(self.modtool, EnvironmentModules):
self.assertTrue(os.environ.get('EBROOTGCC'))
@@ -671,6 +688,7 @@ def test_get_software_root_version_libdir(self):
self.assertEqual(get_software_root(name), root)
self.assertEqual(get_software_version(name), version)
self.assertEqual(get_software_libdir(name), 'lib')
+ self.assertEqual(get_software_libdir(name, full_path=True), os.path.join(root, 'lib'))
os.environ.pop('EBROOT%s' % env_var_name)
os.environ.pop('EBVERSION%s' % env_var_name)
@@ -679,30 +697,39 @@ def test_get_software_root_version_libdir(self):
root = os.path.join(tmpdir, name)
mkdir(os.path.join(root, 'lib64'))
os.environ['EBROOT%s' % env_var_name] = root
+
+ def check_get_software_libdir(expected, **additional_args):
+ self.assertEqual(get_software_libdir(name, **additional_args), expected)
+ if isinstance(expected, list):
+ expected = [os.path.join(root, d) for d in expected]
+ elif expected:
+ expected = os.path.join(root, expected)
+ self.assertEqual(get_software_libdir(name, full_path=True, **additional_args), expected)
+
write_file(os.path.join(root, 'lib', 'libfoo.a'), 'foo')
- self.assertEqual(get_software_libdir(name), 'lib')
+ check_get_software_libdir('lib')
remove_file(os.path.join(root, 'lib', 'libfoo.a'))
# also check vice versa with *shared* library in lib64
shlib_ext = get_shared_lib_ext()
write_file(os.path.join(root, 'lib64', 'libfoo.' + shlib_ext), 'foo')
- self.assertEqual(get_software_libdir(name), 'lib64')
+ check_get_software_libdir('lib64')
remove_file(os.path.join(root, 'lib64', 'libfoo.' + shlib_ext))
# check expected result of get_software_libdir with multiple lib subdirs
self.assertErrorRegex(EasyBuildError, "Multiple library subdirectories found.*", get_software_libdir, name)
- self.assertEqual(get_software_libdir(name, only_one=False), ['lib', 'lib64'])
+ check_get_software_libdir(only_one=False, expected=['lib', 'lib64'])
# only directories containing files in specified list should be retained
write_file(os.path.join(root, 'lib64', 'foo'), 'foo')
- self.assertEqual(get_software_libdir(name, fs=['foo']), 'lib64')
+ check_get_software_libdir(fs=['foo'], expected='lib64')
# duplicate paths due to symlink get filtered
remove_dir(os.path.join(root, 'lib64'))
symlink(os.path.join(root, 'lib'), os.path.join(root, 'lib64'))
- self.assertEqual(get_software_libdir(name), 'lib')
+ check_get_software_libdir('lib')
# same goes for lib symlinked to lib64
remove_file(os.path.join(root, 'lib64'))
@@ -710,19 +737,20 @@ def test_get_software_root_version_libdir(self):
mkdir(os.path.join(root, 'lib64'))
symlink(os.path.join(root, 'lib64'), os.path.join(root, 'lib'))
# still returns 'lib' because that's the first subdir considered
- self.assertEqual(get_software_libdir(name), 'lib')
+ check_get_software_libdir('lib')
# clean up for previous tests
os.environ.pop('EBROOT%s' % env_var_name)
# if root/version for specified software package can not be found, these functions should return None
- self.assertEqual(get_software_root('foo'), None)
- self.assertEqual(get_software_version('foo'), None)
- self.assertEqual(get_software_libdir('foo'), None)
+ self.assertEqual(get_software_root(name), None)
+ self.assertEqual(get_software_version(name), None)
+ check_get_software_libdir(None)
# if no library subdir is found, get_software_libdir should return None
os.environ['EBROOTFOO'] = tmpdir
self.assertEqual(get_software_libdir('foo'), None)
+ self.assertEqual(get_software_libdir('foo', full_path=True), None)
os.environ.pop('EBROOTFOO')
shutil.rmtree(tmpdir)
@@ -1331,9 +1359,10 @@ def test_module_use_bash(self):
modulepath = os.environ['MODULEPATH']
self.assertIn(modules_dir, modulepath)
- out, _ = run_cmd("bash -c 'echo MODULEPATH: $MODULEPATH'", simple=False)
- self.assertEqual(out.strip(), "MODULEPATH: %s" % modulepath)
- self.assertIn(modules_dir, out)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd("bash -c 'echo MODULEPATH: $MODULEPATH'")
+ self.assertEqual(res.output.strip(), f"MODULEPATH: {modulepath}")
+ self.assertIn(modules_dir, res.output)
def test_load_in_hierarchy(self):
"""Test whether loading a module in a module hierarchy results in loading the correct module."""
@@ -1395,7 +1424,7 @@ def test_exit_code_check(self):
if isinstance(self.modtool, Lmod):
error_pattern = "Module command '.*load nosuchmoduleavailableanywhere' failed with exit code"
else:
- # Tcl implementations exit with 0 even when a non-existing module is loaded...
+ # Environment Modules exits with 0 even when a non-existing module is loaded...
error_pattern = "Unable to locate a modulefile for 'nosuchmoduleavailableanywhere'"
self.assertErrorRegex(EasyBuildError, error_pattern, self.modtool.load, ['nosuchmoduleavailableanywhere'])
@@ -1537,8 +1566,10 @@ def test_modulecmd_strip_source(self):
os.environ['PATH'] = '%s:%s' % (self.test_prefix, os.getenv('PATH'))
- modtool = EnvironmentModulesC()
- modtool.run_module('load', 'test123')
+ self.allow_deprecated_behaviour()
+ with self.mocked_stdout_stderr():
+ modtool = EnvironmentModulesC()
+ modtool.run_module('load', 'test123')
self.assertEqual(os.getenv('TEST123'), 'test123')
def test_get_setenv_value_from_modulefile(self):
@@ -1554,7 +1585,8 @@ def test_get_setenv_value_from_modulefile(self):
write_file(test_ec, "\nmodextravars = {'FOO': 'value with spaces'}", append=True)
toy_eb = EasyBlock(EasyConfig(test_ec))
- toy_eb.make_module_step()
+ with self.mocked_stdout_stderr():
+ toy_eb.make_module_step()
expected_root = os.path.join(self.test_installpath, 'software', 'toy', '0.0')
ebroot = self.modtool.get_setenv_value_from_modulefile('toy/0.0', 'EBROOTTOY')
@@ -1569,6 +1601,247 @@ def test_get_setenv_value_from_modulefile(self):
res = self.modtool.get_setenv_value_from_modulefile('toy/0.0', 'NO_SUCH_VARIABLE_SET')
self.assertEqual(res, None)
+ def test_module_environment_variable(self):
+ """Test for ModuleEnvironmentVariable object"""
+ test_paths = ['lib', 'lib64']
+ mod_envar = mod.ModuleEnvironmentVariable(test_paths)
+ self.assertTrue(hasattr(mod_envar, 'contents'))
+ self.assertTrue(hasattr(mod_envar, 'type'))
+ self.assertTrue(hasattr(mod_envar, 'delimiter'))
+ self.assertEqual(mod_envar.contents, test_paths)
+ self.assertEqual(repr(mod_envar), repr(test_paths))
+ self.assertEqual(str(mod_envar), 'lib:lib64')
+
+ mod_envar_custom_delim = mod.ModuleEnvironmentVariable(test_paths, delimiter='|')
+ self.assertEqual(mod_envar_custom_delim.contents, test_paths)
+ self.assertEqual(repr(mod_envar_custom_delim), repr(test_paths))
+ self.assertEqual(str(mod_envar_custom_delim), 'lib|lib64')
+
+ mod_envar_custom_type = mod.ModuleEnvironmentVariable(test_paths, var_type='STRING')
+ self.assertEqual(mod_envar_custom_type.contents, test_paths)
+ self.assertEqual(mod_envar_custom_type.type, mod.ModEnvVarType.STRING)
+ self.assertEqual(mod_envar_custom_type.is_path, False)
+ mod_envar_custom_type.type = 'PATH'
+ self.assertEqual(mod_envar_custom_type.type, mod.ModEnvVarType.PATH)
+ self.assertEqual(mod_envar_custom_type.is_path, True)
+
+ mod_envar_custom_type.type = mod.ModEnvVarType.PATH
+ self.assertEqual(mod_envar_custom_type.is_path, True)
+
+ mod_envar_custom_type.type = 'PATH_WITH_FILES'
+ self.assertEqual(mod_envar_custom_type.type, mod.ModEnvVarType.PATH_WITH_FILES)
+ self.assertEqual(mod_envar_custom_type.is_path, True)
+
+ mod_envar_custom_type.type = mod.ModEnvVarType.PATH_WITH_FILES
+ self.assertEqual(mod_envar_custom_type.is_path, True)
+
+ mod_envar_custom_type.type = 'PATH_WITH_TOP_FILES'
+ self.assertEqual(mod_envar_custom_type.type, mod.ModEnvVarType.PATH_WITH_TOP_FILES)
+ self.assertEqual(mod_envar_custom_type.is_path, True)
+
+ mod_envar_custom_type.type = mod.ModEnvVarType.PATH_WITH_TOP_FILES
+ self.assertEqual(mod_envar_custom_type.is_path, True)
+
+ self.assertRaises(EasyBuildError, setattr, mod_envar_custom_type, 'type', 'NONEXISTENT')
+ self.assertRaises(EasyBuildError, mod.ModuleEnvironmentVariable, test_paths, var_type='NONEXISTENT')
+
+ mod_envar.contents = []
+ self.assertEqual(mod_envar.contents, [])
+ self.assertRaises(TypeError, setattr, mod_envar, 'contents', None)
+ mod_envar.contents = (1, 3, 2, 3)
+ self.assertEqual(mod_envar.contents, ['1', '3', '2'])
+ mod_envar.contents = 'include'
+ self.assertEqual(mod_envar.contents, ['include'])
+
+ mod_envar.append('share')
+ self.assertEqual(mod_envar.contents, ['include', 'share'])
+ mod_envar.append('share')
+ self.assertEqual(mod_envar.contents, ['include', 'share'])
+ self.assertRaises(TypeError, mod_envar.append, 'arg1', 'arg2')
+
+ mod_envar.extend(test_paths)
+ self.assertEqual(mod_envar.contents, ['include', 'share', 'lib', 'lib64'])
+ mod_envar.extend(test_paths)
+ self.assertEqual(mod_envar.contents, ['include', 'share', 'lib', 'lib64'])
+ mod_envar.extend(test_paths + ['lib128'])
+ self.assertEqual(mod_envar.contents, ['include', 'share', 'lib', 'lib64', 'lib128'])
+ self.assertRaises(TypeError, mod_envar.append, ['list1'], ['list2'])
+
+ mod_envar.remove('lib128')
+ self.assertEqual(mod_envar.contents, ['include', 'share', 'lib', 'lib64'])
+ mod_envar.remove('nonexistent')
+ self.assertEqual(mod_envar.contents, ['include', 'share', 'lib', 'lib64'])
+ self.assertRaises(TypeError, mod_envar.remove, 'arg1', 'arg2')
+
+ mod_envar.prepend('bin')
+ self.assertEqual(mod_envar.contents, ['bin', 'include', 'share', 'lib', 'lib64'])
+
+ mod_envar.update('new_path')
+ self.assertEqual(mod_envar.contents, ['new_path'])
+ mod_envar.update(['new_path_1', 'new_path_2'])
+ self.assertEqual(mod_envar.contents, ['new_path_1', 'new_path_2'])
+ self.assertRaises(TypeError, mod_envar.update, 'arg1', 'arg2')
+
+ def test_module_load_environment(self):
+ """Test for ModuleLoadEnvironment object"""
+ mod_load_env = mod.ModuleLoadEnvironment()
+
+ # test setting attributes
+ test_contents = ['lib', 'lib64']
+ mod_load_env.TEST_VAR = test_contents
+ self.assertTrue(hasattr(mod_load_env, 'TEST_VAR'))
+ self.assertEqual(mod_load_env.TEST_VAR.contents, test_contents)
+
+ error_pattern = "Name of ModuleLoadEnvironment attribute does not conform to shell naming rules.*'test_lower'"
+ self.assertErrorRegex(EasyBuildError, error_pattern, setattr, mod_load_env, 'test_lower', test_contents)
+
+ mod_load_env.TEST_STR = 'some/path'
+ self.assertTrue(hasattr(mod_load_env, 'TEST_STR'))
+ self.assertEqual(mod_load_env.TEST_STR.contents, ['some/path'])
+
+ mod_load_env.TEST_VARTYPE = {'contents': test_contents, 'var_type': "STRING"}
+ self.assertTrue(hasattr(mod_load_env, 'TEST_VARTYPE'))
+ self.assertEqual(mod_load_env.TEST_VARTYPE.contents, test_contents)
+ self.assertEqual(mod_load_env.TEST_VARTYPE.type, mod.ModEnvVarType.STRING)
+
+ mod_load_env.TEST_VARTYPE.type = "PATH"
+ self.assertEqual(mod_load_env.TEST_VARTYPE.type, mod.ModEnvVarType.PATH)
+ env_wrong_params = {'contents': test_contents, 'unkown_param': True}
+ self.assertRaises(EasyBuildError, setattr, mod_load_env, 'TEST_UNKNONW', env_wrong_params)
+
+ mod_load_env._UNDERSCORE_VAR = test_contents
+ self.assertTrue(hasattr(mod_load_env, '_UNDERSCORE_VAR'))
+ self.assertEqual(mod_load_env._UNDERSCORE_VAR.contents, test_contents)
+
+ mod_load_env.__DOUBLE_UNDERSCORE_VAR = test_contents
+ self.assertTrue(hasattr(mod_load_env, '__DOUBLE_UNDERSCORE_VAR'))
+ self.assertEqual(mod_load_env.__DOUBLE_UNDERSCORE_VAR.contents, test_contents)
+
+ mod_load_env.___TRIPLE__UNDERSCORE_VAR = test_contents
+ self.assertTrue(hasattr(mod_load_env, '___TRIPLE__UNDERSCORE_VAR'))
+ self.assertEqual(mod_load_env.___TRIPLE__UNDERSCORE_VAR.contents, test_contents)
+
+ # test retrieval of environment
+ # use copy of public attributes as reference
+ ref_load_env = mod_load_env._env_vars.copy()
+ self.assertCountEqual(list(mod_load_env), ref_load_env.keys())
+
+ ref_load_env_item_list = list(ref_load_env.items())
+ self.assertCountEqual(list(mod_load_env.items()), ref_load_env_item_list)
+
+ ref_load_env_item_list = dict(ref_load_env.items())
+ self.assertCountEqual(mod_load_env.as_dict, ref_load_env_item_list)
+
+ ref_load_env_environ = {key: str(value) for key, value in ref_load_env.items()}
+ self.assertDictEqual(mod_load_env.environ, ref_load_env_environ)
+
+ # test updating environment
+ new_test_env = {
+ 'TEST_VARTYPE': 'replaced_path',
+ 'TEST_NEW_VAR': ['new_path1', 'new_path2'],
+ }
+ mod_load_env.update(new_test_env)
+ self.assertTrue(hasattr(mod_load_env, 'TEST_VARTYPE'))
+ self.assertEqual(mod_load_env.TEST_VARTYPE.contents, ['replaced_path'])
+ self.assertEqual(mod_load_env.TEST_VARTYPE.type, mod.ModEnvVarType.PATH_WITH_FILES)
+ self.assertTrue(hasattr(mod_load_env, 'TEST_NEW_VAR'))
+ self.assertEqual(mod_load_env.TEST_NEW_VAR.contents, ['new_path1', 'new_path2'])
+ self.assertEqual(mod_load_env.TEST_NEW_VAR.type, mod.ModEnvVarType.PATH_WITH_FILES)
+
+ # check that previous variables still exist
+ self.assertTrue(hasattr(mod_load_env, 'TEST_VAR'))
+ self.assertEqual(mod_load_env.TEST_VAR.contents, test_contents)
+ self.assertTrue(hasattr(mod_load_env, 'TEST_STR'))
+ self.assertEqual(mod_load_env.TEST_STR.contents, ['some/path'])
+
+ # test removal of envars
+ mod_load_env.REMOVABLE_VAR = test_contents
+ self.assertTrue('REMOVABLE_VAR' in mod_load_env.vars)
+ mod_load_env.remove('REMOVABLE_VAR')
+ self.assertFalse('REMOVABLE_VAR' in mod_load_env.vars)
+ self.assertFalse('NONEXISTENT' in mod_load_env.vars)
+ mod_load_env.remove('NONEXISTENT')
+ self.assertFalse('NONEXISTENT' in mod_load_env.vars)
+
+ # test removal with delattr
+ mod_load_env.REMOVABLE_VAR = test_contents
+ self.assertTrue('REMOVABLE_VAR' in mod_load_env.vars)
+ delattr(mod_load_env, 'REMOVABLE_VAR')
+ self.assertFalse('REMOVABLE_VAR' in mod_load_env.vars)
+ mod_load_env._REMOVABLE_VAR = test_contents
+ self.assertTrue('_REMOVABLE_VAR' in mod_load_env.vars)
+ delattr(mod_load_env, '_REMOVABLE_VAR')
+ self.assertFalse('_REMOVABLE_VAR' in mod_load_env.vars)
+ mod_load_env.__REMOVABLE_VAR = test_contents
+ self.assertTrue('__REMOVABLE_VAR' in mod_load_env.vars)
+ delattr(mod_load_env, '__REMOVABLE_VAR')
+ self.assertFalse('__REMOVABLE_VAR' in mod_load_env.vars)
+ self.assertRaises(EasyBuildError, delattr, mod_load_env, 'NONEXISTENT')
+
+ # test replacing of env vars
+ env_vars = sorted(mod_load_env.as_dict.keys())
+ expected = ['ACLOCAL_PATH', 'CLASSPATH', 'CMAKE_LIBRARY_PATH', 'CMAKE_PREFIX_PATH', 'GI_TYPELIB_PATH',
+ 'LD_LIBRARY_PATH', 'LIBRARY_PATH', 'MANPATH', 'PATH', 'PKG_CONFIG_PATH', 'TEST_NEW_VAR',
+ 'TEST_STR', 'TEST_VAR', 'TEST_VARTYPE', 'XDG_DATA_DIRS', '_UNDERSCORE_VAR',
+ '__DOUBLE_UNDERSCORE_VAR', '___TRIPLE__UNDERSCORE_VAR']
+ self.assertEqual(env_vars, expected)
+
+ mod_load_env.replace({'FOO': 'foo', 'BAR': 'bar'})
+ env_vars = sorted(mod_load_env.as_dict.keys())
+ self.assertEqual(env_vars, ['BAR', 'FOO'])
+ self.assertEqual(mod_load_env.BAR.contents, ['bar'])
+ self.assertEqual(mod_load_env.FOO.contents, ['foo'])
+
+ # test aliases
+ aliases = {
+ 'ALIAS1': ['ALIAS_VAR11', 'ALIAS_VAR12'],
+ 'ALIAS2': ['ALIAS_VAR21'],
+ }
+ alias_load_env = mod.ModuleLoadEnvironment(aliases=aliases)
+ self.assertEqual(alias_load_env._aliases, aliases)
+ self.assertEqual(sorted(alias_load_env.alias_vars('ALIAS1')), ['ALIAS_VAR11', 'ALIAS_VAR12'])
+ self.assertEqual(alias_load_env.alias_vars('ALIAS2'), ['ALIAS_VAR21'])
+ # set a known alias
+ alias_load_env.set_alias_vars('ALIAS1', 'alias1_path')
+ self.assertTrue('ALIAS_VAR11' in alias_load_env.vars)
+ self.assertEqual(alias_load_env.ALIAS_VAR11.contents, ['alias1_path'])
+ self.assertEqual(alias_load_env.ALIAS_VAR11.type, mod.ModEnvVarType.PATH_WITH_FILES)
+ self.assertTrue('ALIAS_VAR12' in alias_load_env.vars)
+ self.assertEqual(alias_load_env.ALIAS_VAR12.contents, ['alias1_path'])
+ self.assertEqual(alias_load_env.ALIAS_VAR12.type, mod.ModEnvVarType.PATH_WITH_FILES)
+ self.assertFalse('ALIAS_VAR21' in alias_load_env.vars)
+ for envar in alias_load_env.alias('ALIAS1'):
+ self.assertEqual(envar.contents, ['alias1_path'])
+ self.assertEqual(envar.type, mod.ModEnvVarType.PATH_WITH_FILES)
+ # set a second known alias
+ alias_load_env.set_alias_vars('ALIAS2', 'alias2_path')
+ self.assertTrue('ALIAS_VAR11' in alias_load_env.vars)
+ self.assertEqual(alias_load_env.ALIAS_VAR11.contents, ['alias1_path'])
+ self.assertEqual(alias_load_env.ALIAS_VAR11.type, mod.ModEnvVarType.PATH_WITH_FILES)
+ self.assertTrue('ALIAS_VAR21' in alias_load_env.vars)
+ self.assertEqual(alias_load_env.ALIAS_VAR21.contents, ['alias2_path'])
+ self.assertEqual(alias_load_env.ALIAS_VAR21.type, mod.ModEnvVarType.PATH_WITH_FILES)
+ # add a new alias
+ alias_load_env.update_alias('ALIAS3', 'ALIAS_VAR31')
+ self.assertEqual(alias_load_env.alias_vars('ALIAS3'), ['ALIAS_VAR31'])
+ alias_load_env.update_alias('ALIAS3', ['ALIAS_VAR31', 'ALIAS_VAR32'])
+ self.assertEqual(sorted(alias_load_env.alias_vars('ALIAS3')), ['ALIAS_VAR31', 'ALIAS_VAR32'])
+ alias_load_env.set_alias_vars('ALIAS3', 'alias3_path')
+ for envar in alias_load_env.alias('ALIAS3'):
+ self.assertEqual(envar.contents, ['alias3_path'])
+ self.assertEqual(envar.type, mod.ModEnvVarType.PATH_WITH_FILES)
+ # append path to existing alias
+ for envar in alias_load_env.alias('ALIAS3'):
+ envar.append('new_path')
+ self.assertEqual(sorted(envar.contents), ['alias3_path', 'new_path'])
+ self.assertEqual(alias_load_env.ALIAS_VAR31.contents, ['alias3_path', 'new_path'])
+ self.assertEqual(alias_load_env.ALIAS_VAR32.contents, ['alias3_path', 'new_path'])
+
+ error_pattern = "Wrong format for aliases defitions passed to ModuleLoadEnvironment"
+ self.assertErrorRegex(EasyBuildError, error_pattern, mod.ModuleLoadEnvironment, aliases=False)
+ self.assertErrorRegex(EasyBuildError, error_pattern, mod.ModuleLoadEnvironment, aliases='wrong')
+ self.assertErrorRegex(EasyBuildError, error_pattern, mod.ModuleLoadEnvironment, aliases=['some', 'list'])
+
def suite():
""" returns all the testcases in this module """
diff --git a/test/framework/modulestool.py b/test/framework/modulestool.py
index 3cf4493f02..594b8b59f4 100644
--- a/test/framework/modulestool.py
+++ b/test/framework/modulestool.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2014-2024 Ghent University
+# Copyright 2014-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -36,7 +36,7 @@
from unittest import TextTestRunner
from easybuild.base import fancylogger
-from easybuild.tools import modules, StrictVersion
+from easybuild.tools import modules, LooseVersion
from easybuild.tools.build_log import EasyBuildError
from easybuild.tools.filetools import read_file, which, write_file
from easybuild.tools.modules import EnvironmentModules, Lmod
@@ -76,7 +76,7 @@ def test_mock(self):
mmt = MockModulesTool(mod_paths=[], testing=True)
# the version of the MMT is the commandline option
- self.assertEqual(mmt.version, StrictVersion(MockModulesTool.VERSION_OPTION))
+ self.assertEqual(mmt.version, LooseVersion(MockModulesTool.VERSION_OPTION))
cmd_abspath = which(MockModulesTool.COMMAND)
@@ -100,7 +100,7 @@ def test_environment_command(self):
bmmt = BrokenMockModulesTool(mod_paths=[], testing=True)
cmd_abspath = which(MockModulesTool.COMMAND)
- self.assertEqual(bmmt.version, StrictVersion(MockModulesTool.VERSION_OPTION))
+ self.assertEqual(bmmt.version, LooseVersion(MockModulesTool.VERSION_OPTION))
self.assertEqual(bmmt.cmd, cmd_abspath)
# clean it up
@@ -209,12 +209,36 @@ def test_environment_modules_specific(self):
mt = EnvironmentModules(testing=True)
self.assertIsInstance(mt.loaded_modules(), list) # dummy usage
+ # test updating module cache
+ test_modulepath = os.path.join(self.test_installpath, 'modules', 'all')
+ os.environ['MODULEPATH'] = test_modulepath
+ test_module_dir = os.path.join(test_modulepath, 'test')
+ test_module_file = os.path.join(test_module_dir, '1.2.3')
+ write_file(test_module_file, '#%Module')
+ build_options = {
+ 'update_modules_tool_cache': True,
+ }
+ init_config(build_options=build_options)
+ mt = EnvironmentModules(testing=True)
+ out = mt.update()
+ os.remove(test_module_file)
+ os.rmdir(test_module_dir)
+
+ # test cache file has been created if module tool supports it
+ if LooseVersion(mt.version) >= LooseVersion('5.3.0'):
+ cache_fp = os.path.join(test_modulepath, '.modulecache')
+ expected = "Creating %s\n" % cache_fp
+ self.assertEqual(expected, out, "Module cache created")
+ self.assertTrue(os.path.exists(cache_fp))
+ os.remove(cache_fp)
+
# initialize Environment Modules tool with non-official version number
# pass (fake) full path to 'modulecmd.tcl' via $MODULES_CMD
fake_path = os.path.join(self.test_installpath, 'libexec', 'modulecmd.tcl')
fake_modulecmd_txt = '\n'.join([
- 'puts stderr {Modules Release 5.3.1+unload-188-g14b6b59b (2023-10-21)}',
- "puts {os.environ['FOO'] = 'foo'}",
+ '#!/bin/bash',
+ 'echo "Modules Release 5.3.1+unload-188-g14b6b59b (2023-10-21)" >&2',
+ 'echo "os.environ[\'FOO\'] = \'foo\'"',
])
write_file(fake_path, fake_modulecmd_txt)
os.chmod(fake_path, stat.S_IRUSR | stat.S_IXUSR)
diff --git a/test/framework/options.py b/test/framework/options.py
index 2208465db0..f54bfc4f1a 100644
--- a/test/framework/options.py
+++ b/test/framework/options.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -36,9 +36,9 @@
import sys
import tempfile
import textwrap
-import warnings
-from easybuild.tools import LooseVersion
+from importlib import reload
from unittest import TextTestRunner
+from urllib.request import URLError
import easybuild.main
import easybuild.tools.build_log
@@ -63,20 +63,16 @@
from easybuild.tools.modules import Lmod
from easybuild.tools.options import EasyBuildOptions, opts_dict_to_eb_opts, parse_external_modules_metadata
from easybuild.tools.options import set_up_configuration, set_tmpdir, use_color
-from easybuild.tools.py2vs3 import URLError, reload, sort_looseversions
from easybuild.tools.toolchain.utilities import TC_CONST_PREFIX
-from easybuild.tools.run import run_cmd
-from easybuild.tools.systemtools import HAVE_ARCHSPEC
+from easybuild.tools.run import run_shell_cmd
+from easybuild.tools.systemtools import DARWIN, HAVE_ARCHSPEC, get_os_type
from easybuild.tools.version import VERSION
from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered, cleanup, init_config
try:
import pycodestyle # noqa
except ImportError:
- try:
- import pep8 # noqa
- except ImportError:
- pass
+ pass
EXTERNAL_MODULES_METADATA = """[foobar/1.2.3]
@@ -203,7 +199,8 @@ def test_help_rst(self):
def test_no_args(self):
"""Test using no arguments."""
- outtxt = self.eb_main([])
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main([])
error_msg = "ERROR.* Please provide one or multiple easyconfig files,"
error_msg += " or use software build options to make EasyBuild search for easyconfigs"
@@ -218,7 +215,8 @@ def test_debug(self):
'nosuchfile.eb',
debug_arg,
]
- outtxt = self.eb_main(args)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args)
for log_msg_type in ['DEBUG', 'INFO', 'ERROR']:
res = re.search(' %s ' % log_msg_type, outtxt)
@@ -232,7 +230,8 @@ def test_info(self):
'nosuchfile.eb',
info_arg,
]
- outtxt = self.eb_main(args)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args)
error_tmpl = "%s log messages are included when using %s ( out: %s)"
for log_msg_type in ['INFO', 'ERROR']:
@@ -247,7 +246,8 @@ def test_quiet(self):
"""Test enabling quiet logging (errors only)."""
for quiet_arg in ['--quiet']:
args = ['nosuchfile.eb', quiet_arg]
- out = self.eb_main(args)
+ with self.mocked_stdout_stderr():
+ out = self.eb_main(args)
for log_msg_type in ['ERROR']:
res = re.search(' %s ' % log_msg_type, out)
@@ -264,13 +264,15 @@ def test_force(self):
# use GCC-4.6.3.eb easyconfig file that comes with the tests
eb_file = os.path.join(os.path.dirname(__file__), 'easyconfigs', 'test_ecs', 'g', 'GCC', 'GCC-4.6.3.eb')
+ self.mock_stdout(True)
# check log message without --force
args = [
eb_file,
'--debug',
]
- outtxt, error_thrown = self.eb_main(args, return_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt, error_thrown = self.eb_main(args, return_error=True)
error_msg = "No error is thrown if software is already installed (error_thrown: %s)" % error_thrown
self.assertTrue(not error_thrown, error_msg)
@@ -284,12 +286,13 @@ def test_force(self):
# check that --force and --rebuild work
for arg in ['--force', '--rebuild']:
- outtxt = self.eb_main([eb_file, '--debug', arg])
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main([eb_file, '--debug', arg])
self.assertTrue(not re.search(already_msg, outtxt), "Already installed message not there with %s" % arg)
+ self.mock_stdout(False)
def test_skip(self):
"""Test skipping installation of module (--skip, -k)."""
-
# use toy-0.0.eb easyconfig file that comes with the tests
topdir = os.path.abspath(os.path.dirname(__file__))
toy_ec = os.path.join(topdir, 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb')
@@ -297,13 +300,11 @@ def test_skip(self):
# check log message with --skip for existing module
args = [
toy_ec,
- '--sourcepath=%s' % self.test_sourcepath,
- '--buildpath=%s' % self.test_buildpath,
- '--installpath=%s' % self.test_installpath,
'--force',
'--debug',
]
- self.eb_main(args, do_build=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True)
args.append('--skip')
self.mock_stdout(True)
@@ -321,24 +322,20 @@ def test_skip(self):
# check log message with --skip for non-existing module
args = [
toy_ec,
- '--sourcepath=%s' % self.test_sourcepath,
- '--buildpath=%s' % self.test_buildpath,
- '--installpath=%s' % self.test_installpath,
'--try-software-version=1.2.3.4.5.6.7.8.9',
'--try-amend=sources=toy-0.0.tar.gz,toy-0.0.tar.gz', # hackish, but fine
'--force',
'--debug',
'--skip',
]
- outtxt = self.eb_main(args, do_build=True, verbose=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, do_build=True, verbose=True)
- found_msg = "Module toy/1.2.3.4.5.6.7.8.9 found."
- found = re.search(found_msg, outtxt)
- self.assertTrue(not found, "Module found message not there with --skip for non-existing modules: %s" % outtxt)
+ self.assertNotIn("Module toy/1.2.3.4.5.6.7.8.9 found.", outtxt,
+ "Module found message should not be there with --skip for non-existing modules")
- not_found_msg = "No module toy/1.2.3.4.5.6.7.8.9 found. Not skipping anything."
- not_found = re.search(not_found_msg, outtxt)
- self.assertTrue(not_found, "Module not found message there with --skip for non-existing modules: %s" % outtxt)
+ self.assertIn("No module toy/1.2.3.4.5.6.7.8.9 found. Not skipping anything.", outtxt,
+ "Module not found message should be there with --skip for non-existing modules")
toy_mod_glob = os.path.join(self.test_installpath, 'modules', 'all', 'toy', '*')
for toy_mod in glob.glob(toy_mod_glob):
@@ -357,26 +354,26 @@ def test_skip(self):
'--force',
]
error_pattern = "Sanity check failed: no file found at 'bin/nosuchfile'"
- self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, do_build=True, raise_error=True)
+ self.assertErrorRegex(EasyBuildError, error_pattern, self.mocked_main, args, do_build=True, raise_error=True)
- # check use of skipsteps to skip sanity check
- test_ec_txt += "\nskipsteps = ['sanitycheck']\n"
- write_file(test_ec, test_ec_txt)
- self.eb_main(args, do_build=True, raise_error=True)
+ def test_module_only_param(self):
+ """check use of module_only parameter"""
+ topdir = os.path.abspath(os.path.dirname(__file__))
+ toy_ec = os.path.join(topdir, 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb')
- self.assertEqual(len(glob.glob(toy_mod_glob)), 1)
+ test_ec = os.path.join(self.test_prefix, 'test.eb')
+ test_ec_txt = read_file(toy_ec)
+ test_ec_txt += "\nmodule_only=True\n"
+ test_ec_txt += "\nskipsteps = ['sanitycheck']\n" # Software does not exist, so sanity check would fail
+ write_file(test_ec, test_ec_txt)
- # check use of module_only parameter
- remove_dir(os.path.join(self.test_installpath, 'modules', 'all', 'toy'))
- remove_dir(os.path.join(self.test_installpath, 'software', 'toy', '0.0'))
args = [
test_ec,
'--rebuild',
]
- test_ec_txt += "\nmodule_only = True\n"
- write_file(test_ec, test_ec_txt)
- self.eb_main(args, do_build=True, raise_error=True)
+ self.mocked_main(args, do_build=True, raise_error=True)
+ toy_mod_glob = os.path.join(self.test_installpath, 'modules', 'all', 'toy', '*')
self.assertEqual(len(glob.glob(toy_mod_glob)), 1)
# check that no software was installed
@@ -385,6 +382,46 @@ def test_skip(self):
easybuild_dir = os.path.join(installdir, 'easybuild')
self.assertEqual(installdir_glob, [easybuild_dir])
+ def test_skipsteps(self):
+ """Test skipping of steps using skipsteps."""
+ # use toy-0.0.eb easyconfig file that comes with the tests
+ topdir = os.path.abspath(os.path.dirname(__file__))
+ toy_ec = os.path.join(topdir, 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb')
+
+ # make sure that sanity check is *NOT* skipped
+ test_ec = os.path.join(self.test_prefix, 'test.eb')
+ test_ec_txt = read_file(toy_ec)
+ regex = re.compile(r"sanity_check_paths = \{(.|\n)*\}", re.M)
+ test_ec_txt = regex.sub("sanity_check_paths = {'files': ['bin/nosuchfile'], 'dirs': []}", test_ec_txt)
+ write_file(test_ec, test_ec_txt)
+ args = [
+ test_ec,
+ '--rebuild',
+ ]
+ error_pattern = "Sanity check failed: no file found at 'bin/nosuchfile'"
+ self.assertErrorRegex(EasyBuildError, error_pattern, self.mocked_main, args, do_build=True, raise_error=True)
+
+ # Verify a wrong step name is caught
+ test_ec_txt += "\nskipsteps = ['wrong-step-name']\n"
+ write_file(test_ec, test_ec_txt)
+ error_pattern = "Found one or more unknown step names in 'skipsteps' easyconfig parameter:\n"
+ error_pattern += r"\* wrong-step-name"
+ self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, do_build=True, raise_error=True)
+ # 'source' step was renamed to 'extract' in EasyBuild 5.0,
+ # see https://github.com/easybuilders/easybuild-framework/pull/4629
+ test_ec_txt += "\nskipsteps = ['source']\n"
+ write_file(test_ec, test_ec_txt)
+ error_pattern = error_pattern.replace('wrong-step-name', r"source \(did you mean 'extract'\?\)")
+ self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, do_build=True, raise_error=True)
+
+ # check use of skipsteps to skip sanity check
+ test_ec_txt += "\nskipsteps = ['sanitycheck']\n"
+ write_file(test_ec, test_ec_txt)
+ self.mocked_main(args, do_build=True, raise_error=True)
+
+ toy_mod_glob = os.path.join(self.test_installpath, 'modules', 'all', 'toy', '*')
+ self.assertEqual(len(glob.glob(toy_mod_glob)), 1)
+
def test_skip_test_step(self):
"""Test skipping testing the build (--skip-test-step)."""
@@ -453,24 +490,28 @@ def test_skip_sanity_check(self):
args = [test_ec, '--rebuild']
err_msg = "Sanity check failed"
- self.assertErrorRegex(EasyBuildError, err_msg, self.eb_main, args, do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, err_msg, self.eb_main, args, do_build=True, raise_error=True)
args.append('--skip-sanity-check')
- outtext = self.eb_main(args, do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtext = self.eb_main(args, do_build=True, raise_error=True)
self.assertNotIn('sanity checking...', outtext)
# Passing skip and only options is disallowed
args.append('--sanity-check-only')
error_pattern = 'Found both skip-sanity-check and sanity-check-only enabled'
- self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, do_build=True, raise_error=True)
def test_job(self):
"""Test submitting build as a job."""
# use gzip-1.4.eb easyconfig file that comes with the tests
- eb_file = os.path.join(os.path.dirname(__file__), 'easyconfigs', 'test_ecs', 'g', 'gzip', 'gzip-1.4.eb')
+ test_ecs = os.path.join(os.path.dirname(__file__), 'easyconfigs', 'test_ecs')
+ eb_file = os.path.join(test_ecs, 'g', 'gzip', 'gzip-1.4.eb')
- def check_args(job_args, passed_args=None):
+ def check_args(job_args, passed_args=None, msgstrs=None, try_opts='', tweaked_eb_file='gzip-1.4.eb'):
"""Check whether specified args yield expected result."""
if passed_args is None:
passed_args = job_args[:]
@@ -482,22 +523,50 @@ def check_args(job_args, passed_args=None):
eb_file,
'--job',
] + job_args
- outtxt = self.eb_main(args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, raise_error=True)
job_msg = r"INFO.* Command template for jobs: .* && eb %%\(spec\)s.* %s.*\n" % ' .*'.join(passed_args)
assertmsg = "Info log msg with job command template for --job (job_msg: %s, outtxt: %s)" % (job_msg, outtxt)
self.assertTrue(re.search(job_msg, outtxt), assertmsg)
+ if msgstrs is None:
+ msgstrs = [(tweaked_eb_file, eb_file + try_opts)]
+
+ assertmsg = "Info log msg with creating job for --job (job_msg: %s, outtxt: %s)" % (job_msg, outtxt)
+ for msgstr in msgstrs:
+ job_msg = r"INFO creating job for ec: %s using %s\n" % msgstr
+ self.assertTrue(re.search(job_msg, outtxt), assertmsg)
+
# options passed are reordered, so order here matters to make tests pass
check_args(['--debug'])
check_args(['--debug', '--stop=configure', '--try-software-name=foo'],
- passed_args=['--debug', "--stop='configure'"])
+ passed_args=['--debug', "--stop='configure'"],
+ try_opts=" --try-software-name='foo'",
+ tweaked_eb_file="foo-1.4.eb")
check_args(['--debug', '--robot-paths=/tmp/foo:/tmp/bar'],
passed_args=['--debug', "--robot-paths='/tmp/foo:/tmp/bar'"])
# --robot has preference over --robot-paths, --robot is not passed down
check_args(['--debug', '--robot-paths=/tmp/foo', '--robot=%s' % self.test_prefix],
passed_args=['--debug', "--robot-paths='%s:/tmp/foo'" % self.test_prefix])
+ # check if libtoy dep uses --try-toolchain but gzip does not (easyconfig exists already)
+ eb_file = os.path.join(self.test_buildpath, 'toy-0.0-with-deps.eb')
+ copy_file(os.path.join(test_ecs, 't', 'toy', 'toy-0.0.eb'), eb_file)
+ write_file(eb_file, "dependencies = [('libtoy', '0.0'), ('gzip', '1.4')]\n", append=True)
+ try_opts = " --try-toolchain='GCC,4.9.3-2.26'"
+ tweaked_eb_file = "toy-0.0-GCC-4.9.3-2.26.eb"
+ gzip_eb_file = 'gzip-1.4-GCC-4.9.3-2.26.eb'
+ check_args(['--debug', '--stop=configure', '--try-toolchain=GCC,4.9.3-2.26', '--robot'],
+ passed_args=['--debug', "--stop='configure'"],
+ msgstrs=[
+ (tweaked_eb_file, eb_file + try_opts),
+ ('libtoy-0.0-GCC-4.9.3-2.26.eb',
+ os.path.join(test_ecs, 'l', 'libtoy', 'libtoy-0.0.eb') + try_opts),
+ (gzip_eb_file, os.path.join(test_ecs, 'g', 'gzip', gzip_eb_file))],
+ try_opts=try_opts,
+ tweaked_eb_file=tweaked_eb_file)
+
# 'zzz' prefix in the test name is intentional to make this test run last,
# since it fiddles with the logging infrastructure which may break things
def test_zzz_logtostdout(self):
@@ -535,7 +604,6 @@ def test_zzz_logtostdout(self):
self.mock_stdout(False)
self.assertIn("Auto-enabling streaming output", stdout)
- self.assertIn("== (streaming) output for command 'gcc toy.c -o toy':", stdout)
if os.path.exists(dummylogfn):
os.remove(dummylogfn)
@@ -564,6 +632,7 @@ def run_test(fmt=None):
r'^``ARCH``\s*``(aarch64|ppc64le|x86_64)``\s*CPU architecture .*',
r'^``EXTERNAL_MODULE``.*',
r'^``HOME``.*',
+ r'^``MODULE_LOAD_ENV_HEADERS``.*Environment variables .*',
r'``OS_NAME``.*',
r'``OS_PKG_IBVERBS_DEV``.*',
]
@@ -572,6 +641,7 @@ def run_test(fmt=None):
r'^\s*ARCH: (aarch64|ppc64le|x86_64) \(CPU architecture .*\)',
r'^\s*EXTERNAL_MODULE:.*',
r'^\s*HOME:.*',
+ r'^\s*MODULE_LOAD_ENV_HEADERS:.*\(Environment variables.*\)',
r'\s*OS_NAME: .*',
r'\s*OS_PKG_IBVERBS_DEV: .*',
]
@@ -664,7 +734,8 @@ def run_test(custom=None, extra_params=[], fmt=None):
if custom is not None:
args.extend(['-e', custom])
- self.eb_main(args, logfile=dummylogfn, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, logfile=dummylogfn, verbose=True, raise_error=True)
logtxt = read_file(self.logfile)
# check whether all parameter types are listed
@@ -730,8 +801,8 @@ def test_avail_hooks(self):
" post_fetch_hook",
" pre_ready_hook",
" post_ready_hook",
- " pre_source_hook",
- " post_source_hook",
+ " pre_extract_hook",
+ " post_extract_hook",
" pre_patch_hook",
" post_patch_hook",
" pre_prepare_hook",
@@ -768,6 +839,7 @@ def test_avail_hooks(self):
" post_build_and_install_loop_hook",
" end_hook",
" cancel_hook",
+ " crash_hook",
" fail_hook",
" pre_run_shell_cmd_hook",
" post_run_shell_cmd_hook",
@@ -787,7 +859,8 @@ def test__list_toolchains(self):
'--list-toolchains',
'--unittest-file=%s' % self.logfile,
]
- self.eb_main(args, logfile=dummylogfn, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, logfile=dummylogfn, raise_error=True)
regex = re.compile(r"INFO List of known toolchains \(toolchain name: module\[, module, \.\.\.\]\):")
logtxt = read_file(self.logfile)
@@ -869,7 +942,7 @@ def test_avail_lists(self):
os.close(fd)
name_items = {
- 'modules-tools': ['EnvironmentModulesC', 'Lmod'],
+ 'modules-tools': ['EnvironmentModules', 'Lmod'],
'module-naming-schemes': ['EasyBuildMNS', 'HierarchicalMNS', 'CategorizedHMNS'],
}
for (name, items) in name_items.items():
@@ -877,14 +950,15 @@ def test_avail_lists(self):
'--avail-%s' % name,
'--unittest-file=%s' % self.logfile,
]
- self.eb_main(args, logfile=dummylogfn)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, logfile=dummylogfn)
logtxt = read_file(self.logfile)
words = name.replace('-', ' ')
info_msg = r"INFO List of supported %s:" % words
self.assertTrue(re.search(info_msg, logtxt), "Info message with list of available %s" % words)
for item in items:
- res = re.findall(r"^\s*%s" % item, logtxt, re.M)
+ res = re.findall(r"^\s*%s\n" % item, logtxt, re.M)
self.assertTrue(res, "%s is included in list of available %s" % (item, words))
# every item should only be mentioned once
n = len(res)
@@ -913,7 +987,8 @@ def test_avail_cfgfile_constants(self):
'--avail-cfgfile-constants',
'--unittest-file=%s' % self.logfile,
]
- self.eb_main(args, logfile=dummylogfn)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, logfile=dummylogfn)
logtxt = read_file(self.logfile)
cfgfile_constants = {
'DEFAULT_ROBOT_PATHS': os.path.join(tmpdir, 'easybuild', 'easyconfigs'),
@@ -945,41 +1020,51 @@ def test_000_list_easyblocks(self):
list_arg,
'--unittest-file=%s' % self.logfile,
]
- self.eb_main(args, logfile=dummylogfn, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, logfile=dummylogfn, raise_error=True)
logtxt = read_file(self.logfile)
expected = '\n'.join([
- r'EasyBlock',
- r'\|-- bar',
- r'\|-- ConfigureMake',
- r'\| \|-- MakeCp',
- r'\|-- EB_EasyBuildMeta',
- r'\|-- EB_FFTW',
- r'\|-- EB_foo',
- r'\| \|-- EB_foofoo',
- r'\|-- EB_GCC',
- r'\|-- EB_HPL',
- r'\|-- EB_libtoy',
- r'\|-- EB_OpenBLAS',
- r'\|-- EB_OpenMPI',
- r'\|-- EB_ScaLAPACK',
- r'\|-- EB_toy_buggy',
- r'\|-- ExtensionEasyBlock',
- r'\| \|-- DummyExtension',
- r'\| \|-- EB_toy',
- r'\| \| \|-- EB_toy_eula',
- r'\| \| \|-- EB_toytoy',
- r'\| \|-- Toy_Extension',
- r'\|-- ModuleRC',
- r'\|-- PythonBundle',
- r'\|-- Toolchain',
- r'Extension',
- r'\|-- ExtensionEasyBlock',
- r'\| \|-- DummyExtension',
- r'\| \|-- EB_toy',
- r'\| \| \|-- EB_toy_eula',
- r'\| \| \|-- EB_toytoy',
- r'\| \|-- Toy_Extension',
+ "EasyBlock",
+ "|-- bar",
+ "|-- ConfigureMake",
+ "| |-- MakeCp",
+ "|-- EB_EasyBuildMeta",
+ "|-- EB_FFTW",
+ "|-- EB_foo",
+ "| |-- EB_foofoo",
+ "|-- EB_GCC",
+ "|-- EB_HPL",
+ "|-- EB_libtoy",
+ "|-- EB_OpenBLAS",
+ "|-- EB_OpenMPI",
+ "|-- EB_ScaLAPACK",
+ "|-- EB_toy_buggy",
+ "|-- ExtensionEasyBlock",
+ "| |-- DummyExtension",
+ "| | |-- CustomDummyExtension",
+ "| | | |-- ChildCustomDummyExtension",
+ "| | |-- DeprecatedDummyExtension",
+ "| | | |-- ChildDeprecatedDummyExtension",
+ "| |-- EB_toy",
+ "| | |-- EB_toy_eula",
+ "| | |-- EB_toytoy",
+ "| |-- Toy_Extension",
+ "|-- ModuleRC",
+ "|-- PythonBundle",
+ "|-- Toolchain",
+ "Extension",
+ "|-- ExtensionEasyBlock",
+ "| |-- DummyExtension",
+ "| | |-- CustomDummyExtension",
+ "| | | |-- ChildCustomDummyExtension",
+ "| | |-- DeprecatedDummyExtension",
+ "| | | |-- ChildDeprecatedDummyExtension",
+ "| |-- EB_toy",
+ "| | |-- EB_toy_eula",
+ "| | |-- EB_toytoy",
+ "| |-- Toy_Extension",
+ "",
])
regex = re.compile(expected, re.M)
self.assertTrue(regex.search(logtxt), "Pattern '%s' found in: %s" % (regex.pattern, logtxt))
@@ -992,7 +1077,8 @@ def test_000_list_easyblocks(self):
'--list-easyblocks=detailed',
'--unittest-file=%s' % self.logfile,
]
- self.eb_main(args, logfile=dummylogfn)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, logfile=dummylogfn)
logtxt = read_file(self.logfile)
patterns = [
@@ -1128,7 +1214,8 @@ def test_search(self):
for opt in ['--search', '-S', '--search-short']:
for pattern in ['*foo', '(foo', ')foo', 'foo)', 'foo(']:
args = [opt, pattern, '--robot', test_easyconfigs_dir]
- self.assertErrorRegex(EasyBuildError, "Invalid search query", self.eb_main, args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, "Invalid search query", self.eb_main, args, raise_error=True)
def test_ignore_index(self):
"""
@@ -1233,12 +1320,7 @@ def mocked_main(self, args, **kwargs):
if not kwargs:
kwargs = {'raise_error': True}
- self.mock_stderr(True)
- self.mock_stdout(True)
- self.eb_main(args, **kwargs)
- stderr, stdout = self.get_stderr(), self.get_stdout()
- self.mock_stderr(False)
- self.mock_stdout(False)
+ stdout, stderr = self._run_mock_eb(args, **kwargs)
self.assertEqual(stderr, '')
return stdout.strip()
@@ -1327,7 +1409,8 @@ def check_copied_files():
self.assertTrue(os.path.isfile(target))
args = ['--copy-ec', 'toy-0.0.eb', 'bzip2-1.0.6-GCC-4.9.2.eb', target]
error_pattern = ".*/test.eb exists but is not a directory"
- self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, raise_error=True)
# test use of --copy-ec with only one argument: copy to current working directory
test_working_dir = os.path.join(self.test_prefix, 'test_working_dir')
@@ -1345,7 +1428,8 @@ def check_copied_files():
# --copy-ec without arguments results in a proper error
args = ['--copy-ec']
error_pattern = "One or more files to copy should be specified!"
- self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, raise_error=True)
def test_github_copy_ec_from_pr(self):
"""Test combination of --copy-ec with --from-pr."""
@@ -1359,60 +1443,59 @@ def test_github_copy_ec_from_pr(self):
# Make sure the test target directory doesn't exist
remove_dir(test_target_dir)
- all_files_pr8007 = [
- 'Arrow-0.7.1-intel-2017b-Python-3.6.3.eb',
- 'bat-0.3.3-fix-pyspark.patch',
- 'bat-0.3.3-intel-2017b-Python-3.6.3.eb',
+ all_files_pr22345 = [
+ 'QuantumESPRESSO-7.4-foss-2024a.eb',
+ 'QuantumESPRESSO-7.4-parallel-symmetrization.patch',
]
# test use of --copy-ec with --from-pr to the current working directory
cwd = change_dir(test_working_dir)
- args = ['--copy-ec', '--from-pr', '8007']
+ args = ['--copy-ec', '--from-pr', '22345']
stdout = self.mocked_main(args)
- regex = re.compile(r"3 file\(s\) copied to .*/%s" % os.path.basename(test_working_dir))
+ regex = re.compile(r"2 file\(s\) copied to .*/%s" % os.path.basename(test_working_dir))
self.assertTrue(regex.search(stdout), "Pattern '%s' should be found in: %s" % (regex.pattern, stdout))
# check that the files exist
- for pr_file in all_files_pr8007:
+ for pr_file in all_files_pr22345:
self.assertExists(os.path.join(test_working_dir, pr_file))
remove_file(os.path.join(test_working_dir, pr_file))
# copying all files touched by PR to a non-existing target directory (which is created automatically)
self.assertNotExists(test_target_dir)
- args = ['--copy-ec', '--from-pr', '8007', test_target_dir]
+ args = ['--copy-ec', '--from-pr', '22345', test_target_dir]
stdout = self.mocked_main(args)
- regex = re.compile(r"3 file\(s\) copied to .*/%s" % os.path.basename(test_target_dir))
+ regex = re.compile(r"2 file\(s\) copied to .*/%s" % os.path.basename(test_target_dir))
self.assertTrue(regex.search(stdout), "Pattern '%s' should be found in: %s" % (regex.pattern, stdout))
- for pr_file in all_files_pr8007:
+ for pr_file in all_files_pr22345:
self.assertExists(os.path.join(test_target_dir, pr_file))
remove_dir(test_target_dir)
# test where we select a single easyconfig file from a PR
mkdir(test_target_dir)
- ec_filename = 'bat-0.3.3-intel-2017b-Python-3.6.3.eb'
- args = ['--copy-ec', '--from-pr', '8007', ec_filename, test_target_dir]
+ ec_filename = 'QuantumESPRESSO-7.4-foss-2024a.eb'
+ args = ['--copy-ec', '--from-pr', '22345', ec_filename, test_target_dir]
stdout = self.mocked_main(args)
regex = re.compile(r"%s copied to .*/%s" % (ec_filename, os.path.basename(test_target_dir)))
self.assertTrue(regex.search(stdout), "Pattern '%s' should be found in: %s" % (regex.pattern, stdout))
self.assertEqual(os.listdir(test_target_dir), [ec_filename])
- self.assertIn("name = 'bat'", read_file(os.path.join(test_target_dir, ec_filename)))
+ self.assertIn("name = 'QuantumESPRESSO'", read_file(os.path.join(test_target_dir, ec_filename)))
remove_dir(test_target_dir)
# test copying of a single easyconfig file from a PR to a non-existing path
- bat_ec = os.path.join(self.test_prefix, 'bat.eb')
- args[-1] = bat_ec
+ environ_ec = os.path.join(self.test_prefix, 'QuantumESPRESSO.eb')
+ args[-1] = environ_ec
stdout = self.mocked_main(args)
- regex = re.compile(r"%s copied to .*/bat.eb" % ec_filename)
+ regex = re.compile(r"%s copied to .*/QuantumESPRESSO.eb" % ec_filename)
self.assertTrue(regex.search(stdout), "Pattern '%s' should be found in: %s" % (regex.pattern, stdout))
- self.assertExists(bat_ec)
- self.assertIn("name = 'bat'", read_file(bat_ec))
+ self.assertExists(environ_ec)
+ self.assertIn("name = 'QuantumESPRESSO'", read_file(environ_ec))
change_dir(cwd)
remove_dir(test_working_dir)
@@ -1420,8 +1503,8 @@ def test_github_copy_ec_from_pr(self):
change_dir(test_working_dir)
# test copying of a patch file from a PR via --copy-ec to current directory
- patch_fn = 'bat-0.3.3-fix-pyspark.patch'
- args = ['--copy-ec', '--from-pr', '8007', patch_fn, '.']
+ patch_fn = 'QuantumESPRESSO-7.4-parallel-symmetrization.patch'
+ args = ['--copy-ec', '--from-pr', '22345', patch_fn, '.']
stdout = self.mocked_main(args)
self.assertEqual(os.listdir(test_working_dir), [patch_fn])
@@ -1432,18 +1515,18 @@ def test_github_copy_ec_from_pr(self):
# test the same thing but where we don't provide a target location
change_dir(test_working_dir)
- args = ['--copy-ec', '--from-pr', '8007', ec_filename]
+ args = ['--copy-ec', '--from-pr', '22345', ec_filename]
stdout = self.mocked_main(args)
regex = re.compile(r"%s copied to .*/%s" % (ec_filename, os.path.basename(test_working_dir)))
self.assertTrue(regex.search(stdout), "Pattern '%s' should be found in: %s" % (regex.pattern, stdout))
self.assertEqual(os.listdir(test_working_dir), [ec_filename])
- self.assertIn("name = 'bat'", read_file(os.path.join(test_working_dir, ec_filename)))
+ self.assertIn("name = 'QuantumESPRESSO'", read_file(os.path.join(test_working_dir, ec_filename)))
# also test copying of patch file to current directory (without specifying target location)
change_dir(test_working_dir)
- args = ['--copy-ec', '--from-pr', '8007', patch_fn]
+ args = ['--copy-ec', '--from-pr', '22345', patch_fn]
stdout = self.mocked_main(args)
regex = re.compile(r"%s copied to .*/%s" % (patch_fn, os.path.basename(test_working_dir)))
@@ -1457,13 +1540,13 @@ def test_github_copy_ec_from_pr(self):
# test with only one ec in the PR (final argument is taken as a filename)
test_ec = os.path.join(self.test_prefix, 'test.eb')
- args = ['--copy-ec', '--from-pr', '11521', test_ec]
- ec_pr11521 = "ExifTool-12.00-GCCcore-9.3.0.eb"
+ args = ['--copy-ec', '--from-pr', '22380', test_ec]
+ ec_pr22380 = "PySide2-5.14.2.3-GCCcore-10.2.0.eb"
stdout = self.mocked_main(args)
- regex = re.compile(r'.*/%s copied to %s' % (ec_pr11521, test_ec))
+ regex = re.compile(r'.*/%s copied to %s' % (ec_pr22380, test_ec))
self.assertTrue(regex.search(stdout), "Pattern '%s' found in: %s" % (regex.pattern, stdout))
self.assertExists(test_ec)
- self.assertIn("name = 'ExifTool'", read_file(test_ec))
+ self.assertIn("name = 'PySide2'", read_file(test_ec))
remove_file(test_ec)
def test_copy_ec_from_commit(self):
@@ -1532,15 +1615,19 @@ def test_copy_ec_from_commit(self):
def test_dry_run(self):
"""Test dry run (long format)."""
+
+ # first test with --robot
fd, dummylogfn = tempfile.mkstemp(prefix='easybuild-dummy', suffix='.log')
os.close(fd)
args = [
'gzip-1.4-GCC-4.6.3.eb',
- '--dry-run', # implies enabling dependency resolution
+ '--dry-run',
+ '--robot', # implies enabling dependency resolution
'--unittest-file=%s' % self.logfile,
]
- self.eb_main(args, logfile=dummylogfn)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, logfile=dummylogfn)
logtxt = read_file(self.logfile)
info_msg = r"Dry run: printing build status of easyconfigs and dependencies"
@@ -1553,6 +1640,70 @@ def test_dry_run(self):
regex = re.compile(r" \* \[%s\] \S+%s \(module: %s\)" % (mark, ec, mod), re.M)
self.assertTrue(regex.search(logtxt), "Found match for pattern %s in '%s'" % (regex.pattern, logtxt))
+ # next test without --robot
+ fd, dummylogfn = tempfile.mkstemp(prefix='easybuild-dummy', suffix='.log')
+ os.close(fd)
+
+ args = [
+ 'gzip-1.4-GCC-4.6.3.eb',
+ '--dry-run',
+ '--unittest-file=%s' % self.logfile,
+ ]
+ self.eb_main(args, logfile=dummylogfn)
+ logtxt = read_file(self.logfile)
+
+ info_msg = r"Dry run: printing build status of easyconfigs"
+ self.assertTrue(re.search(info_msg, logtxt, re.M), "Info message dry running in '%s'" % logtxt)
+ ec, mod, mark = ("gzip-1.4-GCC-4.6.3.eb", "gzip/1.4-GCC-4.6.3", ' ')
+ regex = re.compile(r" \* \[%s\] \S+%s \(module: %s\)" % (mark, ec, mod), re.M)
+ self.assertTrue(regex.search(logtxt), "Found match for pattern %s in '%s'" % (regex.pattern, logtxt))
+
+ def test_persistence_copying_restrictions(self):
+ """
+ Test that EasyBuild fails when instructed to move logs or artifacts inside the build directory
+
+ Moving log files or artifacts inside the build directory modifies the build artifacts, and in the case of
+ build artifacts it is also copying directories into themselves.
+ """
+ base_args = [
+ 'gzip-1.4-GCC-4.6.3.eb',
+ '--dry-run',
+ '--robot',
+ ]
+
+ def test_eb_with(option_flag, is_valid):
+ with tempfile.TemporaryDirectory() as root_dir:
+ build_dir = os.path.join(root_dir, 'build_dir')
+ if is_valid:
+ persist_path = os.path.join(root_dir, 'persist_dir')
+ else:
+ persist_path = os.path.join(root_dir, 'build_dir', 'persist_dir')
+
+ extra_args = [
+ f"--buildpath={build_dir}",
+ f"{option_flag}={persist_path}",
+ ]
+
+ pattern = rf"The {option_flag} \(.*\) cannot reside in a subdirectory of the --buildpath \(.*\)"
+
+ args = base_args
+ args.extend(extra_args)
+
+ if is_valid:
+ try:
+ self.eb_main(args, raise_error=True)
+ except EasyBuildError:
+ self.fail(
+ "Should not fail with --buildpath='{build_dir}' and {option_flag}='{persist_path}'."
+ )
+ else:
+ self.assertErrorRegex(EasyBuildError, pattern, self.eb_main, args, raise_error=True)
+
+ test_eb_with(option_flag='--failed-install-logs-path', is_valid=True)
+ test_eb_with(option_flag='--failed-install-logs-path', is_valid=False)
+ test_eb_with(option_flag='--failed-install-build-dirs-path', is_valid=True)
+ test_eb_with(option_flag='--failed-install-build-dirs-path', is_valid=False)
+
def test_missing(self):
"""Test use of --missing/-M."""
@@ -1631,7 +1782,8 @@ def test_dry_run_short(self):
'--robot=%s' % robot_decoy,
'--unittest-file=%s' % self.logfile,
]
- outtxt = self.eb_main(args, logfile=dummylogfn, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, logfile=dummylogfn, raise_error=True)
info_msg = r"Dry run: printing build status of easyconfigs and dependencies"
self.assertTrue(re.search(info_msg, outtxt, re.M), "Info message dry running in '%s'" % outtxt)
@@ -1668,9 +1820,6 @@ def test_try_robot_force(self):
args = [
eb1,
eb2,
- '--sourcepath=%s' % self.test_sourcepath,
- '--buildpath=%s' % self.test_buildpath,
- '--installpath=%s' % self.test_installpath,
'--debug',
'--force',
'--robot=%s' % test_ecs,
@@ -1678,7 +1827,8 @@ def test_try_robot_force(self):
'--dry-run',
'--unittest-file=%s' % self.logfile,
]
- outtxt = self.eb_main(args, logfile=dummylogfn)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, logfile=dummylogfn)
scalapack_ver = '2.0.2-gompi-2018b-OpenBLAS-0.2.20'
ecs_mods = [
@@ -1705,16 +1855,19 @@ def test_try_toolchain_mapping(self):
gzip_ec,
'--try-toolchain=iccifort,2016.1.150-GCC-4.9.3-2.25',
'--dry-run',
+ '--robot',
]
# by default, toolchain mapping is enabled
# if it fails, an error is printed
error_pattern = "Toolchain iccifort is not equivalent to toolchain foss in terms of capabilities."
- self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, raise_error=True, do_build=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, raise_error=True, do_build=True)
# can continue anyway using --disable-map-toolchains
args.append('--disable-map-toolchains')
- outtxt = self.eb_main(args, raise_error=True, do_build=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, raise_error=True, do_build=True)
patterns = [
r"^ \* \[ \] .*/iccifort-2016.1.150-GCC-4.9.3-2.25.eb \(module: iccifort/.*\)$",
@@ -1759,12 +1912,15 @@ def test_try_update_deps(self):
'--try-toolchain-version=6.4.0-2.28',
'--try-update-deps',
'-D',
+ '--robot',
]
- self.assertErrorRegex(EasyBuildError, "Experimental functionality", self.eb_main, args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, "Experimental functionality", self.eb_main, args, raise_error=True)
args.append('--experimental')
- outtxt = self.eb_main(args, raise_error=True, do_build=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, raise_error=True, do_build=True)
patterns = [
# toolchain got updated
@@ -1817,7 +1973,8 @@ def test_try_update_deps(self):
# Now verify that we can ignore versionsuffixes
args.append('--try-ignore-versionsuffixes')
- outtxt = self.eb_main(args, raise_error=True, do_build=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, raise_error=True, do_build=True)
patterns = [
# toolchain got updated
r"^ \* \[x\] .*/test_ecs/g/GCC/GCC-6.4.0-2.28.eb \(module: GCC/6.4.0-2.28\)$",
@@ -1840,13 +1997,15 @@ def test_dry_run_hierarchical(self):
'gzip-1.5-foss-2018a.eb',
'OpenMPI-2.1.2-GCC-6.4.0-2.28.eb',
'--dry-run',
+ '--robot',
'--unittest-file=%s' % self.logfile,
'--module-naming-scheme=HierarchicalMNS',
'--ignore-osdeps',
'--force',
'--debug',
]
- outtxt = self.eb_main(args, logfile=dummylogfn, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, logfile=dummylogfn, verbose=True, raise_error=True)
ecs_mods = [
# easyconfig, module subdir, (short) module name
@@ -1880,13 +2039,15 @@ def test_dry_run_categorized(self):
'gzip-1.5-foss-2018a.eb',
'OpenMPI-2.1.2-GCC-6.4.0-2.28.eb',
'--dry-run',
+ '--robot',
'--unittest-file=%s' % self.logfile,
'--module-naming-scheme=CategorizedHMNS',
'--ignore-osdeps',
'--force',
'--debug',
]
- outtxt = self.eb_main(args, logfile=dummylogfn, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, logfile=dummylogfn, verbose=True, raise_error=True)
ecs_mods = [
# easyconfig, module subdir, (short) module name, mark
@@ -1922,8 +2083,8 @@ def test_github_from_pr(self):
tmpdir = tempfile.mkdtemp()
args = [
- # PR for foss/2018b, see https://github.com/easybuilders/easybuild-easyconfigs/pull/6424/files
- '--from-pr=6424',
+ # PR for XCrySDen/1.6.2-foss-2024a, see https://github.com/easybuilders/easybuild-easyconfigs/pull/22227
+ '--from-pr=22227',
'--dry-run',
# an argument must be specified to --robot, since easybuild-easyconfigs may not be installed
'--robot=%s' % os.path.join(os.path.dirname(__file__), 'easyconfigs'),
@@ -1932,17 +2093,10 @@ def test_github_from_pr(self):
'--tmpdir=%s' % tmpdir,
]
try:
- outtxt = self.eb_main(args, logfile=dummylogfn, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, logfile=dummylogfn, raise_error=True)
modules = [
- (tmpdir, 'FFTW/3.3.8-gompi-2018b'),
- (tmpdir, 'foss/2018b'),
- ('.*', 'GCC/7.3.0-2.30'), # not included in PR
- (tmpdir, 'gompi/2018b'),
- (tmpdir, 'HPL/2.2-foss-2018b'),
- ('.*', 'hwloc/1.11.8-GCC-7.3.0-2.30'),
- ('.*', 'OpenBLAS/0.3.1-GCC-7.3.0-2.30'),
- ('.*', 'OpenMPI/3.1.1-GCC-7.3.0-2.30'),
- (tmpdir, 'ScaLAPACK/2.0.2-gompi-2018b-OpenBLAS-0.3.1'),
+ (tmpdir, 'XCrySDen/1.6.2-foss-2024a'),
]
for path_prefix, module in modules:
ec_fn = "%s.eb" % '-'.join(module.split('/'))
@@ -1950,11 +2104,7 @@ def test_github_from_pr(self):
regex = re.compile(r"^ \* \[.\] %s.*%s \(module: %s\)$" % (path, ec_fn, module), re.M)
self.assertTrue(regex.search(outtxt), "Found pattern %s in %s" % (regex.pattern, outtxt))
- # make sure that *only* these modules are listed, no others
- regex = re.compile(r"^ \* \[.\] .*/(?P.*) \(module: (?P.*)\)$", re.M)
- self.assertEqual(sorted(x[1] for x in regex.findall(outtxt)), sorted(x[1] for x in modules))
-
- pr_tmpdir = os.path.join(tmpdir, r'eb-\S{6,8}', 'files_pr6424')
+ pr_tmpdir = os.path.join(tmpdir, r'eb-\S{6,8}', 'files_pr22227')
regex = re.compile(r"Extended list of robot search paths with \['%s'\]:" % pr_tmpdir, re.M)
self.assertTrue(regex.search(outtxt), "Found pattern %s in %s" % (regex.pattern, outtxt))
except URLError as err:
@@ -1964,8 +2114,8 @@ def test_github_from_pr(self):
# test with multiple prs
tmpdir = tempfile.mkdtemp()
args = [
- # PRs for ReFrame 3.4.1 and 3.5.0
- '--from-pr=12150,12366',
+ # PRs for various easyconfigs
+ '--from-pr=22227,19834',
'--dry-run',
# an argument must be specified to --robot, since easybuild-easyconfigs may not be installed
'--robot=%s' % os.path.join(os.path.dirname(__file__), 'easyconfigs'),
@@ -1974,10 +2124,11 @@ def test_github_from_pr(self):
'--tmpdir=%s' % tmpdir,
]
try:
- outtxt = self.eb_main(args, logfile=dummylogfn, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, logfile=dummylogfn, raise_error=True)
modules = [
- (tmpdir, 'ReFrame/3.4.1'),
- (tmpdir, 'ReFrame/3.5.0'),
+ (tmpdir, 'quarto/1.5.57-x64'),
+ (tmpdir, 'Gblocks/0.91b'),
]
for path_prefix, module in modules:
ec_fn = "%s.eb" % '-'.join(module.split('/'))
@@ -1985,11 +2136,7 @@ def test_github_from_pr(self):
regex = re.compile(r"^ \* \[.\] %s.*%s \(module: %s\)$" % (path, ec_fn, module), re.M)
self.assertTrue(regex.search(outtxt), "Found pattern %s in %s" % (regex.pattern, outtxt))
- # make sure that *only* these modules are listed, no others
- regex = re.compile(r"^ \* \[.\] .*/(?P.*) \(module: (?P.*)\)$", re.M)
- self.assertEqual(sorted(x[1] for x in regex.findall(outtxt)), sorted(x[1] for x in modules))
-
- for pr in ('12150', '12366'):
+ for pr in ('22227', '19834'):
pr_tmpdir = os.path.join(tmpdir, r'eb-\S{6,8}', 'files_pr%s' % pr)
regex = re.compile(r"Extended list of robot search paths with .*%s.*:" % pr_tmpdir, re.M)
self.assertTrue(regex.search(outtxt), "Found pattern '%s' in: %s" % (regex.pattern, outtxt))
@@ -2008,8 +2155,8 @@ def test_github_from_pr_token_log(self):
os.close(fd)
args = [
- # PR for foss/2018b, see https://github.com/easybuilders/easybuild-easyconfigs/pull/6424/files
- '--from-pr=6424',
+ # PR for XCrySDen/1.6.2-foss-2024a, see https://github.com/easybuilders/easybuild-easyconfigs/pull/22227
+ '--from-pr=22227',
'--dry-run',
'--debug',
# an argument must be specified to --robot, since easybuild-easyconfigs may not be installed
@@ -2052,10 +2199,10 @@ def test_github_from_pr_listed_ecs(self):
tmpdir = tempfile.mkdtemp()
args = [
'toy-0.0.eb',
- 'gompi-2018b.eb', # also pulls in GCC, OpenMPI (which pulls in hwloc)
+ 'XCrySDen-1.6.2-foss-2024a.eb',
'GCC-4.6.3.eb',
- # PR for foss/2018b, see https://github.com/easybuilders/easybuild-easyconfigs/pull/6424/files
- '--from-pr=6424',
+ # PR for XCrySDen/1.6.2-foss-2024a, see https://github.com/easybuilders/easybuild-easyconfigs/pull/22227
+ '--from-pr=22227',
'--dry-run',
# an argument must be specified to --robot, since easybuild-easyconfigs may not be installed
'--robot=%s' % test_ecs_path,
@@ -2064,13 +2211,12 @@ def test_github_from_pr_listed_ecs(self):
'--tmpdir=%s' % tmpdir,
]
try:
- outtxt = self.eb_main(args, logfile=dummylogfn, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, logfile=dummylogfn, raise_error=True)
modules = [
(test_ecs_path, 'toy/0.0'), # not included in PR
- (test_ecs_path, 'GCC/7.3.0-2.30'), # not included in PR, available locally
- (test_ecs_path, 'hwloc/1.11.8-GCC-7.3.0-2.30'),
- (test_ecs_path, 'OpenMPI/3.1.1-GCC-7.3.0-2.30'),
- ('.*%s' % os.path.dirname(tmpdir), 'gompi/2018b'),
+ ('.*%s' % os.path.dirname(tmpdir), 'XCrySDen/1.6.2-foss-2024a'),
+ ('.*%s' % os.path.dirname(tmpdir), 'Togl/2.0-GCCcore-13.3.0'),
(test_ecs_path, 'GCC/4.6.3'), # not included in PR, available locally
]
for path_prefix, module in modules:
@@ -2096,19 +2242,15 @@ def test_github_from_pr_x(self):
os.close(fd)
args = [
- # PR for foss/2018b, see https://github.com/easybuilders/easybuild-easyconfigs/pull/6424/files
- '--from-pr=6424',
- 'FFTW-3.3.8-gompi-2018b.eb',
+ # PR for XCrySDen/1.6.2-foss-2024a, see https://github.com/easybuilders/easybuild-easyconfigs/pull/22227
+ '--from-pr=22227',
+ 'XCrySDen-1.6.2-foss-2024a.eb',
# an argument must be specified to --robot, since easybuild-easyconfigs may not be installed
'--github-user=%s' % GITHUB_TEST_ACCOUNT, # a GitHub token should be available for this user
'--tmpdir=%s' % self.test_prefix,
'--extended-dry-run',
]
try:
- # PR #6424 includes easyconfigs that use 'dummy' toolchain,
- # so we need to allow triggering deprecated behaviour
- self.allow_deprecated_behaviour()
-
self.mock_stderr(True) # just to capture deprecation warning
self.mock_stdout(True)
self.mock_stderr(True)
@@ -2119,8 +2261,8 @@ def test_github_from_pr_x(self):
msg_regexs = [
re.compile(r"^== Build succeeded for 1 out of 1", re.M),
- re.compile(r"^\*\*\* DRY RUN using 'EB_FFTW' easyblock", re.M),
- re.compile(r"^== building and installing FFTW/3.3.8-gompi-2018b\.\.\.", re.M),
+ re.compile(r"^\*\*\* DRY RUN using 'EB_XCrySDen' easyblock", re.M),
+ re.compile(r"^== building and installing XCrySDen/1.6.2-foss-2024a\.\.\.", re.M),
re.compile(r"^building... \[DRY RUN\]", re.M),
re.compile(r"^== COMPLETED: Installation ended successfully \(took .* secs?\)", re.M),
]
@@ -2187,6 +2329,7 @@ def test_from_commit(self):
args = [
'--from-commit=%s' % test_commit,
'--dry-run',
+ '--robot',
'--tmpdir=%s' % tmpdir,
'--include-easyblocks=' + os.path.join(self.test_prefix, 'easyblocks', '*.py'),
]
@@ -2269,7 +2412,8 @@ def test_no_such_software(self):
'--robot=.',
'--debug',
]
- outtxt = self.eb_main(args)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args)
# error message when template is not found
error_msg1 = "ERROR.* No easyconfig files found for software nosuchsoftware, and no templates available. "
@@ -2318,15 +2462,13 @@ def test_header_footer(self):
# check log message with --skip for existing module
args = [
eb_file,
- '--sourcepath=%s' % self.test_sourcepath,
- '--buildpath=%s' % self.test_buildpath,
- '--installpath=%s' % self.test_installpath,
'--debug',
'--force',
'--modules-header=%s' % modules_header,
'--modules-footer=%s' % modules_footer,
]
- self.eb_main(args, do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True, raise_error=True)
toy_module = os.path.join(self.test_installpath, 'modules', 'all', 'toy', '0.0')
if get_module_syntax() == 'Lua':
@@ -2358,14 +2500,12 @@ def test_recursive_module_unload(self):
for lastarg in lastargs:
args = [
eb_file,
- '--sourcepath=%s' % self.test_sourcepath,
- '--buildpath=%s' % self.test_buildpath,
- '--installpath=%s' % self.test_installpath,
'--debug',
'--force',
lastarg,
]
- self.eb_main(args, do_build=True, verbose=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True, verbose=True)
toy_module = os.path.join(self.test_installpath, 'modules', 'all', 'toy', '0.0-deps')
@@ -2391,13 +2531,11 @@ def test_tmpdir(self):
# check log message with --skip for existing module
args = [
eb_file,
- '--sourcepath=%s' % self.test_sourcepath,
- '--buildpath=%s' % self.test_buildpath,
- '--installpath=%s' % self.test_installpath,
'--debug',
'--tmpdir=%s' % tmpdir,
]
- outtxt = self.eb_main(args, do_build=True, reset_env=False)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, do_build=True, reset_env=False)
tmpdir_msg = r"Using %s\S+ as temporary directory" % os.path.join(tmpdir, 'eb-')
found = re.search(tmpdir_msg, outtxt, re.M)
@@ -2434,7 +2572,8 @@ def test_ignore_osdeps(self):
args = [
eb_file,
]
- outtxt = self.eb_main(args, do_build=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, do_build=True)
regex = re.compile("Checking OS dependencies")
self.assertTrue(regex.search(outtxt), "OS dependencies are checked, outtxt: %s" % outtxt)
@@ -2449,7 +2588,8 @@ def test_ignore_osdeps(self):
'--ignore-osdeps',
'--dry-run',
]
- outtxt = self.eb_main(args, do_build=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, do_build=True)
regex = re.compile("Not checking OS dependencies", re.M)
self.assertTrue(regex.search(outtxt), "OS dependencies are ignored with --ignore-osdeps, outtxt: %s" % outtxt)
@@ -2460,7 +2600,8 @@ def test_ignore_osdeps(self):
eb_file,
'--dry-run', # no explicit --ignore-osdeps, but implied by --dry-run
]
- outtxt = self.eb_main(args, do_build=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, do_build=True)
regex = re.compile("stop provided 'notavalidstop' is not valid", re.M)
self.assertTrue(regex.search(outtxt), "Validations are performed with --ignore-osdeps, outtxt: %s" % outtxt)
@@ -2572,7 +2713,8 @@ def test_allow_modules_tool_mismatch(self):
'--modules-tool=MockModulesTool',
'--module-syntax=Tcl', # Lua would require Lmod
]
- self.eb_main(args, do_build=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True)
outtxt = read_file(self.logfile)
error_regex = re.compile("ERROR .*pattern .* not found in defined 'module' function")
self.assertTrue(error_regex.search(outtxt), "Found error w.r.t. module function mismatch: %s" % outtxt[-600:])
@@ -2585,7 +2727,8 @@ def test_allow_modules_tool_mismatch(self):
'--module-syntax=Tcl', # Lua would require Lmod
'--allow-modules-tool-mismatch',
]
- self.eb_main(args, do_build=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True)
outtxt = read_file(self.logfile)
warn_regex = re.compile("WARNING .*pattern .* not found in defined 'module' function")
self.assertTrue(warn_regex.search(outtxt), "Found warning w.r.t. module function mismatch: %s" % outtxt[-600:])
@@ -2598,7 +2741,8 @@ def test_allow_modules_tool_mismatch(self):
'--module-syntax=Tcl', # Lua would require Lmod
'--debug',
]
- self.eb_main(args, do_build=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True)
outtxt = read_file(self.logfile)
found_regex = re.compile("DEBUG Found pattern .* in defined 'module' function")
self.assertTrue(found_regex.search(outtxt), "Found debug message w.r.t. module function: %s" % outtxt[-600:])
@@ -2618,9 +2762,6 @@ def test_try(self):
args = [
tweaked_toy_ec,
- '--sourcepath=%s' % self.test_sourcepath,
- '--buildpath=%s' % self.test_buildpath,
- '--installpath=%s' % self.test_installpath,
'--dry-run',
'--robot=%s' % ecs_path,
]
@@ -2657,18 +2798,21 @@ def test_try(self):
]
for extra_args, mod in test_cases:
- outtxt = self.eb_main(args + extra_args, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args + extra_args, verbose=True, raise_error=True)
mod_regex = re.compile(r"\(module: %s\)$" % mod, re.M)
self.assertTrue(mod_regex.search(outtxt), "Pattern %s found in %s" % (mod_regex.pattern, outtxt))
for extra_arg in ['--try-software=foo', '--try-toolchain=gompi', '--try-toolchain=gomp,2018a,-a-suffix']:
allargs = args + [extra_arg]
- self.assertErrorRegex(EasyBuildError, "problems validating the options",
- self.eb_main, allargs, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, "problems validating the options",
+ self.eb_main, allargs, raise_error=True)
# no --try used, so no tweaked easyconfig files are generated
allargs = args + ['--software-version=1.2.3', '--toolchain=gompi,2018a']
- self.assertErrorRegex(EasyBuildError, "version .* not available", self.eb_main, allargs, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, "version .* not available", self.eb_main, allargs, raise_error=True)
# Try changing only name or version of toolchain
args.pop(0) # Remove EC filename
@@ -2695,9 +2839,6 @@ def test_try_with_copy(self):
args = [
tweaked_toy_ec,
- '--sourcepath=%s' % self.test_sourcepath,
- '--buildpath=%s' % self.test_buildpath,
- '--installpath=%s' % self.test_installpath,
'--dry-run',
'--robot=%s' % ecs_path,
'--copy-ec',
@@ -2740,13 +2881,11 @@ def test_software_version_ordering(self):
args = [
'--software=GCC,4.10.1',
- '--sourcepath=%s' % self.test_sourcepath,
- '--buildpath=%s' % self.test_buildpath,
- '--installpath=%s' % self.test_installpath,
'--dry-run',
'--robot=%s:%s' % (ecs_path, self.test_prefix),
]
- out = self.eb_main(['--software=GCC,4.10.1'] + args[1:], raise_error=True)
+ with self.mocked_stdout_stderr():
+ out = self.eb_main(['--software=GCC,4.10.1'] + args[1:], raise_error=True)
regex = re.compile(r"GCC-4.10.1.eb \(module: GCC/4.10.1\)$", re.M)
self.assertTrue(regex.search(out), "Pattern '%s' found in: %s" % (regex.pattern, out))
@@ -2762,8 +2901,6 @@ def test_recursive_try(self):
args = [
tweaked_toy_ec,
'--sourcepath=%s' % sourcepath,
- '--buildpath=%s' % self.test_buildpath,
- '--installpath=%s' % self.test_installpath,
'--try-toolchain=gompi,2018a',
'--robot=%s' % ecs_path,
'--ignore-osdeps',
@@ -2771,7 +2908,8 @@ def test_recursive_try(self):
]
for extra_args in [[], ['--module-naming-scheme=HierarchicalMNS']]:
- outtxt = self.eb_main(args + extra_args, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args + extra_args, verbose=True, raise_error=True)
# toolchain GCC/4.7.2 (subtoolchain of gompi/2018a) should be listed (and present)
tc_regex = re.compile(r"^ \* \[x\] .*/GCC-6.4.0-2.28.eb \(module: .*GCC/6.4.0-2.28\)$", re.M)
@@ -2792,7 +2930,9 @@ def test_recursive_try(self):
# recursive try also when --(try-)software(-X) is involved
for extra_args in [[],
['--module-naming-scheme=HierarchicalMNS']]:
- outtxt = self.eb_main(args + extra_args + ['--try-software-version=1.2.3'], verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args + extra_args + ['--try-software-version=1.2.3'], verbose=True,
+ raise_error=True)
# toolchain GCC/6.4.0-2.28 (subtoolchain of gompi/2018a) should be listed (and present)
tc_regex = re.compile(r"^ \* \[x\] .*/GCC-6.4.0-2.28.eb \(module: .*GCC/6.4.0-2.28\)$", re.M)
@@ -2814,7 +2954,8 @@ def test_recursive_try(self):
# no recursive try if --disable-map-toolchains is involved
for extra_args in [['--try-software-version=1.2.3'], ['--software-version=1.2.3']]:
- outtxt = self.eb_main(args + ['--disable-map-toolchains'] + extra_args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args + ['--disable-map-toolchains'] + extra_args, raise_error=True)
for mod in ['toy/1.2.3-gompi-2018a', 'gompi/2018a', 'GCC/6.4.0-2.28']:
mod_regex = re.compile(r"\(module: %s\)$" % mod, re.M)
self.assertTrue(mod_regex.search(outtxt), "Pattern %s found in %s" % (mod_regex.pattern, outtxt))
@@ -2831,13 +2972,15 @@ def test_cleanup_builddir(self):
toy_ec,
'--force',
]
- self.eb_main(args, do_build=True, verbose=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True, verbose=True)
# make sure build directory is properly cleaned up after a successful build (default behavior)
self.assertFalse(os.path.exists(toy_buildpath), "Build dir %s removed after successful build" % toy_buildpath)
# make sure --disable-cleanup-builddir works
args.append('--disable-cleanup-builddir')
- self.eb_main(args, do_build=True, verbose=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True, verbose=True)
self.assertExists(toy_buildpath, "Build dir %s is retained when requested" % toy_buildpath)
shutil.rmtree(toy_buildpath)
@@ -2858,12 +3001,11 @@ def test_filter_deps(self):
os.environ['MODULEPATH'] = os.path.join(test_dir, 'modules')
args = [
ec_file,
- '--buildpath=%s' % self.test_buildpath,
- '--installpath=%s' % self.test_installpath,
'--robot=%s' % os.path.join(test_dir, 'easyconfigs'),
'--dry-run',
]
- outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
# note: using loose regex pattern when we expect no match, strict pattern when we do expect a match
self.assertTrue(re.search('module: FFTW/3.3.7-gompi', outtxt))
@@ -2875,7 +3017,8 @@ def test_filter_deps(self):
# filter deps (including a non-existing dep, i.e. zlib)
args.extend(['--filter-deps', 'FFTW,ScaLAPACK,zlib'])
- outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
self.assertFalse(re.search('module: FFTW/3.3.7-gompi', outtxt))
self.assertFalse(re.search('module: ScaLAPACK/2.0.2-gompi', outtxt))
self.assertFalse(re.search('module: zlib', outtxt))
@@ -2884,7 +3027,8 @@ def test_filter_deps(self):
# filter specific version of deps
args[-1] = 'FFTW=3.2.3,zlib,ScaLAPACK=2.0.2'
- outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
self.assertTrue(re.search('module: FFTW/3.3.7-gompi', outtxt))
self.assertFalse(re.search('module: ScaLAPACK', outtxt))
self.assertFalse(re.search('module: zlib', outtxt))
@@ -2892,7 +3036,8 @@ def test_filter_deps(self):
write_file(self.logfile, '')
args[-1] = 'zlib,FFTW=3.3.7,ScaLAPACK=2.0.1'
- outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
self.assertFalse(re.search('module: FFTW', outtxt))
self.assertTrue(re.search('module: ScaLAPACK/2.0.2-gompi', outtxt))
self.assertFalse(re.search('module: zlib', outtxt))
@@ -2901,7 +3046,8 @@ def test_filter_deps(self):
# filter deps with version range: only filter FFTW 3.x, ScaLAPACK 1.x
args[-1] = 'zlib,ScaLAPACK=]1.0:2.0[,FFTW=[3.0:4.0['
- outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
self.assertFalse(re.search('module: FFTW', outtxt))
self.assertTrue(re.search('module: ScaLAPACK/2.0.2-gompi', outtxt))
self.assertFalse(re.search('module: zlib', outtxt))
@@ -2910,7 +3056,8 @@ def test_filter_deps(self):
# also test open ended ranges
args[-1] = 'zlib,ScaLAPACK=[1.0:,FFTW=:4.0['
- outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
self.assertFalse(re.search('module: FFTW', outtxt))
self.assertFalse(re.search('module: ScaLAPACK', outtxt))
self.assertFalse(re.search('module: zlib', outtxt))
@@ -2918,14 +3065,16 @@ def test_filter_deps(self):
write_file(self.logfile, '')
args[-1] = 'zlib,ScaLAPACK=[2.1:,FFTW=:3.0['
- outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
self.assertTrue(re.search('module: FFTW/3.3.7-gompi', outtxt))
self.assertTrue(re.search('module: ScaLAPACK/2.0.2-gompi', outtxt))
self.assertFalse(re.search('module: zlib', outtxt))
# test corner cases where version to filter in equal to low/high range limit
args[-1] = 'FFTW=[3.3.7:4.0],zlib,ScaLAPACK=[1.0:2.0.2]'
- outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
self.assertFalse(re.search('module: FFTW', outtxt))
self.assertFalse(re.search('module: ScaLAPACK', outtxt))
self.assertFalse(re.search('module: zlib', outtxt))
@@ -2934,7 +3083,8 @@ def test_filter_deps(self):
# FFTW & ScaLAPACK versions are not included in range, so no filtering
args[-1] = 'FFTW=]3.3.7:4.0],zlib,ScaLAPACK=[1.0:2.0.2['
- outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
self.assertTrue(re.search('module: FFTW/3.3.7-gompi', outtxt))
self.assertTrue(re.search('module: ScaLAPACK/2.0.2-gompi', outtxt))
self.assertFalse(re.search('module: zlib', outtxt))
@@ -2943,14 +3093,16 @@ def test_filter_deps(self):
# also test mix of ranges & specific versions
args[-1] = 'FFTW=3.3.7,zlib,ScaLAPACK=[1.0:2.0.2['
- outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
self.assertFalse(re.search('module: FFTW', outtxt))
self.assertTrue(re.search('module: ScaLAPACK/2.0.2-gompi', outtxt))
self.assertFalse(re.search('module: zlib', outtxt))
write_file(self.logfile, '')
args[-1] = 'FFTW=]3.3.7:4.0],zlib,ScaLAPACK=2.0.2'
- outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
self.assertTrue(re.search('module: FFTW/3.3.7-gompi', outtxt))
self.assertFalse(re.search('module: ScaLAPACK', outtxt))
self.assertFalse(re.search('module: zlib', outtxt))
@@ -2961,7 +3113,8 @@ def test_filter_deps(self):
ec_file = os.path.join(test_dir, 'easyconfigs', 'test_ecs', 'f', 'foss', 'foss-2018a-broken.eb')
args[0] = ec_file
args[-1] = 'FFTW=3.3.7,CMake=:2.8.10],zlib'
- outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
# dictionaries can be printed in any order
regexp = "filtered out dependency.*('name': 'CMake'.*'version': '2.8.10'|'version': '2.8.10'.*'name': 'CMake')"
self.assertTrue(re.search(regexp, outtxt))
@@ -2971,7 +3124,8 @@ def test_filter_deps(self):
ec_file = os.path.join(test_dir, 'easyconfigs', 'test_ecs', 'f', 'foss', 'foss-2018a-broken.eb')
args[0] = ec_file
args[-1] = 'FFTW=3.3.7,CMake=:2.8.10],zlib'
- outtxt = self.eb_main(args + ['--minimal-toolchains'], do_build=True, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args + ['--minimal-toolchains'], do_build=True, verbose=True, raise_error=True)
self.assertTrue(re.search(regexp, outtxt))
def test_hide_deps(self):
@@ -2981,12 +3135,11 @@ def test_hide_deps(self):
os.environ['MODULEPATH'] = os.path.join(test_dir, 'modules')
args = [
ec_file,
- '--buildpath=%s' % self.test_buildpath,
- '--installpath=%s' % self.test_installpath,
'--robot=%s' % os.path.join(test_dir, 'easyconfigs'),
'--dry-run',
]
- outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
self.assertTrue(re.search('module: GCC/6.4.0-2.28', outtxt))
self.assertTrue(re.search('module: OpenMPI/2.1.2-GCC-6.4.0-2.28', outtxt))
self.assertTrue(re.search('module: OpenBLAS/0.2.20-GCC-6.4.0-2.28', outtxt))
@@ -3000,7 +3153,8 @@ def test_hide_deps(self):
# hide deps (including a non-existing dep, i.e. zlib)
args.append('--hide-deps=FFTW,ScaLAPACK,zlib')
- outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
self.assertTrue(re.search('module: GCC/6.4.0-2.28', outtxt))
self.assertTrue(re.search('module: OpenMPI/2.1.2-GCC-6.4.0-2.28', outtxt))
self.assertTrue(re.search('module: OpenBLAS/0.2.20-GCC-6.4.0-2.28', outtxt))
@@ -3018,9 +3172,11 @@ def test_hide_toolchains(self):
args = [
ec_file,
'--dry-run',
+ '--robot',
'--hide-toolchains=GCC',
]
- outtxt = self.eb_main(args)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args)
self.assertTrue(re.search(r'module: GCC/\.4\.9\.2', outtxt))
self.assertTrue(re.search(r'module: gzip/1\.6-GCC-4\.9\.2', outtxt))
@@ -3221,16 +3377,13 @@ def toy(extra_args=None):
eb_file = os.path.join(os.path.dirname(__file__), 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb')
args = [
eb_file,
- '--sourcepath=%s' % self.test_sourcepath,
- '--buildpath=%s' % self.test_buildpath,
- '--installpath=%s' % self.test_installpath,
'--force',
'--debug',
]
if extra_args is not None:
args.extend(extra_args)
- self.eb_main(args, do_build=True, raise_error=True, verbose=True)
-
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True, raise_error=True, verbose=True)
software_path = os.path.join(self.test_installpath, 'software', 'toy', '0.0')
test_report_path_pattern = os.path.join(software_path, 'easybuild', 'easybuild-toy-0.0*test_report.md')
test_report_txt = read_file(glob.glob(test_report_path_pattern)[0])
@@ -3278,7 +3431,8 @@ def test_robot(self):
'--robot-paths=%s' % test_ecs_path,
]
error_regex = r"Missing modules for dependencies .*: toy/\.0.0-deps"
- self.assertErrorRegex(EasyBuildError, error_regex, self.eb_main, args, raise_error=True, do_build=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_regex, self.eb_main, args, raise_error=True, do_build=True)
# enable robot, but without passing path required to resolve toy dependency => FAIL
# note that --dry-run is now robust against missing easyconfig, so shouldn't use it here
@@ -3286,11 +3440,13 @@ def test_robot(self):
eb_file,
'--robot',
]
- self.assertErrorRegex(EasyBuildError, 'Missing dependencies', self.eb_main, args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, 'Missing dependencies', self.eb_main, args, raise_error=True)
# add path to test easyconfigs to robot paths, so dependencies can be resolved
args.append('--dry-run')
- self.eb_main(args + ['--robot-paths=%s' % test_ecs_path], raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args + ['--robot-paths=%s' % test_ecs_path], raise_error=True)
# copy test easyconfigs to easybuild/easyconfigs subdirectory of temp directory
# to check whether easyconfigs install path is auto-included in robot path
@@ -3302,7 +3458,8 @@ def test_robot(self):
del os.environ['EASYBUILD_ROBOT_PATHS']
orig_sys_path = sys.path[:]
sys.path.insert(0, tmpdir)
- self.eb_main(args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, raise_error=True)
shutil.rmtree(tmpdir)
sys.path[:] = orig_sys_path
@@ -3314,7 +3471,8 @@ def test_robot(self):
'--robot-paths=%s' % os.path.join(tmpdir, 'easybuild', 'easyconfigs'),
'--dry-run',
]
- outtxt = self.eb_main(args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, raise_error=True)
ecfiles = [
'g/GCC/GCC-4.6.3.eb',
@@ -3334,34 +3492,40 @@ def test_robot_path_check(self):
error_pattern = "Argument passed to --robot is not an existing directory"
for robot in ['--robot=foo', '--robot=%s' % empty_file]:
args = ['toy-0.0.eb', '--dry-run', robot]
- self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, raise_error=True)
toy_regex = re.compile('module: toy/0.0')
# works fine is directory exists
args = ['toy-0.0.eb', '-r', self.test_prefix, '--dry-run']
- outtxt = self.eb_main(args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, raise_error=True)
self.assertTrue(toy_regex.search(outtxt), "Pattern '%s' not found in: %s" % (toy_regex.pattern, outtxt))
# no error when name of an easyconfig file is specified to --robot (even if it doesn't exist)
args = ['--dry-run', '--robot', 'toy-0.0.eb']
- outtxt = self.eb_main(args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, raise_error=True)
self.assertTrue(toy_regex.search(outtxt), "Pattern '%s' not found in: %s" % (toy_regex.pattern, outtxt))
# different error when a non-existing easyconfig file is specified to --robot
args = ['--dry-run', '--robot', 'no_such_easyconfig_file_in_robot_search_path.eb']
error_pattern = "One or more files not found: no_such_easyconfig_file_in_robot_search_path.eb"
- self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, raise_error=True)
for robot in ['-r%s' % self.test_prefix, '--robot=%s' % self.test_prefix]:
args = ['toy-0.0.eb', '--dry-run', robot]
- outtxt = self.eb_main(args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, raise_error=True)
self.assertTrue(toy_regex.search(outtxt), "Pattern '%s' not found in: %s" % (toy_regex.pattern, outtxt))
# no problem with using combos of single-letter options with -r included, no matter the order
for arg in ['-Dr', '-rD', '-frkD', '-rfDk']:
args = ['toy-0.0.eb', arg]
- outtxt = self.eb_main(args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, raise_error=True)
self.assertTrue(toy_regex.search(outtxt), "Pattern '%s' not found in: %s" % (toy_regex.pattern, outtxt))
# unknown options are still recognized, even when used in single-letter combo arguments
@@ -3377,7 +3541,8 @@ def test_missing_cfgfile(self):
"""Test behaviour when non-existing config file is specified."""
args = ['--configfiles=/no/such/cfgfile.foo']
error_regex = "parseconfigfiles: configfile .* not found"
- self.assertErrorRegex(EasyBuildError, error_regex, self.eb_main, args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_regex, self.eb_main, args, raise_error=True)
def test_show_default_moduleclasses(self):
"""Test --show-default-moduleclasses."""
@@ -3389,7 +3554,8 @@ def test_show_default_moduleclasses(self):
'--show-default-moduleclasses',
]
write_file(self.logfile, '')
- self.eb_main(args, logfile=dummylogfn, verbose=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, logfile=dummylogfn, verbose=True)
logtxt = read_file(self.logfile)
lst = ["\t%s:[ ]*%s" % (c, d.replace('(', '\\(').replace(')', '\\)')) for (c, d) in DEFAULT_MODULECLASSES]
@@ -3424,12 +3590,13 @@ def test_show_default_configfiles(self):
'',
"* user-level: ${XDG_CONFIG_HOME:-$HOME/.config}/easybuild/config.cfg",
" -> %s",
- "* system-level: ${XDG_CONFIG_DIRS:-/etc}/easybuild.d/*.cfg",
+ "* system-level: ${XDG_CONFIG_DIRS:-/etc/xdg}/easybuild.d/*.cfg",
" -> %s/easybuild.d/*.cfg => ",
])
write_file(self.logfile, '')
- self.eb_main(args, logfile=dummylogfn, verbose=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, logfile=dummylogfn, verbose=True)
logtxt = read_file(self.logfile)
homecfgfile = os.path.join(os.environ['HOME'], '.config', 'easybuild', 'config.cfg')
@@ -3438,18 +3605,19 @@ def test_show_default_configfiles(self):
homecfgfile_str += " => found"
else:
homecfgfile_str += " => not found"
- expected = expected_tmpl % ('(not set)', '(not set)', homecfgfile_str, '{/etc}')
+ expected = expected_tmpl % ('(not set)', '(not set)', homecfgfile_str, '{/etc/xdg}')
self.assertIn(expected, logtxt)
# to predict the full output, we need to take control over $HOME and $XDG_CONFIG_DIRS
os.environ['HOME'] = self.test_prefix
- xdg_config_dirs = os.path.join(self.test_prefix, 'etc')
+ xdg_config_dirs = os.path.join(self.test_prefix, 'etc', 'xdg')
os.environ['XDG_CONFIG_DIRS'] = xdg_config_dirs
expected_tmpl += '\n'.join([
"%s",
'',
- "Default list of existing configuration files (%d): %s",
+ "Default list of existing configuration files (%d, most important last):",
+ "%s",
])
# put dummy cfgfile in place in $HOME (to predict last line of output which only lists *existing* files)
@@ -3459,7 +3627,8 @@ def test_show_default_configfiles(self):
reload(easybuild.tools.options)
write_file(self.logfile, '')
- self.eb_main(args, logfile=dummylogfn, verbose=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, logfile=dummylogfn, verbose=True)
logtxt = read_file(self.logfile)
expected = expected_tmpl % ('(not set)', xdg_config_dirs, "%s => found" % homecfgfile, '{%s}' % xdg_config_dirs,
'(no matches)', 1, homecfgfile)
@@ -3467,12 +3636,12 @@ def test_show_default_configfiles(self):
xdg_config_home = os.path.join(self.test_prefix, 'home')
os.environ['XDG_CONFIG_HOME'] = xdg_config_home
- xdg_config_dirs = [os.path.join(self.test_prefix, 'etc'), os.path.join(self.test_prefix, 'moaretc')]
+ xdg_config_dirs = [os.path.join(self.test_prefix, 'moaretc'), os.path.join(self.test_prefix, 'etc', 'xdg')]
os.environ['XDG_CONFIG_DIRS'] = os.pathsep.join(xdg_config_dirs)
# put various dummy cfgfiles in place
cfgfiles = [
- os.path.join(self.test_prefix, 'etc', 'easybuild.d', 'config.cfg'),
+ os.path.join(self.test_prefix, 'etc', 'xdg', 'easybuild.d', 'config.cfg'),
os.path.join(self.test_prefix, 'moaretc', 'easybuild.d', 'bar.cfg'),
os.path.join(self.test_prefix, 'moaretc', 'easybuild.d', 'foo.cfg'),
os.path.join(xdg_config_home, 'easybuild', 'config.cfg'),
@@ -3483,12 +3652,13 @@ def test_show_default_configfiles(self):
reload(easybuild.tools.options)
write_file(self.logfile, '')
- self.eb_main(args, logfile=dummylogfn, verbose=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, logfile=dummylogfn, verbose=True)
logtxt = read_file(self.logfile)
expected = expected_tmpl % (xdg_config_home, os.pathsep.join(xdg_config_dirs),
"%s => found" % os.path.join(xdg_config_home, 'easybuild', 'config.cfg'),
'{' + ', '.join(xdg_config_dirs) + '}',
- ', '.join(cfgfiles[:-1]), 4, ', '.join(cfgfiles))
+ ', '.join(cfgfiles[1:3]+[cfgfiles[0]]), 4, ', '.join(cfgfiles))
self.assertIn(expected, logtxt)
del os.environ['XDG_CONFIG_DIRS']
@@ -3556,7 +3726,8 @@ def test_xxx_include_easyblocks(self):
'--list-easyblocks=detailed',
'--unittest-file=%s' % self.logfile,
]
- self.eb_main(args, logfile=dummylogfn, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, logfile=dummylogfn, raise_error=True)
logtxt = read_file(self.logfile)
test_easyblocks = os.path.dirname(os.path.abspath(__file__))
@@ -3613,7 +3784,8 @@ def __init__(self, *args, **kwargs):
'--list-easyblocks=detailed',
'--unittest-file=%s' % self.logfile,
]
- self.eb_main(args, logfile=dummylogfn, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, logfile=dummylogfn, raise_error=True)
logtxt = read_file(self.logfile)
path_pattern = os.path.join(self.test_prefix, '.*', 'included-easyblocks-.*', 'easybuild', 'easyblocks',
@@ -3672,7 +3844,8 @@ def test_xxx_include_generic_easyblocks(self):
'--list-easyblocks=detailed',
'--unittest-file=%s' % self.logfile,
]
- self.eb_main(args, logfile=dummylogfn, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, logfile=dummylogfn, raise_error=True)
logtxt = read_file(self.logfile)
path_pattern = os.path.join(self.test_prefix, '.*', 'included-easyblocks-.*', 'easybuild', 'easyblocks',
@@ -3717,7 +3890,8 @@ def test_xxx_include_generic_easyblocks(self):
write_file(os.path.join(self.test_prefix, 'generictest.py'), txt)
args[0] = '--include-easyblocks=%s/*.py' % self.test_prefix
- self.eb_main(args, logfile=dummylogfn, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, logfile=dummylogfn, raise_error=True)
logtxt = read_file(self.logfile)
mod_pattern = 'easybuild.easyblocks.generic.generictest'
@@ -3759,7 +3933,7 @@ def test_github_xxx_include_easyblocks_from_pr(self):
args = [
'--include-easyblocks=%s/*.py' % self.test_prefix, # this shouldn't interfere
- '--include-easyblocks-from-pr=1915', # a PR for CMakeMake easyblock
+ '--include-easyblocks-from-pr=3399', # a PR for CMakeMake easyblock
'--list-easyblocks=detailed',
'--unittest-file=%s' % self.logfile,
'--github-user=%s' % GITHUB_TEST_ACCOUNT,
@@ -3773,7 +3947,7 @@ def test_github_xxx_include_easyblocks_from_pr(self):
logtxt = read_file(self.logfile)
self.assertFalse(stderr)
- self.assertEqual(stdout, "== easyblock cmakemake.py included from PR #1915\n")
+ self.assertEqual(stdout, "== easyblock cmakemake.py included from PR #3399\n")
# easyblock included from pr is found
path_pattern = os.path.join(self.test_prefix, '.*', 'included-easyblocks-.*', 'easybuild', 'easyblocks')
@@ -3803,7 +3977,7 @@ def test_github_xxx_include_easyblocks_from_pr(self):
# including the same easyblock twice should work and give priority to the one from the PR
args = [
'--include-easyblocks=%s/*.py' % self.test_prefix,
- '--include-easyblocks-from-pr=1915',
+ '--include-easyblocks-from-pr=3399',
'--list-easyblocks=detailed',
'--unittest-file=%s' % self.logfile,
'--github-user=%s' % GITHUB_TEST_ACCOUNT,
@@ -3817,9 +3991,9 @@ def test_github_xxx_include_easyblocks_from_pr(self):
logtxt = read_file(self.logfile)
expected = "WARNING: One or more easyblocks included from multiple locations: "
- expected += "cmakemake.py (the one(s) from PR #1915 will be used)"
+ expected += "cmakemake.py (the one(s) from PR #3399 will be used)"
self.assertEqual(stderr.strip(), expected)
- self.assertEqual(stdout, "== easyblock cmakemake.py included from PR #1915\n")
+ self.assertEqual(stdout, "== easyblock cmakemake.py included from PR #3399\n")
# easyblock included from pr is found
path_pattern = os.path.join(self.test_prefix, '.*', 'included-easyblocks-.*', 'easybuild', 'easyblocks')
@@ -3853,8 +4027,8 @@ def test_github_xxx_include_easyblocks_from_pr(self):
write_file(self.logfile, '')
args = [
- '--from-pr=10487', # PR for CMake easyconfig
- '--include-easyblocks-from-pr=1936,2204', # PRs for EB_CMake and Siesta easyblock
+ '--from-pr=22227', # PR for Togl easyconfig
+ '--include-easyblocks-from-pr=3563,3634', # PRs for ConfigureMake and GROMACS easyblock
'--unittest-file=%s' % self.logfile,
'--github-user=%s' % GITHUB_TEST_ACCOUNT,
'--extended-dry-run',
@@ -3868,25 +4042,32 @@ def test_github_xxx_include_easyblocks_from_pr(self):
logtxt = read_file(self.logfile)
self.assertFalse(stderr)
- self.assertEqual(stdout, "== easyblock cmake.py included from PR #1936\n" +
- "== easyblock siesta.py included from PR #2204\n")
+ self.assertEqual(stdout, "== easyblock configuremake.py included from PR #3563\n" +
+ "== easyblock gromacs.py included from PR #3634\n")
# easyconfig from pr is found
- ec_pattern = os.path.join(self.test_prefix, '.*', 'files_pr10487', 'c', 'CMake',
- 'CMake-3.16.4-GCCcore-9.3.0.eb')
+ ec_pattern = os.path.join(self.test_prefix, '.*', 'files_pr22227', 't', 'Togl',
+ 'Togl-2.0-GCCcore-13.3.0.eb')
ec_regex = re.compile(r"Parsing easyconfig file %s" % ec_pattern, re.M)
self.assertTrue(ec_regex.search(logtxt), "Pattern '%s' found in: %s" % (ec_regex.pattern, logtxt))
# easyblock included from pr is found
- eb_regex = re.compile(r"Successfully obtained EB_CMake class instance from easybuild.easyblocks.cmake", re.M)
+ eb_regex = re.compile(
+ r"Derived full easyblock module path for ConfigureMake: easybuild.easyblocks.generic.configuremake", re.M)
self.assertTrue(eb_regex.search(logtxt), "Pattern '%s' found in: %s" % (eb_regex.pattern, logtxt))
# easyblock is found via get_easyblock_class
- klass = get_easyblock_class('EB_CMake')
+ klass = get_easyblock_class('ConfigureMake')
self.assertTrue(issubclass(klass, EasyBlock), "%s is an EasyBlock derivative class" % klass)
# 'undo' import of easyblocks
- del sys.modules['easybuild.easyblocks.cmake']
+ del sys.modules['easybuild.easyblocks.gromacs']
+ del sys.modules['easybuild.easyblocks.generic.configuremake']
+ sys.path[:] = orig_local_sys_path
+ import easybuild.easyblocks
+ reload(easybuild.easyblocks)
+ import easybuild.easyblocks.generic
+ reload(easybuild.easyblocks.generic)
def mk_eb_test_cmd(self, args):
"""Construct test command for 'eb' with given options."""
@@ -3917,8 +4098,9 @@ def test_include_module_naming_schemes(self):
# try and make sure top-level directory is in $PYTHONPATH if it isn't yet
pythonpath = self.env_pythonpath
- _, ec = run_cmd("cd %s; python -c 'import easybuild.framework'" % self.test_prefix, log_ok=False)
- if ec > 0:
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd("cd {self.test_prefix}; python -c 'import easybuild.framework'", fail_on_error=False)
+ if res.exit_code != 0:
pythonpath = '%s:%s' % (topdir, pythonpath)
fd, dummylogfn = tempfile.mkstemp(prefix='easybuild-dummy', suffix='.log')
@@ -3932,8 +4114,10 @@ def test_include_module_naming_schemes(self):
# TestIncludedMNS module naming scheme is not available by default
args = ['--avail-module-naming-schemes']
test_cmd = self.mk_eb_test_cmd(args)
- logtxt, _ = run_cmd(test_cmd, simple=False)
- self.assertFalse(mns_regex.search(logtxt), "Unexpected pattern '%s' found in: %s" % (mns_regex.pattern, logtxt))
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(test_cmd)
+ self.assertFalse(mns_regex.search(res.output),
+ f"Unexpected pattern '{mns_regex.pattern}' found in: {res.output}")
# include extra test MNS
mns_txt = '\n'.join([
@@ -3948,8 +4132,10 @@ def test_include_module_naming_schemes(self):
args.append('--include-module-naming-schemes=%s/*.py' % self.test_prefix)
test_cmd = self.mk_eb_test_cmd(args)
- logtxt, _ = run_cmd(test_cmd, simple=False)
- self.assertTrue(mns_regex.search(logtxt), "Pattern '%s' *not* found in: %s" % (mns_regex.pattern, logtxt))
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(test_cmd)
+ self.assertTrue(mns_regex.search(res.output),
+ f"Pattern '{mns_regex.pattern}' *not* found in: {res.output}")
def test_use_included_module_naming_scheme(self):
"""Test using an included module naming scheme."""
@@ -3978,11 +4164,13 @@ def test_use_included_module_naming_scheme(self):
# selecting a module naming scheme that doesn't exist leads to 'invalid choice'
error_regex = "Selected module naming scheme \'AnotherTestIncludedMNS\' is unknown"
- self.assertErrorRegex(EasyBuildError, error_regex, self.eb_main, args, logfile=dummylogfn,
- raise_error=True, raise_systemexit=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_regex, self.eb_main, args, logfile=dummylogfn,
+ raise_error=True, raise_systemexit=True)
args.append('--include-module-naming-schemes=%s/*.py' % self.test_prefix)
- self.eb_main(args, logfile=dummylogfn, do_build=True, raise_error=True, raise_systemexit=True, verbose=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, logfile=dummylogfn, do_build=True, raise_error=True, raise_systemexit=True, verbose=True)
toy_mod = os.path.join(self.test_installpath, 'modules', 'all', 'toy', '0.0')
if get_module_syntax() == 'Lua':
toy_mod += '.lua'
@@ -4002,8 +4190,9 @@ def test_include_toolchains(self):
# try and make sure top-level directory is in $PYTHONPATH if it isn't yet
pythonpath = self.env_pythonpath
- _, ec = run_cmd("cd %s; python -c 'import easybuild.framework'" % self.test_prefix, log_ok=False)
- if ec > 0:
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(f"cd {self.test_prefix}; python -c 'import easybuild.framework'", fail_on_error=False)
+ if res.exit_code != 0:
pythonpath = '%s:%s' % (topdir, pythonpath)
fd, dummylogfn = tempfile.mkstemp(prefix='easybuild-dummy', suffix='.log')
@@ -4020,8 +4209,10 @@ def test_include_toolchains(self):
# TestIncludedCompiler is not available by default
args = ['--list-toolchains']
test_cmd = self.mk_eb_test_cmd(args)
- logtxt, _ = run_cmd(test_cmd, simple=False)
- self.assertFalse(tc_regex.search(logtxt), "Pattern '%s' *not* found in: %s" % (tc_regex.pattern, logtxt))
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(test_cmd)
+ self.assertFalse(tc_regex.search(res.output),
+ f"Pattern '{tc_regex.pattern}' *not* found in: {res.output}")
# include extra test toolchain
comp_txt = '\n'.join([
@@ -4041,8 +4232,10 @@ def test_include_toolchains(self):
args.append('--include-toolchains=%s/*.py,%s/*/*.py' % (self.test_prefix, self.test_prefix))
test_cmd = self.mk_eb_test_cmd(args)
- logtxt, _ = run_cmd(test_cmd, simple=False)
- self.assertTrue(tc_regex.search(logtxt), "Pattern '%s' found in: %s" % (tc_regex.pattern, logtxt))
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(test_cmd)
+ self.assertTrue(tc_regex.search(res.output),
+ f"Pattern '{tc_regex.pattern}' found in: {res.output}")
def test_cleanup_tmpdir(self):
"""Test --cleanup-tmpdir."""
@@ -4106,17 +4299,17 @@ def test_github_review_pr(self):
self.mock_stdout(True)
self.mock_stderr(True)
- # PR for gzip 1.10 easyconfig, see https://github.com/easybuilders/easybuild-easyconfigs/pull/9921
+ # PR for bwidget 1.10.1 easyconfig, see https://github.com/easybuilders/easybuild-easyconfigs/pull/22227
args = [
'--color=never',
'--github-user=%s' % GITHUB_TEST_ACCOUNT,
- '--review-pr=9921',
+ '--review-pr=22227',
]
self.eb_main(args, raise_error=True)
txt = self.get_stdout()
self.mock_stdout(False)
self.mock_stderr(False)
- regex = re.compile(r"^Comparing gzip-1.10-\S* with gzip-1.10-")
+ regex = re.compile(r"^Comparing bwidget-1.10.1-\S* with bwidget-")
self.assertTrue(regex.search(txt), "Pattern '%s' not found in: %s" % (regex.pattern, txt))
self.mock_stdout(True)
@@ -4248,6 +4441,7 @@ def test_minimal_toolchains(self):
'--minimal-toolchains',
'--module-naming-scheme=HierarchicalMNS',
'--dry-run',
+ '--robot',
]
self.mock_stdout(True)
self.eb_main(args, do_build=True, raise_error=True, testing=False)
@@ -4263,17 +4457,9 @@ def test_extended_dry_run(self):
ec_file = os.path.join(os.path.dirname(__file__), 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb')
args = [
ec_file,
- '--sourcepath=%s' % self.test_sourcepath,
- '--buildpath=%s' % self.test_buildpath,
- '--installpath=%s' % self.test_installpath,
'--debug',
+ '--disable-rpath',
]
- # *no* output in testing mode (honor 'silent')
- self.mock_stdout(True)
- self.eb_main(args + ['--extended-dry-run'], do_build=True, raise_error=True, testing=True)
- stdout = self.get_stdout()
- self.mock_stdout(False)
- self.assertEqual(len(stdout), 0)
msg_regexs = [
re.compile(r"the actual build \& install procedure that will be performed may diverge", re.M),
@@ -4322,7 +4508,8 @@ def test_last_log(self):
# run something that fails first, we need a log file to find
last_log_path = os.path.join(tempfile.gettempdir(), 'eb-tmpdir0', 'easybuild-last.log')
mkdir(os.path.dirname(last_log_path))
- self.eb_main(['thisisaneasyconfigthatdoesnotexist.eb'], logfile=last_log_path, raise_error=False)
+ with self.mocked_stdout_stderr():
+ self.eb_main(['thisisaneasyconfigthatdoesnotexist.eb'], logfile=last_log_path, raise_error=False)
# $TMPDIR determines path to build log, we need to get it right to make the test check what we want it to
os.environ['TMPDIR'] = tmpdir
@@ -4373,14 +4560,14 @@ def _assert_regexs(self, regexs, txt, assert_true=True):
self.assertFalse(regex.search(txt), "Pattern '%s' NOT found in: %s" % (regex.pattern, txt))
def _run_mock_eb(self, args, strip=False, **kwargs):
- """Helper function to mock easybuild runs"""
- self.mock_stdout(True)
- self.mock_stderr(True)
- self.eb_main(args, **kwargs)
- stdout_txt = self.get_stdout()
- stderr_txt = self.get_stderr()
- self.mock_stdout(False)
- self.mock_stderr(False)
+ """Helper function to mock easybuild runs
+
+ Return (stdout, stderr) optionally stripped of whitespace at start/end
+ """
+ with self.mocked_stdout_stderr() as (stdout, stderr):
+ self.eb_main(args, **kwargs)
+ stdout_txt = stdout.getvalue()
+ stderr_txt = stderr.getvalue()
if strip:
stdout_txt = stdout_txt.strip()
stderr_txt = stderr_txt.strip()
@@ -4465,7 +4652,7 @@ def test_new_branch_github(self):
def test_github_new_pr_from_branch(self):
"""Test --new-pr-from-branch."""
if self.github_token is None:
- print("Skipping test_new_pr_from_branch, no GitHub token available?")
+ print("Skipping test_github_new_pr_from_branch, no GitHub token available?")
return
# see https://github.com/boegel/easybuild-easyconfigs/tree/test_new_pr_from_branch_DO_NOT_REMOVE
@@ -4497,8 +4684,8 @@ def test_github_new_pr_from_branch(self):
r"^\* from: boegel/easybuild-easyconfigs:test_new_pr_from_branch_DO_NOT_REMOVE$",
r'^\* title: "\{tools\}\[system/system\] toy v0\.0"$',
r'^"an easyconfig for toy"$',
- r"^ 1 file changed, 32 insertions\(\+\)$",
- r"^\* overview of changes:\n easybuild/easyconfigs/t/toy/toy-0\.0\.eb | 32",
+ r"^ 1 file changed, [0-9]+ insertions\(\+\)$",
+ r"^\* overview of changes:\n easybuild/easyconfigs/t/toy/toy-0\.0\.eb | [0-9]+",
]
self._assert_regexs(regexs, txt)
@@ -4526,7 +4713,7 @@ def test_update_branch_github(self):
r"^== fetching branch 'develop' from https://github.com/%s.git\.\.\." % full_repo,
r"^== copying files to .*/git-working-dir.*/easybuild-easyconfigs...",
r"^== pushing branch 'develop' to remote '.*' \(git@github.com:%s.git\) \[DRY RUN\]" % full_repo,
- r"^Overview of changes:\n.*/easyconfigs/t/toy/toy-0.0.eb \| 32",
+ r"^Overview of changes:\n.*/easyconfigs/t/toy/toy-0.0.eb \| [0-9]+",
r"== pushed updated branch 'develop' to boegel/easybuild-easyconfigs \[DRY RUN\]",
]
self._assert_regexs(regexs, txt)
@@ -4599,7 +4786,7 @@ def test_github_new_update_pr(self):
'--git-working-dirs-path=%s' % git_working_dir,
':bzip2-1.0.6.eb',
])
- error_msg = "A meaningful commit message must be specified via --pr-commit-msg"
+ error_msg = "A meaningful commit message must be specified via --pr-commit-msg.*\nDeleted: bzip2-1.0.6.eb"
self.mock_stdout(True)
self.assertErrorRegex(EasyBuildError, error_msg, self.eb_main, args, raise_error=True, testing=False)
@@ -4678,7 +4865,8 @@ def test_github_new_update_pr(self):
gcc_ec,
'-D',
]
- error_msg = "A meaningful commit message must be specified via --pr-commit-msg"
+ error_msg = "A meaningful commit message must be specified via --pr-commit-msg.*\n"
+ error_msg += "Modified: " + os.path.basename(gcc_ec)
self.mock_stdout(True)
self.assertErrorRegex(EasyBuildError, error_msg, self.eb_main, args, raise_error=True)
self.mock_stdout(False)
@@ -5059,25 +5247,25 @@ def test_github_merge_pr(self):
# note: we frequently need to change to a more recent PR here,
# to avoid that this test starts failing because commit status is set to None for old commits
del args[-1]
- # easyconfig PR for EasyBuild v4.8.2
- args[1] = '19105'
+ # easyconfig PR for EasyBuild v5.0.0
+ args[1] = '22405'
stdout, stderr = self._run_mock_eb(args, do_build=True, raise_error=True, testing=False)
expected_stdout = '\n'.join([
- "Checking eligibility of easybuilders/easybuild-easyconfigs PR #19105 for merging...",
+ "Checking eligibility of easybuilders/easybuild-easyconfigs PR #22405 for merging...",
"* targets develop branch: OK",
"* test suite passes: OK",
"* last test report is successful: OK",
"* no pending change requests: OK",
- "* approved review: OK (by SebastianAchilles)",
- "* milestone is set: OK (4.9.0)",
+ "* approved review: OK (by verdurin)",
+ "* milestone is set: OK (5.0.0)",
"* mergeable state is clean: PR is already merged",
'',
"Review OK, merging pull request!",
'',
- "[DRY RUN] Adding comment to easybuild-easyconfigs issue #19105: 'Going in, thanks @boegel!'",
- "[DRY RUN] Merged easybuilders/easybuild-easyconfigs pull request #19105",
+ "[DRY RUN] Adding comment to easybuild-easyconfigs issue #22405: 'Going in, thanks @PetrKralCZ!'",
+ "[DRY RUN] Merged easybuilders/easybuild-easyconfigs pull request #22405",
])
expected_stderr = ''
self.assertEqual(stderr.strip(), expected_stderr)
@@ -5086,7 +5274,7 @@ def test_github_merge_pr(self):
# --merge-pr also works on easyblocks (& framework) PRs
args = [
'--merge-pr',
- '2995',
+ '3582',
'--pr-target-repo=easybuild-easyblocks',
'-D',
'--github-user=%s' % GITHUB_TEST_ACCOUNT,
@@ -5094,12 +5282,12 @@ def test_github_merge_pr(self):
stdout, stderr = self._run_mock_eb(args, do_build=True, raise_error=True, testing=False)
self.assertEqual(stderr.strip(), '')
expected_stdout = '\n'.join([
- "Checking eligibility of easybuilders/easybuild-easyblocks PR #2995 for merging...",
+ "Checking eligibility of easybuilders/easybuild-easyblocks PR #3582 for merging...",
"* targets develop branch: OK",
"* test suite passes: OK",
"* no pending change requests: OK",
- "* approved review: OK (by boegel)",
- "* milestone is set: OK (4.8.1)",
+ "* approved review: OK (by hajgato)",
+ "* milestone is set: OK (5.0.0)",
"* mergeable state is clean: PR is already merged",
'',
"Review OK, merging pull request!",
@@ -5116,7 +5304,8 @@ def test_github_empty_pr(self):
full_url = URL_SEPARATOR.join([GITHUB_RAW, GITHUB_EB_MAIN, GITHUB_EASYCONFIGS_REPO,
'develop/easybuild/easyconfigs/z/zlib/zlib-1.2.11-GCCcore-6.4.0.eb'])
ec_fn = os.path.basename(full_url)
- ec = download_file(ec_fn, full_url, path=os.path.join(self.test_prefix, ec_fn))
+ with self.mocked_stdout_stderr():
+ ec = download_file(ec_fn, full_url, path=os.path.join(self.test_prefix, ec_fn))
# try to open new pr with unchanged file
args = [
@@ -5173,6 +5362,7 @@ def test_show_config(self):
r"installpath\s* \(E\) = " + os.path.join(self.test_prefix, 'tmp.*'),
r"repositorypath\s* \(D\) = " + os.path.join(default_prefix, 'ebfiles_repo'),
r"robot-paths\s* \(E\) = " + os.path.join(test_dir, 'easyconfigs', 'test_ecs'),
+ r"rpath\s* \(D\) = " + ('False' if get_os_type() == DARWIN else 'True'),
r"sourcepath\s* \(E\) = " + os.path.join(test_dir, 'sandbox', 'sources'),
r"subdir-modules\s* \(F\) = mods",
]
@@ -5225,19 +5415,19 @@ def test_show_config_cfg_levels(self):
# configuring --modules-tool and --module-syntax on different levels should NOT cause problems
# cfr. bug report https://github.com/easybuilders/easybuild-framework/issues/2564
- os.environ['EASYBUILD_MODULES_TOOL'] = 'EnvironmentModulesC'
+ os.environ['EASYBUILD_MODULES_TOOL'] = 'EnvironmentModules'
args = [
'--module-syntax=Tcl',
'--show-config',
]
# set init_config to False to avoid that eb_main (called by _run_mock_eb) re-initialises configuration
- # this fails because $EASYBUILD_MODULES_TOOL=EnvironmentModulesC conflicts with default module syntax (Lua)
+ # this fails because $EASYBUILD_MODULES_TOOL=EnvironmentModules conflicts with default module syntax (Lua)
stdout, _ = self._run_mock_eb(args, raise_error=True, redo_init_config=False)
patterns = [
r"^# Current EasyBuild configuration",
r"^module-syntax\s*\(C\) = Tcl",
- r"^modules-tool\s*\(E\) = EnvironmentModulesC",
+ r"^modules-tool\s*\(E\) = EnvironmentModules",
]
for pattern in patterns:
regex = re.compile(pattern, re.M)
@@ -5249,8 +5439,8 @@ def test_modules_tool_vs_syntax_check(self):
# make sure default module syntax is used
os.environ.pop('EASYBUILD_MODULE_SYNTAX', None)
- # using EnvironmentModulesC modules tool with default module syntax (Lua) is a problem
- os.environ['EASYBUILD_MODULES_TOOL'] = 'EnvironmentModulesC'
+ # using EnvironmentModules modules tool with default module syntax (Lua) is a problem
+ os.environ['EASYBUILD_MODULES_TOOL'] = 'EnvironmentModules'
args = ['--show-full-config']
error_pattern = "Generating Lua module files requires Lmod as modules tool"
self.assertErrorRegex(EasyBuildError, error_pattern, self._run_mock_eb, args, raise_error=True)
@@ -5258,10 +5448,10 @@ def test_modules_tool_vs_syntax_check(self):
patterns = [
r"^# Current EasyBuild configuration",
r"^module-syntax\s*\(C\) = Tcl",
- r"^modules-tool\s*\(E\) = EnvironmentModulesC",
+ r"^modules-tool\s*\(E\) = EnvironmentModules",
]
- # EnvironmentModulesC modules tool + Tcl module syntax is fine
+ # EnvironmentModules modules tool + Tcl module syntax is fine
args.append('--module-syntax=Tcl')
stdout, _ = self._run_mock_eb(args, do_build=True, raise_error=True, testing=False, redo_init_config=False)
for pattern in patterns:
@@ -5282,7 +5472,14 @@ def test_prefix_option(self):
regex = re.compile(r"(?P\S*).*%s.*" % self.test_prefix, re.M)
- expected = ['buildpath', 'containerpath', 'installpath', 'packagepath', 'prefix', 'repositorypath']
+ expected = [
+ 'buildpath',
+ 'containerpath',
+ 'installpath',
+ 'packagepath',
+ 'prefix',
+ 'repositorypath',
+ ]
self.assertEqual(sorted(regex.findall(txt)), expected)
def test_dump_env_script(self):
@@ -5309,7 +5506,8 @@ def test_dump_env_script(self):
os.chdir(self.test_prefix)
args = ['%s.eb' % openmpi, '--dump-env-script']
error_msg = r"Script\(s\) already exists, not overwriting them \(unless --force is used\): %s.env" % openmpi
- self.assertErrorRegex(EasyBuildError, error_msg, self.eb_main, args, do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_msg, self.eb_main, args, do_build=True, raise_error=True)
os.chdir(self.test_prefix)
args.append('--force')
@@ -5329,13 +5527,14 @@ def test_dump_env_script(self):
regex = re.compile("^%s$" % pattern, re.M)
self.assertTrue(regex.search(txt), "Pattern '%s' found in: %s" % (regex.pattern, txt))
- out, ec = run_cmd("function module { echo $@; } && source %s && echo FC: $FC" % env_script, simple=False)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(f"function module {{ echo $@; }} && source {env_script} && echo FC: $FC")
expected_out = '\n'.join([
"load GCC/4.6.4",
"load hwloc/1.11.8-GCC-4.6.4",
"FC: gfortran",
])
- self.assertEqual(out.strip(), expected_out)
+ self.assertEqual(res.output.strip(), expected_out)
def test_dump_env_script_existing_module(self):
toy_ec = 'toy-0.0.eb'
@@ -5384,7 +5583,14 @@ def test_stop(self):
regex = re.compile(r"COMPLETED: Installation STOPPED successfully \(took .* secs?\)", re.M)
self.assertTrue(regex.search(txt), "Pattern '%s' found in: %s" % (regex.pattern, txt))
+ # 'source' step was renamed to 'extract' in EasyBuild 5.0,
+ # see https://github.com/easybuilders/easybuild-framework/pull/4629
+ args = ['toy-0.0.eb', '--force', '--stop=source']
+ _, stderr = self._run_mock_eb(args, do_build=True, raise_error=True, testing=False, strip=True)
+ self.assertIn("option --stop: invalid choice: 'source' (choose from", stderr)
+
def test_fetch(self):
+ """Test use of --fetch"""
options = EasyBuildOptions(go_args=['--fetch'])
self.assertTrue(options.options.fetch)
@@ -5405,10 +5611,10 @@ def test_fetch(self):
# which might trip up the dependency resolution (see #4298)
for ec in ('toy-0.0.eb', 'toy-0.0-deps.eb'):
args = [ec, '--fetch']
- stdout, stderr = self._run_mock_eb(args, raise_error=True, strip=True, testing=False)
+ stdout, _ = self._run_mock_eb(args, raise_error=True, strip=True, testing=False)
patterns = [
- r"^== fetching files\.\.\.$",
+ r"^== fetching files and verifying checksums\.\.\.$",
r"^== COMPLETED: Installation STOPPED successfully \(took .* secs?\)$",
]
for pattern in patterns:
@@ -5418,6 +5624,17 @@ def test_fetch(self):
regex = re.compile(r"^== creating build dir, resetting environment\.\.\.$")
self.assertFalse(regex.search(stdout), "Pattern '%s' found in: %s" % (regex.pattern, stdout))
+ # --fetch should also verify the checksums
+ tmpdir = tempfile.mkdtemp(prefix='easybuild-sources')
+ write_file(os.path.join(tmpdir, 'toy-0.0.tar.gz'), 'Make checksum check fail')
+ args = ['--sourcepath=%s:%s' % (tmpdir, self.test_sourcepath), '--fetch', 'toy-0.0.eb']
+ with self.mocked_stdout_stderr():
+ pattern = 'Checksum verification for .*/toy-0.0.tar.gz .*failed'
+ self.assertErrorRegex(EasyBuildError, pattern, self.eb_main, args, do_build=True, raise_error=True)
+ # We can avoid that failure by ignoring the checksums
+ args.append('--ignore-checksums')
+ self.eb_main(args, do_build=True, raise_error=True)
+
def test_parse_external_modules_metadata(self):
"""Test parse_external_modules_metadata function."""
# by default, provided external module metadata cfg files are picked up
@@ -5530,7 +5747,8 @@ def test_zip_logs(self):
args = ['toy-0.0.eb', '--force', '--debug']
if zip_logs:
args.append(zip_logs)
- self.eb_main(args, do_build=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True)
logs = glob.glob(os.path.join(toy_eb_install_dir, 'easybuild-toy-0.0*log*'))
self.assertEqual(len(logs), 1, "Found exactly 1 log file in %s: %s" % (toy_eb_install_dir, logs))
@@ -5570,19 +5788,23 @@ def test_list_prs(self):
"""Test --list-prs."""
args = ['--list-prs', 'foo']
error_msg = r"must be one of \['open', 'closed', 'all'\]"
- self.assertErrorRegex(EasyBuildError, error_msg, self.eb_main, args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_msg, self.eb_main, args, raise_error=True)
args = ['--list-prs', 'open,foo']
error_msg = r"must be one of \['created', 'updated', 'popularity', 'long-running'\]"
- self.assertErrorRegex(EasyBuildError, error_msg, self.eb_main, args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_msg, self.eb_main, args, raise_error=True)
args = ['--list-prs', 'open,created,foo']
error_msg = r"must be one of \['asc', 'desc'\]"
- self.assertErrorRegex(EasyBuildError, error_msg, self.eb_main, args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_msg, self.eb_main, args, raise_error=True)
args = ['--list-prs', 'open,created,asc,foo']
error_msg = r"must be in the format 'state\[,order\[,direction\]\]"
- self.assertErrorRegex(EasyBuildError, error_msg, self.eb_main, args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_msg, self.eb_main, args, raise_error=True)
args = ['--list-prs', 'closed,updated,asc']
txt, _ = self._run_mock_eb(args, testing=False)
@@ -5761,14 +5983,9 @@ def test_parse_optarch(self):
def test_check_contrib_style(self):
"""Test style checks performed by --check-contrib + dedicated --check-style option."""
- try:
- import pycodestyle # noqa
- except ImportError:
- try:
- import pep8 # noqa
- except ImportError:
- print("Skipping test_check_contrib_style, since pycodestyle or pep8 is not available")
- return
+ if 'pycodestyle' not in sys.modules:
+ print("Skipping test_check_contrib_style pycodestyle is not available")
+ return
regex = re.compile(r"Running style check on 2 easyconfig\(s\)(.|\n)*>> All style checks PASSed!", re.M)
args = [
@@ -5821,8 +6038,8 @@ def test_check_contrib_style(self):
def test_check_contrib_non_style(self):
"""Test non-style checks performed by --check-contrib."""
- if not ('pycodestyle' in sys.modules or 'pep8' in sys.modules):
- print("Skipping test_check_contrib_non_style (no pycodestyle or pep8 available)")
+ if 'pycodestyle' not in sys.modules:
+ print("Skipping test_check_contrib_non_style pycodestyle is not available")
return
args = [
@@ -5880,7 +6097,8 @@ def test_allow_use_as_root(self):
# running as root is disallowed by default
error_msg = "You seem to be running EasyBuild with root privileges which is not wise, so let's end this here"
- self.assertErrorRegex(EasyBuildError, error_msg, self.eb_main, ['toy-0.0.eb'], raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_msg, self.eb_main, ['toy-0.0.eb'], raise_error=True)
# running as root is allowed under --allow-use-as-root, but does result in a warning being printed to stderr
args = ['toy-0.0.eb', '--allow-use-as-root-and-accept-consequences']
@@ -5908,7 +6126,8 @@ def test_verify_easyconfig_filenames(self):
]
# filename of provided easyconfig doesn't matter by default
- self.eb_main(args, logfile=dummylogfn, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, logfile=dummylogfn, raise_error=True)
logtxt = read_file(self.logfile)
self.assertIn('module: toy/0.0', logtxt)
@@ -5919,12 +6138,15 @@ def test_verify_easyconfig_filenames(self):
error_pattern = r"Easyconfig filename 'test.eb' does not match with expected filename 'toy-0.0.eb' \(specs: "
error_pattern += r"name: 'toy'; version: '0.0'; versionsuffix: ''; "
error_pattern += r"toolchain name, version: 'system', 'system'\)"
- self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, logfile=dummylogfn, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, logfile=dummylogfn,
+ raise_error=True)
write_file(self.logfile, '')
args[0] = toy_ec
- self.eb_main(args, logfile=dummylogfn, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, logfile=dummylogfn, raise_error=True)
logtxt = read_file(self.logfile)
self.assertIn('module: toy/0.0', logtxt)
@@ -5933,7 +6155,8 @@ def test_set_default_module(self):
topdir = os.path.dirname(os.path.abspath(__file__))
toy_ec = os.path.join(topdir, 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0-deps.eb')
- self.eb_main([toy_ec, '--set-default-module'], do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main([toy_ec, '--set-default-module'], do_build=True, raise_error=True)
toy_mod_dir = os.path.join(self.test_installpath, 'modules', 'all', 'toy')
toy_mod = os.path.join(toy_mod_dir, '0.0-deps')
@@ -6008,7 +6231,8 @@ def test_set_default_module_robot(self):
'--robot',
self.test_prefix,
]
- self.eb_main(args, do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True, raise_error=True)
# default module is set for specified easyconfig, but *not* for its dependency
modfiles_dir = os.path.join(self.test_installpath, 'modules', 'all')
@@ -6135,8 +6359,10 @@ def test_inject_checksums(self):
{'toy-0.0_fix-silly-typo-in-printf-statement.patch': toy_patch_sha256}])
self.assertEqual(ec['exts_default_options'], {'source_urls': ['http://example.com/%(name)s']})
self.assertEqual(ec['exts_list'][0], 'ulimit')
+ expected_buildopts = " && gcc bar.c -o anotherbar && "
+ expected_buildopts += 'echo "TOY_EXAMPLES=$TOY_EXAMPLES" > %(installdir)s/toy_libs_path.txt'
self.assertEqual(ec['exts_list'][1], ('bar', '0.0', {
- 'buildopts': " && gcc bar.c -o anotherbar",
+ 'buildopts': expected_buildopts,
'checksums': [
{'bar-0.0.tar.gz': bar_tar_gz_sha256},
{'bar-0.0_fix-silly-typo-in-printf-statement.patch': bar_patch_sha256},
@@ -6146,7 +6372,7 @@ def test_inject_checksums(self):
'patches': [bar_patch, bar_patch_bis],
'toy_ext_param': "mv anotherbar bar_bis",
'unknowneasyconfigparameterthatshouldbeignored': 'foo',
- 'keepsymlinks': True,
+ 'keepsymlinks': False,
}))
self.assertEqual(ec['exts_list'][2], ('barbar', '1.2', {
'checksums': ['d5bd9908cdefbe2d29c6f8d5b45b2aaed9fd904b5e6397418bb5094fbdb3d838'],
@@ -6191,18 +6417,19 @@ def test_inject_checksums(self):
self.assertNotIn('checksums = ', toy_ec_txt)
write_file(test_ec, toy_ec_txt)
- args = [test_ec, '--inject-checksums=md5']
+ args = [test_ec, '--inject-checksums=sha256']
stdout, stderr = self._run_mock_eb(args, raise_error=True, strip=True)
patterns = [
- r"^== injecting md5 checksums in .*/test\.eb$",
+ r"^== injecting sha256 checksums in .*/test\.eb$",
r"^== fetching sources & patches for test\.eb\.\.\.$",
r"^== backup of easyconfig file saved to .*/test\.eb\.bak_[0-9]+_[0-9]+$",
- r"^== injecting md5 checksums for sources & patches in test\.eb\.\.\.$",
- r"^== \* toy-0.0\.tar\.gz: be662daa971a640e40be5c804d9d7d10$",
- r"^== \* toy-0\.0_fix-silly-typo-in-printf-statement\.patch: a99f2a72cee1689a2f7e3ace0356efb1$",
- r"^== \* toy-extra\.txt: 3b0787b3bf36603ae1398c4a49097893$",
+ r"^== injecting sha256 checksums for sources & patches in test\.eb\.\.\.$",
+ r"^== \* toy-0.0\.tar\.gz: 44332000aa33b99ad1e00cbd1a7da769220d74647060a10e807b916d73ea27bc$",
+ r"^== \* toy-0\.0_fix-silly-typo-in-printf-statement\.patch: " # no comma, continues on next line
+ r"81a3accc894592152f81814fbf133d39afad52885ab52c25018722c7bda92487$",
+ r"^== \* toy-extra\.txt: 4196b56771140d8e2468fb77f0240bc48ddbf5dabafe0713d612df7fafb1e458$",
]
for pattern in patterns:
regex = re.compile(pattern, re.M)
@@ -6218,9 +6445,10 @@ def test_inject_checksums(self):
# no parse errors for updated easyconfig file...
ec = EasyConfigParser(test_ec).get_config_dict()
checksums = [
- {'toy-0.0.tar.gz': 'be662daa971a640e40be5c804d9d7d10'},
- {'toy-0.0_fix-silly-typo-in-printf-statement.patch': 'a99f2a72cee1689a2f7e3ace0356efb1'},
- {'toy-extra.txt': '3b0787b3bf36603ae1398c4a49097893'},
+ {'toy-0.0.tar.gz': '44332000aa33b99ad1e00cbd1a7da769220d74647060a10e807b916d73ea27bc'},
+ {'toy-0.0_fix-silly-typo-in-printf-statement.patch':
+ '81a3accc894592152f81814fbf133d39afad52885ab52c25018722c7bda92487'},
+ {'toy-extra.txt': '4196b56771140d8e2468fb77f0240bc48ddbf5dabafe0713d612df7fafb1e458'},
]
self.assertEqual(ec['checksums'], checksums)
@@ -6349,7 +6577,6 @@ def test_force_download(self):
'--sourcepath=%s' % self.test_prefix,
]
stdout, stderr = self._run_mock_eb(args, do_build=True, raise_error=True, verbose=True, strip=True)
- self.assertEqual(stdout, '')
regex = re.compile(r"^WARNING: Found file toy-0.0.tar.gz at .*, but re-downloading it anyway\.\.\.$")
self.assertTrue(regex.match(stderr), "Pattern '%s' matches: %s" % (regex.pattern, stderr))
@@ -6369,14 +6596,15 @@ def test_enforce_checksums(self):
args = [
test_ec,
- '--stop=source',
+ '--stop=fetch',
'--enforce-checksums',
]
# checksum is missing for patch of 'bar' extension, so --enforce-checksums should result in an error
copy_file(toy_ec, test_ec)
error_pattern = r"Missing checksum for bar-0.0[^ ]*\.patch"
- self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, do_build=True, raise_error=True)
# get rid of checksums for extensions, should result in different error message
# because of missing checksum for source of 'bar' extension
@@ -6385,7 +6613,8 @@ def test_enforce_checksums(self):
self.assertNotIn("'checksums':", test_ec_txt)
write_file(test_ec, test_ec_txt)
error_pattern = r"Missing checksum for bar-0\.0\.tar\.gz"
- self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, do_build=True, raise_error=True)
# wipe both exts_list and checksums, so we can check whether missing checksum for main source is caught
test_ec_txt = read_file(test_ec)
@@ -6396,7 +6625,8 @@ def test_enforce_checksums(self):
write_file(test_ec, test_ec_txt)
error_pattern = "Missing checksum for toy-0.0.tar.gz"
- self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, do_build=True, raise_error=True)
def test_show_system_info(self):
"""Test for --show-system-info."""
@@ -6474,14 +6704,12 @@ def test_tmp_logdir(self):
# check log message with --skip for existing module
args = [
toy_ec,
- '--sourcepath=%s' % self.test_sourcepath,
- '--buildpath=%s' % self.test_buildpath,
- '--installpath=%s' % self.test_installpath,
'--force',
'--debug',
'--tmp-logdir=%s' % tmp_logdir,
]
- self.eb_main(args, do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True, raise_error=True)
tmp_logs = os.listdir(tmp_logdir)
self.assertEqual(len(tmp_logs), 1)
@@ -6509,19 +6737,21 @@ def test_sanity_check_only(self):
write_file(test_ec, test_ec_txt)
# sanity check fails if software was not installed yet
- outtxt, error_thrown = self.eb_main([test_ec, '--sanity-check-only'], do_build=True, return_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt, error_thrown = self.eb_main([test_ec, '--sanity-check-only'], do_build=True, return_error=True)
self.assertIn("Sanity check failed", str(error_thrown))
# actually install, then try --sanity-check-only again;
# need to use --force to install toy because module already exists (but installation doesn't)
- self.eb_main([test_ec, '--force'], do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main([test_ec, '--force'], do_build=True, raise_error=True)
args = [test_ec, '--sanity-check-only']
stdout = self.mocked_main(args + ['--trace'], do_build=True, raise_error=True, testing=False)
skipped = [
- "fetching files",
+ "fetching files and verifying checksums",
"creating build dir, resetting environment",
"unpacking",
"patching",
@@ -6561,7 +6791,8 @@ def test_sanity_check_only(self):
libbarbar = os.path.join(ebroottoy, 'lib', 'libbarbar.a')
move_file(libbarbar, libbarbar + '.moved')
- outtxt, error_thrown = self.eb_main(args + ['--debug'], do_build=True, return_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt, error_thrown = self.eb_main(args + ['--debug'], do_build=True, return_error=True)
error_msg = str(error_thrown)
error_patterns = [
r"Sanity check failed",
@@ -6572,7 +6803,8 @@ def test_sanity_check_only(self):
self.assertTrue(regex.search(error_msg), "Pattern '%s' should be found in: %s" % (regex.pattern, error_msg))
# failing sanity check for extension can be bypassed via --skip-extensions
- outtxt = self.eb_main(args + ['--skip-extensions'], do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args + ['--skip-extensions'], do_build=True, raise_error=True)
self.assertIn("Sanity check for toy successful", outtxt)
# restore fail, we want a passing sanity check for the next check
@@ -6597,7 +6829,8 @@ def test_sanity_check_only(self):
"]",
])
write_file(test_ec, test_ec_txt)
- self.eb_main(args, do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True, raise_error=True)
# also check when using easyblock that enables build_in_installdir in its constructor
test_ebs = os.path.join(topdir, 'sandbox', 'easybuild', 'easyblocks')
@@ -6618,7 +6851,8 @@ def test_sanity_check_only(self):
orig_local_sys_path = sys.path[:]
args.append('--include-easyblocks=%s' % toy_eb)
- self.eb_main(args, do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True, raise_error=True)
# undo import of the toy easyblock, to avoid problems with other tests
del sys.modules['easybuild.easyblocks.toy']
@@ -6650,7 +6884,8 @@ def test_skip_extensions(self):
write_file(test_ec, test_ec_txt)
args = [test_ec, '--force', '--skip-extensions']
- self.eb_main(args, do_build=True, return_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True, return_error=True)
toy_mod = os.path.join(self.test_installpath, 'modules', 'all', 'toy', '0.0')
if get_module_syntax() == 'Lua':
@@ -6711,50 +6946,6 @@ def test_installdir(self):
eb = EasyBlock(EasyConfig(toy_ec))
self.assertTrue(eb.installdir.endswith('/software/Core/toy/0.0'))
- def test_sort_looseversions(self):
- """Test sort_looseversions function."""
- # Test twice: With the standard distutils LooseVersion (when available) and with our class
- # Note that our class directly allows sorting but should also work with sort_loosversions
- for use_distutils in (True, False):
- if use_distutils:
- try:
- from distutils.version import LooseVersion as version_class
- except ImportError:
- continue
- else:
- version_class = LooseVersion
-
- with warnings.catch_warnings():
- if use_distutils:
- warnings.simplefilter("ignore", category=DeprecationWarning)
- ver1 = version_class('1.2.3')
- ver2 = version_class('4.5.6')
- ver3 = version_class('1.2.3dev')
- ver4 = version_class('system')
- ver5 = version_class('rc3')
- ver6 = version_class('v1802')
-
- # some versions are included multiple times on purpose,
- # to also test comparison between equal LooseVersion instances
- input = [ver3, ver5, ver1, ver2, ver4, ver6, ver3, ver4, ver1]
- expected = [ver1, ver1, ver3, ver3, ver2, ver5, ver4, ver4, ver6]
- self.assertEqual(sort_looseversions(input), expected)
- if not use_distutils:
- self.assertEqual(sorted(input), expected)
-
- # also test on list of tuples consisting of a LooseVersion instance + a string
- # (as in the list_software_* functions)
- suff1 = ''
- suff2 = '-foo'
- suff3 = '-bar'
- input = [(ver3, suff1), (ver5, suff3), (ver1, suff2), (ver2, suff3), (ver4, suff1),
- (ver6, suff2), (ver3, suff3), (ver4, suff3), (ver1, suff1)]
- expected = [(ver1, suff1), (ver1, suff2), (ver3, suff1), (ver3, suff3), (ver2, suff3),
- (ver5, suff3), (ver4, suff1), (ver4, suff3), (ver6, suff2)]
- self.assertEqual(sort_looseversions(input), expected)
- if not use_distutils:
- self.assertEqual(sorted(input), expected)
-
def test_cuda_compute_capabilities(self):
"""Test --cuda-compute-capabilities configuration option."""
args = ['--cuda-compute-capabilities=3.5,6.2,7.0', '--show-config']
@@ -6872,14 +7063,16 @@ def test_accept_eula_for(self):
# by default, no EULAs are accepted at all
args = [test_ec, '--force']
error_pattern = r"The End User License Agreement \(EULA\) for toy is currently not accepted!"
- self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, do_build=True, raise_error=True)
toy_modfile = os.path.join(self.test_installpath, 'modules', 'all', 'toy', '0.0')
if get_module_syntax() == 'Lua':
toy_modfile += '.lua'
# installation proceeds if EasyBuild is configured to accept EULA for specified software via --accept-eula-for
for val in ('foo,toy,bar', '.*', 't.y'):
- self.eb_main(args + ['--accept-eula-for=' + val], do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args + ['--accept-eula-for=' + val], do_build=True, raise_error=True)
self.assertExists(toy_modfile)
@@ -6888,7 +7081,8 @@ def test_accept_eula_for(self):
# also check use of $EASYBUILD_ACCEPT_EULA to accept EULA for specified software
os.environ['EASYBUILD_ACCEPT_EULA_FOR'] = val
- self.eb_main(args, do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True, raise_error=True)
self.assertExists(toy_modfile)
remove_dir(self.test_installpath)
@@ -6896,36 +7090,10 @@ def test_accept_eula_for(self):
del os.environ['EASYBUILD_ACCEPT_EULA_FOR']
- # also check deprecated --accept-eula configuration option
- self.allow_deprecated_behaviour()
-
- self.mock_stderr(True)
- self.eb_main(args + ['--accept-eula=foo,toy,bar'], do_build=True, raise_error=True)
- stderr = self.get_stderr()
- self.mock_stderr(False)
- self.assertIn("Use accept-eula-for configuration setting rather than accept-eula", stderr)
-
- remove_dir(self.test_installpath)
- self.assertNotExists(toy_modfile)
-
- # also via $EASYBUILD_ACCEPT_EULA
- self.mock_stderr(True)
- os.environ['EASYBUILD_ACCEPT_EULA'] = 'toy'
- self.eb_main(args, do_build=True, raise_error=True)
- stderr = self.get_stderr()
- self.mock_stderr(False)
-
- self.assertExists(toy_modfile)
- self.assertIn("Use accept-eula-for configuration setting rather than accept-eula", stderr)
-
- remove_dir(self.test_installpath)
- self.assertNotExists(toy_modfile)
-
# also check accepting EULA via 'accept_eula = True' in easyconfig file
- self.disallow_deprecated_behaviour()
- del os.environ['EASYBUILD_ACCEPT_EULA']
write_file(test_ec, test_ec_txt + '\naccept_eula = True')
- self.eb_main(args, do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True, raise_error=True)
self.assertExists(toy_modfile)
def test_config_abs_path(self):
@@ -7016,7 +7184,8 @@ def test_easystack_wrong_read(self):
toy_easystack = os.path.join(topdir, 'easystacks', 'test_easystack_nonexistent.yaml')
args = ['--easystack', toy_easystack, '--experimental']
expected_err = "No such file or directory: '%s'" % toy_easystack
- self.assertErrorRegex(EasyBuildError, expected_err, self.eb_main, args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, expected_err, self.eb_main, args, raise_error=True)
# testing basics - end-to-end
# expecting successful build
@@ -7026,7 +7195,8 @@ def test_easystack_basic(self):
toy_easystack = os.path.join(topdir, 'easystacks', 'test_easystack_basic.yaml')
args = ['--easystack', toy_easystack, '--debug', '--experimental', '--dry-run']
- stdout = self.eb_main(args, do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ stdout = self.eb_main(args, do_build=True, raise_error=True)
patterns = [
r"INFO Building from easystack:",
r"DEBUG Parsed easystack:\n"
@@ -7077,7 +7247,8 @@ def test_easystack_opts(self):
'--easystack', test_es_path,
'--installpath', self.test_installpath,
]
- self.eb_main(args, do_build=True, raise_error=True, redo_init_config=False)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True, raise_error=True, redo_init_config=False)
mod_ext = '.lua' if get_module_syntax() == 'Lua' else ''
@@ -7135,7 +7306,8 @@ def test_easystack_easyconfigs_cache(self):
'--dry-run',
'--robot=%s' % self.test_prefix,
]
- stdout = self.eb_main(args, do_build=True, raise_error=True, redo_init_config=False)
+ with self.mocked_stdout_stderr():
+ stdout = self.eb_main(args, do_build=True, raise_error=True, redo_init_config=False)
# check whether libtoy-0.0.eb comes from 2nd
regex = re.compile(r"^ \* \[ \] %s" % libtoy_ec, re.M)
@@ -7252,6 +7424,15 @@ def test_opts_dict_to_eb_opts(self):
]
self.assertEqual(opts_dict_to_eb_opts(opts_dict), expected)
+ # multi-call options
+ opts_dict = {'try-amend': ['a=1', 'b=2', 'c=3']}
+ expected = ['--try-amend=a=1', '--try-amend=b=2', '--try-amend=c=3']
+ self.assertEqual(opts_dict_to_eb_opts(opts_dict), expected)
+
+ opts_dict = {'amend': ['a=1', 'b=2', 'c=3']}
+ expected = ['--amend=a=1', '--amend=b=2', '--amend=c=3']
+ self.assertEqual(opts_dict_to_eb_opts(opts_dict), expected)
+
def suite():
""" returns all the testcases in this module """
diff --git a/test/framework/output.py b/test/framework/output.py
index 4429fe29ee..d8b43c8c55 100644
--- a/test/framework/output.py
+++ b/test/framework/output.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2021-2024 Ghent University
+# Copyright 2021-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -35,7 +35,7 @@
from easybuild.tools.build_log import EasyBuildError
from easybuild.tools.config import build_option, get_output_style, update_build_option
from easybuild.tools.output import PROGRESS_BAR_EXTENSIONS, PROGRESS_BAR_TYPES
-from easybuild.tools.output import DummyRich, colorize, get_progress_bar, show_progress_bars
+from easybuild.tools.output import DummyRich, colorize, get_progress_bar, print_error, show_progress_bars
from easybuild.tools.output import start_progress_bar, status_bar, stop_progress_bar, update_progress_bar, use_rich
try:
@@ -139,6 +139,26 @@ def test_colorize(self):
self.assertErrorRegex(EasyBuildError, "Unknown color: nosuchcolor", colorize, 'test', 'nosuchcolor')
+ def test_print_error(self):
+ """
+ Test print_error function
+ """
+ msg = "This is yellow: " + colorize("a banana", color='yellow')
+ self.mock_stderr(True)
+ self.mock_stdout(True)
+ print_error(msg)
+ stderr = self.get_stderr()
+ stdout = self.get_stdout()
+ self.mock_stderr(False)
+ self.mock_stdout(False)
+ self.assertEqual(stdout, '')
+ if HAVE_RICH:
+ # when using Rich, message printed to stderr won't have funny terminal escape characters for the color
+ expected = '\n\nThis is yellow: a banana\n\n'
+ else:
+ expected = '\nThis is yellow: \x1b[1;33ma banana\x1b[0m\n\n'
+ self.assertEqual(stderr, expected)
+
def test_get_progress_bar(self):
"""
Test get_progress_bar.
diff --git a/test/framework/package.py b/test/framework/package.py
index b839fb32bb..d230ae265e 100644
--- a/test/framework/package.py
+++ b/test/framework/package.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2015-2024 Ghent University
+# Copyright 2015-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -44,7 +44,7 @@
FPM_OUTPUT_FILE = 'fpm_mocked.out'
-# purposely using non-bash script, to detect issues with shebang line being ignored (run_cmd with shell=False)
+# purposely using non-bash script, to detect issues with shebang line being ignored (run_shell_cmd with use_bash=False)
MOCKED_FPM = """#!/usr/bin/env python
import os, sys
@@ -199,6 +199,7 @@ def test_active_pns(self):
def test_package(self):
"""Test package function."""
+ self.mock_stdout(True)
build_options = {
'package_tool_options': '--foo bar',
'silent': True,
@@ -265,6 +266,7 @@ def test_package(self):
self.assertTrue(regex_pkg.search(pkgtxt), "Pattern '%s' not found in: %s" % (regex_pkg.pattern, pkgtxt))
regex_pkg = re.compile(r"""DESCRIPTION:.*\nand newlines""", re.MULTILINE)
self.assertTrue(regex_pkg.search(pkgtxt), "Pattern '%s' not found in: %s" % (regex_pkg.pattern, pkgtxt))
+ self.mock_stdout(False)
def suite():
diff --git a/test/framework/parallelbuild.py b/test/framework/parallelbuild.py
index 4e75672815..90c4d0df1c 100644
--- a/test/framework/parallelbuild.py
+++ b/test/framework/parallelbuild.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2014-2024 Ghent University
+# Copyright 2014-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -112,6 +112,7 @@ class ParallelBuildTest(EnhancedTestCase):
def test_build_easyconfigs_in_parallel_pbs_python(self):
"""Test build_easyconfigs_in_parallel(), using (mocked) pbs_python as backend for --job."""
+ self.mock_stdout(True)
# put mocked functions in place
PbsPython__init__ = PbsPython.__init__
PbsPython_check_version = PbsPython._check_version
@@ -212,6 +213,7 @@ def test_build_easyconfigs_in_parallel_pbs_python(self):
PbsPython.connect_to_server = PbsPython_connect_to_server
PbsPython.ppn = PbsPython_ppn
pbs_python.PbsJob = pbs_python_PbsJob
+ self.mock_stdout(False)
def test_build_easyconfigs_in_parallel_gc3pie(self):
"""Test build_easyconfigs_in_parallel(), using GC3Pie with local config as backend for --job."""
@@ -221,6 +223,8 @@ def test_build_easyconfigs_in_parallel_gc3pie(self):
print("GC3Pie not available, skipping test")
return
+ self.allow_deprecated_behaviour()
+
# put GC3Pie config in place to use local host and fork/exec
resourcedir = os.path.join(self.test_prefix, 'gc3pie')
gc3pie_cfgfile = os.path.join(self.test_prefix, 'gc3pie_local.ini')
@@ -260,7 +264,9 @@ def test_build_easyconfigs_in_parallel_gc3pie(self):
topdir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
test_easyblocks_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'sandbox')
cmd = "PYTHONPATH=%s:%s:$PYTHONPATH eb %%(spec)s -df" % (topdir, test_easyblocks_path)
- build_easyconfigs_in_parallel(cmd, ordered_ecs, prepare_first=False)
+
+ with self.mocked_stdout_stderr():
+ build_easyconfigs_in_parallel(cmd, ordered_ecs, prepare_first=False)
toy_modfile = os.path.join(self.test_installpath, 'modules', 'all', 'toy', '0.0')
if get_module_syntax() == 'Lua':
@@ -278,12 +284,13 @@ def test_build_easyconfigs_in_parallel_gc3pie(self):
ecs = resolve_dependencies(process_easyconfig(test_ecfile), self.modtool)
error = "1 jobs failed: toy-1.2.3"
- self.assertErrorRegex(EasyBuildError, error, build_easyconfigs_in_parallel, cmd, ecs, prepare_first=False)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error, build_easyconfigs_in_parallel, cmd, ecs, prepare_first=False)
def test_submit_jobs(self):
"""Test submit_jobs"""
test_easyconfigs_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'easyconfigs', 'test_ecs')
- toy_ec = os.path.join(test_easyconfigs_dir, 't', 'toy', 'toy-0.0.eb')
+ toy_ec = process_easyconfig(os.path.join(test_easyconfigs_dir, 't', 'toy', 'toy-0.0.eb'))
args = [
'--debug',
@@ -296,7 +303,7 @@ def test_submit_jobs(self):
'--job-cores=3',
]
eb_go = parse_options(args=args)
- cmd = submit_jobs([toy_ec], eb_go.generate_cmd_line(), testing=True)
+ cmd = submit_jobs(toy_ec, eb_go.generate_cmd_line(), testing=True)
# these patterns must be found
regexs = [
@@ -324,7 +331,7 @@ def test_submit_jobs(self):
# test again with custom EasyBuild command to use in jobs
update_build_option('job_eb_cmd', "/just/testing/bin/eb --debug")
- cmd = submit_jobs([toy_ec], eb_go.generate_cmd_line(), testing=True)
+ cmd = submit_jobs(toy_ec, eb_go.generate_cmd_line(), testing=True)
regex = re.compile(r" && /just/testing/bin/eb --debug %\(spec\)s ")
self.assertTrue(regex.search(cmd), "Pattern '%s' found in: %s" % (regex.pattern, cmd))
diff --git a/test/framework/repository.py b/test/framework/repository.py
index feb96a13cf..aeb4f97155 100644
--- a/test/framework/repository.py
+++ b/test/framework/repository.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -35,7 +35,6 @@
from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered
from unittest import TextTestRunner
-import easybuild.tools.build_log
from easybuild.framework.easyconfig.parser import EasyConfigParser
from easybuild.tools.build_log import EasyBuildError
from easybuild.tools.filetools import read_file
@@ -43,7 +42,7 @@
from easybuild.tools.repository.gitrepo import GitRepository
from easybuild.tools.repository.svnrepo import SvnRepository
from easybuild.tools.repository.repository import init_repository
-from easybuild.tools.run import run_cmd
+from easybuild.tools.run import run_shell_cmd
from easybuild.tools.version import VERSION
@@ -94,10 +93,11 @@ def test_gitrepo(self):
# filepath
tmpdir = tempfile.mkdtemp()
cmd = "cd %s && git clone --bare %s" % (tmpdir, test_repo_url)
- _, ec = run_cmd(cmd, simple=False, log_all=False, log_ok=False)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd, fail_on_error=False)
# skip remainder of test if creating bare git repo didn't work
- if ec == 0:
+ if res.exit_code == 0:
repo = GitRepository(os.path.join(tmpdir, 'testrepository.git'))
repo.init()
toy_ec_file = os.path.join(os.path.dirname(__file__), 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb')
@@ -163,23 +163,6 @@ def check_ec(path, expected_buildstats):
path = repo.add_easyconfig(toy_eb_file, 'test', '1.0', {'time': 1.23, 'size': 123}, [{'time': 0.9, 'size': 2}])
check_ec(path, [{'time': 0.9, 'size': 2}, {'time': 1.23, 'size': 123}])
- orig_experimental = easybuild.tools.build_log.EXPERIMENTAL
- easybuild.tools.build_log.EXPERIMENTAL = True
-
- if 'yaml' in sys.modules:
- toy_yeb_file = os.path.join(test_easyconfigs, 'yeb', 'toy-0.0.yeb')
- path = repo.add_easyconfig(toy_yeb_file, 'test', '1.0', {'time': 1.23}, None)
- check_ec(path, [{'time': 1.23}])
-
- stats1 = {'time': 1.23, 'size': 123}
- stats2 = [{'time': 0.9, 'size': 2}]
- path = repo.add_easyconfig(toy_yeb_file, 'test', '1.0', stats1, stats2)
- check_ec(path, stats2 + [stats1])
-
- easybuild.tools.build_log.EXPERIMENTAL = orig_experimental
- else:
- print("Skipping .yeb part of test_add_easyconfig (no PyYAML available)")
-
def tearDown(self):
"""Clean up after test."""
super(RepositoryTest, self).tearDown()
diff --git a/test/framework/robot.py b/test/framework/robot.py
index bbfe8e2f03..746d71639e 100644
--- a/test/framework/robot.py
+++ b/test/framework/robot.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -783,9 +783,9 @@ def test_github_det_easyconfig_paths_from_pr(self):
args = [
os.path.join(test_ecs_path, 't', 'toy', 'toy-0.0.eb'),
test_ec, # relative path, should be resolved via robot search path
- # PR for foss/2018b, see https://github.com/easybuilders/easybuild-easyconfigs/pull/6424/files
- '--from-pr=6424',
- 'FFTW-3.3.8-gompi-2018b.eb',
+ # PR for XCrySDen/1.6.2-foss-2024a, see https://github.com/easybuilders/easybuild-easyconfigs/pull/22227
+ '--from-pr=22227',
+ 'XCrySDen-1.6.2-foss-2024a.eb',
'gompi-2018b-test.eb', # relative path, available in robot search path
'--dry-run',
'--robot',
@@ -796,8 +796,10 @@ def test_github_det_easyconfig_paths_from_pr(self):
]
self.mock_stderr(True)
+ self.mock_stdout(True)
outtxt = self.eb_main(args, logfile=dummylogfn, raise_error=True)
self.mock_stderr(False)
+ self.mock_stdout(False)
# full path doesn't matter (helps to avoid failing tests due to resolved symlinks)
test_ecs_path = os.path.join('.*', 'test', 'framework', 'easyconfigs', 'test_ecs')
@@ -807,9 +809,9 @@ def test_github_det_easyconfig_paths_from_pr(self):
(self.test_prefix, 'intel/2018a'), # dependency, found in robot search path
(self.test_prefix, 'toy/0.0-deps'), # specified easyconfig, found in robot search path
(self.test_prefix, 'gompi/2018b-test'), # specified easyconfig, found in robot search path
- ('.*/files_pr6424', 'FFTW/3.3.8-gompi-2018b'), # specified easyconfig
- (test_ecs_path, 'gompi/2018b'), # part of PR easyconfigs, found in robot search path
- (test_ecs_path, 'GCC/7.3.0-2.30'), # dependency for PR easyconfigs, found in robot search path
+ ('.*/files_pr22227', 'XCrySDen/1.6.2-foss-2024a'), # specified easyconfig
+ ('.*/files_pr22227', 'Togl/2.0-GCCcore-13.3.0'), # part of PR easyconfigs, found in robot search path
+ ('.*/files_pr22227', 'GCC/13.3.0'), # dependency for PR easyconfigs, found in robot search path
]
for path_prefix, module in modules:
ec_fn = "%s.eb" % '-'.join(module.split('/'))
@@ -1137,7 +1139,8 @@ def test_tweak_robotpath(self):
# Tweak the toolchain version of the easyconfig
tweak_specs = {'toolchain_version': '6.4.0-2.28'}
- easyconfigs = tweak(easyconfigs, tweak_specs, self.modtool, targetdirs=tweaked_ecs_paths)
+ easyconfigs, tweak_map = tweak(easyconfigs, tweak_specs, self.modtool, targetdirs=tweaked_ecs_paths,
+ return_map=True)
# Check that all expected tweaked easyconfigs exists
tweaked_openmpi = os.path.join(tweaked_ecs_paths[0], 'OpenMPI-2.1.2-GCC-6.4.0-2.28.eb')
@@ -1153,6 +1156,10 @@ def test_tweak_robotpath(self):
# Check it picks up the untweaked dependency of the tweaked OpenMPI
untweaked_hwloc = os.path.join(test_easyconfigs, 'h', 'hwloc', 'hwloc-1.11.8-GCC-6.4.0-2.28.eb')
self.assertIn(untweaked_hwloc, specs)
+ # Check correctness of tweak_map (maps back to the original untweaked file, even for hwloc, where the
+ # tweaked version is generated but not used)
+ self.assertEqual(tweak_map, {tweaked_openmpi: untweaked_openmpi,
+ tweaked_hwloc: untweaked_hwloc.replace("6.4.0-2.28", "4.6.4")})
def test_robot_find_subtoolchain_for_dep(self):
"""Test robot_find_subtoolchain_for_dep."""
diff --git a/test/framework/run.py b/test/framework/run.py
index bb2e5c0558..1c5cf3a422 100644
--- a/test/framework/run.py
+++ b/test/framework/run.py
@@ -1,6 +1,6 @@
# #
# -*- coding: utf-8 -*-
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -24,7 +24,7 @@
# along with EasyBuild. If not, see .
# #
"""
-Unit tests for filetools.py
+Unit tests for run.py
@author: Toon Willems (Ghent University)
@author: Kenneth Hoste (Ghent University)
@@ -35,25 +35,28 @@
import os
import re
import signal
+import string
import stat
import subprocess
import sys
import tempfile
import textwrap
import time
+from concurrent.futures import ThreadPoolExecutor
from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered, init_config
-from unittest import TextTestRunner
+from unittest import TextTestRunner, mock
from easybuild.base.fancylogger import setLogLevelDebug
import easybuild.tools.asyncprocess as asyncprocess
import easybuild.tools.utilities
from easybuild.tools.build_log import EasyBuildError, init_logging, stop_logging
from easybuild.tools.config import update_build_option
-from easybuild.tools.filetools import adjust_permissions, read_file, write_file
-from easybuild.tools.run import check_async_cmd, check_log_for_errors, complete_cmd, get_output_from_process
-from easybuild.tools.run import parse_log_for_error, run_cmd, run_cmd_qa
+from easybuild.tools.filetools import adjust_permissions, change_dir, mkdir, read_file, remove_dir, write_file
+from easybuild.tools.modules import EnvironmentModules, Lmod
+from easybuild.tools.run import RunShellCmdResult, RunShellCmdError, check_async_cmd, check_log_for_errors
+from easybuild.tools.run import complete_cmd, fileprefix_from_cmd, get_output_from_process, parse_log_for_error
+from easybuild.tools.run import run_cmd, run_cmd_qa, run_shell_cmd, subprocess_terminate
from easybuild.tools.config import ERROR, IGNORE, WARN
-from easybuild.tools.py2vs3 import subprocess_terminate
class RunTest(EnhancedTestCase):
@@ -74,6 +77,9 @@ def tearDown(self):
def test_get_output_from_process(self):
"""Test for get_output_from_process utility function."""
+ # use of get_output_from_process is deprecated, so we need to allow it here
+ self.allow_deprecated_behaviour()
+
@contextlib.contextmanager
def get_proc(cmd, asynchronous=False):
if asynchronous:
@@ -90,57 +96,67 @@ def get_proc(cmd, asynchronous=False):
subprocess_terminate(proc, timeout=1)
# get all output at once
- with get_proc("echo hello") as proc:
- out = get_output_from_process(proc)
- self.assertEqual(out, 'hello\n')
+ with self.mocked_stdout_stderr():
+ with get_proc("echo hello") as proc:
+ out = get_output_from_process(proc)
+ self.assertEqual(out, 'hello\n')
# first get 100 bytes, then get the rest all at once
- with get_proc("echo hello") as proc:
- out = get_output_from_process(proc, read_size=100)
- self.assertEqual(out, 'hello\n')
- out = get_output_from_process(proc)
- self.assertEqual(out, '')
+ with self.mocked_stdout_stderr():
+ with get_proc("echo hello") as proc:
+ out = get_output_from_process(proc, read_size=100)
+ self.assertEqual(out, 'hello\n')
+ out = get_output_from_process(proc)
+ self.assertEqual(out, '')
# get output in small bits, keep trying to get output (which shouldn't fail)
- with get_proc("echo hello") as proc:
- out = get_output_from_process(proc, read_size=1)
- self.assertEqual(out, 'h')
- out = get_output_from_process(proc, read_size=3)
- self.assertEqual(out, 'ell')
- out = get_output_from_process(proc, read_size=2)
- self.assertEqual(out, 'o\n')
- out = get_output_from_process(proc, read_size=1)
- self.assertEqual(out, '')
- out = get_output_from_process(proc, read_size=10)
- self.assertEqual(out, '')
- out = get_output_from_process(proc)
- self.assertEqual(out, '')
+ with self.mocked_stdout_stderr():
+ with get_proc("echo hello") as proc:
+ out = get_output_from_process(proc, read_size=1)
+ self.assertEqual(out, 'h')
+ out = get_output_from_process(proc, read_size=3)
+ self.assertEqual(out, 'ell')
+ out = get_output_from_process(proc, read_size=2)
+ self.assertEqual(out, 'o\n')
+ out = get_output_from_process(proc, read_size=1)
+ self.assertEqual(out, '')
+ out = get_output_from_process(proc, read_size=10)
+ self.assertEqual(out, '')
+ out = get_output_from_process(proc)
+ self.assertEqual(out, '')
# can also get output asynchronously (read_size is *ignored* in that case)
async_cmd = "echo hello; read reply; echo $reply"
- with get_proc(async_cmd, asynchronous=True) as proc:
- out = get_output_from_process(proc, asynchronous=True)
- self.assertEqual(out, 'hello\n')
- asyncprocess.send_all(proc, 'test123\n')
- out = get_output_from_process(proc)
- self.assertEqual(out, 'test123\n')
+ with self.mocked_stdout_stderr():
+ with get_proc(async_cmd, asynchronous=True) as proc:
+ out = get_output_from_process(proc, asynchronous=True)
+ self.assertEqual(out, 'hello\n')
+ asyncprocess.send_all(proc, 'test123\n')
+ out = get_output_from_process(proc)
+ self.assertEqual(out, 'test123\n')
- with get_proc(async_cmd, asynchronous=True) as proc:
- out = get_output_from_process(proc, asynchronous=True, read_size=1)
- # read_size is ignored when getting output asynchronously, we're getting more than 1 byte!
- self.assertEqual(out, 'hello\n')
- asyncprocess.send_all(proc, 'test123\n')
- out = get_output_from_process(proc, read_size=3)
- self.assertEqual(out, 'tes')
- out = get_output_from_process(proc, read_size=2)
- self.assertEqual(out, 't1')
- out = get_output_from_process(proc)
- self.assertEqual(out, '23\n')
+ with self.mocked_stdout_stderr():
+ with get_proc(async_cmd, asynchronous=True) as proc:
+ out = get_output_from_process(proc, asynchronous=True, read_size=1)
+ # read_size is ignored when getting output asynchronously, we're getting more than 1 byte!
+ self.assertEqual(out, 'hello\n')
+ asyncprocess.send_all(proc, 'test123\n')
+ out = get_output_from_process(proc, read_size=3)
+ self.assertEqual(out, 'tes')
+ out = get_output_from_process(proc, read_size=2)
+ self.assertEqual(out, 't1')
+ out = get_output_from_process(proc)
+ self.assertEqual(out, '23\n')
def test_run_cmd(self):
"""Basic test for run_cmd function."""
- (out, ec) = run_cmd("echo hello")
+
+ # use of run_cmd is deprecated, so we need to allow it here
+ self.allow_deprecated_behaviour()
+
+ with self.mocked_stdout_stderr():
+ (out, ec) = run_cmd("echo hello")
self.assertEqual(out, "hello\n")
# no reason echo hello could fail
self.assertEqual(ec, 0)
@@ -150,18 +166,234 @@ def test_run_cmd(self):
# this is constructed to reproduce errors like:
# UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe2
# UnicodeEncodeError: 'ascii' codec can't encode character u'\u2018'
- for text in [b"foo \xe2 bar", b"foo \u2018 bar"]:
+ for text in [b"foo \xe2 bar", "foo \u2018 bar"]:
test_file = os.path.join(self.test_prefix, 'foo.txt')
write_file(test_file, text)
cmd = "cat %s" % test_file
- (out, ec) = run_cmd(cmd)
+ with self.mocked_stdout_stderr():
+ (out, ec) = run_cmd(cmd)
self.assertEqual(ec, 0)
self.assertTrue(out.startswith('foo ') and out.endswith(' bar'))
self.assertEqual(type(out), str)
+ def test_run_shell_cmd_basic(self):
+ """Basic test for run_shell_cmd function."""
+
+ os.environ['FOOBAR'] = 'foobar'
+
+ cwd = change_dir(self.test_prefix)
+
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd("echo hello")
+ self.assertEqual(res.output, "hello\n")
+ # no reason echo hello could fail
+ self.assertEqual(res.cmd, "echo hello")
+ self.assertEqual(res.exit_code, 0)
+ self.assertTrue(isinstance(res.output, str))
+ self.assertEqual(res.stderr, None)
+ self.assertTrue(res.work_dir and isinstance(res.work_dir, str))
+
+ change_dir(cwd)
+ del os.environ['FOOBAR']
+
+ # check on helper scripts that were generated for this command
+ paths = glob.glob(os.path.join(self.test_prefix, 'eb-*', 'run-shell-cmd-output', 'echo-*'))
+ self.assertEqual(len(paths), 1)
+ cmd_tmpdir = paths[0]
+
+ # check on env.sh script that can be used to set up environment in which command was run
+ env_script = os.path.join(cmd_tmpdir, 'env.sh')
+ self.assertExists(env_script)
+ env_script_txt = read_file(env_script)
+ self.assertIn("export FOOBAR=foobar", env_script_txt)
+ self.assertIn("history -s 'echo hello'", env_script_txt)
+
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(f"source {env_script}; echo $USER; echo $FOOBAR; history")
+ self.assertEqual(res.exit_code, 0)
+ user = os.getenv('USER')
+ self.assertTrue(res.output.startswith(f'{user}\nfoobar\n'))
+ self.assertTrue(res.output.endswith("echo hello\n"))
+
+ # check on cmd.sh script that can be used to create interactive shell environment for command
+ cmd_script = os.path.join(cmd_tmpdir, 'cmd.sh')
+ self.assertExists(cmd_script)
+
+ cmd = f"{cmd_script} -c 'echo pwd: $PWD; echo $FOOBAR; echo $EB_CMD_OUT_FILE; cat $EB_CMD_OUT_FILE'"
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd, fail_on_error=False)
+ self.assertEqual(res.exit_code, 0)
+ regex = re.compile("pwd: .*\nfoobar\n.*/echo-.*/out.txt\nhello$")
+ self.assertTrue(regex.search(res.output), f"Pattern '{regex.pattern}' should be found in {res.output}")
+
+ # check whether working directory is what's expected
+ regex = re.compile('^pwd: .*', re.M)
+ res = regex.findall(res.output)
+ self.assertEqual(len(res), 1)
+ pwd = res[0].strip()[5:]
+ self.assertTrue(os.path.samefile(pwd, self.test_prefix))
+
+ cmd = f"{cmd_script} -c 'module --version'"
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd, fail_on_error=False)
+ self.assertEqual(res.exit_code, 0)
+
+ if isinstance(self.modtool, Lmod):
+ regex = re.compile("^Modules based on Lua: Version [0-9]", re.M)
+ elif isinstance(self.modtool, EnvironmentModules):
+ regex = re.compile("^Modules Release [0-9]", re.M)
+ else:
+ self.fail("Unknown modules tool used!")
+
+ self.assertTrue(regex.search(res.output), f"Pattern '{regex.pattern}' should be found in {res.output}")
+
+ # test running command that emits non-UTF-8 characters
+ # this is constructed to reproduce errors like:
+ # UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe2
+ # UnicodeEncodeError: 'ascii' codec can't encode character u'\u2018'
+ # (such errors are ignored by the 'run' implementation)
+ for text in [b"foo \xe2 bar", "foo \u2018 bar"]:
+ test_file = os.path.join(self.test_prefix, 'foo.txt')
+ write_file(test_file, text)
+ cmd = "cat %s" % test_file
+
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd)
+ self.assertEqual(res.cmd, cmd)
+ self.assertEqual(res.exit_code, 0)
+ self.assertTrue(res.output.startswith('foo ') and res.output.endswith(' bar'))
+ self.assertTrue(isinstance(res.output, str))
+ self.assertTrue(res.work_dir and isinstance(res.work_dir, str))
+
+ def test_run_shell_cmd_perl(self):
+ """
+ Test running of Perl script via run_shell_cmd that detects type of shell
+ """
+ perl_script = os.path.join(self.test_prefix, 'test.pl')
+ perl_script_txt = """#!/usr/bin/perl
+
+ # wait for input, see what happens (should not hang)
+ print STDOUT "foo:\n";
+ STDOUT->autoflush(1);
+ my $stdin = ;
+ print "stdin: $stdin\n";
+
+ # conditional print statements below should *not* be triggered
+ print "stdin+stdout are terminals\n" if -t STDIN && -t STDOUT;
+ print "stdin is terminal\n" if -t STDIN;
+ print "stdout is terminal\n" if -t STDOUT;
+ my $ISA_TTY = -t STDIN && (-t STDOUT || !(-f STDOUT || -c STDOUT)) ;
+ print "ISA_TTY" if $ISA_TTY;
+
+ print "PS1 is set\n" if $ENV{PS1};
+
+ print "tty -s returns 0\n" if system("tty -s") == 0;
+
+ # check if parent process is a shell
+ my $ppid = getppid();
+ my $parent_cmd = `ps -p $ppid -o comm=`;
+ print "parent process is bash\n" if ($parent_cmd =~ '/bash$');
+ """
+ write_file(perl_script, perl_script_txt)
+ adjust_permissions(perl_script, stat.S_IXUSR)
+
+ def handler(signum, _):
+ raise RuntimeError(f"Test for running Perl script via run_shell_cmd took too long, signal {signum}")
+
+ orig_sigalrm_handler = signal.getsignal(signal.SIGALRM)
+
+ try:
+ # set the signal handler and a 3-second alarm
+ signal.signal(signal.SIGALRM, handler)
+ signal.alarm(3)
+
+ res = run_shell_cmd(perl_script, hidden=True)
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(res.output, 'foo:\nstdin: \n')
+
+ res = run_shell_cmd(perl_script, hidden=True, stdin="test")
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(res.output, 'foo:\nstdin: test\n')
+
+ res = run_shell_cmd(perl_script, hidden=True, qa_patterns=[('foo:', 'bar')], qa_timeout=1)
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(res.output, 'foo:\nstdin: bar\n\n')
+
+ error_pattern = "No matching questions found for current command output"
+ self.assertErrorRegex(EasyBuildError, error_pattern, run_shell_cmd, perl_script,
+ hidden=True, qa_patterns=[('bleh', 'blah')], qa_timeout=1)
+ finally:
+ # cleanup: disable the alarm + reset signal handler for SIGALRM
+ signal.signal(signal.SIGALRM, orig_sigalrm_handler)
+ signal.alarm(0)
+
+ def test_run_shell_cmd_env(self):
+ """Test env option in run_shell_cmd."""
+
+ # use 'env' to define environment in which command should be run;
+ # with a few exceptions (like $_, $PWD) no other environment variables will be defined,
+ # so $HOME and $USER will not be set
+ cmd = "env | sort"
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd, env={'FOOBAR': 'foobar', 'PATH': os.getenv('PATH')})
+ self.assertEqual(res.cmd, cmd)
+ self.assertEqual(res.exit_code, 0)
+ self.assertIn("FOOBAR=foobar\n", res.output)
+ self.assertTrue(re.search("^_=.*/env$", res.output, re.M))
+ for var in ('HOME', 'USER'):
+ self.assertFalse(re.search('^' + var + '=.*', res.output, re.M))
+
+ # check on helper scripts that were generated for this command
+ paths = glob.glob(os.path.join(self.test_prefix, 'eb-*', 'run-shell-cmd-output', 'env-*'))
+ self.assertEqual(len(paths), 1)
+ cmd_tmpdir = paths[0]
+
+ # set environment variable in current environment,
+ # this should not be set in shell environment produced by scripts
+ os.environ['TEST123'] = 'test123'
+
+ env_script = os.path.join(cmd_tmpdir, 'env.sh')
+ self.assertExists(env_script)
+ env_script_txt = read_file(env_script)
+ self.assertIn('unset "$var"', env_script_txt)
+ self.assertIn('unset -f "$func"', env_script_txt)
+ self.assertIn('\nexport FOOBAR=foobar\nexport PATH', env_script_txt)
+
+ cmd_script = os.path.join(cmd_tmpdir, 'cmd.sh')
+ self.assertExists(cmd_script)
+
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(f"{cmd_script} -c 'echo $FOOBAR; echo TEST123:$TEST123'", fail_on_error=False)
+ self.assertEqual(res.exit_code, 0)
+ self.assertTrue(res.output.endswith('\nfoobar\nTEST123:\n'))
+
+ def test_fileprefix_from_cmd(self):
+ """test simplifications from fileprefix_from_cmd."""
+ cmds = {
+ 'abd123': 'abd123',
+ 'ab"a': 'aba',
+ 'a{:$:S@"a': 'aSa',
+ 'cmd-with-dash': 'cmd-with-dash',
+ 'cmd_with_underscore': 'cmd_with_underscore',
+ }
+ for cmd, expected_simplification in cmds.items():
+ self.assertEqual(fileprefix_from_cmd(cmd), expected_simplification)
+
+ cmds = {
+ 'abd123': 'abd',
+ 'ab"a': 'aba',
+ '0a{:$:2@"a': 'aa',
+ }
+ for cmd, expected_simplification in cmds.items():
+ self.assertEqual(fileprefix_from_cmd(cmd, allowed_chars=string.ascii_letters), expected_simplification)
+
def test_run_cmd_log(self):
"""Test logging of executed commands."""
+
+ # use of run_cmd is deprecated, so we need to allow it here
+ self.allow_deprecated_behaviour()
+
fd, logfile = tempfile.mkstemp(suffix='.log', prefix='eb-test-')
os.close(fd)
@@ -169,13 +401,15 @@ def test_run_cmd_log(self):
# command output is not logged by default without debug logging
init_logging(logfile, silent=True)
- self.assertTrue(run_cmd("echo hello"))
+ with self.mocked_stdout_stderr():
+ self.assertTrue(run_cmd("echo hello"))
stop_logging(logfile)
self.assertEqual(len(regex.findall(read_file(logfile))), 0)
write_file(logfile, '')
init_logging(logfile, silent=True)
- self.assertTrue(run_cmd("echo hello", log_all=True))
+ with self.mocked_stdout_stderr():
+ self.assertTrue(run_cmd("echo hello", log_all=True))
stop_logging(logfile)
self.assertEqual(len(regex.findall(read_file(logfile))), 1)
write_file(logfile, '')
@@ -184,20 +418,22 @@ def test_run_cmd_log(self):
setLogLevelDebug()
init_logging(logfile, silent=True)
- self.assertTrue(run_cmd("echo hello"))
+ with self.mocked_stdout_stderr():
+ self.assertTrue(run_cmd("echo hello"))
stop_logging(logfile)
self.assertEqual(len(regex.findall(read_file(logfile))), 1)
write_file(logfile, '')
init_logging(logfile, silent=True)
- self.assertTrue(run_cmd("echo hello", log_all=True))
+ with self.mocked_stdout_stderr():
+ self.assertTrue(run_cmd("echo hello", log_all=True))
stop_logging(logfile)
self.assertEqual(len(regex.findall(read_file(logfile))), 1)
write_file(logfile, '')
# Test that we can set the directory for the logfile
log_path = os.path.join(self.test_prefix, 'chicken')
- os.mkdir(log_path)
+ mkdir(log_path)
logfile = None
init_logging(logfile, silent=True, tmp_logdir=log_path)
logfiles = os.listdir(log_path)
@@ -205,8 +441,46 @@ def test_run_cmd_log(self):
self.assertTrue(logfiles[0].startswith("easybuild"))
self.assertTrue(logfiles[0].endswith("log"))
+ def test_run_shell_cmd_log(self):
+ """Test logging of executed commands with run_shell_cmd function."""
+
+ fd, logfile = tempfile.mkstemp(suffix='.log', prefix='eb-test-')
+ os.close(fd)
+
+ regex_start_cmd = re.compile("Running shell command 'echo hello' in /")
+ regex_cmd_exit = re.compile(r"Shell command completed successfully \(see output above\): echo hello")
+
+ # command output is always logged
+ init_logging(logfile, silent=True)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd("echo hello")
+ stop_logging(logfile)
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(res.output, 'hello\n')
+ logtxt = read_file(logfile)
+ self.assertEqual(len(regex_start_cmd.findall(logtxt)), 1)
+ self.assertEqual(len(regex_cmd_exit.findall(logtxt)), 1)
+ write_file(logfile, '')
+
+ # with debugging enabled, exit code and output of command should only get logged once
+ setLogLevelDebug()
+
+ init_logging(logfile, silent=True)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd("echo hello")
+ stop_logging(logfile)
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(res.output, 'hello\n')
+ self.assertEqual(len(regex_start_cmd.findall(read_file(logfile))), 1)
+ self.assertEqual(len(regex_cmd_exit.findall(read_file(logfile))), 1)
+ write_file(logfile, '')
+
def test_run_cmd_negative_exit_code(self):
"""Test run_cmd function with command that has negative exit code."""
+
+ # use of run_cmd/run_cmd_qa is deprecated, so we need to allow it here
+ self.allow_deprecated_behaviour()
+
# define signal handler to call in case run_cmd takes too long
def handler(signum, _):
raise RuntimeError("Signal handler called with signal %s" % signum)
@@ -218,14 +492,16 @@ def handler(signum, _):
signal.signal(signal.SIGALRM, handler)
signal.alarm(3)
- (_, ec) = run_cmd("kill -9 $$", log_ok=False)
+ with self.mocked_stdout_stderr():
+ (_, ec) = run_cmd("kill -9 $$", log_ok=False)
self.assertEqual(ec, -9)
# reset the alarm
signal.alarm(0)
signal.alarm(3)
- (_, ec) = run_cmd_qa("kill -9 $$", {}, log_ok=False)
+ with self.mocked_stdout_stderr():
+ (_, ec) = run_cmd_qa("kill -9 $$", {}, log_ok=False)
self.assertEqual(ec, -9)
finally:
@@ -233,17 +509,205 @@ def handler(signum, _):
signal.signal(signal.SIGALRM, orig_sigalrm_handler)
signal.alarm(0)
+ def test_run_shell_cmd_fail(self):
+ """Test run_shell_cmd function with command that has negative exit code."""
+ # define signal handler to call in case run takes too long
+ def handler(signum, _):
+ raise RuntimeError("Signal handler called with signal %s" % signum)
+
+ # disable trace output for this test (so stdout remains empty)
+ update_build_option('trace', False)
+
+ orig_sigalrm_handler = signal.getsignal(signal.SIGALRM)
+
+ try:
+ # set the signal handler and a 3-second alarm
+ signal.signal(signal.SIGALRM, handler)
+ signal.alarm(3)
+
+ # command to kill parent shell
+ cmd = "kill -9 $$"
+
+ work_dir = os.path.realpath(self.test_prefix)
+ change_dir(work_dir)
+
+ try:
+ run_shell_cmd(cmd)
+ self.assertFalse("This should never be reached, RunShellCmdError should occur!")
+ except RunShellCmdError as err:
+ self.assertEqual(str(err), "Shell command 'kill' failed!")
+ self.assertEqual(err.cmd, "kill -9 $$")
+ self.assertEqual(err.cmd_name, 'kill')
+ self.assertEqual(err.exit_code, -9)
+ self.assertEqual(err.work_dir, work_dir)
+ self.assertEqual(err.output, '')
+ self.assertEqual(err.stderr, None)
+ self.assertTrue(isinstance(err.caller_info, tuple))
+ self.assertEqual(len(err.caller_info), 3)
+ self.assertEqual(err.caller_info[0], __file__)
+ self.assertTrue(isinstance(err.caller_info[1], int)) # line number of calling site
+ self.assertEqual(err.caller_info[2], 'test_run_shell_cmd_fail')
+
+ with self.mocked_stdout_stderr() as (_, stderr):
+ err.print()
+
+ # check error reporting output
+ stderr = stderr.getvalue()
+ patterns = [
+ r"ERROR: Shell command failed!",
+ r"\s+full command\s* -> kill -9 \$\$",
+ r"\s+exit code\s* -> -9",
+ r"\s+working directory\s* -> " + work_dir,
+ r"\s+called from\s* -> 'test_run_shell_cmd_fail' function in "
+ r"(.|\n)*/test/(.|\n)*/run.py \(line [0-9]+\)",
+ r"\s+output \(stdout \+ stderr\)\s* -> (.|\n)*/run-shell-cmd-output/kill-(.|\n)*/out.txt",
+ r"\s+interactive shell script\s* -> (.|\n)*/run-shell-cmd-output/kill-(.|\n)*/cmd.sh",
+ ]
+ for pattern in patterns:
+ regex = re.compile(pattern, re.M)
+ self.assertTrue(regex.search(stderr), "Pattern '%s' should be found in: %s" % (pattern, stderr))
+
+ # check error reporting output when stdout/stderr are collected separately
+ try:
+ run_shell_cmd(cmd, split_stderr=True)
+ self.assertFalse("This should never be reached, RunShellCmdError should occur!")
+ except RunShellCmdError as err:
+ self.assertEqual(str(err), "Shell command 'kill' failed!")
+ self.assertEqual(err.cmd, "kill -9 $$")
+ self.assertEqual(err.cmd_name, 'kill')
+ self.assertEqual(err.exit_code, -9)
+ self.assertEqual(err.work_dir, work_dir)
+ self.assertEqual(err.output, '')
+ self.assertEqual(err.stderr, '')
+ self.assertTrue(isinstance(err.caller_info, tuple))
+ self.assertEqual(len(err.caller_info), 3)
+ self.assertEqual(err.caller_info[0], __file__)
+ self.assertTrue(isinstance(err.caller_info[1], int)) # line number of calling site
+ self.assertEqual(err.caller_info[2], 'test_run_shell_cmd_fail')
+
+ with self.mocked_stdout_stderr() as (_, stderr):
+ err.print()
+
+ # check error reporting output
+ stderr = stderr.getvalue()
+ patterns = [
+ r"ERROR: Shell command failed!",
+ r"\s+full command\s+ -> kill -9 \$\$",
+ r"\s+exit code\s+ -> -9",
+ r"\s+working directory\s+ -> " + work_dir,
+ r"\s+called from\s+ -> 'test_run_shell_cmd_fail' function in "
+ r"(.|\n)*/test/(.|\n)*/run.py \(line [0-9]+\)",
+ r"\s+output \(stdout\)\s+ -> (.|\n)*/run-shell-cmd-output/kill-(.|\n)*/out.txt",
+ r"\s+error/warnings \(stderr\)\s+ -> (.|\n)*/run-shell-cmd-output/kill-(.|\n)*/err.txt",
+ r"\s+interactive shell script\s* -> (.|\n)*/run-shell-cmd-output/kill-(.|\n)*/cmd.sh",
+ ]
+ for pattern in patterns:
+ regex = re.compile(pattern, re.M)
+ self.assertTrue(regex.search(stderr), "Pattern '%s' should be found in: %s" % (pattern, stderr))
+
+ # no error reporting when fail_on_error is disabled
+ with self.mocked_stdout_stderr() as (_, stderr):
+ res = run_shell_cmd(cmd, fail_on_error=False)
+ self.assertEqual(res.exit_code, -9)
+ self.assertEqual(stderr.getvalue(), '')
+
+ finally:
+ # cleanup: disable the alarm + reset signal handler for SIGALRM
+ signal.signal(signal.SIGALRM, orig_sigalrm_handler)
+ signal.alarm(0)
+
def test_run_cmd_bis(self):
"""More 'complex' test for run_cmd function."""
+
+ # use of run_cmd is deprecated, so we need to allow it here
+ self.allow_deprecated_behaviour()
+
# a more 'complex' command to run, make sure all required output is there
- (out, ec) = run_cmd("for j in `seq 1 3`; do for i in `seq 1 100`; do echo hello; done; sleep 1.4; done")
+ with self.mocked_stdout_stderr():
+ (out, ec) = run_cmd("for j in `seq 1 3`; do for i in `seq 1 100`; do echo hello; done; sleep 1.4; done")
self.assertTrue(out.startswith('hello\nhello\n'))
self.assertEqual(len(out), len("hello\n" * 300))
self.assertEqual(ec, 0)
+ def test_run_shell_cmd_bis(self):
+ """More 'complex' test for run_shell_cmd function."""
+ # a more 'complex' command to run, make sure all required output is there
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd("for j in `seq 1 3`; do for i in `seq 1 100`; do echo hello; done; sleep 1.4; done")
+ self.assertTrue(res.output.startswith('hello\nhello\n'))
+ self.assertEqual(len(res.output), len("hello\n" * 300))
+ self.assertEqual(res.exit_code, 0)
+
+ def test_run_cmd_work_dir(self):
+ """
+ Test running command in specific directory with run_cmd function.
+ """
+
+ # use of run_cmd is deprecated, so we need to allow it here
+ self.allow_deprecated_behaviour()
+
+ orig_wd = os.getcwd()
+ self.assertFalse(os.path.samefile(orig_wd, self.test_prefix))
+
+ test_dir = os.path.join(self.test_prefix, 'test')
+ for fn in ('foo.txt', 'bar.txt'):
+ write_file(os.path.join(test_dir, fn), 'test')
+
+ with self.mocked_stdout_stderr():
+ (out, ec) = run_cmd("ls | sort", path=test_dir)
+
+ self.assertEqual(ec, 0)
+ self.assertEqual(out, 'bar.txt\nfoo.txt\n')
+
+ self.assertTrue(os.path.samefile(orig_wd, os.getcwd()))
+
+ def test_run_shell_cmd_work_dir(self):
+ """
+ Test running shell command in specific directory with run_shell_cmd function.
+ """
+ test_dir = os.path.join(self.test_prefix, 'test')
+ test_workdir = os.path.join(self.test_prefix, 'test', 'workdir')
+ for fn in ('foo.txt', 'bar.txt'):
+ write_file(os.path.join(test_workdir, fn), 'test')
+
+ os.chdir(test_dir)
+ orig_wd = os.getcwd()
+ self.assertFalse(os.path.samefile(orig_wd, self.test_prefix))
+
+ cmd = "ls | sort"
+
+ # working directory is not explicitly defined
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd)
+
+ self.assertEqual(res.cmd, cmd)
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(res.output, 'workdir\n')
+ self.assertEqual(res.stderr, None)
+ self.assertEqual(res.work_dir, orig_wd)
+
+ self.assertTrue(os.path.samefile(orig_wd, os.getcwd()))
+
+ # working directory is explicitly defined
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd, work_dir=test_workdir)
+
+ self.assertEqual(res.cmd, cmd)
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(res.output, 'bar.txt\nfoo.txt\n')
+ self.assertEqual(res.stderr, None)
+ self.assertEqual(res.work_dir, test_workdir)
+
+ self.assertTrue(os.path.samefile(orig_wd, os.getcwd()))
+
def test_run_cmd_log_output(self):
"""Test run_cmd with log_output enabled"""
- (out, ec) = run_cmd("seq 1 100", log_output=True)
+
+ # use of run_cmd is deprecated, so we need to allow it here
+ self.allow_deprecated_behaviour()
+
+ with self.mocked_stdout_stderr():
+ (out, ec) = run_cmd("seq 1 100", log_output=True)
self.assertEqual(ec, 0)
self.assertEqual(type(out), str)
self.assertTrue(out.startswith("1\n2\n"))
@@ -266,17 +730,63 @@ def test_run_cmd_log_output(self):
write_file(test_file, text)
cmd = "cat %s" % test_file
- (out, ec) = run_cmd(cmd, log_output=True)
+ with self.mocked_stdout_stderr():
+ (out, ec) = run_cmd(cmd, log_output=True)
self.assertEqual(ec, 0)
self.assertTrue(out.startswith('foo ') and out.endswith(' bar'))
self.assertEqual(type(out), str)
+ def test_run_shell_cmd_split_stderr(self):
+ """Test getting split stdout/stderr output from run_shell_cmd function."""
+ cmd = ';'.join([
+ "echo ok",
+ "echo warning >&2",
+ ])
+
+ # by default, output contains both stdout + stderr
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd)
+ self.assertEqual(res.exit_code, 0)
+ output_lines = res.output.split('\n')
+ self.assertTrue("ok" in output_lines)
+ self.assertTrue("warning" in output_lines)
+ self.assertEqual(res.stderr, None)
+
+ # cleanup of artifacts in between calls to run_shell_cmd
+ remove_dir(self.test_prefix)
+
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd, split_stderr=True)
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(res.stderr, "warning\n")
+ self.assertEqual(res.output, "ok\n")
+
+ # check whether environment variables that point to stdout/stderr output files
+ # are set in environment defined by cmd.sh script
+ paths = glob.glob(os.path.join(self.test_prefix, 'eb-*', 'run-shell-cmd-output', 'echo-*'))
+ self.assertEqual(len(paths), 1)
+ cmd_tmpdir = paths[0]
+ cmd_script = os.path.join(cmd_tmpdir, 'cmd.sh')
+ self.assertExists(cmd_script)
+
+ cmd_cmd = '; '.join([
+ "echo $EB_CMD_OUT_FILE",
+ "cat $EB_CMD_OUT_FILE",
+ "echo $EB_CMD_ERR_FILE",
+ "cat $EB_CMD_ERR_FILE",
+ ])
+ cmd = f"{cmd_script} -c '{cmd_cmd}'"
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd, fail_on_error=False)
+
+ regex = re.compile(".*/echo-.*/out.txt\nok\n.*/echo-.*/err.txt\nwarning$")
+ self.assertTrue(regex.search(res.output), f"Pattern '{regex.pattern}' should be found in {res.output}")
+
def test_run_cmd_trace(self):
- """Test run_cmd under --trace"""
- # replace log.experimental with log.warning to allow experimental code
- easybuild.tools.utilities._log.experimental = easybuild.tools.utilities._log.warning
+ """Test run_cmd in trace mode, and with tracing disabled."""
- init_config(build_options={'trace': True})
+ # use of run_cmd is deprecated, so we need to allow it here
+ self.allow_deprecated_behaviour()
pattern = [
r"^ >> running command:",
@@ -287,6 +797,7 @@ def test_run_cmd_trace(self):
r" >> command completed: exit 0, ran in .*",
]
+ # trace output is enabled by default (since EasyBuild v5.0)
self.mock_stdout(True)
self.mock_stderr(True)
(out, ec) = run_cmd("echo hello")
@@ -294,11 +805,28 @@ def test_run_cmd_trace(self):
stderr = self.get_stderr()
self.mock_stdout(False)
self.mock_stderr(False)
+ self.assertEqual(out, 'hello\n')
self.assertEqual(ec, 0)
- self.assertEqual(stderr, '')
+ self.assertTrue(stderr.strip().startswith("WARNING: Deprecated functionality"))
regex = re.compile('\n'.join(pattern))
self.assertTrue(regex.search(stdout), "Pattern '%s' found in: %s" % (regex.pattern, stdout))
+ init_config(build_options={'trace': False})
+
+ self.mock_stdout(True)
+ self.mock_stderr(True)
+ (out, ec) = run_cmd("echo hello")
+ stdout = self.get_stdout()
+ stderr = self.get_stderr()
+ self.mock_stdout(False)
+ self.mock_stderr(False)
+ self.assertEqual(out, 'hello\n')
+ self.assertEqual(ec, 0)
+ self.assertTrue(stderr.strip().startswith("WARNING: Deprecated functionality"))
+ self.assertEqual(stdout, '')
+
+ init_config(build_options={'trace': True})
+
# also test with command that is fed input via stdin
self.mock_stdout(True)
self.mock_stderr(True)
@@ -307,30 +835,168 @@ def test_run_cmd_trace(self):
stderr = self.get_stderr()
self.mock_stdout(False)
self.mock_stderr(False)
+ self.assertEqual(out, 'hello')
self.assertEqual(ec, 0)
- self.assertEqual(stderr, '')
+ self.assertTrue(stderr.strip().startswith("WARNING: Deprecated functionality"))
pattern.insert(3, r"\t\[input: hello\]")
pattern[-2] = "\tcat"
regex = re.compile('\n'.join(pattern))
self.assertTrue(regex.search(stdout), "Pattern '%s' found in: %s" % (regex.pattern, stdout))
+ init_config(build_options={'trace': False})
+
+ self.mock_stdout(True)
+ self.mock_stderr(True)
+ (out, ec) = run_cmd('cat', inp='hello')
+ stdout = self.get_stdout()
+ stderr = self.get_stderr()
+ self.mock_stdout(False)
+ self.mock_stderr(False)
+ self.assertEqual(out, 'hello')
+ self.assertEqual(ec, 0)
+ self.assertTrue(stderr.strip().startswith("WARNING: Deprecated functionality"))
+ self.assertEqual(stdout, '')
+
# trace output can be disabled on a per-command basis
+ for trace in (True, False):
+ init_config(build_options={'trace': trace})
+
+ self.mock_stdout(True)
+ self.mock_stderr(True)
+ (out, ec) = run_cmd("echo hello", trace=False)
+ stdout = self.get_stdout()
+ stderr = self.get_stderr()
+ self.mock_stdout(False)
+ self.mock_stderr(False)
+ self.assertEqual(out, 'hello\n')
+ self.assertEqual(ec, 0)
+ self.assertEqual(stdout, '')
+ self.assertTrue(stderr.strip().startswith("WARNING: Deprecated functionality"))
+
+ def test_run_shell_cmd_trace(self):
+ """Test run_shell_cmd function in trace mode, and with tracing disabled."""
+
+ pattern = [
+ r"^ >> running shell command:",
+ r"\techo hello",
+ r"\t\[started at: .*\]",
+ r"\t\[working dir: .*\]",
+ r"\t\[output and state saved to .*\]",
+ r" >> command completed: exit 0, ran in .*",
+ ]
+
+ # trace output is enabled by default (since EasyBuild v5.0)
self.mock_stdout(True)
self.mock_stderr(True)
- (out, ec) = run_cmd("echo hello", trace=False)
+ res = run_shell_cmd("echo hello")
+ stdout = self.get_stdout()
+ stderr = self.get_stderr()
+ self.mock_stdout(False)
+ self.mock_stderr(False)
+ self.assertEqual(res.output, 'hello\n')
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(stderr, '')
+ regex = re.compile('\n'.join(pattern))
+ self.assertTrue(regex.search(stdout), "Pattern '%s' found in: %s" % (regex.pattern, stdout))
+
+ init_config(build_options={'trace': False})
+
+ self.mock_stdout(True)
+ self.mock_stderr(True)
+ res = run_shell_cmd("echo hello")
stdout = self.get_stdout()
stderr = self.get_stderr()
self.mock_stdout(False)
self.mock_stderr(False)
+ self.assertEqual(res.output, 'hello\n')
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(stderr, '')
+ self.assertEqual(stdout, '')
+
+ init_config(build_options={'trace': True})
+
+ # trace output can be disabled on a per-command basis via 'hidden' option
+ for trace in (True, False):
+ init_config(build_options={'trace': trace})
+
+ self.mock_stdout(True)
+ self.mock_stderr(True)
+ res = run_shell_cmd("echo hello", hidden=True)
+ stdout = self.get_stdout()
+ stderr = self.get_stderr()
+ self.mock_stdout(False)
+ self.mock_stderr(False)
+ self.assertEqual(res.output, 'hello\n')
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(stdout, '')
+ self.assertEqual(stderr, '')
+
+ def test_run_shell_cmd_trace_stdin(self):
+ """Test run_shell_cmd function under --trace + passing stdin input."""
+
+ init_config(build_options={'trace': True})
+
+ pattern = [
+ r"^ >> running shell command:",
+ r"\techo hello",
+ r"\t\[started at: [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]\]",
+ r"\t\[working dir: .*\]",
+ r"\t\[output and state saved to .*\]",
+ r" >> command completed: exit 0, ran in .*",
+ ]
+
+ self.mock_stdout(True)
+ self.mock_stderr(True)
+ res = run_shell_cmd("echo hello")
+ stdout = self.get_stdout()
+ stderr = self.get_stderr()
+ self.mock_stdout(False)
+ self.mock_stderr(False)
+ self.assertEqual(res.output, 'hello\n')
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(stderr, '')
+ regex = re.compile('\n'.join(pattern))
+ self.assertTrue(regex.search(stdout), "Pattern '%s' found in: %s" % (regex.pattern, stdout))
+
+ # also test with command that is fed input via stdin
+ self.mock_stdout(True)
+ self.mock_stderr(True)
+ res = run_shell_cmd('cat', stdin='hello')
+ stdout = self.get_stdout()
+ stderr = self.get_stderr()
+ self.mock_stdout(False)
+ self.mock_stderr(False)
+ self.assertEqual(res.output, 'hello')
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(stderr, '')
+ pattern.insert(4, r"\t\[input: hello\]")
+ pattern[1] = "\tcat"
+ regex = re.compile('\n'.join(pattern))
+ self.assertTrue(regex.search(stdout), "Pattern '%s' found in: %s" % (regex.pattern, stdout))
+
+ # trace output can be disabled on a per-command basis by enabling 'hidden'
+ self.mock_stdout(True)
+ self.mock_stderr(True)
+ res = run_shell_cmd("echo hello", hidden=True)
+ stdout = self.get_stdout()
+ stderr = self.get_stderr()
+ self.mock_stdout(False)
+ self.mock_stderr(False)
+ self.assertEqual(res.output, 'hello\n')
+ self.assertEqual(res.exit_code, 0)
self.assertEqual(stdout, '')
self.assertEqual(stderr, '')
def test_run_cmd_qa(self):
"""Basic test for run_cmd_qa function."""
+ # use of run_cmd_qa is deprecated, so we need to allow it here
+ self.allow_deprecated_behaviour()
+
cmd = "echo question; read x; echo $x"
qa = {'question': 'answer'}
- (out, ec) = run_cmd_qa(cmd, qa)
+ with self.mocked_stdout_stderr():
+ (out, ec) = run_cmd_qa(cmd, qa)
self.assertEqual(out, "question\nanswer\n")
# no reason echo hello could fail
self.assertEqual(ec, 0)
@@ -342,19 +1008,183 @@ def test_run_cmd_qa(self):
write_file(test_file, b"foo \xe2 bar")
cmd += "; cat %s" % test_file
- (out, ec) = run_cmd_qa(cmd, qa)
+ with self.mocked_stdout_stderr():
+ (out, ec) = run_cmd_qa(cmd, qa)
self.assertEqual(ec, 0)
self.assertTrue(out.startswith("question\nanswer\nfoo "))
self.assertTrue(out.endswith('bar'))
+ # test handling of output that is not actually a question
+ cmd = ';'.join([
+ "echo not-a-question-but-a-statement",
+ "sleep 3",
+ "echo question",
+ "read x",
+ "echo $x",
+ ])
+ qa = {'question': 'answer'}
+
+ # fails because non-question is encountered
+ error_pattern = "Max nohits 1 reached: end of output not-a-question-but-a-statement"
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, run_cmd_qa, cmd, qa, maxhits=1, trace=False)
+
+ with self.mocked_stdout_stderr():
+ (out, ec) = run_cmd_qa(cmd, qa, no_qa=["not-a-question-but-a-statement"], maxhits=1, trace=False)
+ self.assertEqual(out, "not-a-question-but-a-statement\nquestion\nanswer\n")
+ self.assertEqual(ec, 0)
+
+ def test_run_shell_cmd_qa(self):
+ """Basic test for Q&A support in run_shell_cmd function."""
+
+ cmd = '; '.join([
+ "echo question1",
+ "read x",
+ "echo $x",
+ "echo question2",
+ "read y",
+ "echo $y",
+ ])
+ qa = [
+ ('question1', 'answer1'),
+ ('question2', 'answer2'),
+ ]
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd, qa_patterns=qa)
+ self.assertEqual(res.output, "question1\nanswer1\nquestion2\nanswer2\n")
+ # no reason echo hello could fail
+ self.assertEqual(res.exit_code, 0)
+
+ # test running command that emits non-UTF8 characters
+ # this is constructed to reproduce errors like:
+ # UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe2
+ test_file = os.path.join(self.test_prefix, 'foo.txt')
+ write_file(test_file, b"foo \xe2 bar")
+ cmd += "; cat %s" % test_file
+
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd, qa_patterns=qa)
+ self.assertEqual(res.exit_code, 0)
+ self.assertTrue(res.output.startswith("question1\nanswer1\nquestion2\nanswer2\nfoo "))
+ self.assertTrue(res.output.endswith('bar'))
+
+ # check type check on qa_patterns
+ error_pattern = "qa_patterns passed to run_shell_cmd should be a list of 2-tuples!"
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, run_shell_cmd, cmd, qa_patterns={'foo': 'bar'})
+ self.assertErrorRegex(EasyBuildError, error_pattern, run_shell_cmd, cmd, qa_patterns=('foo', 'bar'))
+ self.assertErrorRegex(EasyBuildError, error_pattern, run_shell_cmd, cmd, qa_patterns=(('foo', 'bar'),))
+ self.assertErrorRegex(EasyBuildError, error_pattern, run_shell_cmd, cmd, qa_patterns='foo:bar')
+ self.assertErrorRegex(EasyBuildError, error_pattern, run_shell_cmd, cmd, qa_patterns=['foo:bar'])
+
+ # validate use of qa_timeout to give up if there's no matching question for too long
+ cmd = "sleep 3; echo 'question'; read a; echo $a"
+ error_pattern = "No matching questions found for current command output, giving up after 1 seconds!"
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, run_shell_cmd, cmd, qa_patterns=qa, qa_timeout=1)
+
+ # check using answer that is completed via pattern extracted from question
+ cmd = ';'.join([
+ "echo 'and the magic number is: 42'",
+ "read magic_number",
+ "echo $magic_number",
+ ])
+ qa = [("and the magic number is: (?P[0-9]+)", "%(nr)s")]
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd, qa_patterns=qa)
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(res.output, "and the magic number is: 42\n42\n")
+
+ # test handling of output that is not actually a question
+ cmd = ';'.join([
+ "echo not-a-question-but-a-statement",
+ "sleep 3",
+ "echo question",
+ "read x",
+ "echo $x",
+ ])
+ qa = [('question', 'answer')]
+
+ # fails because non-question is encountered
+ error_pattern = "No matching questions found for current command output, giving up after 1 seconds!"
+ self.assertErrorRegex(EasyBuildError, error_pattern, run_shell_cmd, cmd, qa_patterns=qa, qa_timeout=1,
+ hidden=True)
+
+ qa_wait_patterns = ["not-a-question-but-a-statement"]
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd, qa_patterns=qa, qa_wait_patterns=qa_wait_patterns, qa_timeout=1)
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(res.output, "not-a-question-but-a-statement\nquestion\nanswer\n")
+
+ # test multi-line question
+ cmd = ';'.join([
+ "echo please",
+ "echo answer",
+ "read x",
+ "echo $x",
+ ])
+ qa = [("please answer", "42")]
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd, qa_patterns=qa)
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(res.output, "please\nanswer\n42\n")
+
+ # also test multi-line wait pattern
+ cmd = "echo just; echo wait; sleep 3; " + cmd
+ qa_wait_patterns = ["just wait"]
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd, qa_patterns=qa, qa_wait_patterns=qa_wait_patterns, qa_timeout=1)
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(res.output, "just\nwait\nplease\nanswer\n42\n")
+
+ # test multi-line question pattern with hard space
+ cmd = ';'.join([
+ "echo please",
+ "echo answer",
+ "read x",
+ "echo $x",
+ ])
+ # question pattern uses hard space, should get replaced internally by more liberal whitespace regex pattern
+ qa = [(r"please\ answer", "42")]
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd, qa_patterns=qa, qa_timeout=3)
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(res.output, "please\nanswer\n42\n")
+
+ # test interactive command that takes a while before producing more output that includes second question
+ cmd = ';'.join([
+ "echo question1",
+ "read answer1",
+ "sleep 2",
+ "echo question2",
+ "read answer2",
+ # note: delaying additional output (except the actual questions) is important
+ # to verify that this is working as intended
+ "echo $answer1",
+ "echo $answer2",
+ ])
+ qa = [
+ (r'question1', 'answer1'),
+ (r'question2', 'answer2'),
+ ]
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd, qa_patterns=qa)
+
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(res.output, "question1\nquestion2\nanswer1\nanswer2\n")
+
def test_run_cmd_qa_buffering(self):
"""Test whether run_cmd_qa uses unbuffered output."""
+ # use of run_cmd_qa is deprecated, so we need to allow it here
+ self.allow_deprecated_behaviour()
+
# command that generates a lot of output before waiting for input
# note: bug being fixed can be reproduced reliably using 1000, but not with too high values like 100000!
cmd = 'for x in $(seq 1000); do echo "This is a number you can pick: $x"; done; '
cmd += 'echo "Pick a number: "; read number; echo "Picked number: $number"'
- (out, ec) = run_cmd_qa(cmd, {'Pick a number: ': '42'}, log_all=True, maxhits=5)
+ with self.mocked_stdout_stderr():
+ (out, ec) = run_cmd_qa(cmd, {'Pick a number: ': '42'}, log_all=True, maxhits=5)
self.assertEqual(ec, 0)
regex = re.compile("Picked number: 42$")
@@ -372,14 +1202,52 @@ def test_run_cmd_qa_buffering(self):
write_file(script, script_txt)
adjust_permissions(script, stat.S_IXUSR)
- out, ec = run_cmd_qa(script, {}, log_ok=False)
+ with self.mocked_stdout_stderr():
+ out, ec = run_cmd_qa(script, {}, log_ok=False)
self.assertEqual(ec, 1)
self.assertEqual(out, "Hello, I am about to exit\nERROR: I failed\n")
+ def test_run_shell_cmd_qa_buffering(self):
+ """Test whether run_shell_cmd uses unbuffered output when running interactive commands."""
+
+ # command that generates a lot of output before waiting for input
+ # note: bug being fixed can be reproduced reliably using 1000, but not with too high values like 100000!
+ cmd = 'for x in $(seq 1000); do echo "This is a number you can pick: $x"; done; '
+ cmd += 'echo "Pick a number: "; read number; echo "Picked number: $number"'
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd, qa_patterns=[('Pick a number: ', '42')], qa_timeout=10)
+
+ self.assertEqual(res.exit_code, 0)
+ regex = re.compile("Picked number: 42$")
+ self.assertTrue(regex.search(res.output), f"Pattern '{regex.pattern}' found in: {res.output}")
+
+ # also test with script run as interactive command that quickly exits with non-zero exit code;
+ # see https://github.com/easybuilders/easybuild-framework/issues/3593
+ script_txt = '\n'.join([
+ "#/bin/bash",
+ "echo 'Hello, I am about to exit'",
+ "echo 'ERROR: I failed' >&2",
+ "exit 1",
+ ])
+ script = os.path.join(self.test_prefix, 'test.sh')
+ write_file(script, script_txt)
+ adjust_permissions(script, stat.S_IXUSR)
+
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(script, qa_patterns=[], fail_on_error=False)
+
+ self.assertEqual(res.exit_code, 1)
+ self.assertEqual(res.output, "Hello, I am about to exit\nERROR: I failed\n")
+
def test_run_cmd_qa_log_all(self):
"""Test run_cmd_qa with log_output enabled"""
- (out, ec) = run_cmd_qa("echo 'n: '; read n; seq 1 $n", {'n: ': '5'}, log_all=True)
+
+ # use of run_cmd_qa is deprecated, so we need to allow it here
+ self.allow_deprecated_behaviour()
+
+ with self.mocked_stdout_stderr():
+ (out, ec) = run_cmd_qa("echo 'n: '; read n; seq 1 $n", {'n: ': '5'}, log_all=True)
self.assertEqual(ec, 0)
self.assertEqual(out, "n: \n1\n2\n3\n4\n5\n")
@@ -389,13 +1257,25 @@ def test_run_cmd_qa_log_all(self):
extra_pref = "# output for interactive command: echo 'n: '; read n; seq 1 $n\n\n"
self.assertEqual(run_cmd_log_txt, extra_pref + "n: \n1\n2\n3\n4\n5\n")
+ def test_run_shell_cmd_qa_log(self):
+ """Test temporary log file for run_shell_cmd with qa_patterns"""
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd("echo 'n: '; read n; seq 1 $n", qa_patterns=[('n:', '5')])
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(res.output, "n: \n1\n2\n3\n4\n5\n")
+
+ run_cmd_logs = glob.glob(os.path.join(tempfile.gettempdir(), 'run-shell-cmd-output', 'echo-*', 'out.txt'))
+ self.assertEqual(len(run_cmd_logs), 1)
+ run_cmd_log_txt = read_file(run_cmd_logs[0])
+ self.assertEqual(run_cmd_log_txt, "n: \n1\n2\n3\n4\n5\n")
+
def test_run_cmd_qa_trace(self):
"""Test run_cmd under --trace"""
- # replace log.experimental with log.warning to allow experimental code
- easybuild.tools.utilities._log.experimental = easybuild.tools.utilities._log.warning
- init_config(build_options={'trace': True})
+ # use of run_cmd/run_cmd_qa is deprecated, so we need to allow it here
+ self.allow_deprecated_behaviour()
+ # --trace is enabled by default
self.mock_stdout(True)
self.mock_stderr(True)
(out, ec) = run_cmd_qa("echo 'n: '; read n; seq 1 $n", {'n: ': '5'})
@@ -403,7 +1283,7 @@ def test_run_cmd_qa_trace(self):
stderr = self.get_stderr()
self.mock_stdout(False)
self.mock_stderr(False)
- self.assertEqual(stderr, '')
+ self.assertTrue(stderr.strip().startswith("WARNING: Deprecated functionality"))
pattern = r"^ >> running interactive command:\n"
pattern += r"\t\[started at: .*\]\n"
pattern += r"\t\[working dir: .*\]\n"
@@ -421,128 +1301,316 @@ def test_run_cmd_qa_trace(self):
self.mock_stdout(False)
self.mock_stderr(False)
self.assertEqual(stdout, '')
+ self.assertTrue(stderr.strip().startswith("WARNING: Deprecated functionality"))
+
+ def test_run_shell_cmd_qa_trace(self):
+ """Test run_shell_cmd with qa_patterns under --trace"""
+
+ # --trace is enabled by default
+ self.mock_stdout(True)
+ self.mock_stderr(True)
+ run_shell_cmd("echo 'n: '; read n; seq 1 $n", qa_patterns=[('n: ', '5')])
+ stdout = self.get_stdout()
+ stderr = self.get_stderr()
+ self.mock_stdout(False)
+ self.mock_stderr(False)
+ self.assertEqual(stderr, '')
+ pattern = r"^ >> running interactive shell command:\n"
+ pattern += r"\techo \'n: \'; read n; seq 1 \$n\n"
+ pattern += r"\t\[started at: .*\]\n"
+ pattern += r"\t\[working dir: .*\]\n"
+ pattern += r"\t\[output and state saved to .*\]\n"
+ pattern += r' >> command completed: exit 0, ran in .*'
+ self.assertTrue(re.search(pattern, stdout), "Pattern '%s' found in: %s" % (pattern, stdout))
+
+ # trace output can be disabled on a per-command basis
+ self.mock_stdout(True)
+ self.mock_stderr(True)
+ run_shell_cmd("echo 'n: '; read n; seq 1 $n", qa_patterns=[('n: ', '5')], hidden=True)
+ stdout = self.get_stdout()
+ stderr = self.get_stderr()
+ self.mock_stdout(False)
+ self.mock_stderr(False)
+ self.assertEqual(stdout, '')
self.assertEqual(stderr, '')
def test_run_cmd_qa_answers(self):
"""Test providing list of answers in run_cmd_qa."""
+
+ # use of run_cmd_qa is deprecated, so we need to allow it here
+ self.allow_deprecated_behaviour()
+
cmd = "echo question; read x; echo $x; " * 2
qa = {"question": ["answer1", "answer2"]}
- (out, ec) = run_cmd_qa(cmd, qa)
+ with self.mocked_stdout_stderr():
+ (out, ec) = run_cmd_qa(cmd, qa)
self.assertEqual(out, "question\nanswer1\nquestion\nanswer2\n")
self.assertEqual(ec, 0)
- (out, ec) = run_cmd_qa(cmd, {}, std_qa=qa)
+ with self.mocked_stdout_stderr():
+ (out, ec) = run_cmd_qa(cmd, {}, std_qa=qa)
self.assertEqual(out, "question\nanswer1\nquestion\nanswer2\n")
self.assertEqual(ec, 0)
- self.assertErrorRegex(EasyBuildError, "Invalid type for answer", run_cmd_qa, cmd, {'q': 1})
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, "Invalid type for answer", run_cmd_qa, cmd, {'q': 1})
# test cycling of answers
cmd = cmd * 2
- (out, ec) = run_cmd_qa(cmd, {}, std_qa=qa)
+ with self.mocked_stdout_stderr():
+ (out, ec) = run_cmd_qa(cmd, {}, std_qa=qa)
self.assertEqual(out, "question\nanswer1\nquestion\nanswer2\n" * 2)
self.assertEqual(ec, 0)
+ def test_run_shell_cmd_qa_answers(self):
+ """Test providing list of answers for a question in run_shell_cmd."""
+
+ cmd = "echo question; read x; echo $x; " * 2
+ qa = [("question", ["answer1", "answer2"])]
+
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd, qa_patterns=qa)
+ self.assertEqual(res.output, "question\nanswer1\nquestion\nanswer2\n")
+ self.assertEqual(res.exit_code, 0)
+
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, "Unknown type of answers encountered", run_shell_cmd, cmd,
+ qa_patterns=[('question', 1)])
+
+ # test cycling of answers
+ cmd = cmd * 2
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd, qa_patterns=qa)
+ self.assertEqual(res.output, "question\nanswer1\nquestion\nanswer2\n" * 2)
+ self.assertEqual(res.exit_code, 0)
+
def test_run_cmd_simple(self):
"""Test return value for run_cmd in 'simple' mode."""
- self.assertEqual(True, run_cmd("echo hello", simple=True))
- self.assertEqual(False, run_cmd("exit 1", simple=True, log_all=False, log_ok=False))
+
+ # use of run_cmd is deprecated, so we need to allow it here
+ self.allow_deprecated_behaviour()
+
+ with self.mocked_stdout_stderr():
+ self.assertEqual(True, run_cmd("echo hello", simple=True))
+ self.assertEqual(False, run_cmd("exit 1", simple=True, log_all=False, log_ok=False))
def test_run_cmd_cache(self):
"""Test caching for run_cmd"""
- (first_out, ec) = run_cmd("ulimit -u")
+
+ # use of run_cmd is deprecated, so we need to allow it here
+ self.allow_deprecated_behaviour()
+
+ with self.mocked_stdout_stderr():
+ (first_out, ec) = run_cmd("ulimit -u")
self.assertEqual(ec, 0)
- (cached_out, ec) = run_cmd("ulimit -u")
+ with self.mocked_stdout_stderr():
+ (cached_out, ec) = run_cmd("ulimit -u")
self.assertEqual(ec, 0)
self.assertEqual(first_out, cached_out)
# inject value into cache to check whether executing command again really returns cached value
- run_cmd.update_cache({("ulimit -u", None): ("123456", 123)})
- (cached_out, ec) = run_cmd("ulimit -u")
+ with self.mocked_stdout_stderr():
+ run_cmd.update_cache({("ulimit -u", None): ("123456", 123)})
+ (cached_out, ec) = run_cmd("ulimit -u")
self.assertEqual(ec, 123)
self.assertEqual(cached_out, "123456")
# also test with command that uses stdin
- (out, ec) = run_cmd("cat", inp='foo')
+ with self.mocked_stdout_stderr():
+ (out, ec) = run_cmd("cat", inp='foo')
self.assertEqual(ec, 0)
self.assertEqual(out, 'foo')
# inject different output for cat with 'foo' as stdin to check whether cached value is used
- run_cmd.update_cache({('cat', 'foo'): ('bar', 123)})
- (cached_out, ec) = run_cmd("cat", inp='foo')
+ with self.mocked_stdout_stderr():
+ run_cmd.update_cache({('cat', 'foo'): ('bar', 123)})
+ (cached_out, ec) = run_cmd("cat", inp='foo')
self.assertEqual(ec, 123)
self.assertEqual(cached_out, 'bar')
run_cmd.clear_cache()
+ def test_run_shell_cmd_cache(self):
+ """Test caching for run_shell_cmd function"""
+
+ cmd = "ulimit -u"
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd)
+ first_out = res.output
+ self.assertEqual(res.exit_code, 0)
+
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd)
+ cached_out = res.output
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(first_out, cached_out)
+
+ # inject value into cache to check whether executing command again really returns cached value
+ with self.mocked_stdout_stderr():
+ cached_res = RunShellCmdResult(cmd=cmd, output="123456", exit_code=123, stderr=None,
+ work_dir='/test_ulimit', out_file='/tmp/foo.out', err_file=None,
+ cmd_sh='/tmp/cmd.sh', thread_id=None, task_id=None)
+ run_shell_cmd.update_cache({(cmd, None): cached_res})
+ res = run_shell_cmd(cmd)
+ self.assertEqual(res.cmd, cmd)
+ self.assertEqual(res.exit_code, 123)
+ self.assertEqual(res.output, "123456")
+ self.assertEqual(res.stderr, None)
+ self.assertEqual(res.work_dir, '/test_ulimit')
+
+ # also test with command that uses stdin
+ cmd = "cat"
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd, stdin='foo')
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(res.output, 'foo')
+
+ # inject different output for cat with 'foo' as stdin to check whether cached value is used
+ with self.mocked_stdout_stderr():
+ cached_res = RunShellCmdResult(cmd=cmd, output="bar", exit_code=123, stderr=None,
+ work_dir='/test_cat', out_file='/tmp/cat.out', err_file=None,
+ cmd_sh='/tmp/cmd.sh', thread_id=None, task_id=None)
+ run_shell_cmd.update_cache({(cmd, 'foo'): cached_res})
+ res = run_shell_cmd(cmd, stdin='foo')
+ self.assertEqual(res.cmd, cmd)
+ self.assertEqual(res.exit_code, 123)
+ self.assertEqual(res.output, 'bar')
+ self.assertEqual(res.stderr, None)
+ self.assertEqual(res.work_dir, '/test_cat')
+
+ run_shell_cmd.clear_cache()
+
def test_parse_log_error(self):
"""Test basic parse_log_for_error functionality."""
- errors = parse_log_for_error("error failed", True)
+
+ # use of parse_log_for_error is deprecated, so we need to allow it here
+ self.allow_deprecated_behaviour()
+
+ with self.mocked_stdout_stderr():
+ errors = parse_log_for_error("error failed", True)
self.assertEqual(len(errors), 1)
- def test_dry_run(self):
- """Test use of functions under (extended) dry run."""
+ def test_run_cmd_dry_run(self):
+ """Test use of run_cmd function under (extended) dry run."""
+
+ # use of run_cmd is deprecated, so we need to allow it here
+ self.allow_deprecated_behaviour()
+
build_options = {
'extended_dry_run': True,
'silent': False,
}
init_config(build_options=build_options)
- self.mock_stdout(True)
- run_cmd("somecommand foo 123 bar")
- txt = self.get_stdout()
- self.mock_stdout(False)
+ cmd = "somecommand foo 123 bar"
- expected_regex = re.compile('\n'.join([
- r" running command \"somecommand foo 123 bar\"",
- r" \(in .*\)",
- ]))
- self.assertTrue(expected_regex.match(txt), "Pattern %s matches with: %s" % (expected_regex.pattern, txt))
+ with self.mocked_stdout_stderr():
+ run_cmd(cmd)
+ stdout = self.get_stdout()
+
+ expected = """ running command "somecommand foo 123 bar"\n"""
+ self.assertIn(expected, stdout)
# check disabling 'verbose'
- self.mock_stdout(True)
- run_cmd("somecommand foo 123 bar", verbose=False)
- txt = self.get_stdout()
- self.mock_stdout(False)
- self.assertEqual(txt, '')
+ with self.mocked_stdout_stderr():
+ run_cmd("somecommand foo 123 bar", verbose=False)
+ stdout = self.get_stdout()
+ self.assertNotIn(expected, stdout)
- # check forced run
+ # check forced run_cmd
outfile = os.path.join(self.test_prefix, 'cmd.out')
self.assertNotExists(outfile)
- self.mock_stdout(True)
- run_cmd("echo 'This is always echoed' > %s" % outfile, force_in_dry_run=True)
- txt = self.get_stdout()
- self.mock_stdout(False)
- # nothing printed to stdout, but command was run
- self.assertEqual(txt, '')
+ with self.mocked_stdout_stderr():
+ run_cmd("echo 'This is always echoed' > %s" % outfile, force_in_dry_run=True)
self.assertExists(outfile)
self.assertEqual(read_file(outfile), "This is always echoed\n")
# Q&A commands
+ with self.mocked_stdout_stderr():
+ run_shell_cmd("some_qa_cmd", qa_patterns=[('question1', 'answer1')])
+ stdout = self.get_stdout()
+
+ expected = """ running interactive shell command "some_qa_cmd"\n"""
+ self.assertIn(expected, stdout)
+
+ with self.mocked_stdout_stderr():
+ run_cmd_qa("some_qa_cmd", {'question1': 'answer1'})
+ stdout = self.get_stdout()
+
+ expected = """ running interactive command "some_qa_cmd"\n"""
+ self.assertIn(expected, stdout)
+
+ def test_run_shell_cmd_dry_run(self):
+ """Test use of run_shell_cmd function under (extended) dry run."""
+ build_options = {
+ 'extended_dry_run': True,
+ 'silent': False,
+ }
+ init_config(build_options=build_options)
+
+ cmd = "somecommand foo 123 bar"
+
self.mock_stdout(True)
- run_cmd_qa("some_qa_cmd", {'question1': 'answer1'})
- txt = self.get_stdout()
+ res = run_shell_cmd(cmd)
+ stdout = self.get_stdout()
self.mock_stdout(False)
-
- expected_regex = re.compile('\n'.join([
- r" running interactive command \"some_qa_cmd\"",
- r" \(in .*\)",
- ]))
- self.assertTrue(expected_regex.match(txt), "Pattern %s matches with: %s" % (expected_regex.pattern, txt))
+ # fake output/exit code is returned for commands not actually run in dry run mode
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(res.output, '')
+ self.assertEqual(res.stderr, None)
+ # check dry run output
+ expected = """ running shell command "somecommand foo 123 bar"\n"""
+ self.assertIn(expected, stdout)
+
+ # check enabling 'hidden'
+ self.mock_stdout(True)
+ res = run_shell_cmd(cmd, hidden=True)
+ stdout = self.get_stdout()
+ self.mock_stdout(False)
+ # fake output/exit code is returned for commands not actually run in dry run mode
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(res.output, '')
+ self.assertEqual(res.stderr, None)
+ # dry run output should be missing
+ self.assertNotIn(expected, stdout)
+
+ # check forced run_cmd
+ outfile = os.path.join(self.test_prefix, 'cmd.out')
+ self.assertNotExists(outfile)
+ self.mock_stdout(True)
+ res = run_shell_cmd("echo 'This is always echoed' > %s; echo done; false" % outfile,
+ fail_on_error=False, in_dry_run=True)
+ stdout = self.get_stdout()
+ self.mock_stdout(False)
+ self.assertNotIn('running shell command "', stdout)
+ self.assertNotEqual(res.exit_code, 0)
+ self.assertEqual(res.output, 'done\n')
+ self.assertEqual(res.stderr, None)
+ self.assertExists(outfile)
+ self.assertEqual(read_file(outfile), "This is always echoed\n")
def test_run_cmd_list(self):
"""Test run_cmd with command specified as a list rather than a string"""
+
+ # use of run_cmd is deprecated, so we need to allow it here
+ self.allow_deprecated_behaviour()
+
cmd = ['/bin/sh', '-c', "echo hello"]
- self.assertErrorRegex(EasyBuildError, "When passing cmd as a list then `shell` must be set explictely!",
- run_cmd, cmd)
- (out, ec) = run_cmd(cmd, shell=False)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, "When passing cmd as a list then `shell` must be set explictely!",
+ run_cmd, cmd)
+ (out, ec) = run_cmd(cmd, shell=False)
self.assertEqual(out, "hello\n")
# no reason echo hello could fail
self.assertEqual(ec, 0)
def test_run_cmd_script(self):
"""Testing use of run_cmd with shell=False to call external scripts"""
+
+ # use of run_cmd is deprecated, so we need to allow it here
+ self.allow_deprecated_behaviour()
+
py_test_script = os.path.join(self.test_prefix, 'test.py')
write_file(py_test_script, '\n'.join([
'#!%s' % sys.executable,
@@ -550,16 +1618,41 @@ def test_run_cmd_script(self):
]))
adjust_permissions(py_test_script, stat.S_IXUSR)
- (out, ec) = run_cmd(py_test_script)
+ with self.mocked_stdout_stderr():
+ (out, ec) = run_cmd(py_test_script)
self.assertEqual(ec, 0)
self.assertEqual(out, "hello\n")
- (out, ec) = run_cmd([py_test_script], shell=False)
+ with self.mocked_stdout_stderr():
+ (out, ec) = run_cmd([py_test_script], shell=False)
self.assertEqual(ec, 0)
self.assertEqual(out, "hello\n")
+ def test_run_shell_cmd_no_bash(self):
+ """Testing use of run_shell_cmd with use_bash=False to call external scripts"""
+ py_test_script = os.path.join(self.test_prefix, 'test.py')
+ write_file(py_test_script, '\n'.join([
+ '#!%s' % sys.executable,
+ 'print("hello")',
+ ]))
+ adjust_permissions(py_test_script, stat.S_IXUSR)
+
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(py_test_script)
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(res.output, "hello\n")
+
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd([py_test_script], use_bash=False)
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(res.output, "hello\n")
+
def test_run_cmd_stream(self):
"""Test use of run_cmd with streaming output."""
+
+ # use of run_cmd is deprecated, so we need to allow it here
+ self.allow_deprecated_behaviour()
+
self.mock_stdout(True)
self.mock_stderr(True)
(out, ec) = run_cmd("echo hello", stream_output=True)
@@ -571,21 +1664,79 @@ def test_run_cmd_stream(self):
self.assertEqual(ec, 0)
self.assertEqual(out, "hello\n")
- self.assertEqual(stderr, '')
- expected = '\n'.join([
+ self.assertTrue(stderr.strip().startswith("WARNING: Deprecated functionality"))
+ expected = [
"== (streaming) output for command 'echo hello':",
"hello",
'',
+ ]
+ for line in expected:
+ self.assertIn(line, stdout)
+
+ def test_run_shell_cmd_stream(self):
+ """Test use of run_shell_cmd with streaming output."""
+ self.mock_stdout(True)
+ self.mock_stderr(True)
+ cmd = '; '.join([
+ "echo hello there",
+ "sleep 1",
+ "echo testing command that produces a fair amount of output",
+ "sleep 1",
+ "echo more than 128 bytes which means a whole bunch of characters...",
+ "sleep 1",
+ "echo more than 128 characters in fact, which is quite a bit when you think of it",
+ ])
+ res = run_shell_cmd(cmd, stream_output=True)
+ stdout = self.get_stdout()
+ stderr = self.get_stderr()
+ self.mock_stdout(False)
+ self.mock_stderr(False)
+
+ expected_output = '\n'.join([
+ "hello there",
+ "testing command that produces a fair amount of output",
+ "more than 128 bytes which means a whole bunch of characters...",
+ "more than 128 characters in fact, which is quite a bit when you think of it",
+ '',
])
- self.assertEqual(stdout, expected)
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(res.output, expected_output)
+
+ self.assertEqual(stderr, '')
+ expected = ("running shell command:\n\techo hello" + '\n' + expected_output).split('\n')
+ for line in expected:
+ self.assertIn(line, stdout)
+
+ def test_run_shell_cmd_eof_stdin(self):
+ """Test use of run_shell_cmd with streaming output and blocking stdin read."""
+ cmd = 'timeout 1 cat -'
+
+ inp = 'hello\nworld\n'
+ # test with streaming output
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd, stream_output=True, stdin=inp, fail_on_error=False)
+
+ self.assertEqual(res.exit_code, 0, "Streaming output: Command timed out")
+ self.assertEqual(res.output, inp)
+
+ # test with non-streaming output (proc.communicate() is used)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd, stdin=inp, fail_on_error=False)
+
+ self.assertEqual(res.exit_code, 0, "Non-streaming output: Command timed out")
+ self.assertEqual(res.output, inp)
def test_run_cmd_async(self):
"""Test asynchronously running of a shell command via run_cmd + complete_cmd."""
+ # use of run_cmd/check_async_cmd/get_output_from_process is deprecated, so we need to allow it here
+ self.allow_deprecated_behaviour()
+
os.environ['TEST'] = 'test123'
test_cmd = "echo 'sleeping...'; sleep 2; echo $TEST"
- cmd_info = run_cmd(test_cmd, asynchronous=True)
+ with self.mocked_stdout_stderr():
+ cmd_info = run_cmd(test_cmd, asynchronous=True)
proc = cmd_info[0]
# change value of $TEST to check that command is completed with correct environment
@@ -600,37 +1751,45 @@ def test_run_cmd_async(self):
time.sleep(1)
ec = proc.poll()
- out, ec = complete_cmd(*cmd_info, simple=False)
+ with self.mocked_stdout_stderr():
+ out, ec = complete_cmd(*cmd_info, simple=False)
self.assertEqual(ec, 0)
self.assertEqual(out, 'sleeping...\ntest123\n')
# also test use of check_async_cmd function
os.environ['TEST'] = 'test123'
- cmd_info = run_cmd(test_cmd, asynchronous=True)
+ with self.mocked_stdout_stderr():
+ cmd_info = run_cmd(test_cmd, asynchronous=True)
# first check, only read first 12 output characters
# (otherwise we'll be waiting until command is completed)
- res = check_async_cmd(*cmd_info, output_read_size=12)
+ with self.mocked_stdout_stderr():
+ res = check_async_cmd(*cmd_info, output_read_size=12)
self.assertEqual(res, {'done': False, 'exit_code': None, 'output': 'sleeping...\n'})
# 2nd check with default output size (1024) gets full output
# (keep checking until command is fully done)
- while not res['done']:
- res = check_async_cmd(*cmd_info, output=res['output'])
+ with self.mocked_stdout_stderr():
+ while not res['done']:
+ res = check_async_cmd(*cmd_info, output=res['output'])
self.assertEqual(res, {'done': True, 'exit_code': 0, 'output': 'sleeping...\ntest123\n'})
# check asynchronous running of failing command
error_test_cmd = "echo 'FAIL!' >&2; exit 123"
- cmd_info = run_cmd(error_test_cmd, asynchronous=True)
+ with self.mocked_stdout_stderr():
+ cmd_info = run_cmd(error_test_cmd, asynchronous=True)
time.sleep(1)
error_pattern = 'cmd ".*" exited with exit code 123'
- self.assertErrorRegex(EasyBuildError, error_pattern, check_async_cmd, *cmd_info)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, check_async_cmd, *cmd_info)
- cmd_info = run_cmd(error_test_cmd, asynchronous=True)
- res = check_async_cmd(*cmd_info, fail_on_error=False)
+ with self.mocked_stdout_stderr():
+ cmd_info = run_cmd(error_test_cmd, asynchronous=True)
+ res = check_async_cmd(*cmd_info, fail_on_error=False)
# keep checking until command is fully done
- while not res['done']:
- res = check_async_cmd(*cmd_info, fail_on_error=False, output=res['output'])
+ with self.mocked_stdout_stderr():
+ while not res['done']:
+ res = check_async_cmd(*cmd_info, fail_on_error=False, output=res['output'])
self.assertEqual(res, {'done': True, 'exit_code': 123, 'output': "FAIL!\n"})
# also test with a command that produces a lot of output,
@@ -640,62 +1799,136 @@ def test_run_cmd_async(self):
"for i in $(seq 1 50)",
"do sleep 0.1",
"for j in $(seq 1000)",
- "do echo foo",
+ "do echo foo${i}${j}",
"done",
"done",
"echo done",
])
- cmd_info = run_cmd(verbose_test_cmd, asynchronous=True)
+ with self.mocked_stdout_stderr():
+ cmd_info = run_cmd(verbose_test_cmd, asynchronous=True)
proc = cmd_info[0]
output = ''
ec = proc.poll()
self.assertEqual(ec, None)
- while ec is None:
- time.sleep(1)
- output += get_output_from_process(proc)
- ec = proc.poll()
+ with self.mocked_stdout_stderr():
+ while ec is None:
+ time.sleep(1)
+ output += get_output_from_process(proc)
+ ec = proc.poll()
- out, ec = complete_cmd(*cmd_info, simple=False, output=output)
+ with self.mocked_stdout_stderr():
+ out, ec = complete_cmd(*cmd_info, simple=False, output=output)
self.assertEqual(ec, 0)
self.assertTrue(out.startswith('start\n'))
self.assertTrue(out.endswith('\ndone\n'))
# also test use of check_async_cmd on verbose test command
- cmd_info = run_cmd(verbose_test_cmd, asynchronous=True)
+ with self.mocked_stdout_stderr():
+ cmd_info = run_cmd(verbose_test_cmd, asynchronous=True)
error_pattern = r"Number of output bytes to read should be a positive integer value \(or zero\)"
- self.assertErrorRegex(EasyBuildError, error_pattern, check_async_cmd, *cmd_info, output_read_size=-1)
- self.assertErrorRegex(EasyBuildError, error_pattern, check_async_cmd, *cmd_info, output_read_size='foo')
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, check_async_cmd, *cmd_info, output_read_size=-1)
+ self.assertErrorRegex(EasyBuildError, error_pattern, check_async_cmd, *cmd_info, output_read_size='foo')
# with output_read_size set to 0, no output is read yet, only status of command is checked
- res = check_async_cmd(*cmd_info, output_read_size=0)
+ with self.mocked_stdout_stderr():
+ res = check_async_cmd(*cmd_info, output_read_size=0)
self.assertEqual(res['done'], False)
self.assertEqual(res['exit_code'], None)
self.assertEqual(res['output'], '')
- res = check_async_cmd(*cmd_info)
+ with self.mocked_stdout_stderr():
+ res = check_async_cmd(*cmd_info)
self.assertEqual(res['done'], False)
self.assertEqual(res['exit_code'], None)
self.assertTrue(res['output'].startswith('start\n'))
self.assertFalse(res['output'].endswith('\ndone\n'))
# keep checking until command is complete
- while not res['done']:
- res = check_async_cmd(*cmd_info, output=res['output'])
+ with self.mocked_stdout_stderr():
+ while not res['done']:
+ res = check_async_cmd(*cmd_info, output=res['output'])
self.assertEqual(res['done'], True)
self.assertEqual(res['exit_code'], 0)
- self.assertTrue(res['output'].startswith('start\n'))
- self.assertTrue(res['output'].endswith('\ndone\n'))
+ self.assertEqual(len(res['output']), 435661)
+ self.assertTrue(res['output'].startswith('start\nfoo11\nfoo12\n'))
+ self.assertTrue('\nfoo49999\nfoo491000\nfoo501\n' in res['output'])
+ self.assertTrue(res['output'].endswith('\nfoo501000\ndone\n'))
+
+ def test_run_shell_cmd_async(self):
+ """Test asynchronously running of a shell command via run_shell_cmd """
+
+ thread_pool = ThreadPoolExecutor()
+
+ os.environ['TEST'] = 'test123'
+ env = os.environ.copy()
+
+ test_cmd = "echo 'sleeping...'; sleep 2; echo $TEST"
+ task = thread_pool.submit(run_shell_cmd, test_cmd, hidden=True, asynchronous=True, env=env)
+
+ # change value of $TEST to check that command is completed with correct environment
+ os.environ['TEST'] = 'some_other_value'
+
+ # initial poll should result in None, since it takes a while for the command to complete
+ self.assertEqual(task.done(), False)
+
+ # wait until command is done
+ while not task.done():
+ time.sleep(1)
+ res = task.result()
+
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(res.output, 'sleeping...\ntest123\n')
+
+ # check asynchronous running of failing command
+ error_test_cmd = "echo 'FAIL!' >&2; exit 123"
+ task = thread_pool.submit(run_shell_cmd, error_test_cmd, hidden=True, fail_on_error=False, asynchronous=True)
+ time.sleep(1)
+ res = task.result()
+ self.assertEqual(res.exit_code, 123)
+ self.assertEqual(res.output, "FAIL!\n")
+ self.assertTrue(res.thread_id)
+
+ # also test with a command that produces a lot of output,
+ # since that tends to lock up things unless we frequently grab some output...
+ verbose_test_cmd = ';'.join([
+ "echo start",
+ "for i in $(seq 1 50)",
+ "do sleep 0.1",
+ "for j in $(seq 1000)",
+ "do echo foo${i}${j}",
+ "done",
+ "done",
+ "echo done",
+ ])
+ task = thread_pool.submit(run_shell_cmd, verbose_test_cmd, hidden=True, asynchronous=True)
+
+ while not task.done():
+ time.sleep(1)
+ res = task.result()
+
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(len(res.output), 435661)
+ self.assertTrue(res.output.startswith('start\nfoo11\nfoo12\n'))
+ self.assertTrue('\nfoo49999\nfoo491000\nfoo501\n' in res.output)
+ self.assertTrue(res.output.endswith('\nfoo501000\ndone\n'))
def test_check_log_for_errors(self):
+ """Test for check_log_for_errors"""
+
+ # use of check_log_for_errors is deprecated, so we need to allow it here
+ self.allow_deprecated_behaviour()
+
fd, logfile = tempfile.mkstemp(suffix='.log', prefix='eb-test-')
os.close(fd)
- self.assertErrorRegex(EasyBuildError, "Invalid input:", check_log_for_errors, "", [42])
- self.assertErrorRegex(EasyBuildError, "Invalid input:", check_log_for_errors, "", [(42, IGNORE)])
- self.assertErrorRegex(EasyBuildError, "Invalid input:", check_log_for_errors, "", [("42", "invalid-mode")])
- self.assertErrorRegex(EasyBuildError, "Invalid input:", check_log_for_errors, "", [("42", IGNORE, "")])
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, "Invalid input:", check_log_for_errors, "", [42])
+ self.assertErrorRegex(EasyBuildError, "Invalid input:", check_log_for_errors, "", [(42, IGNORE)])
+ self.assertErrorRegex(EasyBuildError, "Invalid input:", check_log_for_errors, "", [("42", "invalid-mode")])
+ self.assertErrorRegex(EasyBuildError, "Invalid input:", check_log_for_errors, "", [("42", IGNORE, "")])
input_text = "\n".join([
"OK",
@@ -710,20 +1943,24 @@ def test_check_log_for_errors(self):
r"\tthe process crashed with 0"
# String promoted to list
- self.assertErrorRegex(EasyBuildError, expected_msg, check_log_for_errors, input_text,
- r"\b(error|crashed)\b")
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, expected_msg, check_log_for_errors, input_text,
+ r"\b(error|crashed)\b")
# List of string(s)
- self.assertErrorRegex(EasyBuildError, expected_msg, check_log_for_errors, input_text,
- [r"\b(error|crashed)\b"])
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, expected_msg, check_log_for_errors, input_text,
+ [r"\b(error|crashed)\b"])
# List of tuple(s)
- self.assertErrorRegex(EasyBuildError, expected_msg, check_log_for_errors, input_text,
- [(r"\b(error|crashed)\b", ERROR)])
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, expected_msg, check_log_for_errors, input_text,
+ [(r"\b(error|crashed)\b", ERROR)])
expected_msg = "Found 2 potential error(s) in command output:\n"\
"\terror found\n"\
"\tthe process crashed with 0"
init_logging(logfile, silent=True)
- check_log_for_errors(input_text, [(r"\b(error|crashed)\b", WARN)])
+ with self.mocked_stdout_stderr():
+ check_log_for_errors(input_text, [(r"\b(error|crashed)\b", WARN)])
stop_logging(logfile)
self.assertIn(expected_msg, read_file(logfile))
@@ -732,12 +1969,13 @@ def test_check_log_for_errors(self):
r"\ttest failed"
write_file(logfile, '')
init_logging(logfile, silent=True)
- self.assertErrorRegex(EasyBuildError, expected_msg, check_log_for_errors, input_text, [
- r"\berror\b",
- (r"\ballowed-test failed\b", IGNORE),
- (r"(?i)\bCRASHED\b", WARN),
- "fail"
- ])
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, expected_msg, check_log_for_errors, input_text, [
+ r"\berror\b",
+ (r"\ballowed-test failed\b", IGNORE),
+ (r"(?i)\bCRASHED\b", WARN),
+ "fail"
+ ])
stop_logging(logfile)
expected_msg = "Found 1 potential error(s) in command output:\n\tthe process crashed with 0"
self.assertIn(expected_msg, read_file(logfile))
@@ -746,6 +1984,10 @@ def test_run_cmd_with_hooks(self):
"""
Test running command with run_cmd with pre/post run_shell_cmd hooks in place.
"""
+
+ # use of run_cmd/run_cmd_qa is deprecated, so we need to allow it here
+ self.allow_deprecated_behaviour()
+
cwd = os.getcwd()
hooks_file = os.path.join(self.test_prefix, 'my_hooks.py')
@@ -774,6 +2016,9 @@ def post_run_shell_cmd_hook(cmd, *args, **kwargs):
write_file(hooks_file, hooks_file_txt)
update_build_option('hooks', hooks_file)
+ # disable trace output to make checking of generated output produced by hooks easier
+ update_build_option('trace', False)
+
with self.mocked_stdout_stderr():
run_cmd("make")
stdout = self.get_stdout()
@@ -785,6 +2030,17 @@ def post_run_shell_cmd_hook(cmd, *args, **kwargs):
])
self.assertEqual(stdout, expected_stdout)
+ with self.mocked_stdout_stderr():
+ run_shell_cmd("sleep 2; make", qa_patterns=[('q', 'a')])
+ stdout = self.get_stdout()
+
+ expected_stdout = '\n'.join([
+ "pre-run hook interactive 'sleep 2; make' in %s" % cwd,
+ "post-run hook interactive 'sleep 2; echo make' (exit code: 0, output: 'make\n')",
+ '',
+ ])
+ self.assertEqual(stdout, expected_stdout)
+
with self.mocked_stdout_stderr():
run_cmd_qa("sleep 2; make", qa={})
stdout = self.get_stdout()
@@ -796,9 +2052,154 @@ def post_run_shell_cmd_hook(cmd, *args, **kwargs):
])
self.assertEqual(stdout, expected_stdout)
+ def test_run_shell_cmd_with_hooks(self):
+ """
+ Test running shell command with run_shell_cmd function with pre/post run_shell_cmd hooks in place.
+ """
+ cwd = os.getcwd()
+
+ hooks_file = os.path.join(self.test_prefix, 'my_hooks.py')
+ hooks_file_txt = textwrap.dedent("""
+ def pre_run_shell_cmd_hook(cmd, *args, **kwargs):
+ work_dir = kwargs['work_dir']
+ if kwargs.get('interactive'):
+ print("pre-run hook interactive '||%s||' in %s" % (cmd, work_dir))
+ else:
+ print("pre-run hook '%s' in %s" % (cmd, work_dir))
+ import sys
+ sys.stderr.write('pre-run hook done\\n')
+ if not cmd.startswith('echo'):
+ cmds = cmd.split(';')
+ return '; '.join(cmds[:-1] + ["echo " + cmds[-1].lstrip()])
+
+ def post_run_shell_cmd_hook(cmd, *args, **kwargs):
+ exit_code = kwargs.get('exit_code')
+ output = kwargs.get('output')
+ work_dir = kwargs['work_dir']
+ if kwargs.get('interactive'):
+ msg = "post-run hook interactive '%s'" % cmd
+ else:
+ msg = "post-run hook '%s'" % cmd
+ msg += " (exit code: %s, output: '%s')" % (exit_code, output)
+ print(msg)
+ """)
+ write_file(hooks_file, hooks_file_txt)
+ update_build_option('hooks', hooks_file)
+
+ # disable trace output to make checking of generated output produced by hooks easier
+ update_build_option('trace', False)
+
+ with self.mocked_stdout_stderr():
+ run_shell_cmd("make")
+ stdout = self.get_stdout()
+
+ expected_stdout = '\n'.join([
+ f"pre-run hook 'make' in {cwd}",
+ "post-run hook 'echo make' (exit code: 0, output: 'make\n')",
+ '',
+ ])
+ self.assertEqual(stdout, expected_stdout)
+
+ # also check in dry run mode, to verify that pre-run_shell_cmd hook is triggered sufficiently early
+ update_build_option('extended_dry_run', True)
+
+ with self.mocked_stdout_stderr():
+ run_shell_cmd("make")
+ stdout = self.get_stdout()
+
+ expected_stdout = '\n'.join([
+ "pre-run hook 'make' in %s" % cwd,
+ ' running shell command "echo make"',
+ ' (in %s)' % cwd,
+ '',
+ ])
+ self.assertEqual(stdout, expected_stdout)
+
+ # also check with trace output enabled
+ update_build_option('extended_dry_run', False)
+ update_build_option('trace', True)
+
+ with self.mocked_stdout_stderr():
+ run_shell_cmd("make")
+ stdout = self.get_stdout()
+
+ regex = re.compile('>> running shell command:\n\techo make', re.M)
+ self.assertTrue(regex.search(stdout), "Pattern '%s' found in: %s" % (regex.pattern, stdout))
+
+ def test_run_shell_cmd_delete_cwd(self):
+ """
+ Test commands that destroy directories inside initial working directory
+ """
+ workdir = os.path.join(self.test_prefix, 'workdir')
+ sub_workdir = os.path.join(workdir, 'subworkdir')
+
+ # 1. test destruction of CWD which is a subdirectory inside original working directory
+ cmd_subworkdir_rm = (
+ "echo 'Command that jumps to subdir and removes it' && "
+ f"cd {sub_workdir} && pwd && rm -rf {sub_workdir} && "
+ "echo 'Working sub-directory removed.'"
+ )
+
+ # 1.a. in a robust system
+ expected_output = (
+ "Command that jumps to subdir and removes it\n"
+ f"{sub_workdir}\n"
+ "Working sub-directory removed.\n"
+ )
+
+ mkdir(sub_workdir, parents=True)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd_subworkdir_rm, work_dir=workdir)
+
+ self.assertEqual(res.cmd, cmd_subworkdir_rm)
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(res.output, expected_output)
+ self.assertEqual(res.stderr, None)
+ self.assertEqual(res.work_dir, workdir)
+
+ # 1.b. in a flaky system that ends up in an unknown CWD after execution
+ mkdir(sub_workdir, parents=True)
+ fd, logfile = tempfile.mkstemp(suffix='.log', prefix='eb-test-')
+ os.close(fd)
+
+ with self.mocked_stdout_stderr():
+ with mock.patch('os.getcwd') as mock_getcwd:
+ mock_getcwd.side_effect = [
+ workdir,
+ FileNotFoundError(),
+ ]
+ init_logging(logfile, silent=True)
+ res = run_shell_cmd(cmd_subworkdir_rm, work_dir=workdir)
+ stop_logging(logfile)
+
+ self.assertEqual(res.cmd, cmd_subworkdir_rm)
+ self.assertEqual(res.exit_code, 0)
+ self.assertEqual(res.output, expected_output)
+ self.assertEqual(res.stderr, None)
+ self.assertEqual(res.work_dir, workdir)
+
+ expected_warning = f"Changing back to initial working directory: {workdir}\n"
+ logtxt = read_file(logfile)
+ self.assertTrue(logtxt.endswith(expected_warning))
+
+ # 2. test destruction of CWD which is main working directory passed to run_shell_cmd
+ cmd_workdir_rm = (
+ "echo 'Command that removes working directory' && pwd && "
+ f"rm -rf {workdir} && echo 'Working directory removed.'"
+ )
+
+ error_pattern = rf"Failed to return to .*/{os.path.basename(self.test_prefix)}/workdir after executing command"
+
+ mkdir(workdir, parents=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, run_shell_cmd, cmd_workdir_rm, work_dir=workdir)
+
def test_run_cmd_sysroot(self):
"""Test with_sysroot option of run_cmd function."""
+ # use of run_cmd/run_cmd_qa is deprecated, so we need to allow it here
+ self.allow_deprecated_behaviour()
+
# put fake /bin/bash in place that will be picked up when using run_cmd with with_sysroot=True
bin_bash = os.path.join(self.test_prefix, 'bin', 'bash')
bin_bash_txt = '\n'.join([
@@ -811,13 +2212,15 @@ def test_run_cmd_sysroot(self):
update_build_option('sysroot', self.test_prefix)
- (out, ec) = run_cmd("echo hello")
+ with self.mocked_stdout_stderr():
+ (out, ec) = run_cmd("echo hello")
self.assertEqual(ec, 0)
self.assertTrue(out.startswith("Hi there I am a fake /bin/bash in"))
self.assertTrue(out.endswith("\nhello\n"))
# picking up on alternate sysroot is enabled by default, but can be disabled via with_sysroot=False
- (out, ec) = run_cmd("echo hello", with_sysroot=False)
+ with self.mocked_stdout_stderr():
+ (out, ec) = run_cmd("echo hello", with_sysroot=False)
self.assertEqual(ec, 0)
self.assertEqual(out, "hello\n")
diff --git a/test/framework/sandbox/easybuild/easyblocks/b/binutils.py b/test/framework/sandbox/easybuild/easyblocks/b/binutils.py
new file mode 100644
index 0000000000..abae626602
--- /dev/null
+++ b/test/framework/sandbox/easybuild/easyblocks/b/binutils.py
@@ -0,0 +1,32 @@
+##
+# Copyright 2009-2025 Ghent University
+#
+# This file is part of EasyBuild,
+# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
+# with support of Ghent University (http://ugent.be/hpc),
+# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
+# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
+# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
+#
+# https://github.com/easybuilders/easybuild
+#
+# EasyBuild is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation v2.
+#
+# EasyBuild is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with EasyBuild. If not, see .
+##
+"""
+Dummy easyblock for binutils
+"""
+from easybuild.framework.easyblock import EasyBlock
+
+
+class EB_binutils(EasyBlock):
+ pass
diff --git a/test/framework/sandbox/easybuild/easyblocks/b/bzip2.py b/test/framework/sandbox/easybuild/easyblocks/b/bzip2.py
new file mode 100644
index 0000000000..246da8f9d9
--- /dev/null
+++ b/test/framework/sandbox/easybuild/easyblocks/b/bzip2.py
@@ -0,0 +1,32 @@
+##
+# Copyright 2009-2025 Ghent University
+#
+# This file is part of EasyBuild,
+# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
+# with support of Ghent University (http://ugent.be/hpc),
+# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
+# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
+# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
+#
+# https://github.com/easybuilders/easybuild
+#
+# EasyBuild is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation v2.
+#
+# EasyBuild is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with EasyBuild. If not, see .
+##
+"""
+Dummy easyblock for bzip2
+"""
+from easybuild.framework.easyblock import EasyBlock
+
+
+class EB_bzip2(EasyBlock):
+ pass
diff --git a/test/framework/sandbox/easybuild/easyblocks/c/cmake.py b/test/framework/sandbox/easybuild/easyblocks/c/cmake.py
new file mode 100644
index 0000000000..3bf181b3ac
--- /dev/null
+++ b/test/framework/sandbox/easybuild/easyblocks/c/cmake.py
@@ -0,0 +1,32 @@
+##
+# Copyright 2009-2025 Ghent University
+#
+# This file is part of EasyBuild,
+# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
+# with support of Ghent University (http://ugent.be/hpc),
+# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
+# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
+# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
+#
+# https://github.com/easybuilders/easybuild
+#
+# EasyBuild is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation v2.
+#
+# EasyBuild is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with EasyBuild. If not, see .
+##
+"""
+Dummy easyblock for CMake
+"""
+from easybuild.framework.easyblock import EasyBlock
+
+
+class EB_CMake(EasyBlock):
+ pass
diff --git a/test/framework/sandbox/easybuild/easyblocks/e/easybuildmeta.py b/test/framework/sandbox/easybuild/easyblocks/e/easybuildmeta.py
index 7d2ef2d6da..3a15f26b70 100644
--- a/test/framework/sandbox/easybuild/easyblocks/e/easybuildmeta.py
+++ b/test/framework/sandbox/easybuild/easyblocks/e/easybuildmeta.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/sandbox/easybuild/easyblocks/f/fftw.py b/test/framework/sandbox/easybuild/easyblocks/f/fftw.py
index 0871ce62e1..67e23d27b9 100644
--- a/test/framework/sandbox/easybuild/easyblocks/f/fftw.py
+++ b/test/framework/sandbox/easybuild/easyblocks/f/fftw.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/sandbox/easybuild/easyblocks/f/fftwmpi.py b/test/framework/sandbox/easybuild/easyblocks/f/fftwmpi.py
new file mode 100644
index 0000000000..d7c468a8a9
--- /dev/null
+++ b/test/framework/sandbox/easybuild/easyblocks/f/fftwmpi.py
@@ -0,0 +1,32 @@
+##
+# Copyright 2009-2025 Ghent University
+#
+# This file is part of EasyBuild,
+# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
+# with support of Ghent University (http://ugent.be/hpc),
+# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
+# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
+# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
+#
+# https://github.com/easybuilders/easybuild
+#
+# EasyBuild is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation v2.
+#
+# EasyBuild is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with EasyBuild. If not, see .
+##
+"""
+Dummy easyblock for FFTW.MPI
+"""
+from easybuild.framework.easyblock import EasyBlock
+
+
+class EB_FFTW_period_MPI(EasyBlock):
+ pass
diff --git a/test/framework/sandbox/easybuild/easyblocks/f/flex.py b/test/framework/sandbox/easybuild/easyblocks/f/flex.py
new file mode 100644
index 0000000000..d06874c3d4
--- /dev/null
+++ b/test/framework/sandbox/easybuild/easyblocks/f/flex.py
@@ -0,0 +1,32 @@
+##
+# Copyright 2009-2025 Ghent University
+#
+# This file is part of EasyBuild,
+# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
+# with support of Ghent University (http://ugent.be/hpc),
+# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
+# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
+# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
+#
+# https://github.com/easybuilders/easybuild
+#
+# EasyBuild is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation v2.
+#
+# EasyBuild is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with EasyBuild. If not, see .
+##
+"""
+Dummy easyblock for flex
+"""
+from easybuild.framework.easyblock import EasyBlock
+
+
+class EB_flex(EasyBlock):
+ pass
diff --git a/test/framework/sandbox/easybuild/easyblocks/f/foo.py b/test/framework/sandbox/easybuild/easyblocks/f/foo.py
index b5a91d9503..903a2fbc61 100644
--- a/test/framework/sandbox/easybuild/easyblocks/f/foo.py
+++ b/test/framework/sandbox/easybuild/easyblocks/f/foo.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/sandbox/easybuild/easyblocks/f/foofoo.py b/test/framework/sandbox/easybuild/easyblocks/f/foofoo.py
index 6415d6e6e1..86d2a5a934 100644
--- a/test/framework/sandbox/easybuild/easyblocks/f/foofoo.py
+++ b/test/framework/sandbox/easybuild/easyblocks/f/foofoo.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -32,6 +32,14 @@
from easybuild.framework.easyconfig import CUSTOM, MANDATORY
+class dummy1:
+ """Only to verify that unrelated classes in software specific easyblocks are ignored"""
+
+
+class dummy2(dummy1):
+ """Same but with inheritance"""
+
+
class EB_foofoo(EB_foo):
"""Support for building/installing foofoo."""
diff --git a/test/framework/sandbox/easybuild/easyblocks/f/freetype.py b/test/framework/sandbox/easybuild/easyblocks/f/freetype.py
new file mode 100644
index 0000000000..9149ab8e42
--- /dev/null
+++ b/test/framework/sandbox/easybuild/easyblocks/f/freetype.py
@@ -0,0 +1,32 @@
+##
+# Copyright 2009-2025 Ghent University
+#
+# This file is part of EasyBuild,
+# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
+# with support of Ghent University (http://ugent.be/hpc),
+# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
+# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
+# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
+#
+# https://github.com/easybuilders/easybuild
+#
+# EasyBuild is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation v2.
+#
+# EasyBuild is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with EasyBuild. If not, see .
+##
+"""
+Dummy easyblock for freetype
+"""
+from easybuild.framework.easyblock import EasyBlock
+
+
+class EB_freetype(EasyBlock):
+ pass
diff --git a/test/framework/sandbox/easybuild/easyblocks/g/gcc.py b/test/framework/sandbox/easybuild/easyblocks/g/gcc.py
index 5ee5aae1c6..203815b124 100644
--- a/test/framework/sandbox/easybuild/easyblocks/g/gcc.py
+++ b/test/framework/sandbox/easybuild/easyblocks/g/gcc.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/sandbox/easybuild/easyblocks/generic/bar.py b/test/framework/sandbox/easybuild/easyblocks/generic/bar.py
index 8ba475f700..d1bd4700d5 100644
--- a/test/framework/sandbox/easybuild/easyblocks/generic/bar.py
+++ b/test/framework/sandbox/easybuild/easyblocks/generic/bar.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -32,6 +32,18 @@
from easybuild.framework.easyconfig import CUSTOM, MANDATORY
+class dummy1:
+ """Only to verify that unrelated classes in software specific easyblocks are ignored"""
+
+
+class dummy2(dummy1):
+ """Same but with inheritance"""
+
+
+class dummy3:
+ """Class without inheritance before the real easyblock to verify the regex not being too greedy"""
+
+
class bar(EasyBlock):
"""Generic support for building/installing bar."""
diff --git a/easybuild/toolchains/dummy.py b/test/framework/sandbox/easybuild/easyblocks/generic/bundle.py
similarity index 78%
rename from easybuild/toolchains/dummy.py
rename to test/framework/sandbox/easybuild/easyblocks/generic/bundle.py
index f29f6736ae..11cece6a3f 100644
--- a/easybuild/toolchains/dummy.py
+++ b/test/framework/sandbox/easybuild/easyblocks/generic/bundle.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -23,16 +23,12 @@
# along with EasyBuild. If not, see .
##
"""
-EasyBuild support for dummy compiler toolchain.
-
-Authors:
-
-* Kenneth Hoste (Ghent University)
+Dummy support for building and installing a bundle of applications.
"""
+from easybuild.framework.easyblock import EasyBlock
-from easybuild.toolchains.compiler.dummycompiler import DummyCompiler
+class Bundle(EasyBlock):
+ """Dummy support for building and installing a bundle of applications."""
-class Dummy(DummyCompiler):
- """Dummy toolchain."""
- NAME = 'dummy'
+ pass
diff --git a/test/framework/sandbox/easybuild/easyblocks/generic/childcustomdummyextension.py b/test/framework/sandbox/easybuild/easyblocks/generic/childcustomdummyextension.py
new file mode 100644
index 0000000000..e5f80791b0
--- /dev/null
+++ b/test/framework/sandbox/easybuild/easyblocks/generic/childcustomdummyextension.py
@@ -0,0 +1,35 @@
+##
+# Copyright 2009-2025 Ghent University
+#
+# This file is part of EasyBuild,
+# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
+# with support of Ghent University (http://ugent.be/hpc),
+# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
+# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
+# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
+#
+# https://github.com/easybuilders/easybuild
+#
+# EasyBuild is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation v2.
+#
+# EasyBuild is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with EasyBuild. If not, see .
+##
+"""
+Test EasyBlocks building and installing dummy extensions with customized methods
+
+@author: Alex Domingo (Vrije Universiteit Brussel)
+"""
+
+from easybuild.easyblocks.generic.customdummyextension import CustomDummyExtension
+
+
+class ChildCustomDummyExtension(CustomDummyExtension):
+ """Extension EasyBlock inheriting customized install step"""
diff --git a/test/framework/sandbox/easybuild/easyblocks/generic/childdeprecateddummyextension.py b/test/framework/sandbox/easybuild/easyblocks/generic/childdeprecateddummyextension.py
new file mode 100644
index 0000000000..e72b97940a
--- /dev/null
+++ b/test/framework/sandbox/easybuild/easyblocks/generic/childdeprecateddummyextension.py
@@ -0,0 +1,35 @@
+##
+# Copyright 2009-2023 Ghent University
+#
+# This file is part of EasyBuild,
+# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
+# with support of Ghent University (http://ugent.be/hpc),
+# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
+# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
+# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
+#
+# https://github.com/easybuilders/easybuild
+#
+# EasyBuild is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation v2.
+#
+# EasyBuild is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with EasyBuild. If not, see .
+##
+"""
+Test EasyBlocks building and installing dummy extensions with deprecated methods
+
+@author: Alex Domingo (Vrije Universiteit Brussel)
+"""
+
+from easybuild.easyblocks.generic.deprecateddummyextension import DeprecatedDummyExtension
+
+
+class ChildDeprecatedDummyExtension(DeprecatedDummyExtension):
+ """Extension EasyBlock inheriting deprecated install step"""
diff --git a/test/framework/sandbox/easybuild/easyblocks/generic/cmakemake.py b/test/framework/sandbox/easybuild/easyblocks/generic/cmakemake.py
new file mode 100644
index 0000000000..d0728fabbe
--- /dev/null
+++ b/test/framework/sandbox/easybuild/easyblocks/generic/cmakemake.py
@@ -0,0 +1,34 @@
+##
+# Copyright 2009-2025 Ghent University
+#
+# This file is part of EasyBuild,
+# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
+# with support of Ghent University (http://ugent.be/hpc),
+# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
+# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
+# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
+#
+# https://github.com/easybuilders/easybuild
+#
+# EasyBuild is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation v2.
+#
+# EasyBuild is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with EasyBuild. If not, see .
+##
+"""
+Dummy support for building and installing applications with CMake.
+"""
+from easybuild.framework.easyblock import EasyBlock
+
+
+class CMakeMake(EasyBlock):
+ """Dummy support for building and installing applications with CMake."""
+
+ pass
diff --git a/test/framework/sandbox/easybuild/easyblocks/generic/cmdcp.py b/test/framework/sandbox/easybuild/easyblocks/generic/cmdcp.py
new file mode 100644
index 0000000000..f0850dac39
--- /dev/null
+++ b/test/framework/sandbox/easybuild/easyblocks/generic/cmdcp.py
@@ -0,0 +1,34 @@
+##
+# Copyright 2009-2025 Ghent University
+#
+# This file is part of EasyBuild,
+# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
+# with support of Ghent University (http://ugent.be/hpc),
+# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
+# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
+# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
+#
+# https://github.com/easybuilders/easybuild
+#
+# EasyBuild is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation v2.
+#
+# EasyBuild is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with EasyBuild. If not, see .
+##
+"""
+Dummy support for building and installing applications via a command and copy.
+"""
+from easybuild.framework.easyblock import EasyBlock
+
+
+class CmdCp(EasyBlock):
+ """Dummy support for building and installing applications via a command and copy."""
+
+ pass
diff --git a/test/framework/sandbox/easybuild/easyblocks/generic/configuremake.py b/test/framework/sandbox/easybuild/easyblocks/generic/configuremake.py
index b82668434c..c616c97ddd 100644
--- a/test/framework/sandbox/easybuild/easyblocks/generic/configuremake.py
+++ b/test/framework/sandbox/easybuild/easyblocks/generic/configuremake.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/sandbox/easybuild/easyblocks/generic/customdummyextension.py b/test/framework/sandbox/easybuild/easyblocks/generic/customdummyextension.py
new file mode 100644
index 0000000000..17a4434fe0
--- /dev/null
+++ b/test/framework/sandbox/easybuild/easyblocks/generic/customdummyextension.py
@@ -0,0 +1,47 @@
+##
+# Copyright 2009-2023 Ghent University
+#
+# This file is part of EasyBuild,
+# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
+# with support of Ghent University (http://ugent.be/hpc),
+# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
+# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
+# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
+#
+# https://github.com/easybuilders/easybuild
+#
+# EasyBuild is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation v2.
+#
+# EasyBuild is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with EasyBuild. If not, see .
+##
+"""
+Test EasyBlocks building and installing dummy extensions with customized methods
+
+@author: Alex Domingo (Vrije Universiteit Brussel)
+"""
+
+from easybuild.easyblocks.generic.dummyextension import DummyExtension
+
+
+class CustomDummyExtension(DummyExtension):
+ """Extension EasyBlock with customized install step"""
+
+ def pre_install_extension(self):
+
+ return "Extension installed with custom pre_install_extension()"
+
+ def install_extension(self):
+
+ return "Extension installed with custom install_extension()"
+
+ def post_install_extension(self):
+
+ return "Extension installed with custom post_install_extension()"
diff --git a/easybuild/toolchains/compiler/dummycompiler.py b/test/framework/sandbox/easybuild/easyblocks/generic/deprecateddummyextension.py
similarity index 62%
rename from easybuild/toolchains/compiler/dummycompiler.py
rename to test/framework/sandbox/easybuild/easyblocks/generic/deprecateddummyextension.py
index d74c2e873e..6e071417de 100644
--- a/easybuild/toolchains/compiler/dummycompiler.py
+++ b/test/framework/sandbox/easybuild/easyblocks/generic/deprecateddummyextension.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -23,28 +23,25 @@
# along with EasyBuild. If not, see .
##
"""
-Support for dummy compiler.
+Test EasyBlocks building and installing dummy extensions with deprecated methods
-Authors:
-
-* Stijn De Weirdt (Ghent University)
-* Kenneth Hoste (Ghent University)
+@author: Alex Domingo (Vrije Universiteit Brussel)
"""
-from easybuild.tools.toolchain.compiler import Compiler
+from easybuild.easyblocks.generic.dummyextension import DummyExtension
+
+
+class DeprecatedDummyExtension(DummyExtension):
+ """Extension EasyBlock with deprecated install step"""
+ def prerun(self):
-TC_CONSTANT_DUMMY = "DUMMY"
+ return "Extension installed with custom prerun()"
+ def run(self):
-class DummyCompiler(Compiler):
- """Dummy compiler : try not to even use system gcc"""
- COMPILER_MODULE_NAME = []
- COMPILER_FAMILY = TC_CONSTANT_DUMMY
+ return "Extension installed with custom run()"
- COMPILER_CC = '%sCC' % TC_CONSTANT_DUMMY
- COMPILER_CXX = '%sCXX' % TC_CONSTANT_DUMMY
+ def postrun(self):
- COMPILER_F77 = '%sF77' % TC_CONSTANT_DUMMY
- COMPILER_F90 = '%sF90' % TC_CONSTANT_DUMMY
- COMPILER_FC = '%sFC' % TC_CONSTANT_DUMMY
+ return "Extension installed with custom postrun()"
diff --git a/test/framework/sandbox/easybuild/easyblocks/generic/dummyextension.py b/test/framework/sandbox/easybuild/easyblocks/generic/dummyextension.py
index 3466ce28f2..e0429a450b 100644
--- a/test/framework/sandbox/easybuild/easyblocks/generic/dummyextension.py
+++ b/test/framework/sandbox/easybuild/easyblocks/generic/dummyextension.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/sandbox/easybuild/easyblocks/generic/makecp.py b/test/framework/sandbox/easybuild/easyblocks/generic/makecp.py
index dd89704638..dbe1790951 100644
--- a/test/framework/sandbox/easybuild/easyblocks/generic/makecp.py
+++ b/test/framework/sandbox/easybuild/easyblocks/generic/makecp.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/sandbox/easybuild/easyblocks/generic/mesonninja.py b/test/framework/sandbox/easybuild/easyblocks/generic/mesonninja.py
new file mode 100644
index 0000000000..1251af1d0c
--- /dev/null
+++ b/test/framework/sandbox/easybuild/easyblocks/generic/mesonninja.py
@@ -0,0 +1,34 @@
+##
+# Copyright 2009-2025 Ghent University
+#
+# This file is part of EasyBuild,
+# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
+# with support of Ghent University (http://ugent.be/hpc),
+# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
+# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
+# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
+#
+# https://github.com/easybuilders/easybuild
+#
+# EasyBuild is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation v2.
+#
+# EasyBuild is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with EasyBuild. If not, see .
+##
+"""
+Dummy support for building and installing applications with Meson and Ninja.
+"""
+from easybuild.framework.easyblock import EasyBlock
+
+
+class MesonNinja(EasyBlock):
+ """Dummy support for building and installing applications with Meson and Ninja."""
+
+ pass
diff --git a/test/framework/sandbox/easybuild/easyblocks/generic/modulerc.py b/test/framework/sandbox/easybuild/easyblocks/generic/modulerc.py
index 72252a46f4..aea939fda0 100644
--- a/test/framework/sandbox/easybuild/easyblocks/generic/modulerc.py
+++ b/test/framework/sandbox/easybuild/easyblocks/generic/modulerc.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/sandbox/easybuild/easyblocks/generic/perlbundle.py b/test/framework/sandbox/easybuild/easyblocks/generic/perlbundle.py
new file mode 100644
index 0000000000..0644157561
--- /dev/null
+++ b/test/framework/sandbox/easybuild/easyblocks/generic/perlbundle.py
@@ -0,0 +1,33 @@
+##
+# Copyright 2009-2025 Ghent University
+#
+# This file is part of EasyBuild,
+# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
+# with support of Ghent University (http://ugent.be/hpc),
+# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
+# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
+# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
+#
+# https://github.com/easybuilders/easybuild
+#
+# EasyBuild is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation v2.
+#
+# EasyBuild is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with EasyBuild. If not, see .
+##
+"""
+Dummy easyblock for bundle of Perl modules.
+"""
+from easybuild.framework.easyblock import EasyBlock
+
+
+class PerlBundle(EasyBlock):
+ """Dummy support for bundle of Perl modules."""
+ pass
diff --git a/test/framework/sandbox/easybuild/easyblocks/generic/pythonbundle.py b/test/framework/sandbox/easybuild/easyblocks/generic/pythonbundle.py
index 9c01951adf..f93b5190fd 100644
--- a/test/framework/sandbox/easybuild/easyblocks/generic/pythonbundle.py
+++ b/test/framework/sandbox/easybuild/easyblocks/generic/pythonbundle.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/sandbox/easybuild/easyblocks/generic/pythonpackage.py b/test/framework/sandbox/easybuild/easyblocks/generic/pythonpackage.py
new file mode 100644
index 0000000000..35bc926829
--- /dev/null
+++ b/test/framework/sandbox/easybuild/easyblocks/generic/pythonpackage.py
@@ -0,0 +1,44 @@
+##
+# Copyright 2009-2025 Ghent University
+#
+# This file is part of EasyBuild,
+# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
+# with support of Ghent University (http://ugent.be/hpc),
+# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
+# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
+# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
+#
+# https://github.com/easybuilders/easybuild
+#
+# EasyBuild is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation v2.
+#
+# EasyBuild is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with EasyBuild. If not, see .
+##
+"""
+Dummy support for building and installing a Python package.
+"""
+from easybuild.framework.easyblock import EasyBlock
+from easybuild.framework.easyconfig import CUSTOM
+
+
+class PythonPackage(EasyBlock):
+ """Dummy support for building and installing a Python package."""
+
+ @staticmethod
+ def extra_options(extra_vars=None):
+ """Extra easyconfig parameters specific to ConfigureMake."""
+ extra_vars = EasyBlock.extra_options(extra=extra_vars)
+ extra_vars.update({
+ 'install_target': ['', "Just a test", CUSTOM],
+ 'options': ['', "Another test", CUSTOM],
+ 'use_pip': ['', "Test 1, 2, 3", CUSTOM],
+ })
+ return extra_vars
diff --git a/test/framework/sandbox/easybuild/easyblocks/generic/tarball.py b/test/framework/sandbox/easybuild/easyblocks/generic/tarball.py
new file mode 100644
index 0000000000..012c05b67b
--- /dev/null
+++ b/test/framework/sandbox/easybuild/easyblocks/generic/tarball.py
@@ -0,0 +1,34 @@
+##
+# Copyright 2009-2025 Ghent University
+#
+# This file is part of EasyBuild,
+# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
+# with support of Ghent University (http://ugent.be/hpc),
+# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
+# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
+# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
+#
+# https://github.com/easybuilders/easybuild
+#
+# EasyBuild is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation v2.
+#
+# EasyBuild is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with EasyBuild. If not, see .
+##
+"""
+Dummy support for building and installing applications from a Tarball.
+"""
+from easybuild.framework.easyblock import EasyBlock
+
+
+class Tarball(EasyBlock):
+ """Dummy support for building and installing applications from a Tarball."""
+
+ pass
diff --git a/test/framework/sandbox/easybuild/easyblocks/generic/toolchain.py b/test/framework/sandbox/easybuild/easyblocks/generic/toolchain.py
index 5151221a13..39dfde65b4 100644
--- a/test/framework/sandbox/easybuild/easyblocks/generic/toolchain.py
+++ b/test/framework/sandbox/easybuild/easyblocks/generic/toolchain.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/sandbox/easybuild/easyblocks/generic/toy_extension.py b/test/framework/sandbox/easybuild/easyblocks/generic/toy_extension.py
index 9335611f58..12a1e5830b 100644
--- a/test/framework/sandbox/easybuild/easyblocks/generic/toy_extension.py
+++ b/test/framework/sandbox/easybuild/easyblocks/generic/toy_extension.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -27,12 +27,13 @@
@author: Kenneth Hoste (Ghent University)
"""
+import os
from easybuild.framework.easyconfig import CUSTOM
from easybuild.framework.extensioneasyblock import ExtensionEasyBlock
from easybuild.easyblocks.toy import EB_toy, compose_toy_build_cmd
from easybuild.tools.build_log import EasyBuildError
-from easybuild.tools.run import run_cmd
+from easybuild.tools.run import run_shell_cmd
class Toy_Extension(ExtensionEasyBlock):
@@ -59,7 +60,17 @@ def required_deps(self):
else:
raise EasyBuildError("Dependencies for %s are unknown!", self.name)
- def run(self, *args, **kwargs):
+ def pre_install_extension(self):
+ """
+ Prepare installation of toy extension.
+ """
+ super(Toy_Extension, self).pre_install_extension()
+
+ if self.src:
+ super(Toy_Extension, self).install_extension(unpack_src=True)
+ EB_toy.configure_step(self.master, name=self.name, cfg=self.cfg)
+
+ def install_extension(self, *args, **kwargs):
"""
Install toy extension.
"""
@@ -67,35 +78,28 @@ def run(self, *args, **kwargs):
EB_toy.build_step(self.master, name=self.name, cfg=self.cfg)
if self.cfg['toy_ext_param']:
- run_cmd(self.cfg['toy_ext_param'])
+ run_shell_cmd(self.cfg['toy_ext_param'])
return self.module_generator.set_environment('TOY_EXT_%s' % self.name.upper().replace('-', '_'), self.name)
- def prerun(self):
- """
- Prepare installation of toy extension.
- """
- super(Toy_Extension, self).prerun()
-
- if self.src:
- super(Toy_Extension, self).run(unpack_src=True)
- EB_toy.configure_step(self.master, name=self.name, cfg=self.cfg)
-
- def run_async(self):
+ def install_extension_async(self, thread_pool):
"""
Install toy extension asynchronously.
"""
+ task_id = f'ext_{self.name}_{self.version}'
if self.src:
cmd = compose_toy_build_cmd(self.cfg, self.name, self.cfg['prebuildopts'], self.cfg['buildopts'])
- self.async_cmd_start(cmd)
else:
- self.async_cmd_info = False
+ cmd = f"echo 'no sources for {self.name}'"
+
+ return thread_pool.submit(run_shell_cmd, cmd, asynchronous=True, env=os.environ.copy(),
+ fail_on_error=False, task_id=task_id, work_dir=os.getcwd())
- def postrun(self):
+ def post_install_extension(self):
"""
Wrap up installation of toy extension.
"""
- super(Toy_Extension, self).postrun()
+ super(Toy_Extension, self).post_install_extension()
EB_toy.install_step(self.master, name=self.name)
diff --git a/test/framework/sandbox/easybuild/easyblocks/h/hpl.py b/test/framework/sandbox/easybuild/easyblocks/h/hpl.py
index db929d4d31..ffc2263839 100644
--- a/test/framework/sandbox/easybuild/easyblocks/h/hpl.py
+++ b/test/framework/sandbox/easybuild/easyblocks/h/hpl.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/sandbox/easybuild/easyblocks/l/libtoy.py b/test/framework/sandbox/easybuild/easyblocks/l/libtoy.py
index d3e55ed05d..51b089e424 100644
--- a/test/framework/sandbox/easybuild/easyblocks/l/libtoy.py
+++ b/test/framework/sandbox/easybuild/easyblocks/l/libtoy.py
@@ -1,5 +1,5 @@
##
-# Copyright 2021-2024 Ghent University
+# Copyright 2021-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -30,7 +30,7 @@
import os
from easybuild.framework.easyblock import EasyBlock
-from easybuild.tools.run import run_cmd
+from easybuild.tools.run import run_shell_cmd
from easybuild.tools.systemtools import get_shared_lib_ext
SHLIB_EXT = get_shared_lib_ext()
@@ -40,7 +40,7 @@ class EB_libtoy(EasyBlock):
"""Support for building/installing libtoy."""
def banned_linked_shared_libs(self):
- default = '/thiswillnotbethere,libtoytoytoy.%s,toytoytoy' % SHLIB_EXT
+ default = f'/thiswillnotbethere,libtoytoytoy.{SHLIB_EXT},toytoytoy'
return os.getenv('EB_LIBTOY_BANNED_SHARED_LIBS', default).split(',')
def required_linked_shared_libs(self):
@@ -53,8 +53,8 @@ def configure_step(self, name=None):
def build_step(self, name=None, buildopts=None):
"""Build libtoy."""
- run_cmd('make')
+ run_shell_cmd('make')
def install_step(self, name=None):
"""Install libtoy."""
- run_cmd('make install PREFIX="%s"' % self.installdir)
+ run_shell_cmd(f'make install PREFIX="{self.installdir}"')
diff --git a/test/framework/sandbox/easybuild/easyblocks/l/libxml2.py b/test/framework/sandbox/easybuild/easyblocks/l/libxml2.py
new file mode 100644
index 0000000000..d0153d43a3
--- /dev/null
+++ b/test/framework/sandbox/easybuild/easyblocks/l/libxml2.py
@@ -0,0 +1,32 @@
+##
+# Copyright 2009-2025 Ghent University
+#
+# This file is part of EasyBuild,
+# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
+# with support of Ghent University (http://ugent.be/hpc),
+# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
+# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
+# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
+#
+# https://github.com/easybuilders/easybuild
+#
+# EasyBuild is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation v2.
+#
+# EasyBuild is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with EasyBuild. If not, see .
+##
+"""
+Dummy easyblock for libxml2
+"""
+from easybuild.framework.easyblock import EasyBlock
+
+
+class EB_libxml2(EasyBlock):
+ pass
diff --git a/test/framework/sandbox/easybuild/easyblocks/l/llvm.py b/test/framework/sandbox/easybuild/easyblocks/l/llvm.py
new file mode 100644
index 0000000000..b59bae4acb
--- /dev/null
+++ b/test/framework/sandbox/easybuild/easyblocks/l/llvm.py
@@ -0,0 +1,32 @@
+##
+# Copyright 2009-2025 Ghent University
+#
+# This file is part of EasyBuild,
+# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
+# with support of Ghent University (http://ugent.be/hpc),
+# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
+# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
+# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
+#
+# https://github.com/easybuilders/easybuild
+#
+# EasyBuild is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation v2.
+#
+# EasyBuild is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with EasyBuild. If not, see .
+##
+"""
+Dummy easyblock for LLVM
+"""
+from easybuild.framework.easyblock import EasyBlock
+
+
+class EB_LLVM(EasyBlock):
+ pass
diff --git a/test/framework/sandbox/easybuild/easyblocks/m/mesa.py b/test/framework/sandbox/easybuild/easyblocks/m/mesa.py
new file mode 100644
index 0000000000..3086985eb1
--- /dev/null
+++ b/test/framework/sandbox/easybuild/easyblocks/m/mesa.py
@@ -0,0 +1,32 @@
+##
+# Copyright 2009-2025 Ghent University
+#
+# This file is part of EasyBuild,
+# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
+# with support of Ghent University (http://ugent.be/hpc),
+# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
+# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
+# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
+#
+# https://github.com/easybuilders/easybuild
+#
+# EasyBuild is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation v2.
+#
+# EasyBuild is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with EasyBuild. If not, see .
+##
+"""
+Dummy easyblock for Mesa
+"""
+from easybuild.framework.easyblock import EasyBlock
+
+
+class EB_Mesa(EasyBlock):
+ pass
diff --git a/test/framework/sandbox/easybuild/easyblocks/o/openblas.py b/test/framework/sandbox/easybuild/easyblocks/o/openblas.py
index e5dc093347..600998cc1c 100644
--- a/test/framework/sandbox/easybuild/easyblocks/o/openblas.py
+++ b/test/framework/sandbox/easybuild/easyblocks/o/openblas.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/sandbox/easybuild/easyblocks/o/openmpi.py b/test/framework/sandbox/easybuild/easyblocks/o/openmpi.py
index b110b4f921..79b6ef1406 100644
--- a/test/framework/sandbox/easybuild/easyblocks/o/openmpi.py
+++ b/test/framework/sandbox/easybuild/easyblocks/o/openmpi.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/sandbox/easybuild/easyblocks/o/openssl_wrapper.py b/test/framework/sandbox/easybuild/easyblocks/o/openssl_wrapper.py
new file mode 100644
index 0000000000..d3f32361dd
--- /dev/null
+++ b/test/framework/sandbox/easybuild/easyblocks/o/openssl_wrapper.py
@@ -0,0 +1,32 @@
+##
+# Copyright 2009-2025 Ghent University
+#
+# This file is part of EasyBuild,
+# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
+# with support of Ghent University (http://ugent.be/hpc),
+# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
+# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
+# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
+#
+# https://github.com/easybuilders/easybuild
+#
+# EasyBuild is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation v2.
+#
+# EasyBuild is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with EasyBuild. If not, see .
+##
+"""
+Dummy easyblock for OpenSSL_wrapper
+"""
+from easybuild.framework.easyblock import EasyBlock
+
+
+class EB_OpenSSL_wrapper(EasyBlock):
+ pass
diff --git a/test/framework/sandbox/easybuild/easyblocks/p/perl.py b/test/framework/sandbox/easybuild/easyblocks/p/perl.py
new file mode 100644
index 0000000000..463b14b57d
--- /dev/null
+++ b/test/framework/sandbox/easybuild/easyblocks/p/perl.py
@@ -0,0 +1,32 @@
+##
+# Copyright 2009-2025 Ghent University
+#
+# This file is part of EasyBuild,
+# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
+# with support of Ghent University (http://ugent.be/hpc),
+# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
+# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
+# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
+#
+# https://github.com/easybuilders/easybuild
+#
+# EasyBuild is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation v2.
+#
+# EasyBuild is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with EasyBuild. If not, see .
+##
+"""
+Dummy easyblock for Perl
+"""
+from easybuild.framework.easyblock import EasyBlock
+
+
+class EB_Perl(EasyBlock):
+ pass
diff --git a/test/framework/sandbox/easybuild/easyblocks/p/python.py b/test/framework/sandbox/easybuild/easyblocks/p/python.py
new file mode 100644
index 0000000000..e6e43ed0cf
--- /dev/null
+++ b/test/framework/sandbox/easybuild/easyblocks/p/python.py
@@ -0,0 +1,32 @@
+##
+# Copyright 2009-2025 Ghent University
+#
+# This file is part of EasyBuild,
+# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
+# with support of Ghent University (http://ugent.be/hpc),
+# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
+# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
+# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
+#
+# https://github.com/easybuilders/easybuild
+#
+# EasyBuild is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation v2.
+#
+# EasyBuild is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with EasyBuild. If not, see .
+##
+"""
+Dummy easyblock for Python
+"""
+from easybuild.framework.easyblock import EasyBlock
+
+
+class EB_Python(EasyBlock):
+ pass
diff --git a/test/framework/sandbox/easybuild/easyblocks/s/scalapack.py b/test/framework/sandbox/easybuild/easyblocks/s/scalapack.py
index 3ded1247f4..1fb37fbe5c 100644
--- a/test/framework/sandbox/easybuild/easyblocks/s/scalapack.py
+++ b/test/framework/sandbox/easybuild/easyblocks/s/scalapack.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/sandbox/easybuild/easyblocks/t/toy.py b/test/framework/sandbox/easybuild/easyblocks/t/toy.py
index 04186a37ef..5c434e31c7 100644
--- a/test/framework/sandbox/easybuild/easyblocks/t/toy.py
+++ b/test/framework/sandbox/easybuild/easyblocks/t/toy.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -38,7 +38,7 @@
from easybuild.tools.environment import setvar
from easybuild.tools.filetools import mkdir, write_file
from easybuild.tools.modules import get_software_root, get_software_version
-from easybuild.tools.run import run_cmd
+from easybuild.tools.run import run_shell_cmd
def compose_toy_build_cmd(cfg, name, prebuildopts, buildopts):
@@ -73,6 +73,10 @@ def __init__(self, *args, **kwargs):
setvar('TOY', '%s-%s' % (self.name, self.version))
+ # extra paths for environment variables to consider
+ if self.name == 'toy':
+ self.module_load_environment.CPATH.append('toy-headers')
+
def prepare_for_extensions(self):
"""
Prepare for installing toy extensions.
@@ -108,7 +112,7 @@ def configure_step(self, name=None, cfg=None):
'echo "Configured"',
cfg['configopts']
])
- run_cmd(cmd)
+ run_shell_cmd(cmd)
if os.path.exists("%s.source" % name):
os.rename('%s.source' % name, '%s.c' % name)
@@ -124,8 +128,8 @@ def build_step(self, name=None, cfg=None):
cmd = compose_toy_build_cmd(self.cfg, name, cfg['prebuildopts'], cfg['buildopts'])
# purposely run build command without checking exit code;
# we rely on this in test_toy_build_hooks
- (out, ec) = run_cmd(cmd, log_ok=False, log_all=False)
- if ec:
+ res = run_shell_cmd(cmd, fail_on_error=False)
+ if res.exit_code:
print_warning("Command '%s' failed, but we'll ignore it..." % cmd)
def install_step(self, name=None):
@@ -142,6 +146,12 @@ def install_step(self, name=None):
mkdir(libdir, parents=True)
write_file(os.path.join(libdir, 'lib%s.a' % name), name.upper())
+ def post_processing_step(self):
+ """Any postprocessing for toy"""
+ libdir = os.path.join(self.installdir, 'lib')
+ write_file(os.path.join(libdir, 'lib%s_post.a' % self.name), self.name.upper())
+ super(EB_toy, self).post_processing_step()
+
@property
def required_deps(self):
"""Return list of required dependencies for this extension."""
@@ -150,27 +160,29 @@ def required_deps(self):
else:
raise EasyBuildError("Dependencies for %s are unknown!", self.name)
- def prerun(self):
+ def pre_install_extension(self):
"""
Prepare installation of toy as extension.
"""
- super(EB_toy, self).run(unpack_src=True)
+ super(EB_toy, self).install_extension(unpack_src=True)
self.configure_step()
- def run(self):
+ def install_extension(self):
"""
Install toy as extension.
"""
self.build_step()
- def run_async(self):
+ def install_extension_async(self, thread_pool):
"""
Asynchronous installation of toy as extension.
"""
cmd = compose_toy_build_cmd(self.cfg, self.name, self.cfg['prebuildopts'], self.cfg['buildopts'])
- self.async_cmd_start(cmd)
+ task_id = f'ext_{self.name}_{self.version}'
+ return thread_pool.submit(run_shell_cmd, cmd, asynchronous=True, env=os.environ.copy(),
+ fail_on_error=False, task_id=task_id, work_dir=os.getcwd())
- def postrun(self):
+ def post_install_extension(self):
"""
Wrap up installation of toy as extension.
"""
diff --git a/test/framework/sandbox/easybuild/easyblocks/t/toy_buggy.py b/test/framework/sandbox/easybuild/easyblocks/t/toy_buggy.py
index 6445655707..ba3efd236f 100644
--- a/test/framework/sandbox/easybuild/easyblocks/t/toy_buggy.py
+++ b/test/framework/sandbox/easybuild/easyblocks/t/toy_buggy.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -41,7 +41,7 @@ def configure_step(self):
def build_step(self):
"""Build toy."""
# note: import is (purposely) missing, so this will go down hard
- run_cmd('gcc toy.c -o toy') # noqa
+ run_shell_cmd('gcc toy.c -o toy') # noqa
def install_step(self):
"""Install toy."""
diff --git a/test/framework/sandbox/easybuild/easyblocks/t/toy_deprecated.py b/test/framework/sandbox/easybuild/easyblocks/t/toy_deprecated.py
new file mode 100644
index 0000000000..80891ff152
--- /dev/null
+++ b/test/framework/sandbox/easybuild/easyblocks/t/toy_deprecated.py
@@ -0,0 +1,40 @@
+##
+# Copyright 2009-2024 Ghent University
+#
+# This file is part of EasyBuild,
+# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
+# with support of Ghent University (http://ugent.be/hpc),
+# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
+# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
+# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
+#
+# https://github.com/easybuilders/easybuild
+#
+# EasyBuild is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation v2.
+#
+# EasyBuild is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with EasyBuild. If not, see .
+##
+"""
+(deprecated) EasyBuild support for building and installing toy, implemented as an easyblock
+
+@author: Bart Oldeman (McGill University, Calcul Quebec, Digital Research Alliance of Canada)
+"""
+
+from easybuild.easyblocks.toy import EB_toy
+
+
+class EB_toy_deprecated(EB_toy):
+ """Support for building/installing toy with deprecated post_install step."""
+
+ def post_install_step(self):
+ """Any postprocessing for toy (deprecated)"""
+ print("This step is deprecated.")
+ super(EB_toy, self).post_install_step()
diff --git a/test/framework/sandbox/easybuild/easyblocks/t/toy_eula.py b/test/framework/sandbox/easybuild/easyblocks/t/toy_eula.py
index 09442d1a79..add589838d 100644
--- a/test/framework/sandbox/easybuild/easyblocks/t/toy_eula.py
+++ b/test/framework/sandbox/easybuild/easyblocks/t/toy_eula.py
@@ -1,5 +1,5 @@
##
-# Copyright 2020-2024 Ghent University
+# Copyright 2020-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -37,6 +37,6 @@ def prepare_step(self, *args, **kwargs):
"""Constructor"""
super(EB_toy_eula, self).prepare_step(*args, **kwargs)
- # EULA for toy must be accepted via --accept-eula EasyBuild configuration option,
+ # EULA for toy must be accepted via --accept-eula-for EasyBuild configuration option,
# or via 'accept_eula = True' in easyconfig file
self.check_accepted_eula(more_info='https://example.com/toy_eula.txt')
diff --git a/test/framework/sandbox/easybuild/easyblocks/t/toytoy.py b/test/framework/sandbox/easybuild/easyblocks/t/toytoy.py
index a3ef746d53..2cb97836c3 100644
--- a/test/framework/sandbox/easybuild/easyblocks/t/toytoy.py
+++ b/test/framework/sandbox/easybuild/easyblocks/t/toytoy.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/sandbox/easybuild/easyblocks/x/xcrysden.py b/test/framework/sandbox/easybuild/easyblocks/x/xcrysden.py
new file mode 100644
index 0000000000..4c46df68a0
--- /dev/null
+++ b/test/framework/sandbox/easybuild/easyblocks/x/xcrysden.py
@@ -0,0 +1,32 @@
+##
+# Copyright 2009-2025 Ghent University
+#
+# This file is part of EasyBuild,
+# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
+# with support of Ghent University (http://ugent.be/hpc),
+# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
+# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
+# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
+#
+# https://github.com/easybuilders/easybuild
+#
+# EasyBuild is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation v2.
+#
+# EasyBuild is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with EasyBuild. If not, see .
+##
+"""
+Dummy easyblock for XCrySDen
+"""
+from easybuild.framework.easyblock import EasyBlock
+
+
+class EB_XCrySDen(EasyBlock):
+ pass
diff --git a/test/framework/sandbox/easybuild/tools/__init__.py b/test/framework/sandbox/easybuild/tools/__init__.py
index 389c5ff367..b1a57625bf 100644
--- a/test/framework/sandbox/easybuild/tools/__init__.py
+++ b/test/framework/sandbox/easybuild/tools/__init__.py
@@ -1,5 +1,5 @@
##
-# Copyright 2009-2024 Ghent University
+# Copyright 2009-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/sandbox/easybuild/tools/module_naming_scheme/__init__.py b/test/framework/sandbox/easybuild/tools/module_naming_scheme/__init__.py
index a519340298..7eca596ebb 100644
--- a/test/framework/sandbox/easybuild/tools/module_naming_scheme/__init__.py
+++ b/test/framework/sandbox/easybuild/tools/module_naming_scheme/__init__.py
@@ -1,5 +1,5 @@
##
-# Copyright 2011-2024 Ghent University
+# Copyright 2011-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/sandbox/easybuild/tools/module_naming_scheme/broken_module_naming_scheme.py b/test/framework/sandbox/easybuild/tools/module_naming_scheme/broken_module_naming_scheme.py
index 9113d89045..5fd30dadba 100644
--- a/test/framework/sandbox/easybuild/tools/module_naming_scheme/broken_module_naming_scheme.py
+++ b/test/framework/sandbox/easybuild/tools/module_naming_scheme/broken_module_naming_scheme.py
@@ -1,5 +1,5 @@
##
-# Copyright 2014-2024 Ghent University
+# Copyright 2014-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/sandbox/easybuild/tools/module_naming_scheme/test_module_naming_scheme.py b/test/framework/sandbox/easybuild/tools/module_naming_scheme/test_module_naming_scheme.py
index 424fb55d43..47d1b55ccc 100644
--- a/test/framework/sandbox/easybuild/tools/module_naming_scheme/test_module_naming_scheme.py
+++ b/test/framework/sandbox/easybuild/tools/module_naming_scheme/test_module_naming_scheme.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/sandbox/easybuild/tools/module_naming_scheme/test_module_naming_scheme_more.py b/test/framework/sandbox/easybuild/tools/module_naming_scheme/test_module_naming_scheme_more.py
index a0e0429bfe..dbfb79a6f8 100644
--- a/test/framework/sandbox/easybuild/tools/module_naming_scheme/test_module_naming_scheme_more.py
+++ b/test/framework/sandbox/easybuild/tools/module_naming_scheme/test_module_naming_scheme_more.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/sandbox/sources/p/Python/Python-3.7.2.tgz b/test/framework/sandbox/sources/p/Python/Python-3.7.2.tgz
new file mode 100644
index 0000000000..a1583ec2c0
Binary files /dev/null and b/test/framework/sandbox/sources/p/Python/Python-3.7.2.tgz differ
diff --git a/test/framework/sandbox/sources/toy/toy-0.0_add-bug.patch b/test/framework/sandbox/sources/toy/toy-0.0_add-bug.patch
new file mode 100644
index 0000000000..7512447168
--- /dev/null
+++ b/test/framework/sandbox/sources/toy/toy-0.0_add-bug.patch
@@ -0,0 +1,10 @@
+--- a/toy-0.0.orig/toy.source 2014-03-06 18:48:16.000000000 +0100
++++ b/toy-0.0/toy.source 2020-08-18 12:19:35.000000000 +0200
+@@ -2,6 +2,6 @@
+
+ int main(int argc, char* argv[]){
+
+- printf("I'm a toy, and proud of it.\n");
++ printf("I'm a toy, and proud of it.\n")
+ return 0;
+ }
diff --git a/test/framework/style.py b/test/framework/style.py
index bfdf4e026a..0c1d9b140a 100644
--- a/test/framework/style.py
+++ b/test/framework/style.py
@@ -1,5 +1,5 @@
##
-# Copyright 2016-2024 Ghent University
+# Copyright 2016-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -40,10 +40,7 @@
try:
import pycodestyle # noqa
except ImportError:
- try:
- import pep8 # noqa
- except ImportError:
- pass
+ pass
class StyleTest(EnhancedTestCase):
@@ -51,8 +48,8 @@ class StyleTest(EnhancedTestCase):
def test_style_conformance(self):
"""Check the easyconfigs for style"""
- if not ('pycodestyle' in sys.modules or 'pep8' in sys.modules):
- print("Skipping style checks (no pycodestyle or pep8 available)")
+ if 'pycodestyle' not in sys.modules:
+ print("Skipping test_style_conformance pycodestyle is not available")
return
# all available easyconfig files
@@ -66,8 +63,8 @@ def test_style_conformance(self):
def test_check_trailing_whitespace(self):
"""Test for trailing whitespace check."""
- if not ('pycodestyle' in sys.modules or 'pep8' in sys.modules):
- print("Skipping trailing whitespace checks (no pycodestyle or pep8 available)")
+ if 'pycodestyle' not in sys.modules:
+ print("Skipping test_check_trailing_whitespace is not available")
return
lines = [
@@ -77,6 +74,12 @@ def test_check_trailing_whitespace(self):
'''description = """start of long description, ''', # trailing whitespace, but allowed in description
''' continuation of long description ''', # trailing whitespace, but allowed in continued description
''' end of long description"""''',
+ '''citing = """start of long citing text, ''', # trailing whitespace, but allowed in citing
+ ''' continuation of long citing text ''', # trailing whitespace, but allowed in continued citing
+ ''' end of long citing text"""''',
+ '''examples = """start of long examples, ''', # trailing whitespace, but allowed in examples
+ ''' continuation of long examples ''', # trailing whitespace, but allowed in continued examples
+ ''' end of long examples"""''',
"moduleclass = 'tools' ", # trailing whitespace
'',
]
@@ -89,6 +92,12 @@ def test_check_trailing_whitespace(self):
None,
None,
None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
(21, "W299 trailing whitespace"),
]
diff --git a/test/framework/suite.py b/test/framework/suite.py
index 3b8193469c..afec127c83 100755
--- a/test/framework/suite.py
+++ b/test/framework/suite.py
@@ -1,6 +1,6 @@
#!/usr/bin/python
# #
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -81,7 +81,6 @@
import test.framework.tweak as tw
import test.framework.utilities_test as u
import test.framework.variables as v
-import test.framework.yeb as y
# set plain text key ring to be used,
# so a GitHub token stored in it can be obtained without having to provide a password
@@ -121,7 +120,7 @@
# call suite() for each module and then run them all
# note: make sure the options unit tests run first, to avoid running some of them with a readily initialized config
tests = [gen, d, bl, o, r, ef, ev, ebco, ep, e, mg, m, mt, f, run, a, robot, b, v, g, tcv, tc, t, c, s, lic, f_c,
- tw, p, i, pkg, env, et, y, st, h, ct, lib, u, es, ou]
+ tw, p, i, pkg, env, et, st, h, ct, lib, u, es, ou]
SUITE = unittest.TestSuite([x.suite() for x in tests])
res = unittest.TextTestRunner().run(SUITE)
diff --git a/test/framework/systemtools.py b/test/framework/systemtools.py
index deece97702..7529166d72 100644
--- a/test/framework/systemtools.py
+++ b/test/framework/systemtools.py
@@ -1,5 +1,5 @@
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -40,8 +40,7 @@
import easybuild.tools.systemtools as st
from easybuild.tools.build_log import EasyBuildError
from easybuild.tools.filetools import adjust_permissions, read_file, symlink, which, write_file
-from easybuild.tools.py2vs3 import string_type
-from easybuild.tools.run import run_cmd
+from easybuild.tools.run import RunShellCmdResult, run_shell_cmd
from easybuild.tools.systemtools import CPU_ARCHITECTURES, AARCH32, AARCH64, POWER, X86_64
from easybuild.tools.systemtools import CPU_FAMILIES, POWER_LE, DARWIN, LINUX, UNKNOWN
from easybuild.tools.systemtools import CPU_VENDORS, AMD, APM, ARM, CAVIUM, IBM, INTEL
@@ -321,8 +320,8 @@ def mocked_is_readable(mocked_fp, fp):
return fp == mocked_fp
-def mocked_run_cmd(cmd, **kwargs):
- """Mocked version of run_cmd, with specified output for known commands."""
+def mocked_run_shell_cmd(cmd, **kwargs):
+ """Mocked version of run_shell_cmd, with specified output for known commands."""
known_cmds = {
"gcc --version": "gcc (GCC) 5.1.1 20150618 (Red Hat 5.1.1-4)",
"ldd --version": "ldd (GNU libc) 2.12; ",
@@ -341,12 +340,10 @@ def mocked_run_cmd(cmd, **kwargs):
"ulimit -u": '40',
}
if cmd in known_cmds:
- if 'simple' in kwargs and kwargs['simple']:
- return True
- else:
- return (known_cmds[cmd], 0)
+ return RunShellCmdResult(cmd=cmd, exit_code=0, output=known_cmds[cmd], stderr=None, work_dir=os.getcwd(),
+ out_file=None, err_file=None, cmd_sh=None, thread_id=None, task_id=None)
else:
- return run_cmd(cmd, **kwargs)
+ return run_shell_cmd(cmd, **kwargs)
def mocked_uname():
@@ -365,8 +362,8 @@ def setUp(self):
self.orig_get_os_type = st.get_os_type
self.orig_is_readable = st.is_readable
self.orig_read_file = st.read_file
- self.orig_run_cmd = st.run_cmd
- self.orig_platform_dist = getattr(st.platform, 'dist', None)
+ self.orig_run_shell_cmd = st.run_shell_cmd
+ self.orig_platform_dist = st.platform.dist if hasattr(st.platform, 'dist') else None
self.orig_platform_uname = st.platform.uname
self.orig_get_tool_version = st.get_tool_version
self.orig_sys_version_info = st.sys.version_info
@@ -382,7 +379,7 @@ def tearDown(self):
st.get_cpu_architecture = self.orig_get_cpu_architecture
st.get_os_name = self.orig_get_os_name
st.get_os_type = self.orig_get_os_type
- st.run_cmd = self.orig_run_cmd
+ st.run_shell_cmd = self.orig_run_shell_cmd
if self.orig_platform_dist is not None:
st.platform.dist = self.orig_platform_dist
st.platform.uname = self.orig_platform_uname
@@ -413,13 +410,13 @@ def test_avail_core_count_linux(self):
def test_avail_core_count_darwin(self):
"""Test getting core count (mocked for Darwin)."""
st.get_os_type = lambda: st.DARWIN
- st.run_cmd = mocked_run_cmd
+ st.run_shell_cmd = mocked_run_shell_cmd
self.assertEqual(get_avail_core_count(), 10)
def test_cpu_model_native(self):
"""Test getting CPU model."""
cpu_model = get_cpu_model()
- self.assertIsInstance(cpu_model, string_type)
+ self.assertIsInstance(cpu_model, str)
def test_cpu_model_linux(self):
"""Test getting CPU model (mocked for Linux)."""
@@ -451,7 +448,7 @@ def test_cpu_model_linux(self):
def test_cpu_model_darwin(self):
"""Test getting CPU model (mocked for Darwin)."""
st.get_os_type = lambda: st.DARWIN
- st.run_cmd = mocked_run_cmd
+ st.run_shell_cmd = mocked_run_shell_cmd
self.assertEqual(get_cpu_model(), "Intel(R) Core(TM) i5-4258U CPU @ 2.40GHz")
def test_cpu_speed_native(self):
@@ -486,7 +483,7 @@ def test_cpu_speed_linux(self):
def test_cpu_speed_darwin(self):
"""Test getting CPU speed (mocked for Darwin)."""
st.get_os_type = lambda: st.DARWIN
- st.run_cmd = mocked_run_cmd
+ st.run_shell_cmd = mocked_run_shell_cmd
self.assertEqual(get_cpu_speed(), 2400.0)
def test_cpu_features_native(self):
@@ -494,7 +491,7 @@ def test_cpu_features_native(self):
cpu_feat = get_cpu_features()
self.assertIsInstance(cpu_feat, list)
self.assertTrue(len(cpu_feat) >= 0)
- self.assertTrue(all(isinstance(x, string_type) for x in cpu_feat))
+ self.assertTrue(all(isinstance(x, str) for x in cpu_feat))
def test_cpu_features_linux(self):
"""Test getting CPU features (mocked for Linux)."""
@@ -541,7 +538,7 @@ def test_cpu_features_linux(self):
def test_cpu_features_darwin(self):
"""Test getting CPU features (mocked for Darwin)."""
st.get_os_type = lambda: st.DARWIN
- st.run_cmd = mocked_run_cmd
+ st.run_shell_cmd = mocked_run_shell_cmd
expected = ['1gbpage', 'acpi', 'aes', 'apic', 'avx1.0', 'avx2', 'bmi1', 'bmi2', 'clfsh', 'cmov', 'cx16',
'cx8', 'de', 'ds', 'dscpl', 'dtes64', 'em64t', 'erms', 'est', 'f16c', 'fma', 'fpu', 'fpu_csds',
'fxsr', 'htt', 'invpcid', 'lahf', 'lzcnt', 'mca', 'mce', 'mmx', 'mon', 'movbe', 'msr', 'mtrr',
@@ -578,7 +575,7 @@ def test_cpu_architecture(self):
def test_cpu_arch_name_native(self):
"""Test getting CPU arch name."""
arch_name = get_cpu_arch_name()
- self.assertIsInstance(arch_name, string_type)
+ self.assertIsInstance(arch_name, str)
def test_cpu_arch_name(self):
"""Test getting CPU arch name."""
@@ -635,12 +632,12 @@ def test_cpu_vendor_linux(self):
def test_cpu_vendor_darwin(self):
"""Test getting CPU vendor (mocked for Darwin)."""
st.get_os_type = lambda: st.DARWIN
- st.run_cmd = mocked_run_cmd
+ st.run_shell_cmd = mocked_run_shell_cmd
self.assertEqual(get_cpu_vendor(), INTEL)
def test_cpu_family_native(self):
"""Test get_cpu_family function."""
- run_cmd.clear_cache()
+ run_shell_cmd.clear_cache()
cpu_family = get_cpu_family()
self.assertTrue(cpu_family in CPU_FAMILIES or cpu_family == UNKNOWN)
@@ -685,8 +682,8 @@ def test_cpu_family_linux(self):
def test_cpu_family_darwin(self):
"""Test get_cpu_family function (mocked for Darwin)."""
st.get_os_type = lambda: st.DARWIN
- st.run_cmd = mocked_run_cmd
- run_cmd.clear_cache()
+ st.run_shell_cmd = mocked_run_shell_cmd
+ run_shell_cmd.clear_cache()
self.assertEqual(get_cpu_family(), INTEL)
def test_os_type(self):
@@ -712,12 +709,12 @@ def test_shared_lib_ext_darwin(self):
def test_platform_name_native(self):
"""Test getting platform name."""
platform_name_nover = get_platform_name()
- self.assertIsInstance(platform_name_nover, string_type)
+ self.assertIsInstance(platform_name_nover, str)
len_nover = len(platform_name_nover.split('-'))
self.assertTrue(len_nover >= 3)
platform_name_ver = get_platform_name(withversion=True)
- self.assertIsInstance(platform_name_ver, string_type)
+ self.assertIsInstance(platform_name_ver, str)
len_ver = len(platform_name_ver.split('-'))
self.assertTrue(platform_name_ver.startswith(platform_name_ver))
self.assertTrue(len_ver >= len_nover)
@@ -737,12 +734,12 @@ def test_platform_name_darwin(self):
def test_os_name(self):
"""Test getting OS name."""
os_name = get_os_name()
- self.assertTrue(isinstance(os_name, string_type) or os_name == UNKNOWN)
+ self.assertTrue(isinstance(os_name, str) or os_name == UNKNOWN)
def test_os_version(self):
"""Test getting OS version."""
os_version = get_os_version()
- self.assertTrue(isinstance(os_version, string_type) or os_version == UNKNOWN)
+ self.assertTrue(isinstance(os_version, str) or os_version == UNKNOWN)
# make sure that bug fixed in https://github.com/easybuilders/easybuild-framework/issues/3952
# does not surface again, by mocking what's needed to make get_os_version fall into SLES-specific path
@@ -763,29 +760,33 @@ def test_gcc_version_native(self):
"""Test getting gcc version."""
gcc_version = get_gcc_version()
if gcc_version is not None:
- self.assertIsInstance(gcc_version, string_type)
+ self.assertIsInstance(gcc_version, str)
def test_gcc_version_linux(self):
"""Test getting gcc version (mocked for Linux)."""
st.get_os_type = lambda: st.LINUX
- st.run_cmd = mocked_run_cmd
+ st.run_shell_cmd = mocked_run_shell_cmd
self.assertEqual(get_gcc_version(), '5.1.1')
def test_gcc_version_darwin(self):
"""Test getting gcc version (mocked for Darwin)."""
st.get_os_type = lambda: st.DARWIN
- st.run_cmd = lambda *args, **kwargs: ("Apple LLVM version 7.0.0 (clang-700.1.76)", 0)
+ out = "Apple LLVM version 7.0.0 (clang-700.1.76)"
+ cwd = os.getcwd()
+ mocked_run_res = RunShellCmdResult(cmd="gcc --version", exit_code=0, output=out, stderr=None, work_dir=cwd,
+ out_file=None, err_file=None, cmd_sh=None, thread_id=None, task_id=None)
+ st.run_shell_cmd = lambda *args, **kwargs: mocked_run_res
self.assertEqual(get_gcc_version(), None)
def test_glibc_version_native(self):
"""Test getting glibc version."""
glibc_version = get_glibc_version()
- self.assertTrue(isinstance(glibc_version, string_type) or glibc_version == UNKNOWN)
+ self.assertTrue(isinstance(glibc_version, str) or glibc_version == UNKNOWN)
def test_glibc_version_linux(self):
"""Test getting glibc version (mocked for Linux)."""
st.get_os_type = lambda: st.LINUX
- st.run_cmd = mocked_run_cmd
+ st.run_shell_cmd = mocked_run_shell_cmd
self.assertEqual(get_glibc_version(), '2.12')
def test_glibc_version_linux_gentoo(self):
@@ -816,7 +817,7 @@ def test_get_total_memory_linux(self):
def test_get_total_memory_darwin(self):
"""Test the function that gets the total memory."""
st.get_os_type = lambda: st.DARWIN
- st.run_cmd = mocked_run_cmd
+ st.run_shell_cmd = mocked_run_shell_cmd
self.assertEqual(get_total_memory(), 8192)
def test_get_total_memory_native(self):
@@ -847,11 +848,13 @@ def test_det_parallelism_mocked(self):
# mock number of available cores to 8
st.get_avail_core_count = lambda: 8
self.assertTrue(det_parallelism(), 8)
+
# make 'ulimit -u' return '40', which should result in default (max) parallelism of 4 ((40-15)/6)
- st.run_cmd = mocked_run_cmd
- self.assertTrue(det_parallelism(), 4)
- self.assertTrue(det_parallelism(par=6), 4)
- self.assertTrue(det_parallelism(maxpar=2), 2)
+ del det_parallelism._default_parallelism
+ st.run_shell_cmd = mocked_run_shell_cmd
+ self.assertEqual(det_parallelism(), 4)
+ self.assertEqual(det_parallelism(par=6), 6)
+ self.assertEqual(det_parallelism(maxpar=2), 2)
st.get_avail_core_count = orig_get_avail_core_count
@@ -872,19 +875,23 @@ def mock_python_ver(py_maj_ver, py_min_ver):
# mock running with different Python versions
mock_python_ver(1, 4)
- error_pattern = r"EasyBuild is not compatible \(yet\) with Python 1.4"
+ error_pattern = r"EasyBuild is not compatible with Python 1.4"
self.assertErrorRegex(EasyBuildError, error_pattern, check_python_version)
mock_python_ver(4, 0)
error_pattern = r"EasyBuild is not compatible \(yet\) with Python 4.0"
self.assertErrorRegex(EasyBuildError, error_pattern, check_python_version)
- mock_python_ver(2, 6)
- error_pattern = r"Python 2.7 is required when using Python 2, found Python 2.6"
+ mock_python_ver(2, 7)
+ error_pattern = r"EasyBuild is not compatible with Python 2.7"
+ self.assertErrorRegex(EasyBuildError, error_pattern, check_python_version)
+
+ mock_python_ver(3, 5)
+ error_pattern = r"Python 3.6 or higher is required, found Python 3.5"
self.assertErrorRegex(EasyBuildError, error_pattern, check_python_version)
# no problems when running with a supported Python version
- for pyver in [(2, 7), (3, 5), (3, 6), (3, 7)]:
+ for pyver in [(3, 6), (3, 7), (3, 11)]:
mock_python_ver(*pyver)
self.assertEqual(check_python_version(), pyver)
@@ -1028,19 +1035,21 @@ def test_check_linked_shared_libs(self):
self.assertEqual(check_linked_shared_libs(txt_path), None)
self.assertEqual(check_linked_shared_libs(broken_symlink_path), None)
- bin_ls_path = which('ls')
+ bin_bash_path = which('bash')
os_type = get_os_type()
if os_type == LINUX:
- out, _ = run_cmd("ldd %s" % bin_ls_path)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd("ldd %s" % bin_bash_path)
elif os_type == DARWIN:
- out, _ = run_cmd("otool -L %s" % bin_ls_path)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd("otool -L %s" % bin_bash_path)
else:
raise EasyBuildError("Unknown OS type: %s" % os_type)
shlib_ext = get_shared_lib_ext()
lib_path_regex = re.compile(r'(?P[^\s]*/lib[^ ]+\.%s[^ ]*)' % shlib_ext, re.M)
- lib_path = lib_path_regex.search(out).group(1)
+ lib_path = lib_path_regex.search(res.output).group(1)
test_pattern_named_args = [
# if no patterns are specified, result is always True
@@ -1054,7 +1063,7 @@ def test_check_linked_shared_libs(self):
self.assertEqual(check_linked_shared_libs(self.test_prefix, **pattern_named_args), None)
self.assertEqual(check_linked_shared_libs(txt_path, **pattern_named_args), None)
self.assertEqual(check_linked_shared_libs(broken_symlink_path, **pattern_named_args), None)
- for path in (bin_ls_path, lib_path):
+ for path in (bin_bash_path, lib_path):
# path may not exist, especially for library paths obtained via 'otool -L' on macOS
if os.path.exists(path):
error_msg = "Check on linked libs should pass for %s with %s" % (path, pattern_named_args)
@@ -1071,7 +1080,7 @@ def test_check_linked_shared_libs(self):
self.assertEqual(check_linked_shared_libs(self.test_prefix, **pattern_named_args), None)
self.assertEqual(check_linked_shared_libs(txt_path, **pattern_named_args), None)
self.assertEqual(check_linked_shared_libs(broken_symlink_path, **pattern_named_args), None)
- for path in (bin_ls_path, lib_path):
+ for path in (bin_bash_path, lib_path):
error_msg = "Check on linked libs should fail for %s with %s" % (path, pattern_named_args)
self.assertFalse(check_linked_shared_libs(path, **pattern_named_args), error_msg)
diff --git a/test/framework/toolchain.py b/test/framework/toolchain.py
index beeb2e3512..e55ccabe93 100644
--- a/test/framework/toolchain.py
+++ b/test/framework/toolchain.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -50,8 +50,8 @@
from easybuild.tools.environment import setvar
from easybuild.tools.filetools import adjust_permissions, copy_dir, find_eb_script, mkdir
from easybuild.tools.filetools import read_file, symlink, write_file, which
-from easybuild.tools.py2vs3 import string_type
-from easybuild.tools.run import run_cmd
+from easybuild.tools.modules import EnvironmentModules
+from easybuild.tools.run import run_shell_cmd
from easybuild.tools.systemtools import get_shared_lib_ext
from easybuild.tools.toolchain.mpi import get_mpi_cmd_template
from easybuild.tools.toolchain.toolchain import env_vars_external_module
@@ -92,7 +92,7 @@ def get_toolchain(self, name, version=None):
def test_toolchain(self):
"""Test whether toolchain is initialized correctly."""
test_ecs = os.path.join('test', 'framework', 'easyconfigs', 'test_ecs')
- ec_file = find_full_path(os.path.join(test_ecs, 'g', 'gzip', 'gzip-1.4.eb'))
+ ec_file = find_full_path(os.path.join(test_ecs, 'g', 'gzip', 'gzip-1.4-GCC-4.9.3-2.26.eb'))
ec = EasyConfig(ec_file, validate=False)
tc = ec.toolchain
self.assertIn('debug', tc.options)
@@ -114,24 +114,13 @@ def test_foss_toolchain(self):
self.get_toolchain("foss", version="2018a")
def test_get_variable_system_toolchain(self):
- """Test get_variable on system/dummy toolchain"""
+ """Test get_variable on system toolchain"""
# system toolchain version doesn't really matter, but fine...
for ver in ['system', '']:
tc = self.get_toolchain('system', version=ver)
- tc.prepare()
- self.assertEqual(tc.get_variable('CC'), '')
- self.assertEqual(tc.get_variable('CXX', typ=str), '')
- self.assertEqual(tc.get_variable('CFLAGS', typ=list), [])
-
- # dummy toolchain is deprecated, so we need to allow for it (and catch the warnings that get printed)
- self.allow_deprecated_behaviour()
-
- for ver in ['dummy', '']:
- self.mock_stderr(True)
- tc = self.get_toolchain('dummy', version=ver)
- self.mock_stderr(False)
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.get_variable('CC'), '')
self.assertEqual(tc.get_variable('CXX', typ=str), '')
self.assertEqual(tc.get_variable('CFLAGS', typ=list), [])
@@ -153,27 +142,10 @@ def test_is_system_toolchain(self):
tc = self.get_toolchain('intel', version='2018a')
self.assertFalse(tc.is_system_toolchain())
- # using dummy toolchain is deprecated, so to test for that we need to explicitely allow using deprecated stuff
- error_pattern = "Use of 'dummy' toolchain is deprecated"
- for ver in ['dummy', '']:
- self.assertErrorRegex(EasyBuildError, error_pattern, self.get_toolchain, 'dummy', version=ver)
-
- dummy_depr_warning = "WARNING: Deprecated functionality, will no longer work in v5.0: Use of 'dummy' toolchain"
-
- self.allow_deprecated_behaviour()
-
- for ver in ['dummy', '']:
- self.mock_stderr(True)
- tc = self.get_toolchain('dummy', version=ver)
- stderr = self.get_stderr()
- self.mock_stderr(False)
- self.assertTrue(tc.is_system_toolchain())
- self.assertIn(dummy_depr_warning, stderr)
-
def test_toolchain_prepare_sysroot(self):
"""Test build environment setup done by Toolchain.prepare in case --sysroot is specified."""
- sysroot = os.path.join(self.test_prefix, 'test', 'alternate', 'sysroot')
+ sysroot = os.path.join(self.test_prefix, 'test', 'alternative', 'sysroot')
sysroot_pkgconfig = os.path.join(sysroot, 'usr', 'lib', 'pkgconfig')
mkdir(sysroot_pkgconfig, parents=True)
init_config(build_options={'sysroot': sysroot})
@@ -186,7 +158,8 @@ def test_toolchain_prepare_sysroot(self):
self.assertEqual(os.getenv('PKG_CONFIG_PATH'), None)
tc = self.get_toolchain('system', version='system')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(os.getenv('PKG_CONFIG_PATH'), sysroot_pkgconfig)
# usr/lib64/pkgconfig is also picked up
@@ -195,25 +168,29 @@ def test_toolchain_prepare_sysroot(self):
init_config(build_options={'sysroot': sysroot})
del os.environ['PKG_CONFIG_PATH']
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(os.getenv('PKG_CONFIG_PATH'), sysroot_pkgconfig)
# existing $PKG_CONFIG_PATH value is retained
test_pkg_config_path = os.pathsep.join([self.test_prefix, '/foo/bar'])
os.environ['PKG_CONFIG_PATH'] = test_pkg_config_path
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(os.getenv('PKG_CONFIG_PATH'), test_pkg_config_path + os.pathsep + sysroot_pkgconfig)
# no duplicate paths are added
test_pkg_config_path = os.pathsep.join([self.test_prefix, sysroot_pkgconfig, '/foo/bar'])
os.environ['PKG_CONFIG_PATH'] = test_pkg_config_path
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(os.getenv('PKG_CONFIG_PATH'), test_pkg_config_path)
# if no usr/lib*/pkgconfig subdirectory is present in sysroot, then $PKG_CONFIG_PATH is not touched
del os.environ['PKG_CONFIG_PATH']
init_config(build_options={'sysroot': self.test_prefix})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(os.getenv('PKG_CONFIG_PATH'), None)
def unset_compiler_env_vars(self):
@@ -314,7 +291,8 @@ def test_toolchain_compiler_env_vars(self):
# check whether specification in --minimal-build-env is picked up
init_config(build_options={'minimal_build_env': 'CC:g++'})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(os.getenv('CC'), 'g++')
self.assertEqual(os.getenv('CXX'), None)
@@ -361,23 +339,28 @@ def test_toolchain_compiler_env_vars(self):
# incorrect spec in minimal_build_env results in an error
init_config(build_options={'minimal_build_env': 'CC=gcc'})
error_pattern = "Incorrect mapping in --minimal-build-env value: 'CC=gcc'"
- self.assertErrorRegex(EasyBuildError, error_pattern, tc.prepare)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, tc.prepare)
init_config(build_options={'minimal_build_env': 'foo:bar:baz'})
error_pattern = "Incorrect mapping in --minimal-build-env value: 'foo:bar:baz'"
- self.assertErrorRegex(EasyBuildError, error_pattern, tc.prepare)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, tc.prepare)
init_config(build_options={'minimal_build_env': 'CC:gcc,foo'})
error_pattern = "Incorrect mapping in --minimal-build-env value: 'foo'"
- self.assertErrorRegex(EasyBuildError, error_pattern, tc.prepare)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, tc.prepare)
init_config(build_options={'minimal_build_env': 'foo:bar:baz,CC:gcc'})
error_pattern = "Incorrect mapping in --minimal-build-env value: 'foo:bar:baz'"
- self.assertErrorRegex(EasyBuildError, error_pattern, tc.prepare)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, tc.prepare)
init_config(build_options={'minimal_build_env': 'CC:gcc,'})
error_pattern = "Incorrect mapping in --minimal-build-env value: ''"
- self.assertErrorRegex(EasyBuildError, error_pattern, tc.prepare)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, tc.prepare)
# for a full toolchain, a more extensive build environment is set up (incl. $CFLAGS & co),
# and the specs in --minimal-build-env are ignored
@@ -385,9 +368,8 @@ def test_toolchain_compiler_env_vars(self):
tc.set_options({})
# catch potential warning about too long $TMPDIR value that causes trouble for Open MPI (irrelevant here)
- self.mock_stderr(True)
- tc.prepare()
- self.mock_stderr(False)
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(os.getenv('CC'), 'gcc')
self.assertEqual(os.getenv('CXX'), 'g++')
@@ -415,7 +397,8 @@ def test_get_variable_compilers(self):
"""Test get_variable function to obtain compiler variables."""
tc = self.get_toolchain('foss', version='2018a')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.get_variable('CC'), 'gcc')
self.assertEqual(tc.get_variable('CXX'), 'g++')
@@ -458,7 +441,8 @@ def test_get_variable_mpi_compilers(self):
"""Test get_variable function to obtain compiler variables."""
tc = self.get_toolchain('foss', version='2018a')
tc.set_options({'usempi': True})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.check_vars_foss_usempi(tc)
@@ -467,15 +451,18 @@ def test_prepare_iterate(self):
tc = self.get_toolchain('foss', version='2018a')
tc.set_options({'usempi': True})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.check_vars_foss_usempi(tc)
# without a reset, the value is wrong...
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertFalse(tc.get_variable('MPICC') == 'mpicc')
tc.reset()
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.check_vars_foss_usempi(tc)
def test_cray_reset(self):
@@ -484,25 +471,34 @@ def test_cray_reset(self):
init_config(build_options={'optarch': 'test', 'silent': True})
for tcname in ['CrayGNU', 'CrayCCE', 'CrayIntel']:
+ # Cray* modules do not unload other Cray* modules thus loading a second Cray* module
+ # makes environment inconsistent which is not allowed by Environment Modules tool
+ if isinstance(self.modtool, EnvironmentModules):
+ self.modtool.purge()
tc = self.get_toolchain(tcname, version='2015.06-XC')
tc.set_options({'dynamic': True})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(os.environ.get('LIBBLAS'), '')
tc.reset()
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(os.environ.get('LIBBLAS'), '')
tc.reset()
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(os.environ.get('LIBBLAS'), '')
tc.reset()
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(os.environ.get('LIBBLAS'), '')
def test_get_variable_seq_compilers(self):
"""Test get_variable function to obtain compiler variables."""
tc = self.get_toolchain('foss', version='2018a')
tc.set_options({'usempi': True})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.get_variable('CC_SEQ'), 'gcc')
self.assertEqual(tc.get_variable('CXX_SEQ'), 'g++')
@@ -513,12 +509,13 @@ def test_get_variable_seq_compilers(self):
def test_get_variable_libs_list(self):
"""Test get_variable function to obtain list of libraries."""
tc = self.get_toolchain('foss', version='2018a')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
ldflags = tc.get_variable('LDFLAGS', typ=list)
self.assertIsInstance(ldflags, list)
if len(ldflags) > 0:
- self.assertIsInstance(ldflags[0], string_type)
+ self.assertIsInstance(ldflags[0], str)
def test_validate_pass_by_value(self):
"""
@@ -526,7 +523,8 @@ def test_validate_pass_by_value(self):
which is required to ensure correctness.
"""
tc = self.get_toolchain('foss', version='2018a')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
pass_by_value = True
ids = []
@@ -551,7 +549,8 @@ def test_optimization_flags(self):
# check default optimization flag (e.g. -O2)
tc = self.get_toolchain('foss', version='2018a')
tc.set_options({})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
for var in flag_vars:
flags = tc.get_variable(var)
self.assertIn(tc.COMPILER_SHARED_OPTION_MAP['defaultopt'], flags)
@@ -561,7 +560,8 @@ def test_optimization_flags(self):
tc = self.get_toolchain('foss', version='2018a')
for enable in [True, False]:
tc.set_options({opt: enable})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
for var in flag_vars:
flags = tc.get_variable(var)
if enable:
@@ -579,28 +579,31 @@ def test_optimization_flags_combos(self):
# lowest optimization should always be picked
tc = self.get_toolchain('foss', version='2018a')
tc.set_options({'lowopt': True, 'opt': True})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
for var in flag_vars:
flags = tc.get_variable(var)
- flag = '-%s' % tc.COMPILER_SHARED_OPTION_MAP['lowopt']
+ flag = tc.COMPILER_SHARED_OPTION_MAP['lowopt']
self.assertIn(flag, flags)
self.modtool.purge()
tc = self.get_toolchain('foss', version='2018a')
tc.set_options({'noopt': True, 'lowopt': True})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
for var in flag_vars:
flags = tc.get_variable(var)
- flag = '-%s' % tc.COMPILER_SHARED_OPTION_MAP['noopt']
+ flag = tc.COMPILER_SHARED_OPTION_MAP['noopt']
self.assertIn(flag, flags)
self.modtool.purge()
tc = self.get_toolchain('foss', version='2018a')
tc.set_options({'noopt': True, 'lowopt': True, 'opt': True})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
for var in flag_vars:
flags = tc.get_variable(var)
- flag = '-%s' % tc.COMPILER_SHARED_OPTION_MAP['noopt']
+ flag = tc.COMPILER_SHARED_OPTION_MAP['noopt']
self.assertIn(flag, flags)
def test_misc_flags_shared(self):
@@ -613,9 +616,10 @@ def test_misc_flags_shared(self):
for enable in [True, False]:
tc = self.get_toolchain('foss', version='2018a')
tc.set_options({opt: enable})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
# we need to make sure we check for flags, not letter (e.g. 'v' vs '-v')
- flag = '-%s' % tc.COMPILER_SHARED_OPTION_MAP[opt]
+ flag = tc.COMPILER_SHARED_OPTION_MAP[opt]
for var in flag_vars:
flags = tc.get_variable(var).split()
if enable:
@@ -629,7 +633,8 @@ def test_misc_flags_shared(self):
opt = 'extra_' + var.lower()
tc = self.get_toolchain('foss', version='2018a')
tc.set_options({opt: value})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertTrue(tc.get_variable(var).endswith(' ' + value))
self.modtool.purge()
@@ -637,7 +642,8 @@ def test_misc_flags_shared(self):
flag_vars.remove('CXXFLAGS')
tc = self.get_toolchain('foss', version='2018a')
tc.set_options({'extra_cxxflags': value})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertTrue(tc.get_variable('CXXFLAGS').endswith(' ' + value))
for var in flag_vars:
self.assertNotIn(value, tc.get_variable(var))
@@ -656,7 +662,8 @@ def test_misc_flags_unique(self):
for enable in [True, False]:
tc = self.get_toolchain('foss', version='2018a')
tc.set_options({opt: enable})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
if opt == 'optarch':
option = tc.COMPILER_OPTIMAL_ARCHITECTURE_OPTION[(tc.arch, tc.cpu_family)]
else:
@@ -665,8 +672,7 @@ def test_misc_flags_unique(self):
option = {True: option}
for var in flag_vars:
flags = tc.get_variable(var)
- for key, value in option.items():
- flag = "-%s" % value
+ for key, flag in option.items():
if enable == key:
self.assertIn(flag, flags, "%s: %s means %s in %s" % (opt, enable, flag, flags))
else:
@@ -676,15 +682,16 @@ def test_misc_flags_unique(self):
def test_override_optarch(self):
"""Test whether overriding the optarch flag works."""
flag_vars = ['CFLAGS', 'CXXFLAGS', 'FCFLAGS', 'FFLAGS', 'F90FLAGS']
- for optarch_var in ['march=lovelylovelysandybridge', None]:
+ for optarch_var in ['-march=lovelylovelysandybridge', None]:
init_config(build_options={'optarch': optarch_var, 'silent': True})
for enable in [True, False]:
tc = self.get_toolchain('foss', version='2018a')
tc.set_options({'optarch': enable})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
flag = None
if optarch_var is not None:
- flag = '-%s' % optarch_var
+ flag = optarch_var
else:
# default optarch flag
flag = tc.COMPILER_OPTIMAL_ARCHITECTURE_OPTION[(tc.arch, tc.cpu_family)]
@@ -730,7 +737,8 @@ def test_optarch_generic(self):
tcopts.update(custom_tcopts)
tc.set_options(tcopts)
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
for var in flag_vars:
val = tc.get_variable(var)
tup = (key, tcversion, tcopts, generic_flags, val)
@@ -751,32 +759,35 @@ def test_optarch_aarch64_heuristic(self):
st.get_cpu_vendor = lambda: st.ARM
tc = self.get_toolchain("GCC", version="4.6.4")
tc.set_options({})
- tc.prepare()
- self.assertEqual(tc.options.options_map['optarch'], 'mcpu=cortex-a53')
+ with self.mocked_stdout_stderr():
+ tc.prepare()
+ self.assertEqual(tc.options.options_map['optarch'], '-mcpu=cortex-a53')
self.assertIn('-mcpu=cortex-a53', os.environ['CFLAGS'])
self.modtool.purge()
tc = self.get_toolchain("GCCcore", version="6.2.0")
tc.set_options({})
- tc.prepare()
- self.assertEqual(tc.options.options_map['optarch'], 'mcpu=native')
+ with self.mocked_stdout_stderr():
+ tc.prepare()
+ self.assertEqual(tc.options.options_map['optarch'], '-mcpu=native')
self.assertIn('-mcpu=native', os.environ['CFLAGS'])
self.modtool.purge()
st.get_cpu_model = lambda: 'ARM Cortex-A53 + Cortex-A72'
tc = self.get_toolchain("GCC", version="4.6.4")
tc.set_options({})
- tc.prepare()
- self.assertEqual(tc.options.options_map['optarch'], 'mcpu=cortex-a72.cortex-a53')
+ with self.mocked_stdout_stderr():
+ tc.prepare()
+ self.assertEqual(tc.options.options_map['optarch'], '-mcpu=cortex-a72.cortex-a53')
self.assertIn('-mcpu=cortex-a72.cortex-a53', os.environ['CFLAGS'])
self.modtool.purge()
def test_compiler_dependent_optarch(self):
"""Test whether specifying optarch on a per compiler basis works."""
flag_vars = ['CFLAGS', 'CXXFLAGS', 'FCFLAGS', 'FFLAGS', 'F90FLAGS']
- intel_options = [('intelflag', 'intelflag'), ('GENERIC', 'xSSE2'), ('', '')]
- gcc_options = [('gccflag', 'gccflag'), ('march=nocona', 'march=nocona'), ('', '')]
- gcccore_options = [('gcccoreflag', 'gcccoreflag'), ('GENERIC', 'march=x86-64 -mtune=generic'), ('', '')]
+ intel_options = [('intelflag', '-intelflag'), ('GENERIC', '-xSSE2'), ('', '')]
+ gcc_options = [('gccflag', '-gccflag'), ('-march=nocona', '-march=nocona'), ('', '')]
+ gcccore_options = [('gcccoreflag', '-gcccoreflag'), ('GENERIC', '-march=x86-64 -mtune=generic'), ('', '')]
tc_intel = ('iccifort', '2018.1.163')
tc_gcc = ('GCC', '6.4.0-2.28')
@@ -784,6 +795,7 @@ def test_compiler_dependent_optarch(self):
tc_pgi = ('PGI', '16.7-GCC-5.4.0-2.26')
enabled = [True, False]
+ self.allow_deprecated_behaviour() # when testing optarch flags without initial "-" (remove in EB 6.0)
test_cases = []
for i, (tc, options) in enumerate(zip((tc_intel, tc_gcc, tc_gcccore),
(intel_options, gcc_options, gcccore_options))):
@@ -811,7 +823,8 @@ def test_compiler_dependent_optarch(self):
init_config(build_options={'optarch': optarch_var, 'silent': True})
tc = self.get_toolchain(toolchain_name, version=toolchain_ver)
tc.set_options({'optarch': enable})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
flags = None
if toolchain_name == 'iccifort':
flags = intel_flags_exp
@@ -837,8 +850,8 @@ def test_compiler_dependent_optarch(self):
gcc_options[1][1],
gcccore_options[0][1],
gcccore_options[1][1],
- 'xHost', # default optimal for Intel
- 'march=native', # default optimal for GCC
+ '-xHost', # default optimal for Intel
+ '-march=native', # default optimal for GCC
]
else:
blacklist = [flags]
@@ -868,14 +881,16 @@ def test_easyconfig_optarch_flags(self):
toy_txt = read_file(eb_file)
# check that an optarch map raises an error
- write_file(test_ec, toy_txt + "\ntoolchainopts = {'optarch': 'GCC:march=sandrybridge;Intel:xAVX'}")
+ write_file(test_ec, toy_txt + "\ntoolchainopts = {'optarch': 'GCC:-march=sandrybridge;Intel:-xAVX'}")
msg = "syntax is not allowed"
- self.assertErrorRegex(EasyBuildError, msg, self.eb_main, [test_ec], raise_error=True, do_build=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, msg, self.eb_main, [test_ec], raise_error=True, do_build=True)
# check that setting optarch flags work
- write_file(test_ec, toy_txt + "\ntoolchainopts = {'optarch': 'march=sandybridge'}")
- out = self.eb_main([test_ec], raise_error=True, do_build=True)
- regex = re.compile("_set_optimal_architecture: using march=sandybridge as optarch for x86_64")
+ write_file(test_ec, toy_txt + "\ntoolchainopts = {'optarch': '-march=sandybridge'}")
+ with self.mocked_stdout_stderr():
+ out = self.eb_main([test_ec], raise_error=True, do_build=True)
+ regex = re.compile("_set_optimal_architecture: using -march=sandybridge as optarch for x86_64")
self.assertTrue(regex.search(out), "Pattern '%s' found in: %s" % (regex.pattern, out))
def test_misc_flags_unique_fortran(self):
@@ -888,12 +903,13 @@ def test_misc_flags_unique_fortran(self):
for enable in [True, False]:
tc = self.get_toolchain('foss', version='2018a')
tc.set_options({opt: enable})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
flag = tc.COMPILER_UNIQUE_OPTION_MAP[opt]
if isinstance(flag, list):
- flag = ' '.join('-%s' % x for x in flag)
+ flag = ' '.join(flag)
else:
- flag = '-%s' % flag
+ flag = flag
for var in flag_vars:
flags = tc.get_variable(var)
if enable:
@@ -910,7 +926,8 @@ def test_precision_flags(self):
# check default precision: -fno-math-errno flag for GCC
tc = self.get_toolchain('foss', version='2018a')
tc.set_options({})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
flags_regex = re.compile(r"-O2 -ftree-vectorize -m(arch|cpu)=native -fno-math-errno")
for var in flag_vars:
val = os.getenv(var)
@@ -920,13 +937,14 @@ def test_precision_flags(self):
precs = ['strict', 'precise', 'loose', 'veryloose']
prec_flags = {}
for prec in precs:
- prec_flags[prec] = ' '.join('-%s' % x for x in Gcc.COMPILER_UNIQUE_OPTION_MAP[prec])
+ prec_flags[prec] = ' '.join(Gcc.COMPILER_UNIQUE_OPTION_MAP[prec])
for prec in prec_flags:
for enable in [True, False]:
tc = self.get_toolchain('foss', version='2018a')
tc.set_options({prec: enable})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
for var in flag_vars:
if enable:
regex = re.compile(r"-O2 -ftree-vectorize -m(arch|cpu)=native %s" % prec_flags[prec])
@@ -937,10 +955,96 @@ def test_precision_flags(self):
self.modtool.purge()
+ def test_search_path_cpp_headers(self):
+ """Test functionality behind search-path-cpp-headers option"""
+ cpp_headers_mode = {
+ "flags": ["CPPFLAGS"],
+ "cpath": ["CPATH"],
+ "include_paths": ["C_INCLUDE_PATH", "CPLUS_INCLUDE_PATH", "OBJC_INCLUDE_PATH"],
+ }
+ # test without toolchain option
+ for build_opt in cpp_headers_mode:
+ init_config(build_options={"search_path_cpp_headers": build_opt, "silent": True})
+ tc = self.get_toolchain("foss", version="2018a")
+ with self.mocked_stdout_stderr():
+ tc.prepare()
+ for env_var in cpp_headers_mode[build_opt]:
+ assert_fail_msg = (
+ f"Variable {env_var} required by search-path-cpp-headers build option '{build_opt}' "
+ "not found in toolchain environment"
+ )
+ self.assertIn(env_var, tc.variables, assert_fail_msg)
+ self.modtool.purge()
+ # test with toolchain option
+ for build_opt in cpp_headers_mode:
+ init_config(build_options={"search_path_cpp_headers": build_opt, "silent": True})
+ for tc_opt in cpp_headers_mode:
+ tc = self.get_toolchain("foss", version="2018a")
+ tc.set_options({"search-path-cpp-headers": tc_opt})
+ with self.mocked_stdout_stderr():
+ tc.prepare()
+ for env_var in cpp_headers_mode[tc_opt]:
+ assert_fail_msg = (
+ f"Variable {env_var} required by search-path-cpp-headers toolchain option '{tc_opt}' "
+ "not found in toolchain environment"
+ )
+ self.assertIn(env_var, tc.variables, assert_fail_msg)
+ self.modtool.purge()
+ # test wrong toolchain option
+ tc = self.get_toolchain("foss", version="2018a")
+ tc.set_options({"search-path-cpp-headers": "WRONG_MODE"})
+ with self.mocked_stdout_stderr():
+ error_pattern = "Unknown value selected for toolchain option search-path-cpp-headers"
+ self.assertErrorRegex(EasyBuildError, error_pattern, tc.prepare)
+ self.modtool.purge()
+
+ def test_search_path_linker(self):
+ """Test functionality behind search-path-linker option"""
+ linker_mode = {
+ "flags": ["LDFLAGS"],
+ "library_path": ["LIBRARY_PATH"],
+ }
+ # test without toolchain option
+ for build_opt in linker_mode:
+ init_config(build_options={"search_path_linker": build_opt, "silent": True})
+ tc = self.get_toolchain("foss", version="2018a")
+ with self.mocked_stdout_stderr():
+ tc.prepare()
+ for env_var in linker_mode[build_opt]:
+ assert_fail_msg = (
+ f"Variable {env_var} required by search-path-linker build option '{build_opt}' "
+ "not found in toolchain environment"
+ )
+ self.assertIn(env_var, tc.variables, assert_fail_msg)
+ self.modtool.purge()
+ # test with toolchain option
+ for build_opt in linker_mode:
+ init_config(build_options={"search_path_linker": build_opt, "silent": True})
+ for tc_opt in linker_mode:
+ tc = self.get_toolchain("foss", version="2018a")
+ tc.set_options({"search-path-linker": tc_opt})
+ with self.mocked_stdout_stderr():
+ tc.prepare()
+ for env_var in linker_mode[tc_opt]:
+ assert_fail_msg = (
+ f"Variable {env_var} required by search-path-linker toolchain option '{tc_opt}' "
+ "not found in toolchain environment"
+ )
+ self.assertIn(env_var, tc.variables, assert_fail_msg)
+ self.modtool.purge()
+ # test wrong toolchain option
+ tc = self.get_toolchain("foss", version="2018a")
+ tc.set_options({"search-path-linker": "WRONG_MODE"})
+ with self.mocked_stdout_stderr():
+ error_pattern = "Unknown value selected for toolchain option search-path-linker"
+ self.assertErrorRegex(EasyBuildError, error_pattern, tc.prepare)
+ self.modtool.purge()
+
def test_cgoolf_toolchain(self):
"""Test for cgoolf toolchain."""
tc = self.get_toolchain("cgoolf", version="1.1.6")
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.get_variable('CC'), 'clang')
self.assertEqual(tc.get_variable('CXX'), 'clang++')
@@ -951,20 +1055,23 @@ def test_cgoolf_toolchain(self):
def test_comp_family(self):
"""Test determining compiler family."""
tc = self.get_toolchain('foss', version='2018a')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.comp_family(), "GCC")
def test_mpi_family(self):
"""Test determining MPI family."""
# check subtoolchain w/o MPI
tc = self.get_toolchain("GCC", version="6.4.0-2.28")
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.mpi_family(), None)
self.modtool.purge()
# check full toolchain including MPI
tc = self.get_toolchain('foss', version='2018a')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.mpi_family(), "OpenMPI")
self.modtool.purge()
@@ -972,28 +1079,32 @@ def test_mpi_family(self):
self.setup_sandbox_for_intel_fftw(self.test_prefix)
self.modtool.prepend_module_path(self.test_prefix)
tc = self.get_toolchain("intel", version="2018a")
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.mpi_family(), "IntelMPI")
def test_blas_lapack_family(self):
"""Test determining BLAS/LAPACK family."""
# check compiler-only (sub)toolchain
tc = self.get_toolchain("GCC", version="6.4.0-2.28")
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.blas_family(), None)
self.assertEqual(tc.lapack_family(), None)
self.modtool.purge()
# check compiler/MPI-only (sub)toolchain
tc = self.get_toolchain('gompi', version='2018a')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.blas_family(), None)
self.assertEqual(tc.lapack_family(), None)
self.modtool.purge()
# check full toolchain including BLAS/LAPACK
tc = self.get_toolchain('fosscuda', version='2018a')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.blas_family(), 'OpenBLAS')
self.assertEqual(tc.lapack_family(), 'OpenBLAS')
self.modtool.purge()
@@ -1002,14 +1113,16 @@ def test_blas_lapack_family(self):
self.setup_sandbox_for_intel_fftw(self.test_prefix)
self.modtool.prepend_module_path(self.test_prefix)
tc = self.get_toolchain('intel', version='2018a')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.blas_family(), 'IntelMKL')
self.assertEqual(tc.lapack_family(), 'IntelMKL')
def test_fft_env_vars_foss(self):
"""Test setting of $FFT* environment variables using foss toolchain."""
tc = self.get_toolchain('foss', version='2018a')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
fft_static_libs = 'libfftw3.a'
self.assertEqual(tc.get_variable('FFT_STATIC_LIBS'), fft_static_libs)
@@ -1024,7 +1137,8 @@ def test_fft_env_vars_foss(self):
tc = self.get_toolchain('foss', version='2018a')
tc.set_options({'openmp': True})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.get_variable('FFT_STATIC_LIBS'), fft_static_libs)
self.assertEqual(tc.get_variable('FFTW_STATIC_LIBS'), fft_static_libs)
@@ -1037,7 +1151,8 @@ def test_fft_env_vars_foss(self):
tc = self.get_toolchain('foss', version='2018a')
tc.set_options({'usempi': True})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
fft_static_libs = 'libfftw3_mpi.a,libfftw3.a'
self.assertEqual(tc.get_variable('FFT_STATIC_LIBS'), fft_static_libs)
@@ -1054,7 +1169,8 @@ def test_fft_env_vars_foss(self):
self.modtool.prepend_module_path(self.test_prefix)
tc = self.get_toolchain('foss', version='2018a-FFTW.MPI')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
fft_static_libs = 'libfftw3.a'
self.assertEqual(tc.get_variable('FFT_STATIC_LIBS'), fft_static_libs)
@@ -1072,7 +1188,8 @@ def test_fft_env_vars_foss(self):
tc = self.get_toolchain('foss', version='2018a-FFTW.MPI')
tc.set_options({'openmp': True})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.get_variable('FFT_STATIC_LIBS'), fft_static_libs)
self.assertEqual(tc.get_variable('FFTW_STATIC_LIBS'), fft_static_libs)
@@ -1088,7 +1205,8 @@ def test_fft_env_vars_foss(self):
tc = self.get_toolchain('foss', version='2018a-FFTW.MPI')
tc.set_options({'usempi': True})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
fft_static_libs = 'libfftw3_mpi.a,libfftw3.a'
self.assertEqual(tc.get_variable('FFT_STATIC_LIBS'), fft_static_libs)
@@ -1111,7 +1229,8 @@ def test_fft_env_vars_intel(self):
self.modtool.prepend_module_path(self.test_prefix)
tc = self.get_toolchain('intel', version='2018a')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
fft_static_libs = 'libfftw3xc_intel.a,libmkl_intel_lp64.a,libmkl_sequential.a,libmkl_core.a'
self.assertEqual(tc.get_variable('FFT_STATIC_LIBS'), fft_static_libs)
@@ -1132,7 +1251,8 @@ def test_fft_env_vars_intel(self):
tc = self.get_toolchain('intel', version='2018a')
tc.set_options({'openmp': True})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.get_variable('FFT_STATIC_LIBS'), fft_static_libs)
self.assertEqual(tc.get_variable('FFTW_STATIC_LIBS'), fft_static_libs)
@@ -1145,7 +1265,8 @@ def test_fft_env_vars_intel(self):
tc = self.get_toolchain('intel', version='2018a')
tc.set_options({'usempi': True})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
fft_static_libs = 'libfftw3xc_intel.a,libfftw3x_cdft_lp64.a,libmkl_cdft_core.a,libmkl_blacs_intelmpi_lp64.a,'
fft_static_libs += 'libmkl_intel_lp64.a,libmkl_sequential.a,libmkl_core.a'
@@ -1169,7 +1290,8 @@ def test_fft_env_vars_intel(self):
self.modtool.purge()
self.setup_sandbox_for_intel_fftw(self.test_prefix, imklver='2021.4.0')
tc = self.get_toolchain('intel', version='2021b')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
fft_static_libs = 'libmkl_intel_lp64.a,libmkl_sequential.a,libmkl_core.a'
self.assertEqual(tc.get_variable('FFT_STATIC_LIBS'), fft_static_libs)
@@ -1190,7 +1312,8 @@ def test_fft_env_vars_intel(self):
tc = self.get_toolchain('intel', version='2021b')
tc.set_options({'openmp': True})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.get_variable('FFT_STATIC_LIBS'), fft_static_libs)
self.assertEqual(tc.get_variable('FFTW_STATIC_LIBS'), fft_static_libs)
@@ -1206,7 +1329,8 @@ def test_fft_env_vars_intel(self):
tc = self.get_toolchain('intel', version='2021b')
tc.set_options({'usempi': True})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
fft_static_libs = 'libfftw3x_cdft_lp64.a,libmkl_cdft_core.a,libmkl_blacs_intelmpi_lp64.a,'
fft_static_libs += 'libmkl_intel_lp64.a,libmkl_sequential.a,libmkl_core.a'
@@ -1235,10 +1359,11 @@ def test_fosscuda(self):
tc = self.get_toolchain("fosscuda", version="2018a")
opts = {'cuda_gencode': ['arch=compute_35,code=sm_35', 'arch=compute_10,code=compute_10'], 'openmp': True}
tc.set_options(opts)
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
archflags = tc.COMPILER_OPTIMAL_ARCHITECTURE_OPTION[(tc.arch, tc.cpu_family)]
- optflags = "-O2 -ftree-vectorize -%s -fno-math-errno -fopenmp" % archflags
+ optflags = "-O2 -ftree-vectorize %s -fno-math-errno -fopenmp" % archflags
nvcc_flags = r' '.join([
r'-Xcompiler="%s"' % optflags,
# the use of -lcudart in -Xlinker is a bit silly but hard to avoid
@@ -1344,7 +1469,8 @@ def test_intel_toolchain(self):
self.modtool.prepend_module_path(self.test_prefix)
tc = self.get_toolchain("intel", version="2018a")
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.get_variable('CC'), 'icc')
self.assertEqual(tc.get_variable('CXX'), 'icpc')
@@ -1356,7 +1482,8 @@ def test_intel_toolchain(self):
tc = self.get_toolchain("intel", version="2018a")
opts = {'usempi': True}
tc.set_options(opts)
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.get_variable('CC'), 'mpiicc')
self.assertEqual(tc.get_variable('CXX'), 'mpiicpc')
@@ -1373,7 +1500,8 @@ def test_intel_toolchain(self):
tc = self.get_toolchain("intel", version="2018a")
opts = {'usempi': True, 'openmp': True}
tc.set_options(opts)
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
for flag in ['-mt_mpi', '-fopenmp']:
for var in ['CFLAGS', 'CXXFLAGS', 'FCFLAGS', 'FFLAGS', 'F90FLAGS']:
@@ -1402,7 +1530,8 @@ def test_intel_toolchain(self):
tc = self.get_toolchain('intel', version='2012a')
opts = {'openmp': True}
tc.set_options(opts)
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.get_variable('MPIFC'), 'mpiifort')
for var in ['CFLAGS', 'CXXFLAGS', 'FCFLAGS', 'FFLAGS', 'F90FLAGS']:
self.assertIn('-openmp', tc.get_variable(var))
@@ -1417,7 +1546,8 @@ def test_intel_toolchain(self):
tc = self.get_toolchain('intel-compilers', version='2021.4.0')
tc.set_options({})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(os.getenv('CC'), 'icc')
self.assertEqual(os.getenv('CXX'), 'icpc')
@@ -1436,7 +1566,8 @@ def test_intel_toolchain_oneapi(self):
self.modtool.prepend_module_path(self.test_prefix)
tc = self.get_toolchain('intel', version='2021b')
tc.set_options({})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
# default remains classic compilers for now
self.assertEqual(os.getenv('CC'), 'icc')
@@ -1454,7 +1585,8 @@ def test_intel_toolchain_oneapi(self):
self.modtool.purge()
tc = self.get_toolchain('intel', version='2021b')
tc.set_options({'oneapi': True})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(os.getenv('CC'), 'icx')
self.assertEqual(os.getenv('CXX'), 'icpx')
@@ -1470,7 +1602,8 @@ def test_intel_toolchain_oneapi(self):
self.modtool.purge()
tc = self.get_toolchain('intel-compilers', version='2022.2.0')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
# by default (for version >= 2022.2.0): oneAPI C/C++ compiler + classic Fortran compiler
self.assertEqual(os.getenv('CC'), 'icx')
@@ -1482,7 +1615,8 @@ def test_intel_toolchain_oneapi(self):
self.modtool.purge()
tc = self.get_toolchain('intel-compilers', version='2022.2.0')
tc.set_options({'oneapi_fortran': True})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(os.getenv('CC'), 'icx')
self.assertEqual(os.getenv('CXX'), 'icpx')
self.assertEqual(os.getenv('F77'), 'ifx')
@@ -1492,7 +1626,8 @@ def test_intel_toolchain_oneapi(self):
self.modtool.purge()
tc = self.get_toolchain('intel-compilers', version='2022.2.0')
tc.set_options({'oneapi_c_cxx': False, 'oneapi_fortran': True})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(os.getenv('CC'), 'icc')
self.assertEqual(os.getenv('CXX'), 'icpc')
self.assertEqual(os.getenv('F77'), 'ifx')
@@ -1533,7 +1668,8 @@ def test_intel_toolchain_oneapi(self):
self.modtool.purge()
tc = self.get_toolchain('intel', version='2021b')
tc.set_options({'oneapi_c_cxx': True})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(os.getenv('CC'), 'icx')
self.assertEqual(os.getenv('CXX'), 'icpx')
self.assertEqual(os.getenv('F77'), 'ifort')
@@ -1543,7 +1679,8 @@ def test_intel_toolchain_oneapi(self):
self.modtool.purge()
tc = self.get_toolchain('intel', version='2021b')
tc.set_options({'oneapi_fortran': True})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(os.getenv('CC'), 'icc')
self.assertEqual(os.getenv('CXX'), 'icpc')
self.assertEqual(os.getenv('F77'), 'ifx')
@@ -1553,7 +1690,8 @@ def test_intel_toolchain_oneapi(self):
self.modtool.purge()
tc = self.get_toolchain('intel', version='2021b')
tc.set_options({'oneapi_c_cxx': True, 'oneapi_fortran': True})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(os.getenv('CC'), 'icx')
self.assertEqual(os.getenv('CXX'), 'icpx')
self.assertEqual(os.getenv('F77'), 'ifx')
@@ -1563,25 +1701,29 @@ def test_intel_toolchain_oneapi(self):
def test_toolchain_verification(self):
"""Test verification of toolchain definition."""
tc = self.get_toolchain('foss', version='2018a')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.modtool.purge()
# toolchain modules missing a toolchain element should fail verification
error_msg = "List of toolchain dependency modules and toolchain definition do not match"
tc = self.get_toolchain('foss', version='2018a-brokenFFTW')
- self.assertErrorRegex(EasyBuildError, error_msg, tc.prepare)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_msg, tc.prepare)
self.modtool.purge()
# missing optional toolchain elements are fine
tc = self.get_toolchain('fosscuda', version='2018a')
opts = {'cuda_gencode': ['arch=compute_35,code=sm_35', 'arch=compute_10,code=compute_10']}
tc.set_options(opts)
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
def test_nosuchtoolchain(self):
"""Test preparing for a toolchain for which no module is available."""
tc = self.get_toolchain('intel', version='1970.01')
- self.assertErrorRegex(EasyBuildError, "No module found for toolchain", tc.prepare)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, "No module found for toolchain", tc.prepare)
def test_mpi_cmd_prefix(self):
"""Test mpi_exec_nranks function."""
@@ -1598,7 +1740,8 @@ def test_mpi_cmd_prefix(self):
self.assertEqual(tc.mpi_cmd_prefix(nr_ranks=2), "mpirun -n 2")
tc = self.get_toolchain('gompi', version='2018a')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.mpi_cmd_prefix(nr_ranks=2), "mpirun -n 2")
self.assertEqual(tc.mpi_cmd_prefix(nr_ranks='2'), "mpirun -n 2")
self.assertEqual(tc.mpi_cmd_prefix(), "mpirun -n 1")
@@ -1606,7 +1749,8 @@ def test_mpi_cmd_prefix(self):
self.setup_sandbox_for_intel_fftw(self.test_prefix)
tc = self.get_toolchain('intel', version='2018a')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.mpi_cmd_prefix(nr_ranks=2), "mpirun -n 2")
self.assertEqual(tc.mpi_cmd_prefix(nr_ranks='2'), "mpirun -n 2")
self.assertEqual(tc.mpi_cmd_prefix(), "mpirun -n 1")
@@ -1614,7 +1758,8 @@ def test_mpi_cmd_prefix(self):
self.setup_sandbox_for_intel_fftw(self.test_prefix, imklver='10.2.6.038')
tc = self.get_toolchain('intel', version='2012a')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
mpi_exec_nranks_re = re.compile("^mpirun --file=.*/mpdboot -machinefile .*/nodes -np 4")
self.assertTrue(mpi_exec_nranks_re.match(tc.mpi_cmd_prefix(nr_ranks=4)))
@@ -1650,19 +1795,22 @@ def test_mpi_cmd_for(self):
self.assertEqual(tc.mpi_cmd_for('test123', 2), "mpirun -n 2 test123")
tc = self.get_toolchain('gompi', version='2018a')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.mpi_cmd_for('test123', 2), "mpirun -n 2 test123")
self.modtool.purge()
self.setup_sandbox_for_intel_fftw(self.test_prefix)
tc = self.get_toolchain('intel', version='2018a')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.mpi_cmd_for('test123', 2), "mpirun -n 2 test123")
self.modtool.purge()
self.setup_sandbox_for_intel_fftw(self.test_prefix, imklver='10.2.6.038')
tc = self.get_toolchain('intel', version='2012a')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
mpi_cmd_for_re = re.compile("^mpirun --file=.*/mpdboot -machinefile .*/nodes -np 4 test$")
self.assertTrue(mpi_cmd_for_re.match(tc.mpi_cmd_for('test', 4)))
@@ -1729,7 +1877,8 @@ def test_prepare_deps(self):
'build_only': False,
},
]
- tc.prepare(deps=deps)
+ with self.mocked_stdout_stderr():
+ tc.prepare(deps=deps)
mods = ['GCC/6.4.0-2.28', 'hwloc/1.11.8-GCC-6.4.0-2.28', 'OpenMPI/2.1.2-GCC-6.4.0-2.28']
self.assertEqual(sorted(m['mod_name'] for m in self.modtool.list()), sorted(mods))
@@ -1757,7 +1906,8 @@ def test_prepare_deps_external(self):
}
]
tc = self.get_toolchain('GCC', version='6.4.0-2.28')
- tc.prepare(deps=deps)
+ with self.mocked_stdout_stderr():
+ tc.prepare(deps=deps)
mods = ['GCC/6.4.0-2.28', 'hwloc/1.11.8-GCC-6.4.0-2.28', 'OpenMPI/2.1.2-GCC-6.4.0-2.28', 'toy/0.0']
self.assertEqual(sorted(m['mod_name'] for m in self.modtool.list()), sorted(mods))
self.assertTrue(os.environ['EBROOTTOY'].endswith('software/toy/0.0'))
@@ -1778,7 +1928,8 @@ def test_prepare_deps_external(self):
}
tc = self.get_toolchain('GCC', version='6.4.0-2.28')
os.environ['FOOBAR_PREFIX'] = '/foo/bar'
- tc.prepare(deps=deps)
+ with self.mocked_stdout_stderr():
+ tc.prepare(deps=deps)
mods = ['GCC/6.4.0-2.28', 'hwloc/1.11.8-GCC-6.4.0-2.28', 'OpenMPI/2.1.2-GCC-6.4.0-2.28', 'toy/0.0']
self.assertEqual(sorted(m['mod_name'] for m in self.modtool.list()), sorted(mods))
self.assertEqual(os.environ['EBROOTTOY'], '/foo/bar')
@@ -1872,7 +2023,8 @@ def test_old_new_iccifort(self):
scalapack_mt_shared_libs_fosscuda = scalapack_mt_static_libs_fosscuda.replace('.a', '.' + shlib_ext)
tc = self.get_toolchain('fosscuda', version='2018a')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(os.environ['BLAS_SHARED_LIBS'], blas_shared_libs_fosscuda)
self.assertEqual(os.environ['BLAS_STATIC_LIBS'], blas_static_libs_fosscuda)
self.assertEqual(os.environ['BLAS_MT_SHARED_LIBS'], blas_mt_shared_libs_fosscuda)
@@ -1964,7 +2116,8 @@ def test_old_new_iccifort(self):
self.modtool.purge()
tc = self.get_toolchain('intel', version='2018a')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(os.environ.get('BLAS_SHARED_LIBS', "(not set)"), blas_shared_libs_intel4)
self.assertEqual(os.environ.get('BLAS_STATIC_LIBS', "(not set)"), blas_static_libs_intel4)
self.assertEqual(os.environ.get('LAPACK_SHARED_LIBS', "(not set)"), blas_shared_libs_intel4)
@@ -1977,19 +2130,22 @@ def test_old_new_iccifort(self):
self.modtool.purge()
tc = self.get_toolchain('intel', version='2012a')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(os.environ.get('LIBBLAS_MT', "(not set)"), libblas_mt_intel3)
self.assertIn(libscalack_intel3, os.environ['LIBSCALAPACK'])
self.modtool.purge()
tc = self.get_toolchain('intel', version='2018a')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(os.environ.get('LIBBLAS_MT', "(not set)"), libblas_mt_intel4)
self.assertIn(libscalack_intel4, os.environ['LIBSCALAPACK'])
self.modtool.purge()
tc = self.get_toolchain('intel', version='2012a')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(os.environ.get('LIBBLAS_MT', "(not set)"), libblas_mt_intel3)
self.assertIn(libscalack_intel3, os.environ['LIBSCALAPACK'])
self.modtool.purge()
@@ -1998,13 +2154,15 @@ def test_old_new_iccifort(self):
tc = self.get_toolchain('intel', version='2018a')
opts = {'i8': True}
tc.set_options(opts)
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertIn(libscalack_intel4, os.environ['LIBSCALAPACK'])
self.modtool.purge()
tc = self.get_toolchain('fosscuda', version='2018a')
tc.set_options({'openmp': True})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(os.environ['BLAS_SHARED_LIBS'], blas_shared_libs_fosscuda)
self.assertEqual(os.environ['BLAS_STATIC_LIBS'], blas_static_libs_fosscuda)
self.assertEqual(os.environ['BLAS_MT_SHARED_LIBS'], blas_mt_shared_libs_fosscuda)
@@ -2046,7 +2204,8 @@ def test_standalone_iccifort(self):
"""Test whether standalone installation of iccifort matches the iccifort toolchain definition."""
tc = self.get_toolchain('iccifort', version='2018.1.163')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.toolchain_dep_mods, ['icc/2018.1.163', 'ifort/2018.1.163'])
self.modtool.purge()
@@ -2061,7 +2220,8 @@ def test_standalone_iccifort(self):
# toolchain verification fails because icc/ifort are not dependencies of iccifort modules,
# and corresponding environment variables are not set
error_pattern = "List of toolchain dependency modules and toolchain definition do not match"
- self.assertErrorRegex(EasyBuildError, error_pattern, tc.prepare)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, tc.prepare)
self.modtool.purge()
# make iccifort module set $EBROOT* and $EBVERSION* to pass toolchain verification
@@ -2074,7 +2234,8 @@ def test_standalone_iccifort(self):
])
write_file(fake_iccifort, fake_iccifort_txt)
# toolchain preparation (which includes verification) works fine now
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
# no dependencies found in iccifort module
self.assertEqual(tc.toolchain_dep_mods, [])
@@ -2082,7 +2243,8 @@ def test_standalone_iccifortcuda(self):
"""Test whether standalone installation of iccifortcuda matches the iccifortcuda toolchain definition."""
tc = self.get_toolchain('iccifortcuda', version='2018b')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.toolchain_dep_mods, ['icc/2018.1.163', 'ifort/2018.1.163', 'CUDA/9.1.85'])
self.modtool.purge()
@@ -2097,13 +2259,15 @@ def test_standalone_iccifortcuda(self):
# toolchain verification fails because icc/ifort are not dependencies of iccifortcuda modules,
# and corresponding environment variables are not set
error_pattern = "List of toolchain dependency modules and toolchain definition do not match"
- self.assertErrorRegex(EasyBuildError, error_pattern, tc.prepare)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, tc.prepare)
self.modtool.purge()
# Verify that it works loading a module that contains a combined iccifort module
tc = self.get_toolchain('iccifortcuda', version='2019a')
# toolchain preparation (which includes verification) works fine now
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
# dependencies found in iccifortcuda module
self.assertEqual(tc.toolchain_dep_mods, ['iccifort/2019.5.281', 'CUDA/9.1.85'])
@@ -2111,42 +2275,55 @@ def test_independence(self):
"""Test independency of toolchain instances."""
# tweaking --optarch is required for Cray toolchains (craypre- module must be available)
- init_config(build_options={'optarch': 'test', 'silent': True})
-
- tc_cflags = {
- 'CrayCCE': "-O2 -homp -craype-verbose",
- 'CrayGNU': "-O2 -fno-math-errno -fopenmp -craype-verbose",
- 'CrayIntel': "-O2 -ftz -fp-speculation=safe -fp-model source -fopenmp -craype-verbose",
- 'GCC': "-O2 -ftree-vectorize -test -fno-math-errno -fopenmp",
- 'iccifort': "-O2 -test -ftz -fp-speculation=safe -fp-model source -fopenmp",
- 'intel-compilers': "-O2 -test -ftz -fp-speculation=safe -fp-model precise -qopenmp",
- }
-
- toolchains = [
- ('CrayCCE', '2015.06-XC'),
- ('CrayGNU', '2015.06-XC'),
- ('CrayIntel', '2015.06-XC'),
- ('GCC', '6.4.0-2.28'),
- ('iccifort', '2018.1.163'),
- ('intel-compilers', '2022.1.0'),
- ]
+ custom_optarchs = ['test', '-test'] # specifying without initial "-" is deprecated but should still work
+ for custom_optarch in custom_optarchs:
+ init_config(build_options={'optarch': custom_optarch, 'silent': True})
+
+ tc_cflags = {
+ 'CrayCCE': "-O2 -homp -craype-verbose",
+ 'CrayGNU': "-O2 -fno-math-errno -fopenmp -craype-verbose",
+ 'CrayIntel': "-O2 -ftz -fp-speculation=safe -fp-model source -fopenmp -craype-verbose",
+ 'GCC': "-O2 -ftree-vectorize -test -fno-math-errno -fopenmp",
+ 'iccifort': "-O2 -test -ftz -fp-speculation=safe -fp-model source -fopenmp",
+ 'intel-compilers': "-O2 -test -ftz -fp-speculation=safe -fp-model precise -qopenmp",
+ }
- # purposely obtain toolchains several times in a row, value for $CFLAGS should not change
- for _ in range(3):
- for tcname, tcversion in toolchains:
- tc = get_toolchain({'name': tcname, 'version': tcversion}, {},
- mns=ActiveMNS(), modtool=self.modtool)
- # also check whether correct compiler flag for OpenMP is used while we're at it
- # and options for oneAPI compiler for Intel
- if tcname == 'intel-compilers':
- tc.set_options({'oneapi': True, 'openmp': True})
- else:
- tc.set_options({'openmp': True})
- tc.prepare()
- expected_cflags = tc_cflags[tcname]
- msg = "Expected $CFLAGS found for toolchain %s: %s" % (tcname, expected_cflags)
- self.assertEqual(str(tc.variables['CFLAGS']), expected_cflags, msg)
- self.assertEqual(os.environ['CFLAGS'], expected_cflags, msg)
+ toolchains = [
+ ('GCC', '6.4.0-2.28'),
+ ('iccifort', '2018.1.163'),
+ ('intel-compilers', '2022.1.0'),
+ ]
+ if custom_optarch == 'test': # optarch is not used as a flag for Cray
+ toolchains += [
+ ('CrayCCE', '2015.06-XC'),
+ ('CrayGNU', '2015.06-XC'),
+ ('CrayIntel', '2015.06-XC'),
+ ]
+ self.allow_deprecated_behaviour() # test will be automatically converted to -test (remove in 6.0)
+ else:
+ self.disallow_deprecated_behaviour()
+
+ # purposely obtain toolchains several times in a row, value for $CFLAGS should not change
+ for _ in range(3):
+ for tcname, tcversion in toolchains:
+ # Cray* modules do not unload other Cray* modules thus loading a second Cray* module
+ # makes environment inconsistent which is not allowed by Environment Modules tool
+ if isinstance(self.modtool, EnvironmentModules):
+ self.modtool.purge()
+ tc = get_toolchain({'name': tcname, 'version': tcversion}, {},
+ mns=ActiveMNS(), modtool=self.modtool)
+ # also check whether correct compiler flag for OpenMP is used while we're at it
+ # and options for oneAPI compiler for Intel
+ if tcname == 'intel-compilers':
+ tc.set_options({'oneapi': True, 'openmp': True})
+ else:
+ tc.set_options({'openmp': True})
+ with self.mocked_stdout_stderr():
+ tc.prepare()
+ expected_cflags = tc_cflags[tcname]
+ msg = "Expected $CFLAGS found for toolchain %s: %s" % (tcname, expected_cflags)
+ self.assertEqual(str(tc.variables['CFLAGS']), expected_cflags, msg)
+ self.assertEqual(os.environ['CFLAGS'], expected_cflags, msg)
def test_pgi_toolchain(self):
"""Tests for PGI toolchain."""
@@ -2158,7 +2335,8 @@ def test_pgi_toolchain(self):
self.modtool.prepend_module_path(self.test_prefix)
tc = self.get_toolchain('PGI', version='14.9')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.get_variable('CC'), 'pgcc')
self.assertEqual(tc.get_variable('CXX'), 'pgCC')
@@ -2169,7 +2347,8 @@ def test_pgi_toolchain(self):
for pgi_ver in ['14.10', '16.3', '19.1']:
tc = self.get_toolchain('PGI', version=pgi_ver)
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
self.assertEqual(tc.get_variable('CC'), 'pgcc')
self.assertEqual(tc.get_variable('CXX'), 'pgc++')
@@ -2205,7 +2384,8 @@ def test_pgi_imkl(self):
self.modtool.prepend_module_path(self.test_prefix)
tc = self.get_toolchain('pomkl', version='2016.03')
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
liblapack = "-Wl,-Bstatic -Wl,--start-group -lmkl_intel_lp64 -lmkl_sequential -lmkl_core "
liblapack += "-Wl,--end-group -Wl,-Bdynamic -ldl"
@@ -2222,12 +2402,14 @@ def test_compiler_cache(self):
"--force",
"--debug",
"--disable-cleanup-tmpdir",
+ "--disable-rpath",
]
ccache = which('ccache')
if ccache is None:
msg = r"ccache binary not found in \$PATH, required by --use-ccache"
- self.assertErrorRegex(EasyBuildError, msg, self.eb_main, args, raise_error=True, do_build=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, msg, self.eb_main, args, raise_error=True, do_build=True)
# generate shell script to mock ccache/f90cache
for cache_tool in ['ccache', 'f90cache']:
@@ -2256,7 +2438,8 @@ def test_compiler_cache(self):
ccache_dir = os.path.join(self.test_prefix, 'ccache')
mkdir(ccache_dir, parents=True)
- out = self.eb_main(args, raise_error=True, do_build=True, reset_env=False)
+ with self.mocked_stdout_stderr():
+ out = self.eb_main(args, raise_error=True, do_build=True, reset_env=False)
patterns = [
"This is a ccache wrapper",
@@ -2287,7 +2470,8 @@ def test_compiler_cache(self):
mkdir(f90cache_dir, parents=True)
args.append("--use-f90cache=%s" % f90cache_dir)
- out = self.eb_main(args, raise_error=True, do_build=True, reset_env=False)
+ with self.mocked_stdout_stderr():
+ out = self.eb_main(args, raise_error=True, do_build=True, reset_env=False)
for pattern in patterns:
regex = re.compile(pattern)
self.assertTrue(regex.search(out), "Pattern '%s' found in: %s" % (regex.pattern, out))
@@ -2315,8 +2499,9 @@ def test_rpath_args_script(self):
])
# simplest possible compiler command
- out, ec = run_cmd("%s gcc '' '%s' -c foo.c" % (script, rpath_inc), simple=False)
- self.assertEqual(ec, 0)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(f"{script} gcc '' '{rpath_inc}' -c foo.c")
+ self.assertEqual(res.exit_code, 0)
cmd_args = [
"'-Wl,-rpath=%s/lib'" % self.test_prefix,
"'-Wl,-rpath=%s/lib64'" % self.test_prefix,
@@ -2327,11 +2512,12 @@ def test_rpath_args_script(self):
"'-c'",
"'foo.c'",
]
- self.assertEqual(out.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
+ self.assertEqual(res.output.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
# linker command, --enable-new-dtags should be replaced with --disable-new-dtags
- out, ec = run_cmd("%s ld '' '%s' --enable-new-dtags foo.o" % (script, rpath_inc), simple=False)
- self.assertEqual(ec, 0)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(f"{script} ld '' '{rpath_inc}' --enable-new-dtags foo.o")
+ self.assertEqual(res.exit_code, 0)
cmd_args = [
"'-rpath=%s/lib'" % self.test_prefix,
"'-rpath=%s/lib64'" % self.test_prefix,
@@ -2342,11 +2528,12 @@ def test_rpath_args_script(self):
"'--disable-new-dtags'",
"'foo.o'",
]
- self.assertEqual(out.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
+ self.assertEqual(res.output.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
# compiler command, -Wl,--enable-new-dtags should be replaced with -Wl,--disable-new-dtags
- out, ec = run_cmd("%s gcc '' '%s' -Wl,--enable-new-dtags foo.c" % (script, rpath_inc), simple=False)
- self.assertEqual(ec, 0)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(f"{script} gcc '' '{rpath_inc}' -Wl,--enable-new-dtags foo.c")
+ self.assertEqual(res.exit_code, 0)
cmd_args = [
"'-Wl,-rpath=%s/lib'" % self.test_prefix,
"'-Wl,-rpath=%s/lib64'" % self.test_prefix,
@@ -2357,11 +2544,12 @@ def test_rpath_args_script(self):
"'-Wl,--disable-new-dtags'",
"'foo.c'",
]
- self.assertEqual(out.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
+ self.assertEqual(res.output.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
# test passing no arguments
- out, ec = run_cmd("%s gcc '' '%s'" % (script, rpath_inc), simple=False)
- self.assertEqual(ec, 0)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(f"{script} gcc '' '{rpath_inc}'")
+ self.assertEqual(res.exit_code, 0)
cmd_args = [
"'-Wl,-rpath=%s/lib'" % self.test_prefix,
"'-Wl,-rpath=%s/lib64'" % self.test_prefix,
@@ -2370,11 +2558,12 @@ def test_rpath_args_script(self):
"'-Wl,-rpath=$ORIGIN/../lib64'",
"'-Wl,--disable-new-dtags'",
]
- self.assertEqual(out.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
+ self.assertEqual(res.output.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
# test passing a single empty argument
- out, ec = run_cmd("%s ld.gold '' '%s' ''" % (script, rpath_inc), simple=False)
- self.assertEqual(ec, 0)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(f"{script} ld.gold '' '{rpath_inc}' ''")
+ self.assertEqual(res.exit_code, 0)
cmd_args = [
"'-rpath=%s/lib'" % self.test_prefix,
"'-rpath=%s/lib64'" % self.test_prefix,
@@ -2384,12 +2573,13 @@ def test_rpath_args_script(self):
"'--disable-new-dtags'",
"''",
]
- self.assertEqual(out.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
+ self.assertEqual(res.output.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
# single -L argument, but non-existing path => not used in RPATH, but -L option is retained
- cmd = "%s gcc '' '%s' foo.c -L%s/foo -lfoo" % (script, rpath_inc, self.test_prefix)
- out, ec = run_cmd(cmd, simple=False)
- self.assertEqual(ec, 0)
+ cmd = f"{script} gcc '' '{rpath_inc}' foo.c -L{self.test_prefix}/foo -lfoo"
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd)
+ self.assertEqual(res.exit_code, 0)
cmd_args = [
"'-Wl,-rpath=%s/lib'" % self.test_prefix,
"'-Wl,-rpath=%s/lib64'" % self.test_prefix,
@@ -2401,12 +2591,13 @@ def test_rpath_args_script(self):
"'-L%s/foo'" % self.test_prefix,
"'-lfoo'",
]
- self.assertEqual(out.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
+ self.assertEqual(res.output.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
# single -L argument again, with existing path
mkdir(os.path.join(self.test_prefix, 'foo'))
- out, ec = run_cmd(cmd, simple=False)
- self.assertEqual(ec, 0)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd)
+ self.assertEqual(res.exit_code, 0)
cmd_args = [
"'-Wl,-rpath=%s/lib'" % self.test_prefix,
"'-Wl,-rpath=%s/lib64'" % self.test_prefix,
@@ -2419,11 +2610,12 @@ def test_rpath_args_script(self):
"'-L%s/foo'" % self.test_prefix,
"'-lfoo'",
]
- self.assertEqual(out.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
+ self.assertEqual(res.output.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
# relative paths passed to -L are *not* RPATH'ed in
- out, ec = run_cmd("%s gcc '' '%s' foo.c -L../lib -lfoo" % (script, rpath_inc), simple=False)
- self.assertEqual(ec, 0)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(f"{script} gcc '' '{rpath_inc}' foo.c -L../lib -lfoo")
+ self.assertEqual(res.exit_code, 0)
cmd_args = [
"'-Wl,-rpath=%s/lib'" % self.test_prefix,
"'-Wl,-rpath=%s/lib64'" % self.test_prefix,
@@ -2435,12 +2627,13 @@ def test_rpath_args_script(self):
"'-L../lib'",
"'-lfoo'",
]
- self.assertEqual(out.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
+ self.assertEqual(res.output.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
# single -L argument, with value separated by a space
- cmd = "%s gcc '' '%s' foo.c -L %s/foo -lfoo" % (script, rpath_inc, self.test_prefix)
- out, ec = run_cmd(cmd, simple=False)
- self.assertEqual(ec, 0)
+ cmd = f"{script} gcc '' '{rpath_inc}' foo.c -L {self.test_prefix}/foo -lfoo"
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd)
+ self.assertEqual(res.exit_code, 0)
cmd_args = [
"'-Wl,-rpath=%s/lib'" % self.test_prefix,
"'-Wl,-rpath=%s/lib64'" % self.test_prefix,
@@ -2453,7 +2646,7 @@ def test_rpath_args_script(self):
"'-L%s/foo'" % self.test_prefix,
"'-lfoo'",
]
- self.assertEqual(out.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
+ self.assertEqual(res.output.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
mkdir(os.path.join(self.test_prefix, 'bar'))
mkdir(os.path.join(self.test_prefix, 'lib64'))
@@ -2474,8 +2667,9 @@ def test_rpath_args_script(self):
'-L/usr/lib',
'-L%s/bar' % self.test_prefix,
])
- out, ec = run_cmd(cmd, simple=False)
- self.assertEqual(ec, 0)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd)
+ self.assertEqual(res.exit_code, 0)
cmd_args = [
"'-rpath=%s/lib'" % self.test_prefix,
"'-rpath=%s/lib64'" % self.test_prefix,
@@ -2496,7 +2690,7 @@ def test_rpath_args_script(self):
"'-L/usr/lib'",
"'-L%s/bar'" % self.test_prefix,
]
- self.assertEqual(out.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
+ self.assertEqual(res.output.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
# test specifying of custom rpath filter
cmd = ' '.join([
@@ -2511,8 +2705,9 @@ def test_rpath_args_script(self):
'-L/bar',
'-lbar',
])
- out, ec = run_cmd(cmd, simple=False)
- self.assertEqual(ec, 0)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd)
+ self.assertEqual(res.exit_code, 0)
cmd_args = [
"'-rpath=%s/lib'" % self.test_prefix,
"'-rpath=%s/lib64'" % self.test_prefix,
@@ -2528,7 +2723,7 @@ def test_rpath_args_script(self):
"'-L/bar'",
"'-lbar'",
]
- self.assertEqual(out.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
+ self.assertEqual(res.output.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
# slightly trimmed down real-life example (compilation of XZ)
for subdir in ['icc/lib/intel64', 'imkl/lib', 'imkl/mkl/lib/intel64', 'gettext/lib']:
@@ -2550,8 +2745,9 @@ def test_rpath_args_script(self):
'-Wl,-rpath',
'-Wl,/example/software/XZ/5.2.2-intel-2016b/lib',
])
- out, ec = run_cmd("%s icc '' '%s' %s" % (script, rpath_inc, args), simple=False)
- self.assertEqual(ec, 0)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(f"{script} icc '' '{rpath_inc}' {args}")
+ self.assertEqual(res.exit_code, 0)
cmd_args = [
"'-Wl,-rpath=%s/lib'" % self.test_prefix,
"'-Wl,-rpath=%s/lib64'" % self.test_prefix,
@@ -2578,7 +2774,7 @@ def test_rpath_args_script(self):
"'-Wl,-rpath'",
"'-Wl,/example/software/XZ/5.2.2-intel-2016b/lib'",
]
- self.assertEqual(out.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
+ self.assertEqual(res.output.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
# trimmed down real-life example involving quotes and escaped quotes (compilation of GCC)
args = [
@@ -2594,8 +2790,9 @@ def test_rpath_args_script(self):
'../../gcc/version.c',
]
cmd = "%s g++ '' '%s' %s" % (script, rpath_inc, ' '.join(args))
- out, ec = run_cmd(cmd, simple=False)
- self.assertEqual(ec, 0)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd)
+ self.assertEqual(res.exit_code, 0)
cmd_args = [
"'-Wl,-rpath=%s/lib'" % self.test_prefix,
@@ -2615,14 +2812,16 @@ def test_rpath_args_script(self):
"'-o' 'build/version.o'",
"'../../gcc/version.c'",
]
- self.assertEqual(out.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
+ self.assertEqual(res.output.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
# verify that no -rpath arguments are injected when command is run in 'version check' mode
for extra_args in ["-v", "-V", "--version", "-dumpversion", "-v -L/test/lib"]:
cmd = "%s g++ '' '%s' %s" % (script, rpath_inc, extra_args)
- out, ec = run_cmd(cmd, simple=False)
- self.assertEqual(ec, 0)
- self.assertEqual(out.strip(), "CMD_ARGS=(%s)" % ' '.join(["'%s'" % x for x in extra_args.split(' ')]))
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd)
+ self.assertEqual(res.exit_code, 0)
+ cmd_args = ' '.join(["'%s'" % x for x in extra_args.split(' ')])
+ self.assertEqual(res.output.strip(), f"CMD_ARGS=({cmd_args})")
# if a compiler command includes "-x c++-header" or "-x c-header" (which imply no linking is done),
# we should *not* inject -Wl,-rpath options, since those enable linking as a side-effect;
@@ -2634,10 +2833,11 @@ def test_rpath_args_script(self):
]
for extra_args in test_cases:
cmd = "%s g++ '' '%s' foo.c -O2 %s" % (script, rpath_inc, extra_args)
- out, ec = run_cmd(cmd, simple=False)
- self.assertEqual(ec, 0)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd)
+ self.assertEqual(res.exit_code, 0)
cmd_args = ["'foo.c'", "'-O2'"] + ["'%s'" % x for x in extra_args.split(' ')]
- self.assertEqual(out.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
+ self.assertEqual(res.output.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
# check whether $LIBRARY_PATH is taken into account
test_cmd_gcc = "%s gcc '' '%s' -c foo.c" % (script, rpath_inc)
@@ -2702,15 +2902,17 @@ def test_rpath_args_script(self):
os.environ['LIBRARY_PATH'] = ':'.join(library_path)
- out, ec = run_cmd(test_cmd_gcc, simple=False)
- self.assertEqual(ec, 0)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(test_cmd_gcc)
+ self.assertEqual(res.exit_code, 0)
cmd_args = pre_cmd_args_gcc + ["'-Wl,-rpath=%s'" % x for x in library_path if x] + post_cmd_args_gcc
- self.assertEqual(out.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
+ self.assertEqual(res.output.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
- out, ec = run_cmd(test_cmd_ld, simple=False)
- self.assertEqual(ec, 0)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(test_cmd_ld)
+ self.assertEqual(res.exit_code, 0)
cmd_args = pre_cmd_args_ld + ["'-rpath=%s'" % x for x in library_path if x] + post_cmd_args_ld
- self.assertEqual(out.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
+ self.assertEqual(res.output.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
# paths already listed via -L don't get included again as RPATH option
new_lib64 = os.path.join(self.test_prefix, 'new', 'lib64')
@@ -2728,18 +2930,20 @@ def test_rpath_args_script(self):
]
os.environ['LIBRARY_PATH'] = ':'.join(library_path)
- out, ec = run_cmd(test_cmd_gcc, simple=False)
- self.assertEqual(ec, 0)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(test_cmd_gcc)
+ self.assertEqual(res.exit_code, 0)
# no -L options in GCC command, so all $LIBRARY_PATH entries are retained except for last one (lib symlink)
cmd_args = pre_cmd_args_gcc + ["'-Wl,-rpath=%s'" % x for x in library_path[:-1] if x] + post_cmd_args_gcc
- self.assertEqual(out.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
+ self.assertEqual(res.output.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
- out, ec = run_cmd(test_cmd_ld, simple=False)
- self.assertEqual(ec, 0)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(test_cmd_ld)
+ self.assertEqual(res.exit_code, 0)
# only new path from $LIBRARY_PATH is included as -rpath option,
# since others are already included via corresponding -L flag
cmd_args = pre_cmd_args_ld + ["'-rpath=%s'" % new_lib64] + post_cmd_args_ld
- self.assertEqual(out.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
+ self.assertEqual(res.output.strip(), "CMD_ARGS=(%s)" % ' '.join(cmd_args))
def test_toolchain_prepare_rpath(self):
"""Test toolchain.prepare under --rpath"""
@@ -2763,7 +2967,8 @@ def test_toolchain_prepare_rpath(self):
# setting 'rpath' toolchain option to false implies no RPATH wrappers being used
tc.set_options({'rpath': False})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
res = which('g++', retain_all=True)
self.assertTrue(len(res) >= 1)
self.assertFalse(tc.is_rpath_wrapper(res[0]))
@@ -2772,7 +2977,8 @@ def test_toolchain_prepare_rpath(self):
# enable 'rpath' toolchain option again (equivalent to the default setting)
tc.set_options({'rpath': True})
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
# check that wrapper is indeed in place
res = which('g++', retain_all=True)
@@ -2854,8 +3060,9 @@ def test_toolchain_prepare_rpath(self):
"'$FOO'",
'-DX="\\"\\""',
])
- out, ec = run_cmd(cmd)
- self.assertEqual(ec, 0)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd)
+ self.assertEqual(res.exit_code, 0)
expected = ' '.join([
'-Wl,--disable-new-dtags',
'-Wl,-rpath=%s/foo' % self.test_prefix,
@@ -2865,7 +3072,7 @@ def test_toolchain_prepare_rpath(self):
'$FOO',
'-DX=""',
])
- self.assertEqual(out.strip(), expected % {'user': os.getenv('USER')})
+ self.assertEqual(res.output.strip(), expected % {'user': os.getenv('USER')})
# check whether 'stubs' library directory are correctly filtered out
paths = [
@@ -2890,8 +3097,9 @@ def test_toolchain_prepare_rpath(self):
args = ['-L%s' % x for x in paths]
cmd = "g++ ${USER}.c %s" % ' '.join(args)
- out, ec = run_cmd(cmd, simple=False)
- self.assertEqual(ec, 0)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(cmd)
+ self.assertEqual(res.exit_code, 0)
expected = ' '.join([
'-Wl,--disable-new-dtags',
@@ -2916,11 +3124,12 @@ def test_toolchain_prepare_rpath(self):
'-L%s/prefix/software/bleh/0/lib/stubs' % self.test_prefix,
'-L%s/prefix/software/foobar/4.5/stubsbutnotreally' % self.test_prefix,
])
- self.assertEqual(out.strip(), expected % {'user': os.getenv('USER')})
+ self.assertEqual(res.output.strip(), expected % {'user': os.getenv('USER')})
# calling prepare() again should *not* result in wrapping the existing RPATH wrappers
# this can happen when building extensions
- tc.prepare()
+ with self.mocked_stdout_stderr():
+ tc.prepare()
res = which('g++', retain_all=True)
self.assertTrue(len(res) >= 2)
self.assertTrue(tc.is_rpath_wrapper(res[0]))
@@ -2960,16 +3169,13 @@ def prep():
# $TMPDIR is left untouched with OpenMPI 2.x if $TMPDIR is sufficiently short
os.environ['TMPDIR'] = orig_tmpdir
tc, stdout, stderr = prep()
- self.assertEqual(stdout, '')
self.assertEqual(stderr, '')
self.assertEqual(os.environ.get('TMPDIR'), orig_tmpdir)
# warning is printed and $TMPDIR is set to shorter path if existing $TMPDIR is too long
os.environ['TMPDIR'] = long_tmpdir
tc, stdout, stderr = prep()
- self.assertEqual(stdout, '')
- # basename of tmpdir will be 6 chars in Python 2, 8 chars in Python 3
- regex = re.compile(r"^WARNING: Long \$TMPDIR .* problems with OpenMPI 2.x, using shorter path: /tmp/.{6,8}$")
+ regex = re.compile(r"^WARNING: Long \$TMPDIR .* problems with OpenMPI 2.x, using shorter path: /tmp/.{8}$")
self.assertTrue(regex.match(stderr), "Pattern '%s' found in: %s" % (regex.pattern, stderr))
# new $TMPDIR should be /tmp/xxxxxx
@@ -2999,14 +3205,12 @@ def prep():
# $TMPDIR is left untouched with OpenMPI 1.6.4
tc, stdout, stderr = prep()
- self.assertEqual(stdout, '')
self.assertEqual(stderr, '')
self.assertEqual(os.environ.get('TMPDIR'), orig_tmpdir)
# ... even with long $TMPDIR
os.environ['TMPDIR'] = long_tmpdir
tc, stdout, stderr = prep()
- self.assertEqual(stdout, '')
self.assertEqual(stderr, '')
self.assertEqual(os.environ.get('TMPDIR'), long_tmpdir)
os.environ['TMPDIR'] = orig_tmpdir
@@ -3038,10 +3242,10 @@ def test_get_flag(self):
tc = self.get_toolchain('gompi', version='2018a')
checks = {
- '-a': 'a',
- '-openmp': 'openmp',
- '-foo': ['foo'],
- '-foo -bar': ['foo', 'bar'],
+ '-a': '-a',
+ '-openmp': '-openmp',
+ '-foo': ['-foo'],
+ '-foo -bar': ['-foo', '-bar'],
}
for flagstring, flags in checks.items():
diff --git a/test/framework/toolchainvariables.py b/test/framework/toolchainvariables.py
index e61bb9031c..4ad56d904e 100644
--- a/test/framework/toolchainvariables.py
+++ b/test/framework/toolchainvariables.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -62,16 +62,16 @@ class TCV(ToolchainVariables):
tcv.join('MPICC', 'CC')
self.assertEqual(str(tcv['MPICC']), "gcc")
- tcv['F90'] = ['gfortran', 'foo', 'bar']
- self.assertEqual(tcv['F90'].__repr__(), "[['gfortran', 'foo', 'bar']]")
+ tcv['F90'] = ['gfortran', '-foo', '-bar']
+ self.assertEqual(tcv['F90'].__repr__(), "[['gfortran', '-foo', '-bar']]")
self.assertEqual(str(tcv['F90']), "gfortran -foo -bar")
- tcv.nappend('FLAGS', ['one', 'two'])
- x = tcv.nappend('FLAGS', ['three', 'four'])
+ tcv.nappend('FLAGS', ['-one', '-two'])
+ x = tcv.nappend('FLAGS', ['-three', '-four'])
x.POSITION = -5 # sanitize will reorder, default POSITION is 0
- self.assertEqual(tcv['FLAGS'].__repr__(), "[['one', 'two'], ['three', 'four']]")
+ self.assertEqual(tcv['FLAGS'].__repr__(), "[['-one', '-two'], ['-three', '-four']]")
tcv['FLAGS'].sanitize() # sort on position, called by __str__ also
- self.assertEqual(tcv['FLAGS'].__repr__(), "[['three', 'four'], ['one', 'two']]")
+ self.assertEqual(tcv['FLAGS'].__repr__(), "[['-three', '-four'], ['-one', '-two']]")
self.assertEqual(str(tcv['FLAGS']), "-three -four -one -two")
# LIBBLAS is a LibraryList
@@ -159,7 +159,7 @@ class TCV(ToolchainVariables):
tcv.nappend('MPICH_CC', 'icc', var_class=CommandFlagList)
self.assertEqual(str(tcv['MPICH_CC']), "icc")
- tcv.nappend('MPICH_CC', 'test')
+ tcv.nappend('MPICH_CC', '-test')
self.assertEqual(str(tcv['MPICH_CC']), "icc -test")
diff --git a/test/framework/toy_build.py b/test/framework/toy_build.py
index 1bd114d9d7..d46e755ebc 100644
--- a/test/framework/toy_build.py
+++ b/test/framework/toy_build.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
##
-# Copyright 2013-2024 Ghent University
+# Copyright 2013-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -40,8 +40,10 @@
import sys
import tempfile
import textwrap
+import filecmp
from easybuild.tools import LooseVersion
-from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered
+from importlib import reload
+from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered, cleanup
from test.framework.package import mock_fpm
from unittest import TextTestRunner
@@ -49,15 +51,15 @@
import easybuild.tools.module_naming_scheme # required to dynamically load test module naming scheme(s)
from easybuild.framework.easyconfig.easyconfig import EasyConfig
from easybuild.framework.easyconfig.parser import EasyConfigParser
+from easybuild.main import main_with_hooks
from easybuild.tools.build_log import EasyBuildError
from easybuild.tools.config import get_module_syntax, get_repositorypath
from easybuild.tools.environment import modify_env
from easybuild.tools.filetools import adjust_permissions, change_dir, copy_file, mkdir, move_file
from easybuild.tools.filetools import read_file, remove_dir, remove_file, which, write_file
from easybuild.tools.module_generator import ModuleGeneratorTcl
-from easybuild.tools.modules import Lmod
-from easybuild.tools.py2vs3 import reload, string_type
-from easybuild.tools.run import run_cmd
+from easybuild.tools.modules import EnvironmentModules, Lmod
+from easybuild.tools.run import run_shell_cmd
from easybuild.tools.utilities import nub
from easybuild.tools.systemtools import get_shared_lib_ext
from easybuild.tools.version import VERSION as EASYBUILD_VERSION
@@ -156,9 +158,9 @@ def check_toy(self, installpath, outtxt, name='toy', version='0.0', versionprefi
devel_module_path = os.path.join(software_path, 'easybuild', '%s-%s-easybuild-devel' % (name, full_version))
self.assertExists(devel_module_path)
- def test_toy_build(self, extra_args=None, ec_file=None, tmpdir=None, verify=True, fails=False, verbose=True,
- raise_error=False, test_report=None, name='toy', versionsuffix='', testing=True,
- raise_systemexit=False, force=True, test_report_regexs=None, debug=True):
+ def _test_toy_build(self, extra_args=None, ec_file=None, tmpdir=None, verify=True, fails=False, verbose=True,
+ raise_error=False, test_report=None, name='toy', versionsuffix='', testing=True,
+ raise_systemexit=False, force=True, test_report_regexs=None, debug=True):
"""Perform a toy build."""
if extra_args is None:
extra_args = []
@@ -169,9 +171,6 @@ def test_toy_build(self, extra_args=None, ec_file=None, tmpdir=None, verify=True
full_ver = '0.0%s' % versionsuffix
args = [
ec_file,
- '--sourcepath=%s' % self.test_sourcepath,
- '--buildpath=%s' % self.test_buildpath,
- '--installpath=%s' % self.test_installpath,
'--unittest-file=%s' % self.logfile,
'--robot=%s' % os.pathsep.join([self.test_buildpath, os.path.dirname(__file__)]),
]
@@ -235,7 +234,7 @@ def run_test_toy_build_with_output(self, *args, **kwargs):
self.mock_stderr(True)
self.mock_stdout(True)
- self.test_toy_build(*args, **kwargs)
+ self._test_toy_build(*args, **kwargs)
stderr = self.get_stderr()
stdout = self.get_stdout()
self.mock_stderr(False)
@@ -243,17 +242,29 @@ def run_test_toy_build_with_output(self, *args, **kwargs):
return stdout, stderr
+ def run_eb_main_capture_output(self, *args, **kwargs):
+ """Run eb_main with specified arguments, capture stdout, and return output from eb_main"""
+ self.mock_stdout(True)
+ outtxt = self.eb_main(*args, **kwargs)
+ self.mock_stdout(False)
+ return outtxt
+
+ def test_toy_build(self):
+ """Base level test build"""
+ with self.mocked_stdout_stderr():
+ self._test_toy_build()
+
def test_toy_broken(self):
"""Test deliberately broken toy build."""
tmpdir = tempfile.mkdtemp()
broken_toy_ec = os.path.join(tmpdir, "toy-broken.eb")
toy_ec_file = os.path.join(os.path.dirname(__file__), 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb')
broken_toy_ec_txt = read_file(toy_ec_file)
- broken_toy_ec_txt += "checksums = ['clearywrongMD5checksumoflength32']"
+ broken_toy_ec_txt += "checksums = ['clearywrongSHA256checksumoflength64-0123456789012345678901234567']"
write_file(broken_toy_ec, broken_toy_ec_txt)
error_regex = "Checksum verification .* failed"
- self.assertErrorRegex(EasyBuildError, error_regex, self.test_toy_build, ec_file=broken_toy_ec, tmpdir=tmpdir,
- verify=False, fails=True, verbose=False, raise_error=True)
+ self.assertErrorRegex(EasyBuildError, error_regex, self.run_test_toy_build_with_output, ec_file=broken_toy_ec,
+ tmpdir=tmpdir, verify=False, fails=True, verbose=False, raise_error=True)
# make sure log file is retained, also for failed build
log_path_pattern = os.path.join(tmpdir, 'eb-*', 'easybuild-toy-0.0*.log')
@@ -265,12 +276,88 @@ def test_toy_broken(self):
# test dumping full test report (doesn't raise an exception)
test_report_fp = os.path.join(self.test_buildpath, 'full_test_report.md')
- self.test_toy_build(ec_file=broken_toy_ec, tmpdir=tmpdir, verify=False, fails=True, verbose=False,
- raise_error=True, test_report=test_report_fp)
+ self.run_test_toy_build_with_output(ec_file=broken_toy_ec, tmpdir=tmpdir, verify=False, fails=True,
+ verbose=False, raise_error=True, test_report=test_report_fp)
# cleanup
shutil.rmtree(tmpdir)
+ def test_toy_broken_copy_log_build_dir(self):
+ """
+ Test whether log files and the build directory are copied to a permanent location
+ after a failed installation.
+ """
+ toy_ec = os.path.join(os.path.dirname(__file__), 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb')
+ toy_ec_txt = read_file(toy_ec)
+
+ test_ec_txt = re.sub(
+ r'toy-0\.0_fix-silly-typo-in-printf-statement\.patch',
+ r'toy-0.0_add-bug.patch',
+ toy_ec_txt
+ )
+ test_ec = os.path.join(self.test_prefix, 'toy-0.0-buggy.eb')
+ write_file(test_ec, test_ec_txt)
+
+ # set up subdirectories where stuff should go
+ tmpdir = os.path.join(self.test_prefix, 'tmp')
+ tmp_log_dir = os.path.join(self.test_prefix, 'tmp-logs')
+ failed_install_build_dirs_path = os.path.join(self.test_prefix, 'failed-install-build-dirs')
+ failed_install_logs_path = os.path.join(self.test_prefix, 'failed-install-logs')
+
+ extra_args = [
+ f'--failed-install-build-dirs-path={failed_install_build_dirs_path}',
+ f'--failed-install-logs-path={failed_install_logs_path}',
+ f'--tmp-logdir={tmp_log_dir}',
+ ]
+ with self.mocked_stdout_stderr():
+ outtxt = self._test_toy_build(ec_file=test_ec, extra_args=extra_args, tmpdir=tmpdir,
+ verify=False, fails=True, verbose=False)
+
+ # find path to temporary log file
+ log_files = glob.glob(os.path.join(tmp_log_dir, '*.log'))
+ self.assertTrue(len(log_files) == 1, f"Expected exactly one log file, found {len(log_files)}: {log_files}")
+ log_file = log_files[0]
+
+ # check that log files were copied
+ saved_log_files = glob.glob(os.path.join(failed_install_logs_path, log_file))
+ self.assertTrue(len(saved_log_files) == 1, f"Unique copy of log file '{log_file}' made")
+ saved_log_file = saved_log_files[0]
+ self.assertTrue(filecmp.cmp(log_file, saved_log_file, shallow=False),
+ f"Log file '{log_file}' copied successfully")
+
+ # check that build directories were copied
+ build_dir = self.test_buildpath
+ topdir = failed_install_build_dirs_path
+
+ app_build_dir = os.path.join(build_dir, 'toy', '0.0', 'system-system')
+ # pattern: --
+ subdir_pattern = '????????-??????-?????'
+
+ # find path to toy.c
+ toy_c_files = glob.glob(os.path.join(app_build_dir, '**', 'toy.c'))
+ self.assertTrue(len(toy_c_files) == 1, f"Exactly one toy.c file found: {toy_c_files}")
+ toy_c_file = toy_c_files[0]
+
+ path = os.path.join(topdir, 'toy-0.0', subdir_pattern, 'toy-0.0', os.path.basename(toy_c_file))
+ res = glob.glob(path)
+ self.assertTrue(len(res) == 1, f"Exactly one hit found for {path}: {res}")
+ copied_toy_c_file = res[0]
+ self.assertTrue(filecmp.cmp(toy_c_file, copied_toy_c_file, shallow=False),
+ f"Copy of {toy_c_file} should be found under {topdir}")
+
+ # check whether compiler error messages are present in build log
+
+ # compiler error because of missing semicolon at end of line, could be:
+ # "error: expected ; before ..."
+ # "error: expected ';' after expression"
+ output_regexs = [r"^\s*toy\.c:5:44: error: expected (;|.;.)"]
+
+ log_txt = read_file(log_file)
+ for regex_pattern in output_regexs:
+ regex = re.compile(regex_pattern, re.M)
+ self.assertRegex(outtxt, regex)
+ self.assertRegex(log_txt, regex)
+
def test_toy_tweaked(self):
"""Test toy build with tweaked easyconfig, for testing extra easyconfig parameters."""
test_ecs_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'easyconfigs')
@@ -284,8 +371,10 @@ def test_toy_tweaked(self):
# tweak easyconfig by appending to it
ec_extra = '\n'.join([
"versionsuffix = '-tweaked'",
- "modextrapaths = {'SOMEPATH': ['foo/bar', 'baz', '']}",
- "modextrapaths_append = {'SOMEPATH_APPEND': ['qux/fred', 'thud', '']}",
+ "modextrapaths = {",
+ " 'SOMEPATH': ['foo/bar', 'baz', ''],",
+ " 'SOMEPATH_APPEND': {'paths': ['qux/fred', 'thud', ''], 'prepend': False},",
+ "}",
"modextravars = {'FOO': 'bar'}",
"modloadmsg = '%s'" % modloadmsg,
"modtclfooter = 'puts stderr \"oh hai!\"'", # ignored when module syntax is Lua
@@ -297,18 +386,29 @@ def test_toy_tweaked(self):
"docurls = ['https://easybuilders.github.io/easybuild/toy/docs.html']",
"upstream_contacts = 'support@toy.org'",
"site_contacts = ['Jim Admin', 'Jane Admin']",
+ "postinstallcmds = [",
+ " 'mkdir \"%(installdir)s/foo\"',"
+ " 'touch \"%(installdir)s/foo/bar\"',"
+ " 'touch \"%(installdir)s/baz\"',"
+ " 'mkdir \"%(installdir)s/qux\"',"
+ " 'touch \"%(installdir)s/qux/fred\"',"
+ " 'touch \"%(installdir)s/thud\"',"
+ "]",
])
write_file(ec_file, ec_extra, append=True)
+ # populate test install dir
+ toy_installdir = os.path.join(self.test_installpath, 'software', 'toy', '0.0-tweaked')
+ mkdir(os.path.join(toy_installdir, 'foo'), parents=True)
+ write_file(os.path.join(toy_installdir, 'foo', 'bar'), "Test install file")
+ mkdir(os.path.join(toy_installdir, 'baz'), parents=True)
+
args = [
ec_file,
- '--sourcepath=%s' % self.test_sourcepath,
- '--buildpath=%s' % self.test_buildpath,
- '--installpath=%s' % self.test_installpath,
'--debug',
'--force',
]
- outtxt = self.eb_main(args, do_build=True, verbose=True, raise_error=True)
+ outtxt = self.run_eb_main_capture_output(args, do_build=True, verbose=True, raise_error=True)
self.check_toy(self.test_installpath, outtxt, versionsuffix='-tweaked')
toy_module = os.path.join(self.test_installpath, 'modules', 'all', 'toy', '0.0-tweaked')
if get_module_syntax() == 'Lua':
@@ -369,8 +469,8 @@ def test_toy_buggy_easyblock(self):
'verify': False,
'verbose': False,
}
- err_regex = r"Traceback[\S\s]*toy_buggy.py.*build_step[\S\s]*name 'run_cmd' is not defined"
- self.assertErrorRegex(EasyBuildError, err_regex, self.test_toy_build, **kwargs)
+ err_regex = r"name 'run_shell_cmd' is not defined"
+ self.assertErrorRegex(NameError, err_regex, self.run_test_toy_build_with_output, **kwargs)
def test_toy_build_formatv2(self):
"""Perform a toy build (format v2)."""
@@ -380,9 +480,6 @@ def test_toy_build_formatv2(self):
args = [
os.path.join(os.path.dirname(__file__), 'easyconfigs', 'v2.0', 'toy.eb'),
- '--sourcepath=%s' % self.test_sourcepath,
- '--buildpath=%s' % self.test_buildpath,
- '--installpath=%s' % self.test_installpath,
'--debug',
'--unittest-file=%s' % self.logfile,
'--force',
@@ -391,7 +488,7 @@ def test_toy_build_formatv2(self):
'--toolchain=system,system',
'--experimental',
]
- outtxt = self.eb_main(args, logfile=self.dummylogfn, do_build=True, verbose=True)
+ outtxt = self.run_eb_main_capture_output(args, logfile=self.dummylogfn, do_build=True, verbose=True)
self.check_toy(self.test_installpath, outtxt)
@@ -416,14 +513,12 @@ def test_toy_build_with_blocks(self):
args = [
'toy-0.0-multiple.eb',
- '--sourcepath=%s' % self.test_sourcepath,
- '--buildpath=%s' % self.test_buildpath,
- '--installpath=%s' % self.test_installpath,
'--debug',
'--unittest-file=%s' % self.logfile,
'--force',
]
- outtxt = self.eb_main(args, logfile=self.dummylogfn, do_build=True, verbose=True)
+ with self.mocked_stdout_stderr():
+ outtxt = self.eb_main(args, logfile=self.dummylogfn, do_build=True, verbose=True)
for toy_prefix, toy_version, toy_suffix in [
('', '0.0', '-somesuffix'),
@@ -451,9 +546,6 @@ def test_toy_build_formatv2_sections(self):
for version, specs in versions.items():
args = [
os.path.join(os.path.dirname(__file__), 'easyconfigs', 'v2.0', 'toy-with-sections.eb'),
- '--sourcepath=%s' % self.test_sourcepath,
- '--buildpath=%s' % self.test_buildpath,
- '--installpath=%s' % self.test_installpath,
'--debug',
'--unittest-file=%s' % self.logfile,
'--force',
@@ -462,7 +554,8 @@ def test_toy_build_formatv2_sections(self):
'--toolchain=system,system',
'--experimental',
]
- outtxt = self.eb_main(args, logfile=self.dummylogfn, do_build=True, verbose=True, raise_error=True)
+ outtxt = self.run_eb_main_capture_output(args, logfile=self.dummylogfn, do_build=True, verbose=True,
+ raise_error=True)
specs['version'] = version
@@ -490,7 +583,7 @@ def test_toy_download_sources(self):
'--unittest-file=%s' % self.logfile,
'--force',
]
- outtxt = self.eb_main(args, logfile=self.dummylogfn, do_build=True, verbose=True)
+ outtxt = self.run_eb_main_capture_output(args, logfile=self.dummylogfn, do_build=True, verbose=True)
self.check_toy(tmpdir, outtxt)
@@ -510,9 +603,6 @@ def test_toy_permissions(self):
write_file(test_ec, test_ec_txt)
args = [
- '--sourcepath=%s' % self.test_sourcepath,
- '--buildpath=%s' % self.test_buildpath,
- '--installpath=%s' % self.test_installpath,
'--debug',
'--unittest-file=%s' % self.logfile,
'--force',
@@ -523,7 +613,8 @@ def test_toy_permissions(self):
# test specifying a non-existing group
allargs = [test_ec] + args + ['--group=thisgroupdoesnotexist']
- outtxt, err = self.eb_main(allargs, logfile=self.dummylogfn, do_build=True, return_error=True)
+ outtxt, err = self.run_eb_main_capture_output(allargs, logfile=self.dummylogfn, do_build=True,
+ return_error=True)
err_regex = re.compile("Failed to get group ID .* group does not exist")
self.assertTrue(err_regex.search(outtxt), "Pattern '%s' found in '%s'" % (err_regex.pattern, outtxt))
@@ -556,7 +647,7 @@ def test_toy_permissions(self):
allargs.append("--umask=%s" % umask)
if cfg_group is not None:
allargs.append("--group=%s" % cfg_group)
- outtxt = self.eb_main(allargs, logfile=self.dummylogfn, do_build=True, verbose=True)
+ outtxt = self.run_eb_main_capture_output(allargs, logfile=self.dummylogfn, do_build=True, verbose=True)
# verify that installation was correct
self.check_toy(self.test_installpath, outtxt)
@@ -620,7 +711,7 @@ def test_toy_permissions_installdir(self):
write_file(test_ec, test_ec_txt)
# first check default behaviour
- self.test_toy_build(ec_file=test_ec)
+ self.run_test_toy_build_with_output(ec_file=test_ec)
toy_install_dir = os.path.join(self.test_installpath, 'software', 'toy', '0.0')
toy_bin = os.path.join(toy_install_dir, 'bin', 'toy')
@@ -649,9 +740,8 @@ def test_toy_permissions_installdir(self):
['--rebuild', '--fetch'],
)
for extra_args in test_cases:
- self.mock_stdout(True)
- self.test_toy_build(ec_file=test_ec, extra_args=['--read-only-installdir'] + extra_args, force=False)
- self.mock_stdout(False)
+ self.run_test_toy_build_with_output(ec_file=test_ec, extra_args=['--read-only-installdir'] + extra_args,
+ force=False)
installdir_perms = os.stat(os.path.dirname(toy_install_dir)).st_mode & 0o777
self.assertEqual(installdir_perms, 0o755, "%s has default permissions" % os.path.dirname(toy_install_dir))
@@ -672,7 +762,7 @@ def test_toy_permissions_installdir(self):
shutil.rmtree(self.test_installpath)
# also check --group-writable-installdir
- self.test_toy_build(ec_file=test_ec, extra_args=['--group-writable-installdir'])
+ self.run_test_toy_build_with_output(ec_file=test_ec, extra_args=['--group-writable-installdir'])
installdir_perms = os.stat(toy_install_dir).st_mode & 0o777
self.assertEqual(installdir_perms, 0o775, "%s has group write permissions" % self.test_installpath)
@@ -683,7 +773,8 @@ def test_toy_permissions_installdir(self):
# this happens when for example using ModuleRC easyblock (because no devel module is created)
test_ec_txt += "\nmake_module = False"
write_file(test_ec, test_ec_txt)
- self.test_toy_build(ec_file=test_ec, extra_args=['--read-only-installdir'], verify=False, raise_error=True)
+ self.run_test_toy_build_with_output(ec_file=test_ec, extra_args=['--read-only-installdir'], verify=False,
+ raise_error=True)
# restore original umask
os.umask(orig_umask)
@@ -699,7 +790,7 @@ def test_toy_gid_sticky_bits(self):
(('modules', 'all', 'toy'), False),
]
# no gid/sticky bits by default
- self.test_toy_build()
+ self.run_test_toy_build_with_output()
for subdir, _ in subdirs:
fullpath = os.path.join(self.test_installpath, *subdir)
perms = os.stat(fullpath).st_mode
@@ -707,7 +798,7 @@ def test_toy_gid_sticky_bits(self):
self.assertFalse(perms & stat.S_ISVTX, "no sticky bit on %s" % fullpath)
# git/sticky bits are set, but only on (re)created directories
- self.test_toy_build(extra_args=['--set-gid-bit', '--sticky-bit'])
+ self.run_test_toy_build_with_output(extra_args=['--set-gid-bit', '--sticky-bit'])
for subdir, bits_set in subdirs:
fullpath = os.path.join(self.test_installpath, *subdir)
perms = os.stat(fullpath).st_mode
@@ -720,7 +811,7 @@ def test_toy_gid_sticky_bits(self):
# start with a clean slate, now gid/sticky bits should be set on everything
shutil.rmtree(self.test_installpath)
- self.test_toy_build(extra_args=['--set-gid-bit', '--sticky-bit'])
+ self.run_test_toy_build_with_output(extra_args=['--set-gid-bit', '--sticky-bit'])
for subdir, _ in subdirs:
fullpath = os.path.join(self.test_installpath, *subdir)
perms = os.stat(fullpath).st_mode
@@ -733,9 +824,10 @@ def test_toy_group_check(self):
os.close(fd)
# figure out a group that we're a member of to use in the test
- out, ec = run_cmd('groups', simple=False)
- self.assertEqual(ec, 0, "Failed to select group to use in test")
- group_name = out.split(' ')[0].strip()
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd('groups')
+ self.assertEqual(res.exit_code, 0, "Failed to select group to use in test")
+ group_name = res.output.split(' ')[0].strip()
toy_ec = os.path.join(os.path.dirname(__file__), 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb')
test_ec = os.path.join(self.test_prefix, 'test.eb')
@@ -747,7 +839,7 @@ def test_toy_group_check(self):
for group in [group_name, (group_name, "Hey, you're not in the '%s' group!" % group_name)]:
- if isinstance(group, string_type):
+ if isinstance(group, str):
write_file(test_ec, read_file(toy_ec) + "\ngroup = '%s'\n" % group)
else:
write_file(test_ec, read_file(toy_ec) + "\ngroup = %s\n" % str(group))
@@ -757,14 +849,9 @@ def test_toy_group_check(self):
self.mock_stdout(False)
if get_module_syntax() == 'Tcl':
- pattern = "Can't generate robust check in TCL modules for users belonging to group %s." % group_name
- regex = re.compile(pattern, re.M)
- self.assertTrue(regex.search(outtxt), "Pattern '%s' found in: %s" % (regex.pattern, outtxt))
-
- elif get_module_syntax() == 'Lua':
- lmod_version = os.getenv('LMOD_VERSION', 'NOT_FOUND')
- if LooseVersion(lmod_version) >= LooseVersion('6.0.8'):
- toy_mod = os.path.join(self.test_installpath, 'modules', 'all', 'toy', '0.0.lua')
+ module_version = LooseVersion(self.modtool.version)
+ if isinstance(self.modtool, EnvironmentModules) and module_version >= LooseVersion('4.6.0'):
+ toy_mod = os.path.join(self.test_installpath, 'modules', 'all', 'toy', '0.0')
toy_mod_txt = read_file(toy_mod)
if isinstance(group, tuple):
@@ -775,17 +862,35 @@ def test_toy_group_check(self):
error_msg_pattern = "You are not part of '%s' group of users" % group_name
pattern = '\n'.join([
- r'^if not \( userInGroup\("%s"\) \) then' % group_name,
- r' LmodError\("%s[^"]*"\)' % error_msg_pattern,
- r'end$',
+ r'^if \{ \!\[ module-info usergroups %s \] \} \{' % group_name,
+ r' error "%s[^"]*"' % error_msg_pattern,
+ r'\}$',
])
regex = re.compile(pattern, re.M)
self.assertTrue(regex.search(outtxt), "Pattern '%s' found in: %s" % (regex.pattern, toy_mod_txt))
else:
- pattern = r"Can't generate robust check in Lua modules for users belonging to group %s. "
- pattern += r"Lmod version not recent enough \(%s\), should be >= 6.0.8" % lmod_version
- regex = re.compile(pattern % group_name, re.M)
+ pattern = "Can't generate robust check in Tcl modules for users belonging to group %s." % group_name
+ regex = re.compile(pattern, re.M)
self.assertTrue(regex.search(outtxt), "Pattern '%s' found in: %s" % (regex.pattern, outtxt))
+
+ elif get_module_syntax() == 'Lua':
+ toy_mod = os.path.join(self.test_installpath, 'modules', 'all', 'toy', '0.0.lua')
+ toy_mod_txt = read_file(toy_mod)
+
+ if isinstance(group, tuple):
+ group_name = group[0]
+ error_msg_pattern = "Hey, you're not in the '%s' group!" % group_name
+ else:
+ group_name = group
+ error_msg_pattern = "You are not part of '%s' group of users" % group_name
+
+ pattern = '\n'.join([
+ r'^if not \( userInGroup\("%s"\) \) then' % group_name,
+ r' LmodError\("%s[^"]*"\)' % error_msg_pattern,
+ r'end$',
+ ])
+ regex = re.compile(pattern, re.M)
+ self.assertTrue(regex.search(outtxt), "Pattern '%s' found in: %s" % (regex.pattern, toy_mod_txt))
else:
self.fail("Unknown module syntax: %s" % get_module_syntax())
@@ -801,7 +906,8 @@ def test_allow_system_deps(self):
shutil.copy2(os.path.join(topdir, 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb'), tmpdir)
ec_file = os.path.join(tmpdir, 'toy-0.0.eb')
write_file(ec_file, "\nallow_system_deps = [('Python', SYS_PYTHON_VERSION)]\n", append=True)
- self.test_toy_build(ec_file=ec_file)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=ec_file)
shutil.rmtree(tmpdir)
def test_toy_hierarchical(self):
@@ -813,9 +919,6 @@ def test_toy_hierarchical(self):
args = [
os.path.join(test_easyconfigs, 't', 'toy', 'toy-0.0.eb'),
- '--sourcepath=%s' % self.test_sourcepath,
- '--buildpath=%s' % self.test_buildpath,
- '--installpath=%s' % self.test_installpath,
'--debug',
'--unittest-file=%s' % self.logfile,
'--force',
@@ -830,7 +933,8 @@ def test_toy_hierarchical(self):
# resolution) we must add an additional build option
'--disable-map-toolchains',
]
- self.eb_main(args + extra_args, logfile=self.dummylogfn, do_build=True, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args + extra_args, logfile=self.dummylogfn, do_build=True, verbose=True, raise_error=True)
# make sure module file is installed in correct path
toy_module_path = os.path.join(mod_prefix, 'MPI', 'GCC', '6.4.0-2.28', 'OpenMPI', '2.1.2', 'toy', '0.0')
@@ -841,9 +945,9 @@ def test_toy_hierarchical(self):
# check that toolchain load is expanded to loads for toolchain dependencies,
# except for the ones that extend $MODULEPATH to make the toy module available
if get_module_syntax() == 'Tcl':
- load_regex_template = "load %s"
+ load_regex_template = "(load|depends-on) %s"
elif get_module_syntax() == 'Lua':
- load_regex_template = r'load\("%s/.*"\)'
+ load_regex_template = r'(load|depends_on)\("%s/.*"\)'
else:
self.fail("Unknown module syntax: %s" % get_module_syntax())
@@ -861,7 +965,8 @@ def test_toy_hierarchical(self):
extra_args = [
'--try-toolchain=GCC,6.4.0-2.28',
]
- self.eb_main(args + extra_args, logfile=self.dummylogfn, do_build=True, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args + extra_args, logfile=self.dummylogfn, do_build=True, verbose=True, raise_error=True)
# make sure module file is installed in correct path
toy_module_path = os.path.join(mod_prefix, 'Compiler', 'GCC', '6.4.0-2.28', 'toy', '0.0')
@@ -878,7 +983,8 @@ def test_toy_hierarchical(self):
'--try-toolchain=GCC,6.4.0-2.28',
'--try-amend=moduleclass=mpi',
]
- self.eb_main(args + extra_args, logfile=self.dummylogfn, do_build=True, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args + extra_args, logfile=self.dummylogfn, do_build=True, verbose=True, raise_error=True)
# make sure module file is installed in correct path
toy_module_path = os.path.join(mod_prefix, 'Compiler', 'GCC', '6.4.0-2.28', 'toy', '0.0')
@@ -901,7 +1007,8 @@ def test_toy_hierarchical(self):
# ... unless they shouldn't be
extra_args.append('--try-amend=include_modpath_extensions=') # pass empty string as equivalent to False
- self.eb_main(args + extra_args, logfile=self.dummylogfn, do_build=True, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args + extra_args, logfile=self.dummylogfn, do_build=True, verbose=True, raise_error=True)
modtxt = read_file(toy_module_path)
modpath_extension = os.path.join(mod_prefix, 'MPI', 'GCC', '6.4.0-2.28', 'toy', '0.0')
if get_module_syntax() == 'Tcl':
@@ -918,7 +1025,8 @@ def test_toy_hierarchical(self):
extra_args = [
'--try-toolchain=system,system',
]
- self.eb_main(args + extra_args, logfile=self.dummylogfn, do_build=True, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args + extra_args, logfile=self.dummylogfn, do_build=True, verbose=True, raise_error=True)
# make sure module file is installed in correct path
toy_module_path = os.path.join(mod_prefix, 'Core', 'toy', '0.0')
@@ -936,7 +1044,8 @@ def test_toy_hierarchical(self):
'--try-toolchain=system,system',
'--try-amend=moduleclass=compiler',
]
- self.eb_main(args + extra_args, logfile=self.dummylogfn, do_build=True, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args + extra_args, logfile=self.dummylogfn, do_build=True, verbose=True, raise_error=True)
# make sure module file is installed in correct path
toy_module_path = os.path.join(mod_prefix, 'Core', 'toy', '0.0')
@@ -969,7 +1078,8 @@ def test_toy_hierarchical(self):
args[0] = os.path.join(test_easyconfigs, 'g', 'gompi', 'gompi-2018a.eb')
self.modtool.purge()
- self.eb_main(args, logfile=self.dummylogfn, do_build=True, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, logfile=self.dummylogfn, do_build=True, verbose=True, raise_error=True)
self.assertExists(gompi_module_path)
def test_toy_hierarchical_subdir_user_modules(self):
@@ -1009,15 +1119,14 @@ def test_toy_hierarchical_subdir_user_modules(self):
args = [
os.path.join(test_easyconfigs, 't', 'toy', 'toy-0.0-gompi-2018a.eb'),
- '--sourcepath=%s' % self.test_sourcepath,
- '--buildpath=%s' % self.test_buildpath,
'--installpath=%s' % home,
'--unittest-file=%s' % self.logfile,
'--force',
'--module-naming-scheme=HierarchicalMNS',
'--try-toolchain=foss,2018a',
]
- self.eb_main(args, logfile=self.dummylogfn, do_build=True, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, logfile=self.dummylogfn, do_build=True, verbose=True, raise_error=True)
mod_ext = ''
if get_module_syntax() == 'Lua':
@@ -1074,7 +1183,8 @@ def test_toy_hierarchical_subdir_user_modules(self):
])
write_file(openmpi_mod + '.lua', openmpi_mod_txt)
- self.eb_main(args, logfile=self.dummylogfn, do_build=True, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, logfile=self.dummylogfn, do_build=True, verbose=True, raise_error=True)
toy_modtxt = read_file(toy_mod)
# No math libs in original toolchain, --try-toolchain is too clever to upgrade it beyond necessary
@@ -1093,7 +1203,8 @@ def test_toy_advanced(self):
test_dir = os.path.abspath(os.path.dirname(__file__))
os.environ['MODULEPATH'] = os.path.join(test_dir, 'modules')
test_ec = os.path.join(test_dir, 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0-gompi-2018a-test.eb')
- self.test_toy_build(ec_file=test_ec, versionsuffix='-gompi-2018a-test', extra_args=['--debug'])
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec, versionsuffix='-gompi-2018a-test', extra_args=['--debug'])
toy_module = os.path.join(self.test_installpath, 'modules', 'all', 'toy', '0.0-gompi-2018a-test')
if get_module_syntax() == 'Lua':
@@ -1109,6 +1220,13 @@ def test_toy_advanced(self):
for pattern in patterns:
self.assertTrue(re.search(pattern, toy_mod_txt, re.M), "Pattern '%s' found in: %s" % (pattern, toy_mod_txt))
+ toy_installdir = os.path.join(self.test_installpath, 'software', 'toy', '0.0-gompi-2018a-test')
+ toy_libs_path = os.path.join(toy_installdir, 'toy_libs_path.txt')
+ self.assertTrue(os.path.exists(toy_libs_path))
+ txt = read_file(toy_libs_path)
+ regex = re.compile('^TOY_EXAMPLES=.*/examples$')
+ self.assertTrue(regex.match(txt), f"Pattern '{regex.pattern}' should match in: {txt}")
+
def test_toy_advanced_filter_deps(self):
"""Test toy build with extensions, and filtered build dependency."""
# test case for bug https://github.com/easybuilders/easybuild-framework/pull/2515
@@ -1124,7 +1242,8 @@ def test_toy_advanced_filter_deps(self):
test_ec = os.path.join(self.test_prefix, 'test.eb')
write_file(test_ec, toy_ec_txt)
- self.test_toy_build(ec_file=test_ec, versionsuffix='-gompi-2018a-test', extra_args=["--filter-deps=FFTW"])
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec, versionsuffix='-gompi-2018a-test', extra_args=["--filter-deps=FFTW"])
toy_module = os.path.join(self.test_installpath, 'modules', 'all', 'toy', '0.0-gompi-2018a-test')
if get_module_syntax() == 'Lua':
@@ -1135,7 +1254,8 @@ def test_toy_hidden_cmdline(self):
"""Test installing a hidden module using the '--hidden' command line option."""
test_ecs = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'easyconfigs', 'test_ecs')
ec_file = os.path.join(test_ecs, 't', 'toy', 'toy-0.0.eb')
- self.test_toy_build(ec_file=ec_file, extra_args=['--hidden'], verify=False)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=ec_file, extra_args=['--hidden'], verify=False)
# module file is hidden
toy_module = os.path.join(self.test_installpath, 'modules', 'all', 'toy', '.0.0')
if get_module_syntax() == 'Lua':
@@ -1153,7 +1273,8 @@ def test_toy_hidden_easyconfig(self):
shutil.copy2(ec_file, self.test_prefix)
ec_file = os.path.join(self.test_prefix, 'toy-0.0.eb')
write_file(ec_file, "\nhidden = True\n", append=True)
- self.test_toy_build(ec_file=ec_file, verify=False)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=ec_file, verify=False)
# module file is hidden
toy_module = os.path.join(self.test_installpath, 'modules', 'all', 'toy', '.0.0')
if get_module_syntax() == 'Lua':
@@ -1172,15 +1293,13 @@ def test_module_filepath_tweaking(self):
eb_file = os.path.join(topdir, 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb')
args = [
eb_file,
- '--sourcepath=%s' % self.test_sourcepath,
- '--buildpath=%s' % self.test_buildpath,
- '--installpath=%s' % self.test_installpath,
'--force',
'--debug',
'--suffix-modules-path=foobarbaz',
'--module-naming-scheme=TestModuleNamingScheme',
]
- self.eb_main(args, do_build=True, verbose=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True, verbose=True)
mod_file_prefix = os.path.join(self.test_installpath, 'modules')
mod_file_suffix = ''
if get_module_syntax() == 'Lua':
@@ -1199,7 +1318,8 @@ def test_toy_archived_easyconfig(self):
'--repository=FileRepository',
'--repositorypath=%s' % repositorypath,
]
- self.test_toy_build(raise_error=True, extra_args=extra_args)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(raise_error=True, extra_args=extra_args)
archived_ec = os.path.join(repositorypath, 'toy', 'toy-0.0.eb')
self.assertExists(archived_ec)
@@ -1214,7 +1334,8 @@ def test_toy_patches(self):
'--repository=FileRepository',
'--repositorypath=%s' % repositorypath,
]
- self.test_toy_build(raise_error=True, extra_args=extra_args)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(raise_error=True, extra_args=extra_args)
installdir = os.path.join(self.test_installpath, 'software', 'toy', '0.0')
@@ -1250,14 +1371,16 @@ def test_toy_extension_patches_postinstallcmds(self):
])
write_file(test_ec, test_ec_txt)
- self.test_toy_build(ec_file=test_ec)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec)
installdir = os.path.join(self.test_installpath, 'software', 'toy', '0.0')
# make sure that patches were actually applied (without them the message producded by 'bar' is different)
bar_bin = os.path.join(installdir, 'bin', 'bar')
- out, _ = run_cmd(bar_bin)
- self.assertEqual(out, "I'm a bar, and very very proud of it.\n")
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(bar_bin)
+ self.assertEqual(res.output, "I'm a bar, and very very proud of it.\n")
# verify that post-install command for 'bar' extension was executed
fn = 'created-via-postinstallcmds.txt'
@@ -1289,7 +1412,8 @@ def test_toy_extension_sources(self):
']',
])
write_file(test_ec, test_ec_txt)
- self.test_toy_build(ec_file=test_ec)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec)
# copy bar-0.0.tar.gz to /bar-0.0-local.tar.gz, to be used below
test_source_path = os.path.join(self.test_prefix, 'sources')
@@ -1317,7 +1441,8 @@ def test_toy_extension_sources(self):
']',
])
write_file(test_ec, test_ec_txt)
- self.test_toy_build(ec_file=test_ec, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec, raise_error=True)
# check that checksums are picked up and verified
test_ec_txt = '\n'.join([
@@ -1338,8 +1463,9 @@ def test_toy_extension_sources(self):
write_file(test_ec, test_ec_txt)
error_pattern = r"Checksum verification for extension source bar-0.0-local.tar.gz failed"
- self.assertErrorRegex(EasyBuildError, error_pattern, self.test_toy_build, ec_file=test_ec,
- raise_error=True, verbose=False)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, self._test_toy_build, ec_file=test_ec,
+ raise_error=True, verbose=False)
# test again with correct checksum for bar-0.0.tar.gz, but faulty checksum for patch file
test_ec_txt = '\n'.join([
@@ -1360,8 +1486,9 @@ def test_toy_extension_sources(self):
write_file(test_ec, test_ec_txt)
error_pattern = r"Checksum verification for extension patch bar-0.0_fix-local.patch failed"
- self.assertErrorRegex(EasyBuildError, error_pattern, self.test_toy_build, ec_file=test_ec,
- raise_error=True, verbose=False)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, self._test_toy_build, ec_file=test_ec,
+ raise_error=True, verbose=False)
# test again with correct checksums
test_ec_txt = '\n'.join([
@@ -1379,7 +1506,8 @@ def test_toy_extension_sources(self):
']',
])
write_file(test_ec, test_ec_txt)
- self.test_toy_build(ec_file=test_ec, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec, raise_error=True)
def test_toy_extension_extract_cmd(self):
"""Test for custom extract_cmd specified for an extension."""
@@ -1402,9 +1530,12 @@ def test_toy_extension_extract_cmd(self):
])
write_file(test_ec, test_ec_txt)
- error_pattern = "unzip .*/bar-0.0.tar.gz.* exited with exit code [1-9]"
- self.assertErrorRegex(EasyBuildError, error_pattern, self.test_toy_build, ec_file=test_ec,
- raise_error=True, verbose=False)
+ error_pattern = r"shell command 'unzip \.\.\.' failed with exit code 9 in extensions step for test.eb"
+ with self.mocked_stdout_stderr():
+ # for now, we expect subprocess.CalledProcessError, but eventually 'run' function will
+ # do proper error reporting
+ self.assertErrorRegex(EasyBuildError, error_pattern,
+ self._test_toy_build, ec_file=test_ec, raise_error=True, verbose=False)
def test_toy_extension_sources_git_config(self):
"""Test install toy that includes extensions with 'sources' spec including 'git_config'."""
@@ -1440,7 +1571,8 @@ def test_toy_extension_sources_git_config(self):
']',
])
write_file(test_ec, test_ec_txt)
- self.test_toy_build(ec_file=test_ec)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec)
def test_toy_module_fulltxt(self):
"""Strict text comparison of generated module file."""
@@ -1510,21 +1642,22 @@ def test_toy_module_fulltxt(self):
r'',
r'conflict\("toy"\)',
r'',
+ r'prepend_path\("CMAKE_LIBRARY_PATH", pathJoin\(root, "lib"\)\)',
r'prepend_path\("CMAKE_PREFIX_PATH", root\)',
r'prepend_path\("LD_LIBRARY_PATH", pathJoin\(root, "lib"\)\)',
r'prepend_path\("LIBRARY_PATH", pathJoin\(root, "lib"\)\)',
r'prepend_path\("PATH", pathJoin\(root, "bin"\)\)',
- r'setenv\("EBROOTTOY", root\)',
- r'setenv\("EBVERSIONTOY", "0.0"\)',
- r'setenv\("EBDEVELTOY", pathJoin\(root, "easybuild/toy-0.0-tweaked-easybuild-devel"\)\)',
- r'',
- r'setenv\("FOO", "bar"\)',
r'prepend_path\("SOMEPATH", pathJoin\(root, "foo/bar"\)\)',
r'prepend_path\("SOMEPATH", pathJoin\(root, "baz"\)\)',
r'prepend_path\("SOMEPATH", root\)',
r'append_path\("SOMEPATH_APPEND", pathJoin\(root, "qux/fred"\)\)',
r'append_path\("SOMEPATH_APPEND", pathJoin\(root, "thud"\)\)',
r'append_path\("SOMEPATH_APPEND", root\)',
+ r'setenv\("EBROOTTOY", root\)',
+ r'setenv\("EBVERSIONTOY", "0.0"\)',
+ r'setenv\("EBDEVELTOY", pathJoin\(root, "easybuild/toy-0.0-tweaked-easybuild-devel"\)\)',
+ r'',
+ r'setenv\("FOO", "bar"\)',
r'',
r'if mode\(\) == "load" then',
] + modloadmsg_lua + [
@@ -1551,21 +1684,22 @@ def test_toy_module_fulltxt(self):
r'',
r'conflict toy',
r'',
+ r'prepend-path CMAKE_LIBRARY_PATH \$root/lib',
r'prepend-path CMAKE_PREFIX_PATH \$root',
r'prepend-path LD_LIBRARY_PATH \$root/lib',
r'prepend-path LIBRARY_PATH \$root/lib',
r'prepend-path PATH \$root/bin',
- r'setenv EBROOTTOY "\$root"',
- r'setenv EBVERSIONTOY "0.0"',
- r'setenv EBDEVELTOY "\$root/easybuild/toy-0.0-tweaked-easybuild-devel"',
- r'',
- r'setenv FOO "bar"',
r'prepend-path SOMEPATH \$root/foo/bar',
r'prepend-path SOMEPATH \$root/baz',
r'prepend-path SOMEPATH \$root',
r'append-path SOMEPATH_APPEND \$root/qux/fred',
r'append-path SOMEPATH_APPEND \$root/thud',
r'append-path SOMEPATH_APPEND \$root',
+ r'setenv EBROOTTOY "\$root"',
+ r'setenv EBVERSIONTOY "0.0"',
+ r'setenv EBDEVELTOY "\$root/easybuild/toy-0.0-tweaked-easybuild-devel"',
+ r'',
+ r'setenv FOO "bar"',
r'',
r'if { \[ module-info mode load \] } {',
] + modloadmsg_tcl + [
@@ -1605,7 +1739,8 @@ def test_external_dependencies(self):
start_env = copy.deepcopy(os.environ)
- self.test_toy_build(ec_file=toy_ec, versionsuffix='-external-deps', verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=toy_ec, versionsuffix='-external-deps', verbose=True, raise_error=True)
self.modtool.load(['toy/0.0-external-deps'])
# note build dependency is not loaded
@@ -1625,8 +1760,9 @@ def test_external_dependencies(self):
else:
err_msg = r"Unable to locate a modulefile for 'nosuchbuilddep/0.0.0'"
- self.assertErrorRegex(EasyBuildError, err_msg, self.test_toy_build, ec_file=toy_ec,
- raise_error=True, verbose=False)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, err_msg, self._test_toy_build, ec_file=toy_ec,
+ raise_error=True, verbose=False)
extraectxt = "\ndependencies += [('nosuchmodule/1.2.3', EXTERNAL_MODULE)]"
extraectxt += "\nversionsuffix = '-external-deps-broken2'"
@@ -1637,11 +1773,13 @@ def test_external_dependencies(self):
else:
err_msg = r"Unable to locate a modulefile for 'nosuchmodule/1.2.3'"
- self.assertErrorRegex(EasyBuildError, err_msg, self.test_toy_build, ec_file=toy_ec,
- raise_error=True, verbose=False)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, err_msg, self._test_toy_build, ec_file=toy_ec,
+ raise_error=True, verbose=False)
# --dry-run still works when external modules are missing; external modules are treated as if they were there
- outtxt = self.test_toy_build(ec_file=toy_ec, verbose=True, extra_args=['--dry-run'], verify=False)
+ with self.mocked_stdout_stderr():
+ outtxt = self._test_toy_build(ec_file=toy_ec, verbose=True, extra_args=['--dry-run'], verify=False)
regex = re.compile(r"^ \* \[ \] .* \(module: toy/0.0-external-deps-broken2\)", re.M)
self.assertTrue(regex.search(outtxt), "Pattern '%s' found in: %s" % (regex.pattern, outtxt))
@@ -1657,9 +1795,6 @@ def test_module_only(self):
# sanity check fails without --force if software is not installed yet
common_args = [
ec_file,
- '--sourcepath=%s' % self.test_sourcepath,
- '--buildpath=%s' % self.test_buildpath,
- '--installpath=%s' % self.test_installpath,
'--debug',
'--unittest-file=%s' % self.logfile,
'--robot=%s' % ec_files_path,
@@ -1667,30 +1802,36 @@ def test_module_only(self):
]
args = common_args + ['--module-only']
err_msg = "Sanity check failed"
- self.assertErrorRegex(EasyBuildError, err_msg, self.eb_main, args, do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, err_msg, self.eb_main, args, do_build=True, raise_error=True)
self.assertNotExists(toy_mod)
- self.eb_main(args + ['--force'], do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args + ['--force'], do_build=True, raise_error=True)
self.assertExists(toy_mod)
# make sure load statements for dependencies are included in additional module file generated with --module-only
modtxt = read_file(toy_mod)
- self.assertTrue(re.search('load.*intel/2018a', modtxt), "load statement for intel/2018a found in module")
- self.assertTrue(re.search('load.*GCC/6.4.0-2.28', modtxt), "load statement for GCC/6.4.0-2.28 found in module")
+ self.assertTrue(re.search('(load|depends[-_]on).*intel/2018a', modtxt),
+ "load statement for intel/2018a found in module")
+ self.assertTrue(re.search('(load|depends[-_]on).*GCC/6.4.0-2.28', modtxt),
+ "load statement for GCC/6.4.0-2.28 found in module")
os.remove(toy_mod)
# --module-only --rebuild should run sanity check
rebuild_args = args + ['--rebuild']
err_msg = "Sanity check failed"
- self.assertErrorRegex(EasyBuildError, err_msg, self.eb_main, rebuild_args, do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, err_msg, self.eb_main, rebuild_args, do_build=True, raise_error=True)
self.assertNotExists(toy_mod)
# installing another module under a different naming scheme and using Lua module syntax works fine
# first actually build and install toy software + module
prefix = os.path.join(self.test_installpath, 'software', 'toy', '0.0-deps')
- self.eb_main(common_args + ['--force'], do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(common_args + ['--force'], do_build=True, raise_error=True)
self.assertExists(toy_mod)
self.assertExists(os.path.join(self.test_installpath, 'software', 'toy', '0.0-deps', 'bin'))
modtxt = read_file(toy_mod)
@@ -1705,7 +1846,8 @@ def test_module_only(self):
]
toy_core_mod = os.path.join(self.test_installpath, 'modules', 'all', 'Core', 'toy', '0.0-deps')
self.assertNotExists(toy_core_mod)
- self.eb_main(args, do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True, raise_error=True)
self.assertExists(toy_core_mod)
# existing install is reused
modtxt2 = read_file(toy_core_mod)
@@ -1715,14 +1857,16 @@ def test_module_only(self):
# make sure load statements for dependencies are included
modtxt = read_file(toy_core_mod)
- self.assertTrue(re.search('load.*intel/2018a', modtxt), "load statement for intel/2018a found in module")
+ self.assertTrue(re.search('(load|depends[-_]on).*intel/2018a', modtxt),
+ "load statement for intel/2018a found in module")
# Test we can create a module even for an installation where we don't have write permissions
os.remove(toy_core_mod)
# remove the write permissions on the installation
adjust_permissions(prefix, stat.S_IRUSR | stat.S_IXUSR, relative=False)
self.assertNotExists(toy_core_mod)
- self.eb_main(args, do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True, raise_error=True)
self.assertExists(toy_core_mod)
# existing install is reused
modtxt2 = read_file(toy_core_mod)
@@ -1732,7 +1876,8 @@ def test_module_only(self):
# make sure load statements for dependencies are included
modtxt = read_file(toy_core_mod)
- self.assertTrue(re.search('load.*intel/2018a', modtxt), "load statement for intel/2018a found in module")
+ self.assertTrue(re.search('(load|depends[-_]on).*intel/2018a', modtxt),
+ "load statement for intel/2018a found in module")
os.remove(toy_core_mod)
os.remove(toy_mod)
@@ -1747,7 +1892,8 @@ def test_module_only(self):
'--modules-tool=Lmod',
]
self.assertNotExists(toy_mod + '.lua')
- self.eb_main(args, do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True, raise_error=True)
self.assertExists(toy_mod + '.lua')
# existing install is reused
modtxt3 = read_file(toy_mod + '.lua')
@@ -1757,7 +1903,8 @@ def test_module_only(self):
# make sure load statements for dependencies are included
modtxt = read_file(toy_mod + '.lua')
- self.assertTrue(re.search('load.*intel/2018a', modtxt), "load statement for intel/2018a found in module")
+ self.assertTrue(re.search('(load|depends[-_]on).*intel/2018a', modtxt),
+ "load statement for intel/2018a found in module")
def test_module_only_extensions(self):
"""
@@ -1791,7 +1938,8 @@ def test_module_only_extensions(self):
self.assertEqual(self.modtool.available('toy'), [])
# install toy/0.0
- self.eb_main([test_ec], do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main([test_ec], do_build=True, raise_error=True)
# remove module file so we can try --module-only
remove_file(toy_mod)
@@ -1802,7 +1950,8 @@ def test_module_only_extensions(self):
del os.environ['EASYBUILD_SOURCEPATH']
# first try normal --module-only, should work fine
- self.eb_main([test_ec, '--module-only'], do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main([test_ec, '--module-only'], do_build=True, raise_error=True)
self.assertExists(toy_mod)
remove_file(toy_mod)
@@ -1813,18 +1962,21 @@ def test_module_only_extensions(self):
# check whether sanity check fails now when using --module-only
error_pattern = 'Sanity check failed: command "ls -l lib/libbarbar.a" failed'
for extra_args in (['--module-only'], ['--module-only', '--rebuild']):
- self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, [test_ec] + extra_args,
- do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, [test_ec] + extra_args,
+ do_build=True, raise_error=True)
self.assertNotExists(toy_mod)
# failing sanity check for barbar extension is ignored when using --module-only --skip-extensions
for extra_args in (['--module-only'], ['--module-only', '--rebuild']):
- self.eb_main([test_ec, '--skip-extensions'] + extra_args, do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main([test_ec, '--skip-extensions'] + extra_args, do_build=True, raise_error=True)
self.assertExists(toy_mod)
remove_file(toy_mod)
# we can force module generation via --force (which skips sanity check entirely)
- self.eb_main([test_ec, '--module-only', '--force'], do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main([test_ec, '--module-only', '--force'], do_build=True, raise_error=True)
self.assertExists(toy_mod)
def test_toy_exts_parallel(self):
@@ -1855,7 +2007,7 @@ def test_toy_exts_parallel(self):
write_file(test_ec, test_ec_txt)
args = ['--parallel-extensions-install', '--experimental', '--force', '--parallel=3']
- stdout, stderr = self.run_test_toy_build_with_output(ec_file=test_ec, extra_args=args)
+ stdout, stderr = self.run_test_toy_build_with_output(ec_file=test_ec, extra_args=args, raise_error=True)
self.assertEqual(stderr, '')
# take into account that each of these lines may appear multiple times,
@@ -1874,7 +2026,7 @@ def test_toy_exts_parallel(self):
# also test skipping of extensions in parallel
args.append('--skip')
- stdout, stderr = self.run_test_toy_build_with_output(ec_file=test_ec, extra_args=args)
+ stdout, stderr = self.run_test_toy_build_with_output(ec_file=test_ec, extra_args=args, raise_error=True)
self.assertEqual(stderr, '')
# order in which these patterns occur is not fixed, so check them one by one
@@ -1900,7 +2052,7 @@ def test_toy_exts_parallel(self):
write_file(toy_ext_eb, toy_ext_eb_txt)
args[-1] = '--include-easyblocks=%s' % toy_ext_eb
- stdout, stderr = self.run_test_toy_build_with_output(ec_file=test_ec, extra_args=args)
+ stdout, stderr = self.run_test_toy_build_with_output(ec_file=test_ec, extra_args=args, raise_error=True)
self.assertEqual(stderr, '')
# take into account that each of these lines may appear multiple times,
# in case no progress was made between checks
@@ -1927,9 +2079,6 @@ def test_backup_modules(self):
common_args = [
ec_file,
- '--sourcepath=%s' % self.test_sourcepath,
- '--buildpath=%s' % self.test_buildpath,
- '--installpath=%s' % self.test_installpath,
'--debug',
'--unittest-file=%s' % self.logfile,
'--robot=%s' % ec_files_path,
@@ -1939,11 +2088,13 @@ def test_backup_modules(self):
args = common_args + ['--module-syntax=Tcl']
# install module once (without --module-only), so it can be backed up
- self.eb_main(args, do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True, raise_error=True)
self.assertExists(toy_mod)
# forced reinstall, no backup of module file because --backup-modules (or --module-only) is not used
- self.eb_main(args, do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True, raise_error=True)
self.assertExists(toy_mod)
toy_mod_backups = glob.glob(os.path.join(toy_mod_dir, '.' + toy_mod_fn + '.bak_*'))
self.assertEqual(len(toy_mod_backups), 0)
@@ -1972,7 +2123,8 @@ def test_backup_modules(self):
self.assertEqual(stderr, '')
# no backup of existing module file if --disable-backup-modules is used
- self.eb_main(args + ['--disable-backup-modules'], do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args + ['--disable-backup-modules'], do_build=True, raise_error=True)
toy_mod_backups = glob.glob(os.path.join(toy_mod_dir, '.' + toy_mod_fn + '.bak_*'))
self.assertEqual(len(toy_mod_backups), 1)
@@ -2007,7 +2159,8 @@ def test_backup_modules(self):
toy_mod = os.path.join(toy_mod_dir, toy_mod_fn + '.lua')
# initial installation of Lua module file
- self.eb_main(args, do_build=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True, raise_error=True)
self.assertExists(toy_mod)
lua_toy_mods = glob.glob(os.path.join(toy_mod_dir, '*.lua*'))
self.assertEqual(len(lua_toy_mods), 1)
@@ -2101,7 +2254,8 @@ def test_package(self):
'--packagepath=%s' % pkgpath,
]
- self.test_toy_build(extra_args=extra_args)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(extra_args=extra_args)
toypkg = os.path.join(pkgpath, 'toy-0.0-eb-%s.321.foo' % EASYBUILD_VERSION)
self.assertExists(toypkg)
@@ -2111,11 +2265,12 @@ def test_package_skip(self):
mock_fpm(self.test_prefix)
pkgpath = os.path.join(self.test_prefix, 'packages') # default path
- self.test_toy_build(['--packagepath=%s' % pkgpath])
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(['--packagepath=%s' % pkgpath])
self.assertNotExists(pkgpath, "%s is not created without use of --package" % pkgpath)
self.mock_stdout(True)
- self.test_toy_build(extra_args=['--package', '--skip'], verify=False)
+ self._test_toy_build(extra_args=['--package', '--skip'], verify=False)
self.mock_stdout(False)
toypkg = os.path.join(pkgpath, 'toy-0.0-eb-%s.1.rpm' % EASYBUILD_VERSION)
@@ -2123,14 +2278,8 @@ def test_package_skip(self):
def test_regtest(self):
"""Test use of --regtest."""
-
- # skip test when using Python 2, since it somehow fails then,
- # cfr. https://github.com/easybuilders/easybuild-framework/pull/4333
- if sys.version_info[0] == 2:
- print("Skipping test_regtest because Python 2.x is being used")
- return
-
- self.test_toy_build(extra_args=['--regtest', '--sequential'], verify=False)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(extra_args=['--regtest', '--sequential'], verify=False)
# just check whether module exists
toy_module = os.path.join(self.test_installpath, 'modules', 'all', 'toy', '0.0')
@@ -2141,17 +2290,12 @@ def test_regtest(self):
def test_minimal_toolchains(self):
"""Test toy build with --minimal-toolchains."""
# this test doesn't check for anything specific to using minimal toolchains, only side-effects
- self.test_toy_build(extra_args=['--minimal-toolchains'])
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(extra_args=['--minimal-toolchains'])
def test_reproducibility(self):
"""Test toy build produces expected reproducibility files"""
- # skip test when using Python 2, since it somehow fails then,
- # cfr. https://github.com/easybuilders/easybuild-framework/pull/4333
- if sys.version_info[0] == 2:
- print("Skipping test_reproducibility because Python 2.x is being used")
- return
-
# We need hooks for a complete test
hooks_filename = 'my_hooks.py'
hooks_file = os.path.join(self.test_prefix, hooks_filename)
@@ -2169,7 +2313,7 @@ def test_reproducibility(self):
# also use the easyblock with inheritance to fully test
self.mock_stdout(True)
- self.test_toy_build(extra_args=['--minimal-toolchains', '--easyblock=EB_toytoy', '--hooks=%s' % hooks_file])
+ self._test_toy_build(extra_args=['--minimal-toolchains', '--easyblock=EB_toytoy', '--hooks=%s' % hooks_file])
self.mock_stdout(False)
# Check whether easyconfig is dumped to reprod/ subdir
@@ -2234,7 +2378,9 @@ def test_reproducibility_ext_easyblocks(self):
])
write_file(ec1, ec1_txt)
- self.test_toy_build(ec_file=ec1, verify=False, extra_args=['--minimal-toolchains', '--easyblock=EB_toytoy'])
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=ec1, verify=False,
+ extra_args=['--minimal-toolchains', '--easyblock=EB_toytoy'])
# Check whether easyconfig is dumped to reprod/ subdir
reprod_dir = os.path.join(self.test_installpath, 'software', 'toy', '0.0', 'easybuild', 'reprod')
@@ -2259,6 +2405,7 @@ def test_reproducibility_ext_easyblocks(self):
def test_toy_toy(self):
"""Test building two easyconfigs in a single go, with one depending on the other."""
+
topdir = os.path.dirname(os.path.abspath(__file__))
toy_ec_file = os.path.join(topdir, 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb')
toy_ec_txt = read_file(toy_ec_file)
@@ -2270,18 +2417,27 @@ def test_toy_toy(self):
])
write_file(ec1, ec1_txt)
+ # adapt toy easyconfig for toy2 to produce a modulefile with a dedicated
+ # name ('toy2' instead of 'toy')
ec2 = os.path.join(self.test_prefix, 'toy2.eb')
ec2_txt = '\n'.join([
toy_ec_txt,
+ "name = 'toy2'",
+ "easyblock = 'EB_toy'",
+ "sources = ['toy/toy-0.0.tar.gz']",
+ "patches = []",
+ "sanity_check_paths = { 'files': ['bin/toy2'], 'dirs': ['bin']}",
+ "prebuildopts = 'mv toy.source toy2.c &&'",
"versionsuffix = '-two'",
"dependencies = [('toy', '0.0', '-one')]",
])
write_file(ec2, ec2_txt)
- self.test_toy_build(ec_file=self.test_prefix, verify=False)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=self.test_prefix, verify=False)
mod1 = os.path.join(self.test_installpath, 'modules', 'all', 'toy', '0.0-one')
- mod2 = os.path.join(self.test_installpath, 'modules', 'all', 'toy', '0.0-two')
+ mod2 = os.path.join(self.test_installpath, 'modules', 'all', 'toy2', '0.0-two')
if get_module_syntax() == 'Lua':
mod1 += '.lua'
mod2 += '.lua'
@@ -2290,19 +2446,19 @@ def test_toy_toy(self):
mod2_txt = read_file(mod2)
- load1_regex = re.compile('load.*toy/0.0-one', re.M)
+ load1_regex = re.compile('(load|depends[-_]on).*toy/0.0-one', re.M)
self.assertTrue(load1_regex.search(mod2_txt), "Pattern '%s' found in: %s" % (load1_regex.pattern, mod2_txt))
# Check the contents of the dumped env in the reprod dir to ensure it contains the dependency load
- reprod_dir = os.path.join(self.test_installpath, 'software', 'toy', '0.0-two', 'easybuild', 'reprod')
- dumpenv_script = os.path.join(reprod_dir, 'toy-0.0-two.env')
+ reprod_dir = os.path.join(self.test_installpath, 'software', 'toy2', '0.0-two', 'easybuild', 'reprod')
+ dumpenv_script = os.path.join(reprod_dir, 'toy2-0.0-two.env')
reprod_dumpenv = os.path.join(reprod_dir, dumpenv_script)
self.assertExists(reprod_dumpenv)
# Check contents of the dumpenv script
patterns = [
"""#!/bin/bash""",
- """# usage: source toy-0.0-two.env""",
+ """# usage: source toy2-0.0-two.env""",
# defining build env
"""module load toy/0.0-one""",
"""# (no build environment defined)""",
@@ -2319,6 +2475,8 @@ def test_toy_sanity_check_commands(self):
test_easyconfigs = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'easyconfigs', 'test_ecs')
toy_ec_txt = read_file(os.path.join(test_easyconfigs, 't', 'toy', 'toy-0.0.eb'))
+ out_file = os.path.join(self.test_prefix, 'out.txt')
+
toy_ec_txt = '\n'.join([
toy_ec_txt,
"toolchain = {'name': 'foss', 'version': '2018a'}",
@@ -2334,6 +2492,8 @@ def test_toy_sanity_check_commands(self):
" True,",
# test command to make sure that '-h' is not passed to commands specified as string ('env -h' fails)
" 'env',"
+ # print current working directory, should *not* be software install directory, but empty dir
+ f" '(pwd && ls | wc -l) > {out_file}',",
"]",
])
@@ -2342,16 +2502,14 @@ def test_toy_sanity_check_commands(self):
args = [
tweaked_toy_ec,
- '--sourcepath=%s' % self.test_sourcepath,
- '--buildpath=%s' % self.test_buildpath,
- '--installpath=%s' % self.test_installpath,
'--debug',
'--unittest-file=%s' % self.logfile,
'--force',
'--robot=%s' % test_easyconfigs,
'--module-naming-scheme=HierarchicalMNS',
]
- self.eb_main(args, logfile=self.dummylogfn, do_build=True, verbose=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, logfile=self.dummylogfn, do_build=True, verbose=True, raise_error=True)
modpath = os.path.join(self.test_installpath, 'modules', 'all')
toy_modfile = os.path.join(modpath, 'MPI', 'GCC', '6.4.0-2.28', 'OpenMPI', '2.1.2', 'toy', '0.0')
@@ -2360,6 +2518,13 @@ def test_toy_sanity_check_commands(self):
self.assertExists(toy_modfile)
+ # check contents of output file created by sanity check commands
+ self.assertExists(out_file)
+ out_txt = read_file(out_file)
+ # working dir for sanity check command should be an empty custom temporary directory
+ regex = re.compile('^.*/eb-[^/]+/eb-sanity-check-[^/]+\n[ ]*0$')
+ self.assertTrue(regex.match(out_txt), f"Pattern '{regex.pattern}' should match in: {out_txt}")
+
def test_sanity_check_paths_lib64(self):
"""Test whether fallback in sanity check for lib64/ equivalents of library files works."""
test_ecs_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'easyconfigs')
@@ -2368,7 +2533,14 @@ def test_sanity_check_paths_lib64(self):
# modify test easyconfig: move lib/libtoy.a to lib64/libtoy.a
ectxt = re.sub(r"\s*'files'.*", "'files': ['bin/toy', ('lib/libtoy.a', 'lib/libfoo.a')],", ectxt)
- postinstallcmd = "mkdir %(installdir)s/lib64 && mv %(installdir)s/lib/libtoy.a %(installdir)s/lib64/libtoy.a"
+ postinstallcmd = ' && '.join([
+ # remove lib64 symlink (if it's there)
+ "rm -f %(installdir)s/lib64",
+ # create empty lib64 dir
+ "mkdir %(installdir)s/lib64",
+ # move libtoy*.a
+ "mv %(installdir)s/lib/libtoy*.a %(installdir)s/lib64/",
+ ])
ectxt = re.sub("postinstallcmds.*", "postinstallcmds = ['%s']" % postinstallcmd, ectxt)
test_ec = os.path.join(self.test_prefix, 'toy-0.0.eb')
@@ -2376,12 +2548,14 @@ def test_sanity_check_paths_lib64(self):
# sanity check fails if lib64 fallback in sanity check is disabled
error_pattern = r"Sanity check failed: no file found at 'lib/libtoy.a' or 'lib/libfoo.a' in "
- self.assertErrorRegex(EasyBuildError, error_pattern, self.test_toy_build, ec_file=test_ec,
- extra_args=['--disable-lib64-fallback-sanity-check', '--disable-lib64-lib-symlink'],
- raise_error=True, verbose=False)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, self._test_toy_build, ec_file=test_ec,
+ extra_args=['--disable-lib64-fallback-sanity-check', '--disable-lib64-lib-symlink'],
+ raise_error=True, verbose=False)
# all is fine is lib64 fallback check is enabled (which it is by default)
- self.test_toy_build(ec_file=test_ec, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec, raise_error=True)
# also check with 'lib' in sanity check dirs (special case)
ectxt = re.sub(r"\s*'files'.*", "'files': ['bin/toy'],", ectxt)
@@ -2389,11 +2563,13 @@ def test_sanity_check_paths_lib64(self):
write_file(test_ec, ectxt)
error_pattern = r"Sanity check failed: no \(non-empty\) directory found at 'lib' in "
- self.assertErrorRegex(EasyBuildError, error_pattern, self.test_toy_build, ec_file=test_ec,
- extra_args=['--disable-lib64-fallback-sanity-check', '--disable-lib64-lib-symlink'],
- raise_error=True, verbose=False)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, self._test_toy_build, ec_file=test_ec,
+ extra_args=['--disable-lib64-fallback-sanity-check', '--disable-lib64-lib-symlink'],
+ raise_error=True, verbose=False)
- self.test_toy_build(ec_file=test_ec, extra_args=['--disable-lib64-lib-symlink'], raise_error=True)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec, extra_args=['--disable-lib64-lib-symlink'], raise_error=True)
# also check other way around (lib64 -> lib)
ectxt = read_file(ec_file)
@@ -2402,12 +2578,14 @@ def test_sanity_check_paths_lib64(self):
# sanity check fails if lib64 fallback in sanity check is disabled, since lib64/libtoy.a is not there
error_pattern = r"Sanity check failed: no file found at 'lib64/libtoy.a' in "
- self.assertErrorRegex(EasyBuildError, error_pattern, self.test_toy_build, ec_file=test_ec,
- extra_args=['--disable-lib64-fallback-sanity-check', '--disable-lib64-lib-symlink'],
- raise_error=True, verbose=False)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, self._test_toy_build, ec_file=test_ec,
+ extra_args=['--disable-lib64-fallback-sanity-check', '--disable-lib64-lib-symlink'],
+ raise_error=True, verbose=False)
# sanity check passes when lib64 fallback is enabled (by default), since lib/libtoy.a is also considered
- self.test_toy_build(ec_file=test_ec, extra_args=['--disable-lib64-lib-symlink'], raise_error=True)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec, extra_args=['--disable-lib64-lib-symlink'], raise_error=True)
# also check with 'lib64' in sanity check dirs (special case)
ectxt = re.sub(r"\s*'files'.*", "'files': ['bin/toy'],", ectxt)
@@ -2415,11 +2593,13 @@ def test_sanity_check_paths_lib64(self):
write_file(test_ec, ectxt)
error_pattern = r"Sanity check failed: no \(non-empty\) directory found at 'lib64' in "
- self.assertErrorRegex(EasyBuildError, error_pattern, self.test_toy_build, ec_file=test_ec,
- extra_args=['--disable-lib64-fallback-sanity-check', '--disable-lib64-lib-symlink'],
- raise_error=True, verbose=False)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, self._test_toy_build, ec_file=test_ec,
+ extra_args=['--disable-lib64-fallback-sanity-check', '--disable-lib64-lib-symlink'],
+ raise_error=True, verbose=False)
- self.test_toy_build(ec_file=test_ec, extra_args=['--disable-lib64-lib-symlink'], raise_error=True)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec, extra_args=['--disable-lib64-lib-symlink'], raise_error=True)
# check whether fallback works for files that's more than 1 subdir deep
ectxt = read_file(ec_file)
@@ -2428,7 +2608,8 @@ def test_sanity_check_paths_lib64(self):
postinstallcmd += "mv %(installdir)s/lib/libtoy.a %(installdir)s/lib64/test/libtoy.a"
ectxt = re.sub("postinstallcmds.*", "postinstallcmds = ['%s']" % postinstallcmd, ectxt)
write_file(test_ec, ectxt)
- self.test_toy_build(ec_file=test_ec, extra_args=['--disable-lib64-lib-symlink'], raise_error=True)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec, extra_args=['--disable-lib64-lib-symlink'], raise_error=True)
def test_toy_build_enhanced_sanity_check(self):
"""Test enhancing of sanity check."""
@@ -2476,7 +2657,7 @@ def test_toy_build_enhanced_sanity_check(self):
# by default, sanity check commands & paths specified by easyblock are used
self.mock_stdout(True)
- self.test_toy_build(ec_file=test_ec, extra_args=eb_args, verify=False, testing=False, raise_error=True)
+ self._test_toy_build(ec_file=test_ec, extra_args=eb_args, verify=False, testing=False, raise_error=True)
stdout = self.get_stdout()
self.mock_stdout(False)
@@ -2509,7 +2690,7 @@ def test_toy_build_enhanced_sanity_check(self):
write_file(test_ec, test_ec_txt)
self.mock_stdout(True)
- self.test_toy_build(ec_file=test_ec, extra_args=eb_args, verify=False, testing=False, raise_error=True)
+ self._test_toy_build(ec_file=test_ec, extra_args=eb_args, verify=False, testing=False, raise_error=True)
stdout = self.get_stdout()
self.mock_stdout(False)
@@ -2533,7 +2714,7 @@ def test_toy_build_enhanced_sanity_check(self):
write_file(test_ec, test_ec_txt)
self.mock_stdout(True)
- self.test_toy_build(ec_file=test_ec, extra_args=eb_args, verify=False, testing=False, raise_error=True)
+ self._test_toy_build(ec_file=test_ec, extra_args=eb_args, verify=False, testing=False, raise_error=True)
stdout = self.get_stdout()
self.mock_stdout(False)
@@ -2566,7 +2747,7 @@ def test_toy_build_enhanced_sanity_check(self):
]
self.mock_stdout(True)
- self.test_toy_build(ec_file=test_ec, extra_args=eb_args, verify=False, testing=False, raise_error=True)
+ self._test_toy_build(ec_file=test_ec, extra_args=eb_args, verify=False, testing=False, raise_error=True)
stdout = self.get_stdout()
self.mock_stdout(False)
@@ -2587,37 +2768,128 @@ def test_toy_build_enhanced_sanity_check(self):
test_ec_txt = test_ec_txt + '\nenhance_sanity_check = False'
write_file(test_ec, test_ec_txt)
- error_pattern = r" Missing mandatory key 'dirs' in sanity_check_paths."
- self.assertErrorRegex(EasyBuildError, error_pattern, self.test_toy_build, ec_file=test_ec,
- extra_args=eb_args, raise_error=True, verbose=False)
+ error_pattern = r"Missing mandatory key 'dirs' in sanity_check_paths."
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, self._test_toy_build, ec_file=test_ec,
+ extra_args=eb_args, raise_error=True, verbose=False)
del sys.modules['easybuild.easyblocks.toy']
+ def test_toy_build_enhanced_sanity_check_templated_multi_dep(self):
+ """Test enhancing of sanity check by easyblocks with templates and in the presence of multi_deps."""
+
+ # if toy easyblock was imported, get rid of corresponding entry in sys.modules,
+ # to avoid that it messes up the use of --include-easyblocks=toy.py below...
+ if 'easybuild.easyblocks.toy' in sys.modules:
+ del sys.modules['easybuild.easyblocks.toy']
+
+ test_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)))
+ toy_ec = os.path.join(test_dir, 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb')
+ toy_ec_txt = read_file(toy_ec)
+
+ test_ec = os.path.join(self.test_prefix, 'test.eb')
+
+ # get rid of custom sanity check paths in test easyconfig
+ regex = re.compile(r'^sanity_check_paths\s*=\s*{[^}]+}', re.M)
+ test_ec_txt = regex.sub('', toy_ec_txt)
+ self.assertNotIn('sanity_check_', test_ec_txt)
+
+ test_ec_txt += "\nmulti_deps = {'Python': ['3.7.2', '2.7.15']}"
+ write_file(test_ec, test_ec_txt)
+
+ # create custom easyblock for toy that has a custom sanity_check_step
+ toy_easyblock = os.path.join(test_dir, 'sandbox', 'easybuild', 'easyblocks', 't', 'toy.py')
+
+ toy_easyblock_txt = read_file(toy_easyblock)
+
+ toy_custom_sanity_check_step = textwrap.dedent("""
+ # Add to class to indent
+ def sanity_check_step(self):
+ paths = {
+ 'files': ['bin/python%(pyshortver)s'],
+ 'dirs': ['lib/py-%(pyshortver)s'],
+ }
+ cmds = ['python%(pyshortver)s']
+ return super(EB_toy, self).sanity_check_step(custom_paths=paths, custom_commands=cmds)
+ """)
+ test_toy_easyblock = os.path.join(self.test_prefix, 'toy.py')
+ write_file(test_toy_easyblock, toy_easyblock_txt + toy_custom_sanity_check_step)
+
+ eb_args = [
+ '--extended-dry-run',
+ '--include-easyblocks=%s' % test_toy_easyblock,
+ ]
+
+ # by default, sanity check commands & paths specified by easyblock are used
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec, extra_args=eb_args, verify=False, testing=False, raise_error=True)
+ stdout = self.get_stdout()
+ # Cut output to start of the toy-ec, after the Python installations
+ stdout = stdout[stdout.index(test_ec):]
+
+ pattern_template = textwrap.dedent(r"""
+ Sanity check paths - file.*
+ \s*\* bin/python{pyshortver}
+ Sanity check paths - \(non-empty\) directory.*
+ \s*\* lib/py-{pyshortver}
+ Sanity check commands
+ \s*\* python{pyshortver}
+ """)
+ for pyshortver in ('2.7', '3.7'):
+ regex = re.compile(pattern_template.format(pyshortver=pyshortver), re.M)
+ self.assertTrue(regex.search(stdout), "Pattern '%s' should be found in: %s" % (regex.pattern, stdout))
+
+ # Enhance sanity check by extra paths to check for, the ones from the easyblock should be kept
+ test_ec_txt += textwrap.dedent("""
+ enhance_sanity_check = True
+ sanity_check_paths = {
+ 'files': ['bin/pip%(pyshortver)s'],
+ 'dirs': ['bin'],
+ }
+ """)
+ write_file(test_ec, test_ec_txt)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec, extra_args=eb_args, verify=False, testing=False, raise_error=True)
+ stdout = self.get_stdout()
+ # Cut output to start of the toy-ec, after the Python installations
+ stdout = stdout[stdout.index(test_ec):]
+
+ pattern_template = textwrap.dedent(r"""
+ Sanity check paths - file.*
+ \s*\* bin/pip{pyshortver}
+ \s*\* bin/python{pyshortver}
+ Sanity check paths - \(non-empty\) directory.*
+ \s*\* bin
+ \s*\* lib/py-{pyshortver}
+ Sanity check commands
+ \s*\* python{pyshortver}
+ """)
+ for pyshortver in ('2.7', '3.7'):
+ regex = re.compile(pattern_template.format(pyshortver=pyshortver), re.M)
+ self.assertTrue(regex.search(stdout), "Pattern '%s' should be found in: %s" % (regex.pattern, stdout))
+
def test_toy_dumped_easyconfig(self):
- """ Test dumping of file in eb_filerepo in both .eb and .yeb format """
+ """ Test dumping of file in eb_filerepo in both .eb format """
filename = 'toy-0.0'
test_ecs_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'easyconfigs')
paths = [
os.path.join(test_ecs_dir, 'test_ecs', 't', 'toy', '%s.eb' % filename),
- os.path.join(test_ecs_dir, 'yeb', '%s.yeb' % filename),
]
for path in paths:
- if path.endswith('.yeb') and 'yaml' not in sys.modules:
- print("Skipping .yeb part of test_toy_dumped_easyconfig (no PyYAML available)")
- continue
-
args = [
path,
'--experimental',
'--force',
]
- self.eb_main(args, do_build=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True)
# test eb build with dumped file
args[0] = os.path.join(get_repositorypath()[0], 'toy', 'toy-0.0%s' % os.path.splitext(path)[-1])
- self.eb_main(args, do_build=True)
+ with self.mocked_stdout_stderr():
+ self.eb_main(args, do_build=True)
easybuild.tools.build_log.EXPERIMENTAL = True
ec = EasyConfig(args[0])
buildstats = ec.parser.get_config_dict()['buildstats']
@@ -2636,12 +2908,14 @@ def test_toy_filter_env_vars(self):
re.compile("prepend[-_]path.*PATH.*bin", re.M),
]
- self.test_toy_build()
+ with self.mocked_stdout_stderr():
+ self._test_toy_build()
toy_mod_txt = read_file(toy_mod_path)
for regex in regexs:
self.assertTrue(regex.search(toy_mod_txt), "Pattern '%s' found in: %s" % (regex.pattern, toy_mod_txt))
- self.test_toy_build(extra_args=['--filter-env-vars=LD_LIBRARY_PATH,PATH'])
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(extra_args=['--filter-env-vars=LD_LIBRARY_PATH,PATH'])
toy_mod_txt = read_file(toy_mod_path)
self.assertFalse(regexs[0].search(toy_mod_txt), "Pattern '%s' found in: %s" % (regexs[0].pattern, toy_mod_txt))
self.assertTrue(regexs[1].search(toy_mod_txt), "Pattern '%s' found in: %s" % (regexs[1].pattern, toy_mod_txt))
@@ -2656,7 +2930,8 @@ def test_toy_iter(self):
for extra_args in [None, ['--minimal-toolchains']]:
# sanity check will make sure all entries in buildopts list were taken into account
- self.test_toy_build(ec_file=toy_ec, extra_args=extra_args, versionsuffix='-iter')
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=toy_ec, extra_args=extra_args, versionsuffix='-iter')
# verify whether dumped easyconfig contains original value for buildopts
dumped_toy_ec = os.path.join(self.test_prefix, 'ebfiles_repo', 'toy', os.path.basename(toy_ec))
@@ -2693,7 +2968,8 @@ def grab_gcc_rpath_wrapper_args():
return {'filter_paths': res_filter.group(1), 'include_paths': res_include.group(1)}
args = ['--rpath']
- self.test_toy_build(extra_args=args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(extra_args=args, raise_error=True)
# by default, /lib and /usr are included in RPATH filter,
# together with temporary directory and build directory
@@ -2705,7 +2981,8 @@ def grab_gcc_rpath_wrapper_args():
# Check that we can use --rpath-override-dirs
args = ['--rpath', '--rpath-override-dirs=/opt/eessi/2021.03/lib:/opt/eessi/lib']
- self.test_toy_build(extra_args=args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(extra_args=args, raise_error=True)
rpath_include_paths = grab_gcc_rpath_wrapper_args()['include_paths'].split(',')
# Make sure our directories appear in dirs to be included in the rpath (and in the right order)
self.assertEqual(rpath_include_paths[0], '/opt/eessi/2021.03/lib')
@@ -2713,7 +2990,8 @@ def grab_gcc_rpath_wrapper_args():
# Check that when we use --rpath-override-dirs empty values are filtered
args = ['--rpath', '--rpath-override-dirs=/opt/eessi/2021.03/lib::/opt/eessi/lib']
- self.test_toy_build(extra_args=args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(extra_args=args, raise_error=True)
rpath_include_paths = grab_gcc_rpath_wrapper_args()['include_paths'].split(',')
# Make sure our directories appear in dirs to be included in the rpath (and in the right order)
self.assertEqual(rpath_include_paths[0], '/opt/eessi/2021.03/lib')
@@ -2722,12 +3000,14 @@ def grab_gcc_rpath_wrapper_args():
# Check that when we use --rpath-override-dirs we can only provide absolute paths
eb_args = ['--rpath', '--rpath-override-dirs=/opt/eessi/2021.03/lib:eessi/lib']
error_pattern = r"Path used in rpath_override_dirs is not an absolute path: eessi/lib"
- self.assertErrorRegex(EasyBuildError, error_pattern, self.test_toy_build, extra_args=eb_args, raise_error=True,
- verbose=False)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, self._test_toy_build, extra_args=eb_args,
+ raise_error=True, verbose=False)
# also test use of --rpath-filter
args.extend(['--rpath-filter=/test.*,/foo/bar.*', '--disable-cleanup-tmpdir'])
- self.test_toy_build(extra_args=args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(extra_args=args, raise_error=True)
# check whether rpath filter was set correctly
rpath_filter_paths = grab_gcc_rpath_wrapper_args()['filter_paths'].split(',')
@@ -2736,13 +3016,15 @@ def grab_gcc_rpath_wrapper_args():
self.assertTrue(any(p.startswith(os.getenv('TMPDIR')) for p in rpath_filter_paths))
self.assertTrue(any(p.startswith(self.test_buildpath) for p in rpath_filter_paths))
- # test use of rpath toolchain option
- test_ecs = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'easyconfigs', 'test_ecs')
- toy_ec_txt = read_file(os.path.join(test_ecs, 't', 'toy', 'toy-0.0.eb'))
- toy_ec_txt += "\ntoolchainopts = {'rpath': False}\n"
- toy_ec = os.path.join(self.test_prefix, 'toy.eb')
- write_file(toy_ec, toy_ec_txt)
- self.test_toy_build(ec_file=toy_ec, extra_args=['--rpath'], raise_error=True)
+ # test use of rpath toolchain option with SYSTEM and gompi 2018b toolchains
+ for toolchain in ['', '-gompi-2018a']:
+ test_ecs = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'easyconfigs', 'test_ecs')
+ toy_ec_txt = read_file(os.path.join(test_ecs, 't', 'toy', f'toy-0.0{toolchain}.eb'))
+ toy_ec_txt += "\ntoolchainopts = {'rpath': False}\n" # overwrites existing toolchainopts
+ toy_ec = os.path.join(self.test_prefix, 'toy.eb')
+ write_file(toy_ec, toy_ec_txt)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=toy_ec, extra_args=['--rpath'], raise_error=True)
def test_toy_filter_rpath_sanity_libs(self):
"""Test use of --filter-rpath-sanity-libs."""
@@ -2751,58 +3033,83 @@ def test_toy_filter_rpath_sanity_libs(self):
toy_ec = os.path.join(test_ecs, 't', 'toy-app', 'toy-app-0.0.eb')
# This should just build succesfully
- args = ['--rpath']
- self.test_toy_build(ec_file=toy_ec, name='toy-app', extra_args=args, raise_error=True)
+ rpath_args = ['--rpath', '--strict-rpath-sanity-check']
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=toy_ec, name='toy-app', extra_args=rpath_args, raise_error=True)
libtoy_libdir = os.path.join(self.test_installpath, 'software', 'libtoy', '0.0', 'lib')
toyapp_bin = os.path.join(self.test_installpath, 'software', 'toy-app', '0.0', 'bin', 'toy-app')
- rpath_regex = re.compile(r"RPATH.*%s" % libtoy_libdir, re.M)
- out, ec = run_cmd("readelf -d %s" % toyapp_bin, simple=False)
- self.assertTrue(rpath_regex.search(out), "Pattern '%s' should be found in: %s" % (rpath_regex.pattern, out))
+ rpath_regex = re.compile(r"\(RPATH\).*" + libtoy_libdir, re.M)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(f"readelf -d {toyapp_bin}")
+ self.assertTrue(rpath_regex.search(res.output),
+ f"Pattern '{rpath_regex.pattern}' should be found in: {res.output}")
- out, ec = run_cmd("ldd %s" % toyapp_bin, simple=False)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(f"ldd {toyapp_bin}")
+ out = res.output
libtoy_regex = re.compile(r"libtoy.so => /.*/libtoy.so", re.M)
notfound = re.compile(r"libtoy\.so\s*=>\s*not found", re.M)
- self.assertTrue(libtoy_regex.search(out), "Pattern '%s' should be found in: %s" % (libtoy_regex.pattern, out))
- self.assertFalse(notfound.search(out), "Pattern '%s' should not be found in: %s" % (notfound.pattern, out))
+ self.assertTrue(libtoy_regex.search(out), f"Pattern '{libtoy_regex.pattern}' should be found in: {out}")
+ self.assertFalse(notfound.search(out), f"Pattern '{notfound.pattern}' should not be found in: {out}")
# test sanity error when --rpath-filter is used to filter a required library
# In this test, libtoy.so will be linked, but not RPATH-ed due to the --rpath-filter
# Thus, the RPATH sanity check is expected to fail with libtoy.so not being found
+ args = rpath_args + ['--rpath-filter=.*libtoy.*']
error_pattern = r"Sanity check failed\: Library libtoy\.so not found"
- self.assertErrorRegex(EasyBuildError, error_pattern, self.test_toy_build, ec_file=toy_ec,
- extra_args=['--rpath', '--rpath-filter=.*libtoy.*'],
- name='toy-app', raise_error=True, verbose=False)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, self._test_toy_build, ec_file=toy_ec,
+ extra_args=args, name='toy-app', raise_error=True, verbose=False)
# test use of --filter-rpath-sanity-libs option. In this test, we use --rpath-filter to make sure libtoy.so is
# not rpath-ed. Then, we use --filter-rpath-sanity-libs to make sure the RPATH sanity checks ignores
# the fact that libtoy.so is not found. Thus, this build should complete succesfully
- args = ['--rpath', '--rpath-filter=.*libtoy.*', '--filter-rpath-sanity-libs=libtoy.so']
- self.test_toy_build(ec_file=toy_ec, name='toy-app', extra_args=args, raise_error=True)
+ args = rpath_args + ['--rpath-filter=.*libtoy.*', '--filter-rpath-sanity-libs=libtoy.so']
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=toy_ec, name='toy-app', extra_args=args, raise_error=True)
- out, ec = run_cmd("readelf -d %s" % toyapp_bin, simple=False)
- self.assertFalse(rpath_regex.search(out),
- "Pattern '%s' should not be found in: %s" % (rpath_regex.pattern, out))
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(f"readelf -d {toyapp_bin}")
+ self.assertFalse(rpath_regex.search(res.output),
+ f"Pattern '{rpath_regex.pattern}' should not be found in: {res.output}")
- out, ec = run_cmd("ldd %s" % toyapp_bin, simple=False)
- self.assertFalse(libtoy_regex.search(out),
- "Pattern '%s' should not be found in: %s" % (libtoy_regex.pattern, out))
- self.assertTrue(notfound.search(out),
- "Pattern '%s' should be found in: %s" % (notfound.pattern, out))
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(f"ldd {toyapp_bin}")
+ self.assertFalse(libtoy_regex.search(res.output),
+ f"Pattern '{libtoy_regex.pattern}' should not be found in: {res.output}")
+ self.assertTrue(notfound.search(res.output),
+ f"Pattern '{notfound.pattern}' should be found in: {res.output}")
# test again with list of library names passed to --filter-rpath-sanity-libs
- args = ['--rpath', '--rpath-filter=.*libtoy.*', '--filter-rpath-sanity-libs=libfoo.so,libtoy.so,libbar.so']
- self.test_toy_build(ec_file=toy_ec, name='toy-app', extra_args=args, raise_error=True)
+ args = rpath_args + ['--rpath-filter=.*libtoy.*', '--filter-rpath-sanity-libs=libfoo.so,libtoy.so,libbar.so']
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=toy_ec, name='toy-app', extra_args=args, raise_error=True)
- out, ec = run_cmd("readelf -d %s" % toyapp_bin, simple=False)
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(f"readelf -d {toyapp_bin}")
self.assertFalse(rpath_regex.search(out),
- "Pattern '%s' should not be found in: %s" % (rpath_regex.pattern, out))
+ f"Pattern '{rpath_regex.pattern}' should not be found in: {res.output}")
+
+ with self.mocked_stdout_stderr():
+ res = run_shell_cmd(f"ldd {toyapp_bin}")
+ self.assertFalse(libtoy_regex.search(res.output),
+ f"Pattern '{libtoy_regex.pattern}' should not be found in: {res.output}")
+ self.assertTrue(notfound.search(res.output),
+ f"Pattern '{notfound.pattern}' should be found in: {res.output}")
+
+ # by default, without using --strict-rpath-sanity-check, there's no failure since RPATH sanity check
+ # doesn't check for missing libraries with $LD_LIBRARY_PATH unset
+ args = ['--rpath', '--rpath-filter=.*libtoy.*']
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=toy_ec, name='toy-app', extra_args=args, raise_error=True, verbose=False)
- out, ec = run_cmd("ldd %s" % toyapp_bin, simple=False)
- self.assertFalse(libtoy_regex.search(out),
- "Pattern '%s' should not be found in: %s" % (libtoy_regex.pattern, out))
- self.assertTrue(notfound.search(out),
- "Pattern '%s' should be found in: %s" % (notfound.pattern, out))
+ # trouble again when $LD_LIBRARY_PATH is not used in generated module file
+ args = ['libtoy-0.0.eb', '--rebuild', '--rpath', '--rpath-filter=.*libtoy.*',
+ '--filter-env-vars=LD_LIBRARY_PATH']
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, self._test_toy_build, ec_file=toy_ec,
+ extra_args=args, name='toy-app', raise_error=True, verbose=False)
def test_toy_modaltsoftname(self):
"""Build two dependent toys as in test_toy_toy but using modaltsoftname"""
@@ -2833,7 +3140,8 @@ def test_toy_modaltsoftname(self):
'--module-naming-scheme=HierarchicalMNS',
'--robot-paths=%s' % self.test_prefix,
]
- self.test_toy_build(ec_file=self.test_prefix, verify=False, extra_args=extra_args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=self.test_prefix, verify=False, extra_args=extra_args, raise_error=True)
software_path = os.path.join(self.test_installpath, 'software')
modules_path = os.path.join(self.test_installpath, 'modules', 'all', 'Core')
@@ -2872,7 +3180,7 @@ def test_toy_build_trace(self):
self.mock_stderr(True)
self.mock_stdout(True)
- self.test_toy_build(ec_file=test_ec, extra_args=['--trace'], verify=False, testing=False)
+ self._test_toy_build(ec_file=test_ec, extra_args=['--trace'], verify=False, testing=False)
stderr = self.get_stderr()
stdout = self.get_stdout()
self.mock_stderr(False)
@@ -2882,14 +3190,15 @@ def test_toy_build_trace(self):
patterns = [
r"^ >> installation prefix: .*/software/toy/0\.0$",
- r"^== fetching files\.\.\.\n >> sources:\n >> .*/toy-0\.0\.tar\.gz \[SHA256: 44332000.*\]$",
+ r"^== fetching files and verifying checksums\.\.\.\n"
+ r" >> sources:\n >> .*/toy-0\.0\.tar\.gz \[SHA256: 44332000.*\]$",
r"^ >> applying patch toy-0\.0_fix-silly-typo-in-printf-statement\.patch$",
r'\n'.join([
- r"^ >> running command:",
+ r"^ >> running shell command:",
+ r"\tgcc toy.c -o toy\n"
r"\t\[started at: .*\]",
r"\t\[working dir: .*\]",
- r"\t\[output logged in .*\]",
- r"\tgcc toy.c -o toy\n"
+ r"\t\[output and state saved to .*\]",
r'',
]),
r" >> command completed: exit 0, ran in .*",
@@ -2960,7 +3269,7 @@ def end_hook():
print('end hook triggered, all done!')
def pre_run_shell_cmd_hook(cmd, *args, **kwargs):
- if cmd.strip() == TOY_COMP_CMD:
+ if isinstance(cmd, str) and cmd.strip() == TOY_COMP_CMD:
print("pre_run_shell_cmd_hook triggered for '%s'" % cmd)
# 'copy_toy_file' command doesn't exist, but don't worry,
# this problem will be fixed in post_run_shell_cmd_hook
@@ -2971,7 +3280,7 @@ def post_run_shell_cmd_hook(cmd, *args, **kwargs):
exit_code = kwargs['exit_code']
output = kwargs['output']
work_dir = kwargs['work_dir']
- if cmd.strip().startswith(TOY_COMP_CMD) and exit_code:
+ if isinstance(cmd, str) and cmd.strip().startswith(TOY_COMP_CMD) and exit_code:
cwd = change_dir(work_dir)
copy_file('toy', 'copy_of_toy')
change_dir(cwd)
@@ -2979,9 +3288,15 @@ def post_run_shell_cmd_hook(cmd, *args, **kwargs):
""")
write_file(hooks_file, hooks_file_txt)
+ extra_args = [
+ '--hooks=%s' % hooks_file,
+ # disable trace output to make checking of generated output produced by hooks easier
+ '--disable-trace',
+ ]
+
self.mock_stderr(True)
self.mock_stdout(True)
- self.test_toy_build(ec_file=test_ec, extra_args=['--hooks=%s' % hooks_file], raise_error=True, debug=False)
+ self._test_toy_build(ec_file=test_ec, extra_args=extra_args, raise_error=True, debug=False)
stderr = self.get_stderr()
stdout = self.get_stdout()
self.mock_stderr(False)
@@ -2991,6 +3306,7 @@ def post_run_shell_cmd_hook(cmd, *args, **kwargs):
toy_mod_file = os.path.join(test_mod_path, 'toy', '0.0')
if get_module_syntax() == 'Lua':
toy_mod_file += '.lua'
+ mod_name = os.path.basename(toy_mod_file)
warnings = nub([x for x in stderr.strip().splitlines() if x])
self.assertEqual(warnings, ["WARNING: Command ' gcc toy.c -o toy ' failed, but we'll ignore it..."])
@@ -3001,7 +3317,7 @@ def post_run_shell_cmd_hook(cmd, *args, **kwargs):
# - for fake module file being created during sanity check (triggered twice, for main + toy install)
# - for final module file
# - for devel module file
- expected_output = textwrap.dedent("""
+ expected_output = textwrap.dedent(f"""
start hook triggered
toy 0.0
['%(name)s-%(version)s.tar.gz']
@@ -3051,6 +3367,7 @@ def test_toy_multi_deps(self):
test_ec_txt += "\nexts_list = [('barbar', '1.2', {'start_dir': 'src'})]"
test_ec_txt += "\nmulti_deps = {'GCC': ['4.6.3', '7.3.0-2.30']}"
+
write_file(test_ec, test_ec_txt)
test_mod_path = os.path.join(self.test_installpath, 'modules', 'all')
@@ -3067,7 +3384,8 @@ def test_toy_multi_deps(self):
# to make situation where GCC/7.3.0-2.30 is loaded when GCC/4.6.3 is already loaded (by default) fail
os.environ['LMOD_DISABLE_SAME_NAME_AUTOSWAP'] = 'yes'
- self.test_toy_build(ec_file=test_ec)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec)
toy_mod_file = os.path.join(test_mod_path, 'toy', '0.0')
if get_module_syntax() == 'Lua':
@@ -3078,15 +3396,25 @@ def test_toy_multi_deps(self):
# check whether (guarded) load statement for first version listed in multi_deps is there
if get_module_syntax() == 'Lua':
expected = '\n'.join([
- 'if not ( isloaded("GCC/4.6.3") ) and not ( isloaded("GCC/7.3.0-2.30") ) then',
- ' load("GCC/4.6.3")',
+ 'if mode() == "unload" or isloaded("GCC/7.3.0-2.30") then',
+ ' depends_on("GCC")',
+ 'else',
+ ' depends_on("GCC/4.6.3")',
'end',
])
else:
+ if isinstance(self.modtool, EnvironmentModules):
+ load_stmt = "module load"
+ else:
+ load_stmt = "depends-on"
expected = '\n'.join([
- 'if { ![ is-loaded GCC/4.6.3 ] && ![ is-loaded GCC/7.3.0-2.30 ] } {',
- ' module load GCC/4.6.3',
+ '',
+ "if { [ module-info mode remove ] || [ is-loaded GCC/7.3.0-2.30 ] } {",
+ " %s GCC" % load_stmt,
+ '} else {',
+ " %s GCC/4.6.3" % load_stmt,
'}',
+ '',
])
self.assertIn(expected, toy_mod_txt)
@@ -3175,7 +3503,8 @@ def check_toy_load(depends_on=False):
write_file(test_ec, test_ec_txt + "\nmulti_deps_load_default = False")
remove_file(toy_mod_file)
- self.test_toy_build(ec_file=test_ec)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec)
toy_mod_txt = read_file(toy_mod_file)
self.assertNotIn(expected, toy_mod_txt)
@@ -3217,7 +3546,8 @@ def check_toy_load(depends_on=False):
if self.modtool.supports_depends_on:
remove_file(toy_mod_file)
- self.test_toy_build(ec_file=test_ec, extra_args=['--module-depends-on'], raise_error=True)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec, extra_args=['--module-depends-on'], raise_error=True)
toy_mod_txt = read_file(toy_mod_file)
@@ -3321,7 +3651,8 @@ def test_fix_shebang(self):
"fix_bash_shebang_for = ['bin/t*.sh', 'bin/b1.py', 'bin/b1.pl', 'bin/toy.sh']",
])
write_file(test_ec, test_ec_txt)
- self.test_toy_build(ec_file=test_ec, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec, raise_error=True)
toy_bindir = os.path.join(self.test_installpath, 'software', 'toy', '0.0', 'bin')
@@ -3355,7 +3686,8 @@ def test_fix_shebang(self):
# now test with a custom env command
extra_args = ['--env-for-shebang=/usr/bin/env -S']
- self.test_toy_build(ec_file=test_ec, extra_args=extra_args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec, extra_args=extra_args, raise_error=True)
toy_bindir = os.path.join(self.test_installpath, 'software', 'toy', '0.0', 'bin')
@@ -3397,7 +3729,8 @@ def check_shebangs():
'--env-for-shebang=/usr/bin/env -S',
'--sysroot=/usr/../', # sysroot must exist, and so must /usr/bin/env when appended to it
]
- self.test_toy_build(ec_file=test_ec, extra_args=extra_args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec, extra_args=extra_args, raise_error=True)
regexes_shebang['py'] = re.compile(r'^#!/usr/../usr/bin/env -S python\n# test$')
regexes_shebang['pl'] = re.compile(r'^#!/usr/../usr/bin/env -S perl\n# test$')
@@ -3409,7 +3742,8 @@ def check_shebangs():
'--env-for-shebang=/usr/../usr/../usr/bin/env -S',
'--sysroot=/usr/../', # sysroot must exist, and so must /usr/bin/env when appended to it
]
- self.test_toy_build(ec_file=test_ec, extra_args=extra_args, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec, extra_args=extra_args, raise_error=True)
regexes_shebang['py'] = re.compile(r'^#!/usr/../usr/../usr/bin/env -S python\n# test$')
regexes_shebang['pl'] = re.compile(r'^#!/usr/../usr/../usr/bin/env -S perl\n# test$')
@@ -3435,7 +3769,8 @@ def test_toy_system_toolchain_alias(self):
test_ec_txt = tc_regex.sub(tc, toy_ec_txt)
write_file(test_ec, test_ec_txt)
- self.test_toy_build(ec_file=test_ec)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec)
def test_toy_ghost_installdir(self):
"""Test whether ghost installation directory is removed under --force."""
@@ -3459,7 +3794,6 @@ def test_toy_ghost_installdir(self):
stdout, stderr = self.run_test_toy_build_with_output()
# by default, a warning is printed for ghost installation directories (but they're left untouched)
- self.assertFalse(stdout)
regex = re.compile("WARNING: Likely ghost installation directory detected: %s" % toy_installdir)
self.assertTrue(regex.search(stderr), "Pattern '%s' found in: %s" % (regex.pattern, stderr))
self.assertExists(toy_installdir)
@@ -3470,8 +3804,8 @@ def test_toy_ghost_installdir(self):
self.assertFalse(stderr)
- regex = re.compile("^== Ghost installation directory %s removed" % toy_installdir)
- self.assertTrue(regex.search(stdout), "Pattern '%s' found in: %s" % (regex.pattern, stdout))
+ regex = re.compile("== Ghost installation directory %s removed" % toy_installdir)
+ self.assertRegex(stdout, regex)
self.assertNotExists(toy_installdir)
@@ -3486,29 +3820,34 @@ def test_toy_build_lock(self):
mkdir(toy_lock_path, parents=True)
error_pattern = "Lock .*_software_toy_0.0.lock already exists, aborting!"
- self.assertErrorRegex(EasyBuildError, error_pattern, self.test_toy_build, raise_error=True, verbose=False)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, self._test_toy_build, raise_error=True, verbose=False)
# lock should still be there after it was hit
self.assertExists(toy_lock_path)
# trying again should give same result
- self.assertErrorRegex(EasyBuildError, error_pattern, self.test_toy_build, raise_error=True, verbose=False)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, self._test_toy_build, raise_error=True, verbose=False)
self.assertExists(toy_lock_path)
locks_dir = os.path.join(self.test_prefix, 'locks')
# no lock in place, so installation proceeds as normal
extra_args = ['--locks-dir=%s' % locks_dir]
- self.test_toy_build(extra_args=extra_args, verify=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(extra_args=extra_args, verify=True, raise_error=True)
# put lock in place in custom locks dir, try again
toy_lock_path = os.path.join(locks_dir, toy_lock_fn)
mkdir(toy_lock_path, parents=True)
- self.assertErrorRegex(EasyBuildError, error_pattern, self.test_toy_build,
- extra_args=extra_args, raise_error=True, verbose=False)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, self._test_toy_build,
+ extra_args=extra_args, raise_error=True, verbose=False)
# also test use of --ignore-locks
- self.test_toy_build(extra_args=extra_args + ['--ignore-locks'], verify=True, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(extra_args=extra_args + ['--ignore-locks'], verify=True, raise_error=True)
orig_sigalrm_handler = signal.getsignal(signal.SIGALRM)
@@ -3531,28 +3870,17 @@ def __exit__(self, type, value, traceback):
signal.alarm(0)
# wait for lock to be removed, with 1 second interval of checking;
- # check with both --wait-on-lock-interval and deprecated --wait-on-lock options
wait_regex = re.compile("^== lock .*_software_toy_0.0.lock exists, waiting 1 seconds", re.M)
ok_regex = re.compile("^== COMPLETED: Installation ended successfully", re.M)
test_cases = [
- ['--wait-on-lock=1'],
- ['--wait-on-lock=1', '--wait-on-lock-interval=60'],
- ['--wait-on-lock=100', '--wait-on-lock-interval=1'],
- ['--wait-on-lock-limit=100', '--wait-on-lock=1'],
['--wait-on-lock-limit=100', '--wait-on-lock-interval=1'],
- ['--wait-on-lock-limit=-1', '--wait-on-lock=1'],
['--wait-on-lock-limit=-1', '--wait-on-lock-interval=1'],
]
for opts in test_cases:
- if any('--wait-on-lock=' in x for x in opts):
- self.allow_deprecated_behaviour()
- else:
- self.disallow_deprecated_behaviour()
-
if not os.path.exists(toy_lock_path):
mkdir(toy_lock_path)
@@ -3564,15 +3892,12 @@ def __exit__(self, type, value, traceback):
with RemoveLockAfter(3, toy_lock_path):
self.mock_stderr(True)
self.mock_stdout(True)
- self.test_toy_build(extra_args=all_args, verify=False, raise_error=True, testing=False)
+ self._test_toy_build(extra_args=all_args, verify=False, raise_error=True, testing=False)
stderr, stdout = self.get_stderr(), self.get_stdout()
self.mock_stderr(False)
self.mock_stdout(False)
- if any('--wait-on-lock=' in x for x in all_args):
- self.assertIn("Use of --wait-on-lock is deprecated", stderr)
- else:
- self.assertEqual(stderr, '')
+ self.assertEqual(stderr, '')
wait_matches = wait_regex.findall(stdout)
# we can't rely on an exact number of 'waiting' messages, so let's go with a range...
@@ -3586,7 +3911,7 @@ def __exit__(self, type, value, traceback):
self.mock_stderr(True)
self.mock_stdout(True)
error_pattern = r"Maximum wait time for lock /.*toy_0.0.lock to be released reached: [0-9]+ sec >= 3 sec"
- self.assertErrorRegex(EasyBuildError, error_pattern, self.test_toy_build, extra_args=all_args,
+ self.assertErrorRegex(EasyBuildError, error_pattern, self._test_toy_build, extra_args=all_args,
verify=False, raise_error=True, testing=False)
stderr, stdout = self.get_stderr(), self.get_stdout()
self.mock_stderr(False)
@@ -3597,12 +3922,12 @@ def __exit__(self, type, value, traceback):
# when there is no lock in place, --wait-on-lock* has no impact
remove_dir(toy_lock_path)
- for opt in ['--wait-on-lock=1', '--wait-on-lock-limit=3', '--wait-on-lock-interval=1']:
+ for opt in ['--wait-on-lock-limit=3', '--wait-on-lock-interval=1']:
all_args = extra_args + [opt]
self.assertNotExists(toy_lock_path)
self.mock_stderr(True)
self.mock_stdout(True)
- self.test_toy_build(extra_args=all_args, verify=False, raise_error=True, testing=False)
+ self._test_toy_build(extra_args=all_args, verify=False, raise_error=True, testing=False)
stderr, stdout = self.get_stderr(), self.get_stdout()
self.mock_stderr(False)
self.mock_stdout(False)
@@ -3615,18 +3940,13 @@ def __exit__(self, type, value, traceback):
extra_args = ['--locks-dir=/']
error_pattern = r"Failed to create lock /.*_software_toy_0.0.lock:.* "
error_pattern += r"(Read-only file system|Permission denied)"
- self.assertErrorRegex(EasyBuildError, error_pattern, self.test_toy_build,
- extra_args=extra_args, raise_error=True, verbose=False)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_pattern, self._test_toy_build,
+ extra_args=extra_args, raise_error=True, verbose=False)
def test_toy_lock_cleanup_signals(self):
"""Test cleanup of locks after EasyBuild session gets a cancellation signal."""
- # skip test when using Python 2, since it somehow fails then,
- # cfr. https://github.com/easybuilders/easybuild-framework/pull/4333
- if sys.version_info[0] == 2:
- print("Skipping test_toy_lock_cleanup_signals because Python 2.x is being used")
- return
-
orig_wd = os.getcwd()
locks_dir = os.path.join(self.test_installpath, 'software', '.locks')
@@ -3657,7 +3977,9 @@ def __exit__(self, type, value, traceback):
toy_ec_txt = read_file(os.path.join(test_ecs_dir, 't', 'toy', 'toy-0.0.eb'))
test_ec = os.path.join(self.test_prefix, 'test.eb')
- write_file(test_ec, toy_ec_txt + '\npostinstallcmds = ["sleep 5"]')
+ write_file(test_ec, toy_ec_txt + '\npostinstallcmds = ["sleep 10"]')
+
+ extra_args = ['--locks-dir=%s' % locks_dir, '--wait-on-lock-limit=3', '--wait-on-lock-interval=3']
signums = [
(signal.SIGABRT, SystemExit),
@@ -3670,15 +3992,15 @@ def __exit__(self, type, value, traceback):
# avoid recycling stderr of previous test
stderr = ''
- with WaitAndSignal(1, signum):
+ with WaitAndSignal(3, signum):
# change back to original working directory before each test
change_dir(orig_wd)
self.mock_stderr(True)
self.mock_stdout(True)
- self.assertErrorRegex(exc, '.*', self.test_toy_build, ec_file=test_ec, verify=False,
- raise_error=True, testing=False, raise_systemexit=True)
+ self.assertErrorRegex(exc, '.*', self._test_toy_build, ec_file=test_ec, verify=False,
+ extra_args=extra_args, raise_error=True, testing=False, raise_systemexit=True)
stderr = self.get_stderr().strip()
self.mock_stderr(False)
@@ -3707,7 +4029,8 @@ def test_toy_build_unicode_description(self):
test_ec = os.path.join(self.test_prefix, 'test.eb')
write_file(test_ec, test_ec_txt)
- self.test_toy_build(ec_file=test_ec, raise_error=True)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec, raise_error=True)
def test_toy_build_lib64_lib_symlink(self):
"""Check whether lib64 symlink to lib subdirectory is created."""
@@ -3715,7 +4038,8 @@ def test_toy_build_lib64_lib_symlink(self):
# see https://github.com/easybuilders/easybuild-easyconfigs/issues/5776
# by default, lib64 -> lib symlink is created (--lib64-lib-symlink is enabled by default)
- self.test_toy_build()
+ with self.mocked_stdout_stderr():
+ self._test_toy_build()
toy_installdir = os.path.join(self.test_installpath, 'software', 'toy', '0.0')
lib_path = os.path.join(toy_installdir, 'lib')
@@ -3736,7 +4060,8 @@ def test_toy_build_lib64_lib_symlink(self):
# cleanup and try again with --disable-lib64-lib-symlink
remove_dir(self.test_installpath)
- self.test_toy_build(extra_args=['--disable-lib64-lib-symlink'])
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(extra_args=['--disable-lib64-lib-symlink'])
self.assertExists(lib_path)
self.assertNotExists(lib64_path)
@@ -3751,41 +4076,42 @@ def test_toy_build_lib_lib64_symlink(self):
toy_ec = os.path.join(test_ecs, 't', 'toy', 'toy-0.0.eb')
test_ec_txt = read_file(toy_ec)
- test_ec_txt += "\npostinstallcmds += ['mv %(installdir)s/lib %(installdir)s/lib64']"
test_ec = os.path.join(self.test_prefix, 'test.eb')
write_file(test_ec, test_ec_txt)
# by default, lib -> lib64 symlink is created (--lib-lib64-symlink is enabled by default)
- self.test_toy_build(ec_file=test_ec)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec)
toy_installdir = os.path.join(self.test_installpath, 'software', 'toy', '0.0')
lib_path = os.path.join(toy_installdir, 'lib')
lib64_path = os.path.join(toy_installdir, 'lib64')
- # lib64 subdir exists, is not a symlink
- self.assertExists(lib64_path)
- self.assertTrue(os.path.isdir(lib64_path))
- self.assertFalse(os.path.islink(lib64_path))
-
- # lib subdir is a symlink to lib64 subdir
+ # lib subdir exists, is not a symlink
self.assertExists(lib_path)
self.assertTrue(os.path.isdir(lib_path))
- self.assertTrue(os.path.islink(lib_path))
- self.assertTrue(os.path.samefile(lib_path, lib64_path))
+ self.assertFalse(os.path.islink(lib_path))
- # lib symlink should point to a relative path
- self.assertFalse(os.path.isabs(os.readlink(lib_path)))
+ # lib64 subdir is a symlink to lib subdir
+ self.assertExists(lib64_path)
+ self.assertTrue(os.path.isdir(lib64_path))
+ self.assertTrue(os.path.islink(lib64_path))
+ self.assertTrue(os.path.samefile(lib64_path, lib_path))
+
+ # lib64 symlink should point to a relative path
+ self.assertFalse(os.path.isabs(os.readlink(lib64_path)))
# cleanup and try again with --disable-lib-lib64-symlink
remove_dir(self.test_installpath)
- self.test_toy_build(ec_file=test_ec, extra_args=['--disable-lib-lib64-symlink'])
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec, extra_args=['--disable-lib64-lib-symlink'])
- self.assertExists(lib64_path)
- self.assertNotExists(lib_path)
- self.assertNotIn('lib', os.listdir(toy_installdir))
- self.assertTrue(os.path.isdir(lib64_path))
- self.assertFalse(os.path.islink(lib64_path))
+ self.assertExists(lib_path)
+ self.assertNotExists(lib64_path)
+ self.assertNotIn('lib64', os.listdir(toy_installdir))
+ self.assertTrue(os.path.isdir(lib_path))
+ self.assertFalse(os.path.islink(lib_path))
def test_toy_build_sanity_check_linked_libs(self):
"""Test sanity checks for banned/requires libraries."""
@@ -3805,53 +4131,62 @@ def test_toy_build_sanity_check_linked_libs(self):
error_msg = "Check for banned/required shared libraries failed for"
# default check is done via EB_libtoy easyblock, which specifies several banned/required libraries
- self.test_toy_build(ec_file=libtoy_ec, raise_error=True, verbose=False, verify=False)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=libtoy_ec, raise_error=True, verbose=False, verify=False)
remove_file(libtoy_modfile_path)
# we can make the check fail by defining environment variables picked up by the EB_libtoy easyblock
os.environ['EB_LIBTOY_BANNED_SHARED_LIBS'] = 'libtoy'
- self.assertErrorRegex(EasyBuildError, error_msg, self.test_toy_build, force=False,
- ec_file=libtoy_ec, extra_args=['--module-only'], raise_error=True, verbose=False)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_msg, self._test_toy_build, force=False,
+ ec_file=libtoy_ec, extra_args=['--module-only'], raise_error=True, verbose=False)
del os.environ['EB_LIBTOY_BANNED_SHARED_LIBS']
os.environ['EB_LIBTOY_REQUIRED_SHARED_LIBS'] = 'thisisnottheremostlikely'
- self.assertErrorRegex(EasyBuildError, error_msg, self.test_toy_build, force=False,
- ec_file=libtoy_ec, extra_args=['--module-only'], raise_error=True, verbose=False)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_msg, self._test_toy_build, force=False,
+ ec_file=libtoy_ec, extra_args=['--module-only'], raise_error=True, verbose=False)
del os.environ['EB_LIBTOY_REQUIRED_SHARED_LIBS']
# make sure default check passes (so we know better what triggered a failing test)
- self.test_toy_build(ec_file=libtoy_ec, extra_args=['--module-only'], force=False,
- raise_error=True, verbose=False, verify=False)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=libtoy_ec, extra_args=['--module-only'], force=False,
+ raise_error=True, verbose=False, verify=False)
remove_file(libtoy_modfile_path)
# check specifying banned/required libraries via EasyBuild configuration option
args = ['--banned-linked-shared-libs=%s,foobarbaz' % libtoy_fn, '--module-only']
- self.assertErrorRegex(EasyBuildError, error_msg, self.test_toy_build, force=False,
- ec_file=libtoy_ec, extra_args=args, raise_error=True, verbose=False)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_msg, self._test_toy_build, force=False,
+ ec_file=libtoy_ec, extra_args=args, raise_error=True, verbose=False)
args = ['--required-linked-shared=libs=foobarbazisnotthereforsure', '--module-only']
- self.assertErrorRegex(EasyBuildError, error_msg, self.test_toy_build, force=False,
- ec_file=libtoy_ec, extra_args=args, raise_error=True, verbose=False)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_msg, self._test_toy_build, force=False,
+ ec_file=libtoy_ec, extra_args=args, raise_error=True, verbose=False)
# check specifying banned/required libraries via easyconfig parameter
test_ec_txt = read_file(libtoy_ec)
test_ec_txt += "\nbanned_linked_shared_libs = ['toy']"
write_file(test_ec, test_ec_txt)
- self.assertErrorRegex(EasyBuildError, error_msg, self.test_toy_build, force=False,
- ec_file=test_ec, extra_args=['--module-only'], raise_error=True, verbose=False)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_msg, self._test_toy_build, force=False,
+ ec_file=test_ec, extra_args=['--module-only'], raise_error=True, verbose=False)
test_ec_txt = read_file(libtoy_ec)
test_ec_txt += "\nrequired_linked_shared_libs = ['thereisnosuchlibraryyoudummy']"
write_file(test_ec, test_ec_txt)
- self.assertErrorRegex(EasyBuildError, error_msg, self.test_toy_build, force=False,
- ec_file=test_ec, extra_args=['--module-only'], raise_error=True, verbose=False)
+ with self.mocked_stdout_stderr():
+ self.assertErrorRegex(EasyBuildError, error_msg, self._test_toy_build, force=False,
+ ec_file=test_ec, extra_args=['--module-only'], raise_error=True, verbose=False)
- # check behaviour when alternate subdirectories are specified
+ # check behaviour when alternative subdirectories are specified
test_ec_txt = read_file(libtoy_ec)
test_ec_txt += "\nbin_lib_subdirs = ['', 'lib', 'lib64']"
write_file(test_ec, test_ec_txt)
- self.test_toy_build(ec_file=test_ec, extra_args=['--module-only'], force=False,
- raise_error=True, verbose=False, verify=False)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec, extra_args=['--module-only'], force=False,
+ raise_error=True, verbose=False, verify=False)
# one last time: supercombo (with patterns that should pass the check)
test_ec_txt = read_file(libtoy_ec)
@@ -3864,8 +4199,47 @@ def test_toy_build_sanity_check_linked_libs(self):
'--required-linked-shared-libs=.*',
'--module-only',
]
- self.test_toy_build(ec_file=test_ec, extra_args=args, force=False,
- raise_error=True, verbose=False, verify=False)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec, extra_args=args, force=False,
+ raise_error=True, verbose=False, verify=False)
+
+ def test_toy_mod_files(self):
+ """Check detection of .mod files"""
+ test_ecs = os.path.join(os.path.dirname(__file__), 'easyconfigs', 'test_ecs')
+ toy_ec = os.path.join(test_ecs, 't', 'toy', 'toy-0.0.eb')
+ test_ec_txt = read_file(toy_ec)
+ test_ec = os.path.join(self.test_prefix, 'test.eb')
+ write_file(test_ec, test_ec_txt)
+
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec)
+
+ test_ec_txt += "\npostinstallcmds += ['touch %(installdir)s/lib/file.mod']"
+ write_file(test_ec, test_ec_txt)
+
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec)
+
+ args = ['--try-toolchain=GCCcore,6.2.0', '--disable-map-toolchains']
+ self.mock_stdout(True)
+ self.mock_stderr(True)
+ self._test_toy_build(ec_file=test_ec, extra_args=args)
+ stderr = self.get_stderr()
+ self.mock_stdout(False)
+ self.mock_stderr(False)
+ pattern = r"WARNING: One or more \.mod files found in .*/software/toy/0.0-GCCcore-6.2.0: .*/lib64/file.mod"
+ self.assertRegex(stderr.strip(), pattern)
+
+ args += ['--fail-on-mod-files-gcccore']
+ pattern = r"Sanity check failed: One or more \.mod files found in .*/toy/0.0-GCCcore-6.2.0: .*/lib/file.mod"
+ self.assertErrorRegex(EasyBuildError, pattern, self.run_test_toy_build_with_output, ec_file=test_ec,
+ extra_args=args, verify=False, fails=True, verbose=False, raise_error=True)
+
+ test_ec_txt += "\nskip_mod_files_sanity_check = True"
+ write_file(test_ec, test_ec_txt)
+
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec, extra_args=args)
def test_toy_ignore_test_failure(self):
"""Check whether use of --ignore-test-failure is mentioned in build output."""
@@ -3887,7 +4261,8 @@ def test_toy_post_install_patches(self):
test_ec = os.path.join(self.test_prefix, 'test.eb')
write_file(test_ec, test_ec_txt)
- self.test_toy_build(ec_file=test_ec)
+ with self.mocked_stdout_stderr():
+ self._test_toy_build(ec_file=test_ec)
toy_installdir = os.path.join(self.test_installpath, 'software', 'toy', '0.0')
toy_readme_txt = read_file(os.path.join(toy_installdir, 'README'))
@@ -3912,9 +4287,9 @@ def test_toy_unavailable_os_dep(self):
self.mock_stderr(True)
self.mock_stdout(True)
- self.test_toy_build(ec_file=test_ec, force=False, raise_error=False, verify=False,
- test_report_regexs=[r"One or more OS dependencies were not found"],
- test_report=test_report_fp)
+ self._test_toy_build(ec_file=test_ec, force=False, raise_error=False, verify=False,
+ test_report_regexs=[r"One or more OS dependencies were not found"],
+ test_report=test_report_fp)
stdout = self.get_stdout()
self.mock_stderr(False)
self.mock_stdout(False)
@@ -3943,7 +4318,7 @@ def test_toy_post_install_messages(self):
self.mock_stderr(True)
self.mock_stdout(True)
- self.test_toy_build(ec_file=test_ec, test_report=test_report_fp)
+ self._test_toy_build(ec_file=test_ec, test_report=test_report_fp)
stdout = self.get_stdout()
self.mock_stderr(False)
self.mock_stdout(False)
@@ -3969,7 +4344,7 @@ def test_toy_build_info_msg(self):
write_file(test_ec, test_ec_txt)
with self.mocked_stdout_stderr():
- self.test_toy_build(ec_file=test_ec, testing=False, verify=False, raise_error=True)
+ self._test_toy_build(ec_file=test_ec, testing=False, verify=False, raise_error=True)
stdout = self.get_stdout()
pattern = '\n'.join([
@@ -3980,6 +4355,186 @@ def test_toy_build_info_msg(self):
regex = re.compile(pattern, re.M)
self.assertTrue(regex.search(stdout), "Pattern '%s' should be found in: %s" % (regex.pattern, stdout))
+ def test_toy_failing_test_step(self):
+ """
+ Test behaviour when test step fails, using toy easyconfig.
+ """
+ test_ecs = os.path.join(os.path.dirname(__file__), 'easyconfigs', 'test_ecs')
+ toy_ec = os.path.join(test_ecs, 't', 'toy', 'toy-0.0.eb')
+
+ test_ec_txt = read_file(toy_ec)
+ test_ec_txt += '\nruntest = "false"'
+ test_ec = os.path.join(self.test_prefix, 'test.eb')
+ write_file(test_ec, test_ec_txt)
+
+ error_pattern = r"shell command 'false \.\.\.' failed in test step"
+ self.assertErrorRegex(EasyBuildError, error_pattern, self.run_test_toy_build_with_output,
+ ec_file=test_ec, raise_error=True)
+
+ def test_eb_crash(self):
+ """
+ Test behaviour when EasyBuild crashes, for example due to a buggy hook
+ """
+ hooks_file = os.path.join(self.test_prefix, 'my_hooks.py')
+ hooks_file_txt = textwrap.dedent("""
+ def pre_configure_hook(self, *args, **kwargs):
+ no_such_thing
+ """)
+ write_file(hooks_file, hooks_file_txt)
+
+ topdir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
+ toy_eb = os.path.join(topdir, 'test', 'framework', 'sandbox', 'easybuild', 'easyblocks', 't', 'toy.py')
+ toy_ec = os.path.join(topdir, 'test', 'framework', 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb')
+
+ args = [
+ toy_ec,
+ f'--hooks={hooks_file}',
+ '--force',
+ f'--installpath={self.test_prefix}',
+ f'--include-easyblocks={toy_eb}',
+ ]
+
+ with self.mocked_stdout_stderr() as (_, stderr):
+ cleanup()
+ try:
+ main_with_hooks(args=args)
+ self.assertFalse("This should never be reached, main function should have crashed!")
+ except NameError as err:
+ self.assertEqual(str(err), "name 'no_such_thing' is not defined")
+
+ regex = re.compile(r"EasyBuild crashed! Please consider reporting a bug, this should not happen")
+ stderr = stderr.getvalue()
+ self.assertTrue(regex.search(stderr), f"Pattern '{regex.pattern}' should be found in {stderr}")
+
+ def test_eb_error(self):
+ """
+ Test whether main function as run by 'eb' command print error messages to stderr.
+ """
+ topdir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
+ toy_ec = os.path.join(topdir, 'test', 'framework', 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb')
+
+ test_ec = os.path.join(self.test_prefix, 'test.eb')
+ test_ec_txt = read_file(toy_ec)
+ test_ec_txt += "\ndependencies = [('nosuchdep', '1.0')]"
+ write_file(test_ec, test_ec_txt)
+
+ with self.mocked_stdout_stderr() as (_, stderr):
+ cleanup()
+ try:
+ main_with_hooks(args=[test_ec, '--robot', '--force'])
+ except SystemExit:
+ pass
+
+ regex = re.compile("^ERROR: Missing dependencies", re.M)
+ stderr = stderr.getvalue()
+ self.assertTrue(regex.search(stderr), f"Pattern '{regex.pattern}' should be found in {stderr}")
+
+ def test_toy_python(self):
+ """
+ Test whether $PYTHONPATH or $EBPYTHONPREFIXES are set correctly.
+ """
+ # generate fake Python modules that we can use as runtime dependency for toy
+ # (required condition for use of $EBPYTHONPREFIXES)
+ fake_mods_path = os.path.join(self.test_prefix, 'modules')
+ for pyver in ('2.7', '3.6'):
+ fake_python_mod = os.path.join(fake_mods_path, 'Python', pyver)
+ if get_module_syntax() == 'Lua':
+ fake_python_mod += '.lua'
+ write_file(fake_python_mod, '')
+ else:
+ write_file(fake_python_mod, '#%Module')
+ self.modtool.use(fake_mods_path)
+
+ test_ecs = os.path.join(os.path.dirname(__file__), 'easyconfigs', 'test_ecs')
+ toy_ec = os.path.join(test_ecs, 't', 'toy', 'toy-0.0.eb')
+
+ test_ec_txt = read_file(toy_ec)
+ test_ec_txt += "\npostinstallcmds.append('mkdir -p %(installdir)s/lib/python3.6/site-packages')"
+ test_ec_txt += "\npostinstallcmds.append('touch %(installdir)s/lib/python3.6/site-packages/foo.py')"
+
+ test_ec = os.path.join(self.test_prefix, 'test.eb')
+ write_file(test_ec, test_ec_txt)
+ self.run_test_toy_build_with_output(ec_file=test_ec)
+
+ toy_mod = os.path.join(self.test_installpath, 'modules', 'all', 'toy', '0.0')
+ if get_module_syntax() == 'Lua':
+ toy_mod += '.lua'
+ toy_mod_txt = read_file(toy_mod)
+
+ pythonpath_regex = re.compile('^prepend.path.*PYTHONPATH.*lib/python3.6/site-packages', re.M)
+
+ self.assertTrue(pythonpath_regex.search(toy_mod_txt),
+ f"Pattern '{pythonpath_regex.pattern}' found in: {toy_mod_txt}")
+
+ # also check when opting in to use $EBPYTHONPREFIXES instead of $PYTHONPATH
+ args = ['--prefer-python-search-path=EBPYTHONPREFIXES']
+ self.run_test_toy_build_with_output(ec_file=test_ec, extra_args=args)
+ toy_mod_txt = read_file(toy_mod)
+ # if Python is not listed as a runtime dependency then $PYTHONPATH is still used,
+ # because the Python dependency used must be aware of $EBPYTHONPREFIXES
+ # (see sitecustomize.py installed by Python easyblock)
+ self.assertTrue(pythonpath_regex.search(toy_mod_txt),
+ f"Pattern '{pythonpath_regex.pattern}' found in: {toy_mod_txt}")
+
+ # if Python is listed as runtime dependency, then $EBPYTHONPREFIXES is used if it's preferred
+ write_file(test_ec, test_ec_txt + "\ndependencies = [('Python', '3.6', '', SYSTEM)]")
+ self.run_test_toy_build_with_output(ec_file=test_ec, extra_args=args)
+ toy_mod_txt = read_file(toy_mod)
+
+ ebpythonprefixes_regex = re.compile('^prepend.path.*EBPYTHONPREFIXES.*root', re.M)
+ self.assertTrue(ebpythonprefixes_regex.search(toy_mod_txt),
+ f"Pattern '{ebpythonprefixes_regex.pattern}' found in: {toy_mod_txt}")
+
+ # if Python is listed in multi_deps, then $EBPYTHONPREFIXES is used, even if it's not explicitely preferred
+ write_file(test_ec, test_ec_txt + "\nmulti_deps = {'Python': ['2.7', '3.6']}")
+ self.run_test_toy_build_with_output(ec_file=test_ec)
+ toy_mod_txt = read_file(toy_mod)
+
+ self.assertTrue(ebpythonprefixes_regex.search(toy_mod_txt),
+ f"Pattern '{ebpythonprefixes_regex.pattern}' found in: {toy_mod_txt}")
+
+ def test_toy_multiple_ecs_module(self):
+ """
+ Verify whether module file is correct when multiple easyconfigs are being installed.
+ """
+ test_ecs = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'easyconfigs', 'test_ecs')
+ toy_ec = os.path.join(test_ecs, 't', 'toy', 'toy-0.0.eb')
+
+ # modify 'toy' easyconfig so toy-headers subdirectory is created,
+ # which is taken into account by EB_toy easyblock for $CPATH
+ test_toy_ec = os.path.join(self.test_prefix, 'test-toy.eb')
+ toy_ec_txt = read_file(toy_ec)
+ toy_ec_txt += "\npostinstallcmds += ['mkdir %(installdir)s/toy-headers']"
+ toy_ec_txt += "\npostinstallcmds += ['touch %(installdir)s/toy-headers/toy.h']"
+ write_file(test_toy_ec, toy_ec_txt)
+
+ # modify 'toy-app' easyconfig so toy-headers subdirectory is created,
+ # which is consider by EB_toy easyblock for $CPATH,
+ # but should *not* be actually used because software name is not 'toy'
+ toy_app_ec = os.path.join(test_ecs, 't', 'toy-app', 'toy-app-0.0.eb')
+ test_toy_app_ec = os.path.join(self.test_prefix, 'test-toy-app.eb')
+ toy_ec_txt = read_file(toy_app_ec)
+ toy_ec_txt += "\npostinstallcmds += ['mkdir %(installdir)s/toy-headers']"
+ toy_ec_txt += "\npostinstallcmds += ['touch %(installdir)s/toy-headers/toy.h']"
+ write_file(test_toy_app_ec, toy_ec_txt)
+
+ self.run_test_toy_build_with_output(ec_file=test_toy_ec, extra_args=[test_toy_app_ec], raise_error=True)
+
+ toy_mod = os.path.join(self.test_installpath, 'modules', 'all', 'toy', '0.0')
+ if get_module_syntax() == 'Lua':
+ toy_mod += '.lua'
+ toy_modtxt = read_file(toy_mod)
+ regex = re.compile('prepend[-_]path.*CPATH.*toy-headers', re.M)
+ self.assertTrue(regex.search(toy_modtxt),
+ f"Pattern '{regex.pattern}' should be found in: {toy_modtxt}")
+
+ toy_app_mod = os.path.join(self.test_installpath, 'modules', 'all', 'toy-app', '0.0')
+ if get_module_syntax() == 'Lua':
+ toy_app_mod += '.lua'
+ toy_app_modtxt = read_file(toy_app_mod)
+ self.assertFalse(regex.search(toy_app_modtxt),
+ f"Pattern '{regex.pattern}' should *not* be found in: {toy_app_modtxt}")
+
def suite():
""" return all the tests in this file """
diff --git a/test/framework/tweak.py b/test/framework/tweak.py
index 24ec85b2c8..79d39ef440 100644
--- a/test/framework/tweak.py
+++ b/test/framework/tweak.py
@@ -1,5 +1,5 @@
##
-# Copyright 2014-2024 Ghent University
+# Copyright 2014-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -510,7 +510,7 @@ def test_map_easyconfig_to_target_tc_hierarchy(self):
update_build_specs={'version': new_version},
update_dep_versions=False)
tweaked_ec = process_easyconfig(tweaked_spec)[0]
- extensions = tweaked_ec['ec']['exts_list']
+ extensions = tweaked_ec['ec'].get_ref('exts_list')
# check one extension with the same name exists and that the version has been updated
hit_extension = 0
for extension in extensions:
diff --git a/test/framework/type_checking.py b/test/framework/type_checking.py
index 8b7edd3215..a9ce5df15b 100644
--- a/test/framework/type_checking.py
+++ b/test/framework/type_checking.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2015-2024 Ghent University
+# Copyright 2015-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -41,7 +41,6 @@
from easybuild.framework.easyconfig.types import to_list_of_strings_and_tuples_and_dicts
from easybuild.framework.easyconfig.types import to_sanity_check_paths_dict, to_toolchain_dict
from easybuild.tools.build_log import EasyBuildError
-from easybuild.tools.py2vs3 import string_type
class TypeCheckingTest(EnhancedTestCase):
@@ -172,16 +171,18 @@ def test_check_type_of_param_value_sanity_check_paths(self):
out = {'files': ['bin/foo', ('bin/bar', 'bin/baz')], 'dirs': [('lib', 'lib64', 'lib32')]}
self.assertEqual(check_type_of_param_value('sanity_check_paths', inp, auto_convert=True), (True, out))
- def test_check_type_of_param_value_checksums(self):
- """Test check_type_of_param_value function for checksums."""
-
- md5_checksum = 'fa618be8435447a017fd1bf2c7ae9224'
- sha256_checksum1 = 'fa618be8435447a017fd1bf2c7ae922d0428056cfc7449f7a8641edf76b48265'
- sha256_checksum2 = 'b5f9cb06105c1d2d30719db5ffb3ea67da60919fb68deaefa583deccd8813551'
- sha256_checksum3 = '033be54514a03e255df75c5aee8f9e672f663f93abb723444caec8fe43437bde'
+ @staticmethod
+ def get_valid_checksums_values():
+ """Return list of values valid for the 'checksums' EC parameter"""
+ # Using (actually invalid) prefix to better detect those in case of errors
+ md5_checksum = 'md518be8435447a017fd1bf2c7ae9224'
+ sha256_checksum1 = 'sha18be8435447a017fd1bf2c7ae922d0428056cfc7449f7a8641edf76b48265'
+ sha256_checksum2 = 'sha2cb06105c1d2d30719db5ffb3ea67da60919fb68deaefa583deccd8813551'
+ sha256_checksum3 = 'sha3e54514a03e255df75c5aee8f9e672f663f93abb723444caec8fe43437bde'
+ filesize = 45617379
# valid values for 'checksums' easyconfig parameters
- inputs = [
+ return [
[],
# single checksum (one file)
[md5_checksum],
@@ -191,6 +192,7 @@ def test_check_type_of_param_value_checksums(self):
# one checksum of specific type (as 2-tuple)
[('md5', md5_checksum)],
[('sha256', sha256_checksum1)],
+ [('size', filesize)],
# alternative checksums for a single file (n-tuple)
[(sha256_checksum1, sha256_checksum2)],
[(sha256_checksum1, sha256_checksum2, sha256_checksum3)],
@@ -214,17 +216,37 @@ def test_check_type_of_param_value_checksums(self):
# two checksums for a single file, *both* should match
[sha256_checksum1, md5_checksum],
# three checksums for a single file, *all* should match
- [sha256_checksum1, ('md5', md5_checksum), {'foo.txt': sha256_checksum1}],
+ [sha256_checksum1, ('md5', md5_checksum), ('size', filesize)],
# single checksum for a single file
sha256_checksum1,
# filename-to-checksum mapping
- {'foo.txt': sha256_checksum1, 'bar.txt': sha256_checksum2},
+ {'foo.txt': sha256_checksum1, 'bar.txt': sha256_checksum2, 'baz.txt': ('size', filesize)},
# 3 alternative checksums for a single file, one match is sufficient
(sha256_checksum1, sha256_checksum2, sha256_checksum3),
- ]
+ # two alternative checksums for a single file (not to be confused by checksum-type & -value tuple)
+ (sha256_checksum1, md5_checksum),
+ # three alternative checksums for a single file of different types
+ (sha256_checksum1, ('md5', md5_checksum), ('size', filesize)),
+ # alternative checksums in dicts are also allowed
+ {'foo.txt': (sha256_checksum2, sha256_checksum3), 'bar.txt': (sha256_checksum1, md5_checksum)},
+ # Same but with lists -> all must match for each file
+ {'foo.txt': [sha256_checksum2, sha256_checksum3], 'bar.txt': [sha256_checksum1, md5_checksum]},
+ ],
+ # None is allowed, meaning skip the checksum
+ [
+ None,
+ # Also in mappings
+ {'foo.txt': sha256_checksum1, 'bar.txt': None},
+ ],
]
- for inp in inputs:
- self.assertEqual(check_type_of_param_value('checksums', inp), (True, inp))
+
+ def test_check_type_of_param_value_checksums(self):
+ """Test check_type_of_param_value function for checksums."""
+
+ for inp in TypeCheckingTest.get_valid_checksums_values():
+ type_ok, newval = check_type_of_param_value('checksums', inp)
+ self.assertIs(type_ok, True, 'Failed for ' + str(inp))
+ self.assertEqual(newval, inp)
def test_check_type_of_param_value_patches(self):
"""Test check_type_of_param_value function for patches."""
@@ -248,9 +270,9 @@ def test_check_type_of_param_value_patches(self):
def test_convert_value_type(self):
"""Test convert_value_type function."""
# to string
- self.assertEqual(convert_value_type(100, string_type), '100')
+ self.assertEqual(convert_value_type(100, str), '100')
self.assertEqual(convert_value_type((100,), str), '(100,)')
- self.assertEqual(convert_value_type([100], string_type), '[100]')
+ self.assertEqual(convert_value_type([100], str), '[100]')
self.assertEqual(convert_value_type(None, str), 'None')
# to int/float
@@ -269,7 +291,7 @@ def test_convert_value_type(self):
self.assertEqual(convert_value_type((), LIST_OF_STRINGS), [])
# idempotency
- self.assertEqual(convert_value_type('foo', string_type), 'foo')
+ self.assertEqual(convert_value_type('foo', str), 'foo')
self.assertEqual(convert_value_type('foo', str), 'foo')
self.assertEqual(convert_value_type(100, int), 100)
self.assertEqual(convert_value_type(1.6, float), 1.6)
@@ -318,9 +340,9 @@ def test_to_toolchain_dict(self):
self.assertErrorRegex(EasyBuildError, errstr, to_toolchain_dict, ['gcc', '4', 'False', '7'])
# invalid truth value
- errstr = "invalid truth value .*"
- self.assertErrorRegex(ValueError, errstr, to_toolchain_dict, "intel, 2015, foo")
- self.assertErrorRegex(ValueError, errstr, to_toolchain_dict, ['gcc', '4', '7'])
+ errstr = "Invalid truth value .*"
+ self.assertErrorRegex(EasyBuildError, errstr, to_toolchain_dict, "intel, 2015, foo")
+ self.assertErrorRegex(EasyBuildError, errstr, to_toolchain_dict, ['gcc', '4', '7'])
# missing keys
self.assertErrorRegex(EasyBuildError, "Incorrect set of keys", to_toolchain_dict, {'name': 'intel'})
@@ -706,19 +728,94 @@ def test_to_sanity_check_paths_dict(self):
def test_to_checksums(self):
"""Test to_checksums function."""
+ # Some hand-crafted examples. Only the types are important, values are for easier verification
test_inputs = [
- ['be662daa971a640e40be5c804d9d7d10'],
- ['be662daa971a640e40be5c804d9d7d10', ('md5', 'be662daa971a640e40be5c804d9d7d10')],
- [['be662daa971a640e40be5c804d9d7d10', ('md5', 'be662daa971a640e40be5c804d9d7d10')]],
- [('md5', 'be662daa971a640e40be5c804d9d7d10')],
- ['be662daa971a640e40be5c804d9d7d10', ('adler32', '0x998410035'), ('crc32', '0x1553842328'),
- ('md5', 'be662daa971a640e40be5c804d9d7d10'), ('sha1', 'f618096c52244539d0e89867405f573fdb0b55b0'),
- ('size', 273)],
+ ['checksumvalue'],
+ [('md5', 'md5checksumvalue')],
+ ['file_1_checksum', ('md5', 'file_2_md5_checksum')],
+ # One checksum per file, some with checksum type
+ [
+ 'be662daa971a640e40be5c804d9d7d10',
+ ('adler32', '0x998410035'),
+ ('crc32', '0x1553842328'),
+ ('md5', 'be662daa971a640e40be5c804d9d7d10'),
+ ('sha1', 'f618096c52244539d0e89867405f573fdb0b55b0'),
+ # int type as the 2nd value
+ ('size', 273),
+ ],
# None values should not be filtered out, but left in place
- [None, 'fa618be8435447a017fd1bf2c7ae922d0428056cfc7449f7a8641edf76b48265', None],
+ [None, 'checksum', None],
+ # Alternative checksums, not to be confused with multiple checksums for a file
+ [('main_checksum', 'alternative_checksum')],
+ [('1st_of_3', '2nd_of_3', '3rd_of_3')],
+ # Lists must be kept: This means all must match
+ [['checksum_1_in_list']],
+ [['checksum_must_match', 'this_must_also_match']],
+ [['1st_of_3_list', '2nd_of_3_list', '3rd_of_3_list']],
+ # Alternative checksums with types
+ [
+ (('adler32', '1st_adler'), ('crc32', '1st_crc')),
+ (('adler32', '2nd_adler'), ('crc32', '2nd_crc'), ('sha1', '2nd_sha')),
+ ],
+ # Entries can be dicts even containing `None`
+ [
+ {
+ 'src-arm.tgz': 'arm_checksum',
+ 'src-x86.tgz': ('mainchecksum', 'altchecksum'),
+ 'src-ppc.tgz': ('mainchecksum', ('md5', 'altchecksum')),
+ 'git-clone.tgz': None,
+ },
+ {
+ 'src': ['checksum_must_match', 'this_must_also_match']
+ },
+ # 2nd required checksum a dict
+ ['first_checksum', {'src-arm': 'arm_checksum'}]
+ ],
]
for checksums in test_inputs:
self.assertEqual(to_checksums(checksums), checksums)
+ # Also reuse the checksums we use in test_check_type_of_param_value_checksums
+ # When a checksum is valid it must not be modified
+ for checksums in TypeCheckingTest.get_valid_checksums_values():
+ self.assertEqual(to_checksums(checksums), checksums)
+
+ # List in list converted to tuple -> alternatives or checksum with type
+ checksums = [['1stchecksum', ['md5', 'md5sum']]]
+ checksums_expected = [['1stchecksum', ('md5', 'md5sum')]]
+ self.assertEqual(to_checksums(checksums), checksums_expected)
+
+ # Error detection
+ wrong_nesting = [('1stchecksum', ('md5', ('md5sum', 'altmd5sum')))]
+ self.assertErrorRegex(EasyBuildError, 'Unexpected type.*md5', to_checksums, wrong_nesting)
+ correct_nesting = [('1stchecksum', ('md5', 'md5sum'), ('md5', 'altmd5sum'))]
+ self.assertEqual(to_checksums(correct_nesting), correct_nesting)
+ # YEB (YAML EC) doesn't has tuples so it uses lists instead which need to get converted
+ correct_nesting_yeb = [[['1stchecksum', ['md5', 'md5sum'], ['md5', 'altmd5sum']]]]
+ correct_nesting_yeb_conv = [[('1stchecksum', ('md5', 'md5sum'), ('md5', 'altmd5sum'))]]
+ self.assertEqual(to_checksums(correct_nesting_yeb), correct_nesting_yeb_conv)
+ self.assertEqual(to_checksums(correct_nesting_yeb_conv), correct_nesting_yeb_conv)
+
+ unexpected_set = [('1stchecksum', {'md5', 'md5sum'})]
+ self.assertErrorRegex(EasyBuildError, 'Unexpected type.*md5', to_checksums, unexpected_set)
+ unexpected_dict = [{'src': ('md5sum', {'src': 'shasum'})}]
+ self.assertErrorRegex(EasyBuildError, 'Unexpected type.*shasum', to_checksums, unexpected_dict)
+ correct_dict = [{'src': ('md5sum', 'shasum')}]
+ self.assertEqual(to_checksums(correct_dict), correct_dict)
+ correct_dict_1 = [{'src': [['md5', 'md5sum'], ['sha', 'shasum']]}]
+ correct_dict_2 = [{'src': [('md5', 'md5sum'), ('sha', 'shasum')]}]
+ self.assertEqual(to_checksums(correct_dict_2), correct_dict_2)
+ self.assertEqual(to_checksums(correct_dict_1), correct_dict_2) # inner lists to tuples
+
+ unexpected_Nones = [
+ [('1stchecksum', None)],
+ [['1stchecksum', None]],
+ [{'src': ('md5sum', None)}],
+ [{'src': ['md5sum', None]}],
+ ]
+ self.assertErrorRegex(EasyBuildError, 'Unexpected None', to_checksums, unexpected_Nones[0])
+ self.assertErrorRegex(EasyBuildError, 'Unexpected None', to_checksums, unexpected_Nones[1])
+ self.assertErrorRegex(EasyBuildError, 'Unexpected None', to_checksums, unexpected_Nones[2])
+ self.assertErrorRegex(EasyBuildError, 'Unexpected None', to_checksums, unexpected_Nones[3])
def test_ensure_iterable_license_specs(self):
"""Test ensure_iterable_license_specs function."""
diff --git a/test/framework/utilities.py b/test/framework/utilities.py
index d07c2202b1..27ba444d96 100644
--- a/test/framework/utilities.py
+++ b/test/framework/utilities.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -37,6 +37,7 @@
import tempfile
import unittest
from contextlib import contextmanager
+from importlib import reload
from easybuild.base import fancylogger
from easybuild.base.testing import TestCase
@@ -48,13 +49,12 @@
from easybuild.framework.easyblock import EasyBlock
from easybuild.main import main
from easybuild.tools import config
-from easybuild.tools.config import GENERAL_CLASS, Singleton, module_classes, update_build_option
+from easybuild.tools.config import GENERAL_CLASS, Singleton, module_classes
from easybuild.tools.configobj import ConfigObj
from easybuild.tools.environment import modify_env
from easybuild.tools.filetools import copy_dir, mkdir, read_file, which
from easybuild.tools.modules import curr_module_paths, modules_tool, reset_module_caches
from easybuild.tools.options import CONFIG_ENV_VAR_PREFIX, EasyBuildOptions, set_tmpdir
-from easybuild.tools.py2vs3 import reload
# make sure tests are robust against any non-default configuration settings;
@@ -106,8 +106,8 @@ def setUp(self):
os.close(fd)
self.cwd = os.getcwd()
- # keep track of original environment to restore
- self.orig_environ = copy.deepcopy(os.environ)
+ # keep track of original environment to restore after tests
+ self._initial_environ = copy.deepcopy(os.environ)
# keep track of original environment/Python search path to restore
self.orig_sys_path = sys.path[:]
@@ -131,17 +131,19 @@ def setUp(self):
if eb_path is not None:
os.environ['EB_SCRIPT_PATH'] = eb_path
+ # disable progress bars when running the tests,
+ # since it messes with test suite progress output when test installations are performed
+ os.environ['EASYBUILD_DISABLE_SHOW_PROGRESS_BAR'] = '1'
+
+ # Store the environment as setup (including the above paths) for tests to restore
+ self.orig_environ = copy.deepcopy(os.environ)
+
# make sure no deprecated behaviour is being triggered (unless intended by the test)
self.orig_current_version = eb_build_log.CURRENT_VERSION
self.disallow_deprecated_behaviour()
init_config()
- # disable progress bars when running the tests,
- # since it messes with test suite progress output when test installations are performed
- os.environ['EASYBUILD_DISABLE_SHOW_PROGRESS_BAR'] = '1'
- update_build_option('show_progress_bar', False)
-
import easybuild
# try to import easybuild.easyblocks(.generic) packages
# it's OK if it fails here, but important to import first before fiddling with sys.path
@@ -210,6 +212,14 @@ def allow_deprecated_behaviour(self):
os.environ.pop('EASYBUILD_DEPRECATED', None)
eb_build_log.CURRENT_VERSION = self.orig_current_version
+ @contextmanager
+ def temporarily_allow_deprecated_behaviour(self):
+ self.allow_deprecated_behaviour()
+ try:
+ yield
+ finally:
+ self.disallow_deprecated_behaviour()
+
@contextmanager
def log_to_testlogfile(self):
"""Context manager class to capture log output in self.logfile for the scope used. Clears the file first"""
@@ -399,7 +409,7 @@ def setup_categorized_hmns_modules(self):
src_mod_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
'modules', 'CategorizedHMNS', mod_subdir)
copy_dir(src_mod_path, os.path.join(mod_prefix, mod_subdir))
- # create empty module file directory to make C/Tcl modules happy
+ # create empty module file directory to make Environment Modules <5.0 happy
mpi_pref = os.path.join(mod_prefix, 'MPI', 'GCC', '6.4.0-2.28', 'OpenMPI', '2.1.2')
mkdir(os.path.join(mpi_pref, 'base'))
@@ -430,7 +440,8 @@ def loadTestsFromTestCase(self, test_case_class, filters):
retained_test_names = []
if len(filters) > 0:
for test_case_name in test_case_names:
- if any(filt in test_case_name for filt in filters):
+ full_test_case_name = '%s.%s' % (test_case_class.__name__, test_case_name)
+ if any(filt in full_test_case_name for filt in filters):
retained_test_names.append(test_case_name)
retained_tests = ', '.join(retained_test_names)
diff --git a/test/framework/utilities_test.py b/test/framework/utilities_test.py
index a17d7beb02..ee36a0bdff 100644
--- a/test/framework/utilities_test.py
+++ b/test/framework/utilities_test.py
@@ -1,5 +1,5 @@
##
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
@@ -36,10 +36,10 @@
from datetime import datetime
from unittest import TextTestRunner
+import easybuild.tools.utilities as tu
from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered
from easybuild.tools import LooseVersion
from easybuild.tools.build_log import EasyBuildError
-from easybuild.tools.utilities import time2str, natural_keys
class UtilitiesTest(EnhancedTestCase):
@@ -77,10 +77,10 @@ def test_time2str(self):
(datetime(2019, 8, 5, 20, 39, 44), "159 hours 25 mins 21 secs"),
]
for end, expected in test_cases:
- self.assertEqual(time2str(end - start), expected)
+ self.assertEqual(tu.time2str(end - start), expected)
error_pattern = "Incorrect value type provided to time2str, should be datetime.timedelta: <.* 'int'>"
- self.assertErrorRegex(EasyBuildError, error_pattern, time2str, 123)
+ self.assertErrorRegex(EasyBuildError, error_pattern, tu.time2str, 123)
def test_natural_keys(self):
"""Test the natural_keys function"""
@@ -98,7 +98,7 @@ def test_natural_keys(self):
]
shuffled_items = sorted_items[:]
random.shuffle(shuffled_items)
- shuffled_items.sort(key=natural_keys)
+ shuffled_items.sort(key=tu.natural_keys)
self.assertEqual(shuffled_items, sorted_items)
def test_LooseVersion(self):
@@ -140,11 +140,22 @@ def test_LooseVersion(self):
self.assertLess(LooseVersion('2.1.5'), LooseVersion('2.2'))
self.assertLess(LooseVersion('2.1.3'), LooseVersion('3'))
self.assertLessEqual(LooseVersion('2.1.0'), LooseVersion('2.2'))
- # Careful here: 1.0 > 1 !!!
- self.assertGreater(LooseVersion('1.0'), LooseVersion('1'))
- self.assertLess(LooseVersion('1'), LooseVersion('1.0'))
-
- # The following test is taken from Python distutils tests
+ # Missing components are either empty strings or zeroes
+ self.assertEqual(LooseVersion('1.0'), LooseVersion('1'))
+ self.assertEqual(LooseVersion('1'), LooseVersion('1.0'))
+ self.assertEqual(LooseVersion('1.0'), LooseVersion('1.'))
+ self.assertGreater(LooseVersion('2.1.a'), LooseVersion('2.1'))
+ self.assertGreater(LooseVersion('2.a'), LooseVersion('2'))
+
+ # checking prereleases
+ version_4beta = LooseVersion('4.0.0-beta')
+ self.assertGreater(version_4beta, LooseVersion('4.0.0'))
+ self.assertTrue(version_4beta.is_prerelease('4.0.0', ['-beta']))
+ self.assertTrue(version_4beta.is_prerelease(LooseVersion('4.0.0'), ['-beta']))
+ self.assertFalse(version_4beta.is_prerelease('4.0.0', ['rc']))
+ self.assertFalse(version_4beta.is_prerelease('4.0.0', ['rc, -beta']))
+
+ # The following test is based on the Python distutils tests
# licensed under the Python Software Foundation License Version 2
versions = (('1.5.1', '1.5.2b2', -1),
('161', '3.10a', 1),
@@ -154,16 +165,21 @@ def test_LooseVersion(self):
('2g6', '11g', -1),
('0.960923', '2.2beta29', -1),
('1.13++', '5.5.kw', -1),
- # Added from https://bugs.python.org/issue14894
('a.12.b.c', 'a.b.3', -1),
- ('1.0', '1', 1),
- ('1', '1.0', -1))
+ ('1.0', '1', 0),
+ ('1.a', '1', 1),
+ )
for v1, v2, wanted in versions:
res = LooseVersion(v1)._cmp(LooseVersion(v2))
self.assertEqual(res, wanted,
'cmp(%s, %s) should be %s, got %s' %
(v1, v2, wanted, res))
+ # Test the inverse
+ res = LooseVersion(v2)._cmp(LooseVersion(v1))
+ self.assertEqual(res, -wanted,
+ 'cmp(%s, %s) should be %s, got %s' %
+ (v2, v1, -wanted, res))
# vstring is the unparsed version
self.assertEqual(LooseVersion(v1).vstring, v1)
@@ -187,6 +203,26 @@ def test_LooseVersion(self):
self.assertEqual(LooseVersion('2.a').version, [2, 'a'])
self.assertEqual(LooseVersion('2.a5').version, [2, 'a', 5])
+ def test_unique_ordered_extend(self):
+ """Test unique_ordered_list_append method"""
+ base = ["potato", "tomato", "orange"]
+ base_orig = base.copy()
+
+ reference = ["potato", "tomato", "orange", "apple"]
+ self.assertEqual(tu.unique_ordered_extend(base, ["apple"]), reference)
+ self.assertEqual(tu.unique_ordered_extend(base, ["apple", "apple"]), reference)
+ self.assertNotEqual(tu.unique_ordered_extend(base, ["apple"]), sorted(reference))
+ # original list should not be modified
+ self.assertEqual(base, base_orig)
+
+ error_pattern = "given affix list is a string"
+ self.assertErrorRegex(EasyBuildError, error_pattern, tu.unique_ordered_extend, base, "apple")
+ error_pattern = "given affix list is not iterable"
+ self.assertErrorRegex(EasyBuildError, error_pattern, tu.unique_ordered_extend, base, 0)
+ base = "potato"
+ error_pattern = "given base cannot be extended"
+ self.assertErrorRegex(EasyBuildError, error_pattern, tu.unique_ordered_extend, base, reference)
+
def suite():
""" return all the tests in this file """
diff --git a/test/framework/variables.py b/test/framework/variables.py
index cc804d6a65..0ff7428e94 100644
--- a/test/framework/variables.py
+++ b/test/framework/variables.py
@@ -1,5 +1,5 @@
# #
-# Copyright 2012-2024 Ghent University
+# Copyright 2012-2025 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
diff --git a/test/framework/yeb.py b/test/framework/yeb.py
deleted file mode 100644
index 346ffa057b..0000000000
--- a/test/framework/yeb.py
+++ /dev/null
@@ -1,214 +0,0 @@
-# #
-# Copyright 2015-2024 Ghent University
-#
-# This file is part of EasyBuild,
-# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
-# with support of Ghent University (http://ugent.be/hpc),
-# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
-# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
-# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
-#
-# https://github.com/easybuilders/easybuild
-#
-# EasyBuild is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation v2.
-#
-# EasyBuild is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with EasyBuild. If not, see .
-# #
-"""
-Unit tests for .yeb easyconfig format
-
-@author: Caroline De Brouwer (Ghent University)
-@author: Kenneth Hoste (Ghent University)
-"""
-import glob
-import os
-import platform
-import sys
-from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered, init_config
-from unittest import TextTestRunner
-
-import easybuild.tools.build_log
-from easybuild.framework.easyconfig.easyconfig import EasyConfig
-from easybuild.framework.easyconfig.format.yeb import is_yeb_format
-from easybuild.tools import LooseVersion
-from easybuild.tools.build_log import EasyBuildError
-from easybuild.tools.config import module_classes
-from easybuild.tools.filetools import read_file
-
-
-try:
- import yaml
-except ImportError:
- pass
-
-
-class YebTest(EnhancedTestCase):
- """ Testcase for run module """
-
- def setUp(self):
- """Test setup."""
- super(YebTest, self).setUp()
- self.orig_experimental = easybuild.tools.build_log.EXPERIMENTAL
- easybuild.tools.build_log.EXPERIMENTAL = True
-
- def tearDown(self):
- """Test cleanup."""
- super(YebTest, self).tearDown()
- easybuild.tools.build_log.EXPERIMENTAL = self.orig_experimental
-
- def test_parse_yeb(self):
- """Test parsing of .yeb easyconfigs."""
- if 'yaml' not in sys.modules:
- print("Skipping test_parse_yeb (no PyYAML available)")
- return
-
- build_options = {
- 'check_osdeps': False,
- 'external_modules_metadata': {},
- 'silent': True,
- 'valid_module_classes': module_classes(),
- }
- init_config(build_options=build_options)
- easybuild.tools.build_log.EXPERIMENTAL = True
-
- testdir = os.path.dirname(os.path.abspath(__file__))
- test_easyconfigs = os.path.join(testdir, 'easyconfigs')
- test_yeb_easyconfigs = os.path.join(testdir, 'easyconfigs', 'yeb')
-
- # test parsing
- test_files = [
- 'bzip2-1.0.6-GCC-4.9.2',
- 'gzip-1.6-GCC-4.9.2',
- 'foss-2018a',
- 'intel-2018a',
- 'SQLite-3.8.10.2-foss-2018a',
- 'Python-2.7.10-intel-2018a',
- 'CrayCCE-5.1.29',
- 'toy-0.0',
- ]
-
- for filename in test_files:
- ec_yeb = EasyConfig(os.path.join(test_yeb_easyconfigs, '%s.yeb' % filename))
- # compare with parsed result of .eb easyconfig
- ec_file = glob.glob(os.path.join(test_easyconfigs, 'test_ecs', '*', '*', '%s.eb' % filename))[0]
- ec_eb = EasyConfig(ec_file)
-
- for key in sorted(ec_yeb.asdict()):
- eb_val = ec_eb[key]
- yeb_val = ec_yeb[key]
- if key == 'description':
- # multi-line string is always terminated with '\n' in YAML, so strip it off
- yeb_val = yeb_val.strip()
-
- self.assertEqual(yeb_val, eb_val)
-
- def test_yeb_get_config_obj(self):
- """Test get_config_dict method."""
- testdir = os.path.dirname(os.path.abspath(__file__))
- test_yeb_easyconfigs = os.path.join(testdir, 'easyconfigs', 'yeb')
- ec = EasyConfig(os.path.join(test_yeb_easyconfigs, 'toy-0.0.yeb'))
- ecdict = ec.parser.get_config_dict()
-
- # changes to this dict should not affect the return value of the next call to get_config_dict
- fn = 'test.tar.gz'
- ecdict['sources'].append(fn)
-
- ecdict_bis = ec.parser.get_config_dict()
- self.assertIn(fn, ecdict['sources'])
- self.assertNotIn(fn, ecdict_bis['sources'])
-
- def test_is_yeb_format(self):
- """ Test is_yeb_format function """
- testdir = os.path.dirname(os.path.abspath(__file__))
- test_yeb = os.path.join(testdir, 'easyconfigs', 'yeb', 'bzip2-1.0.6-GCC-4.9.2.yeb')
- raw_yeb = read_file(test_yeb)
-
- self.assertTrue(is_yeb_format(test_yeb, None))
- self.assertTrue(is_yeb_format(None, raw_yeb))
-
- test_eb = os.path.join(testdir, 'easyconfigs', 'test_ecs', 'g', 'gzip', 'gzip-1.4.eb')
- raw_eb = read_file(test_eb)
-
- self.assertFalse(is_yeb_format(test_eb, None))
- self.assertFalse(is_yeb_format(None, raw_eb))
-
- def test_join(self):
- """ Test yaml_join function """
- # skip test if yaml module was not loaded
- if 'yaml' not in sys.modules:
- print("Skipping test_join (no PyYAML available)")
- return
-
- stream = [
- "variables:",
- " - &f foo",
- " - &b bar",
- "",
- "fb1: !join [foo, bar]",
- "fb2: !join [*f, bar]",
- "fb3: !join [*f, *b]",
- ]
-
- # import here for testing yaml_join separately
- from easybuild.framework.easyconfig.format.yeb import yaml_join # noqa
- if LooseVersion(platform.python_version()) < LooseVersion(u'2.7'):
- loaded = yaml.load('\n'.join(stream))
- else:
- loaded = yaml.load(u'\n'.join(stream), Loader=yaml.SafeLoader)
- for key in ['fb1', 'fb2', 'fb3']:
- self.assertEqual(loaded.get(key), 'foobar')
-
- def test_bad_toolchain_format(self):
- """ Test alternate toolchain format name,version """
- if 'yaml' not in sys.modules:
- print("Skipping test_parse_yeb (no PyYAML available)")
- return
-
- # only test bad cases - the right ones are tested with the test files in test_parse_yeb
- testdir = os.path.dirname(os.path.abspath(__file__))
- test_easyconfigs = os.path.join(testdir, 'easyconfigs', 'yeb')
- expected = r'Can not convert list .* to toolchain dict. Expected 2 or 3 elements'
- self.assertErrorRegex(EasyBuildError, expected, EasyConfig,
- os.path.join(test_easyconfigs, 'bzip-bad-toolchain.yeb'))
-
- def test_external_module_toolchain(self):
- """Test specifying external (build) dependencies in yaml format."""
- if 'yaml' not in sys.modules:
- print("Skipping test_external_module_toolchain (no PyYAML available)")
- return
-
- ecpath = os.path.join(os.path.dirname(__file__), 'easyconfigs', 'yeb', 'CrayCCE-5.1.29.yeb')
- metadata = {
- 'name': ['foo', 'bar'],
- 'version': ['1.2.3', '3.2.1'],
- 'prefix': '/foo/bar',
- }
- build_options = {
- 'external_modules_metadata': {'fftw/3.3.4.0': metadata},
- 'valid_module_classes': module_classes(),
- }
- init_config(build_options=build_options)
- easybuild.tools.build_log.EXPERIMENTAL = True
-
- ec = EasyConfig(ecpath)
-
- self.assertEqual(ec.dependencies()[1]['full_mod_name'], 'fftw/3.3.4.0')
- self.assertEqual(ec.dependencies()[1]['external_module_metadata'], metadata)
-
-
-def suite():
- """ returns all the testcases in this module """
- return TestLoaderFiltered().loadTestsFromTestCase(YebTest, sys.argv[1:])
-
-
-if __name__ == '__main__':
- res = TextTestRunner(verbosity=1).run(suite())
- sys.exit(len(res.failures))