Skip to content

Commit 603e6d2

Browse files
committed
Fix wrapper (passing on MN5) add tests use MpiConfig
1 parent 9e4a36a commit 603e6d2

4 files changed

Lines changed: 153 additions & 41 deletions

File tree

cwltool/mpi.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def __init__(
2323
env_pass: list[str] | None = None,
2424
env_pass_regex: list[str] | None = None,
2525
env_set: Mapping[str, str] | None = None,
26+
shm_enabled: bool = True
2627
) -> None:
2728
"""
2829
Initialize from the argument mapping.
@@ -35,6 +36,7 @@ def __init__(
3536
env_pass: []
3637
env_pass_regex: []
3738
env_set: {}
39+
shm_enabled: True
3840
3941
Any unknown keys will result in an exception.
4042
"""
@@ -45,6 +47,7 @@ def __init__(
4547
self.env_pass = env_pass or []
4648
self.env_pass_regex = env_pass_regex or []
4749
self.env_set = env_set or {}
50+
self.shm_enabled = shm_enabled
4851

4952
@classmethod
5053
def load(cls: type[MpiConfigT], config_file_name: str) -> MpiConfigT:

cwltool/singularity.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -598,13 +598,14 @@ def create_runtime(
598598

599599
mpi_req, is_req = self.builder.get_requirement(MPIRequirementName)
600600
mpi_enabled = mpi_req and is_req
601+
mpi_config = runtime_context.mpi_config
601602
mpi_env_vars_reference_file_name: str | None = None
602603
runtime: list[str] = []
603604
if mpi_enabled:
604-
# Save current environment variables. The ``cwl_singularity_wrapper.sh`` will
605+
# Save current environment variables. The ``singularity_wrapper.sh`` will
605606
# diff it against the env vars produced by mpirun/srun/etc., and use the new
606-
# env vars as Singularity ``--env`` arguments for MPI.
607-
with NamedTemporaryFile(mode="wb", delete=False) as f:
607+
# env vars as SINGULARITYENV_... for Singularity.
608+
with NamedTemporaryFile(mode="w+", delete=False) as f:
608609
for k, v in os.environ.items():
609610
f.write(f"{k}={v}\n")
610611
mpi_env_vars_reference_file_name = f.name
@@ -619,16 +620,9 @@ def delete_mpi_baseline_env() -> None:
619620

620621
runtime.extend([
621622
str(files("cwltool") / "singularity_wrapper.sh"),
622-
mpi_env_vars_reference_file_name
623+
mpi_env_vars_reference_file_name,
624+
"singularity"
623625
])
624-
625-
# MPI implementations like OpenMPI and MPICH use shared memory.
626-
self.append_volume(
627-
runtime,
628-
runtime_context.create_tmpdir(),
629-
"/dev/shm",
630-
writable=True,
631-
)
632626
else:
633627
runtime.append("singularity")
634628

@@ -639,6 +633,15 @@ def delete_mpi_baseline_env() -> None:
639633
"--ipc",
640634
"--cleanenv",
641635
])
636+
if mpi_enabled and mpi_config.shm_enabled:
637+
# MPI implementations like OpenMPI and MPICH use shared memory.
638+
self.append_volume(
639+
runtime,
640+
runtime_context.create_tmpdir(),
641+
"/dev/shm",
642+
writable=True,
643+
)
644+
642645
if is_apptainer_1_1_or_newer() or is_version_3_10_or_newer():
643646
runtime.append("--no-eval")
644647

cwltool/singularity_wrapper.sh

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ set -euo pipefail
77
# Wrapper around Singularity/Apptainer for CWL + MPI + Singularity.
88
#
99
# This script identifies environment variables added by an MPI launcher
10-
# (e.g. srun, mpirun) and adds these environment variables as command
11-
# line arguments to singularity (with --env).
10+
# (e.g. srun, mpirun) and adds these environment variables as Singularity
11+
# environment variables using the format ``SINGULARITYENV_$KEY=$VALUE``.
1212
#
13-
# This allows CWL (which uses --cleanenv) to launch MPI + Singularity.
13+
# This allows CWL (which uses ``--cleanenv``) to launch MPI + Singularity.
1414
#
1515
# USAGE
1616
# singularity_wrapper.sh <baseline-env-file> <singularity-bin> <args>
@@ -28,20 +28,23 @@ set -euo pipefail
2828
# EXAMPLE
2929
# singularity_wrapper.sh env.txt singularity --cleanenv exec image.sif
3030
#
31+
# DEPENDENCIES
32+
# It uses the following binaries:
33+
# - printenv
3134

3235
usage() {
33-
cat <<'EOF' >&2
34-
Usage:
35-
singularity_wrapper.sh <baseline-env-file> <singularity-bin> [args...]
36-
37-
Run with --help to see full documentation.
38-
EOF
36+
# Print usage using the top comment of this file, below the shebang.
37+
# Regexes:
38+
# - /^#!/d -> Delete (/d) a comment starting with #! (the shebang, above);
39+
# - /^#/,$ {} -> Capture the line that starts with # until the end of the file and run {...};
40+
# - /^#/p; -> Print (/p) lines starting with # (any comment after the shebang);
41+
# - /^[^#]/q -> And quit (/q) as soon as the lines doesn't start with # (^[^#]).
42+
sed -n '/^#!/d; /^#/,$ { /^#/p; /^[^#]/q }' "$0" | sed 's/^# \{0,1\}//' >&2
3943
exit 1
4044
}
4145

4246
if [[ "${1:-}" == "--help" ]]; then
43-
grep '^#' "$0" | sed 's/^#//' >&2
44-
exit 0
47+
usage
4548
fi
4649

4750
[[ $# -ge 2 ]] || usage
@@ -55,32 +58,27 @@ if [[ ! -f "$BASELINE_FILE" ]]; then
5558
exit 2
5659
fi
5760

58-
if [[ ! -x "$SINGULARITY_BIN" ]]; then
59-
echo "Error: singularity binary not executable: $SINGULARITY_BIN" >&2
60-
exit 3
61-
fi
62-
6361
# Read baseline env into an array.
6462
declare -A BASE_ENV
6563
while IFS='=' read -r k v; do
6664
[[ -n "$k" && -n "$v" ]] || continue
6765
BASE_ENV["$k"]="$v"
6866
done < "$BASELINE_FILE"
6967

70-
# Build new environment variables.
71-
ENV_ARGS=()
68+
# Build new environment variables for Singularity (i.e. ``SINGULARITYENV_KEY=VALUE``).
69+
# Excludes empty variables and variables whose name do not follow POSIX (e.g. some
70+
# Bash environments on HPC clusters such as BSC MareNostrum5, ``BASH_FUNC_module%%=``).
7271
while IFS='=' read -r k v; do
7372
[[ -n "$k" ]] || continue
74-
# If the current env doesn't exist (! -v) in the given baseline env (BASE_ENV),
75-
# then we want to add it as --env in singularity.
76-
if [[ ! -v BASE_ENV[$k] ]]; then
77-
ENV_ARGS+=(--env "$k=$v")
73+
[[ "$k" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
74+
# If the current env doesn't exist (``! -z``) in the given baseline env (``BASE_ENV``),
75+
# then we want to add it as ``--env`` in singularity.
76+
if [[ -z "${BASE_ENV[$k]+x}" ]]; then
77+
# Debug
78+
# echo "Adding env var for Singularity command: SINGULARITYENV_$k=$v" >&2
79+
export "SINGULARITYENV_$k=$v"
7880
fi
79-
done < <(env)
80-
81-
# Debug
82-
# echo "Adding env vars into Singularity command: ${#ENV_ARGS[@]}" >&2
81+
done < <(printenv)
8382

84-
# Launch wrapped singularity command with the singularity executable, followed
85-
# by the new --env arguments, and finally with all the rest the CWL runner had.
86-
exec "$1" "${ENV_ARGS[@]}" "${@:2}"
83+
# Launch the Singularity binary.
84+
exec "$SINGULARITY_BIN" "${@}"

tests/test_singularity_wrapper.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"""Tests for the Shell wrapper of the Singularity command.
2+
3+
This script tests a Shell script. This script does not contribute to the
4+
project test coverage (although kcov, or bats+kcov could be used in the
5+
future).
6+
"""
7+
8+
import os
9+
import subprocess
10+
from importlib.resources import files
11+
from textwrap import dedent
12+
from typing import TYPE_CHECKING
13+
14+
import pytest
15+
16+
if TYPE_CHECKING:
17+
from pathlib import Path
18+
19+
20+
@pytest.mark.parametrize(
21+
"args,expected_return_code",
22+
[
23+
(["--help"], 1),
24+
([""], 1),
25+
(["singularity"], 1)
26+
],
27+
ids=[
28+
"Print usage because user passed --help",
29+
"Print usage because of missing args",
30+
"Print usage because not all args provided"
31+
]
32+
)
33+
def test_wrapper_usage(args: list[str], expected_return_code: int) -> None:
34+
"""Test the usage of the Singularity wrapper is printed."""
35+
wrapper = str(files("cwltool") / "singularity_wrapper.sh")
36+
command: list[str] = [wrapper] + args
37+
result = subprocess.run(
38+
command,
39+
capture_output=True,
40+
text=True
41+
)
42+
43+
assert result.returncode == expected_return_code
44+
assert "Wrapper around Singularity/Apptainer for CWL + MPI + Singularity" in result.stderr
45+
46+
47+
def test_wrapper_invalid_baseline_env_file() -> None:
48+
"""Test the script fails if the given file is not valid."""
49+
wrapper = str(files("cwltool") / "singularity_wrapper.sh")
50+
command: list[str] = [wrapper, "parangaricutirimicuaro.dat", "foo"]
51+
result = subprocess.run(
52+
command,
53+
capture_output=True,
54+
text=True
55+
)
56+
57+
assert result.returncode == 2
58+
assert "file not found" in result.stderr
59+
60+
61+
def test_wrapper_env_vars(tmp_path: "Path") -> None:
62+
"""Test that the wrapper script adds the new environment variables."""
63+
fake_singularity = tmp_path / "fake_singularity"
64+
fake_singularity.write_text(dedent(f"""\
65+
#!/bin/bash
66+
67+
echo "Fake Singularity script"
68+
env
69+
"""))
70+
fake_singularity.chmod(0o755)
71+
72+
new_env_var = "TEST_WRAPPER_ENV_VARS_INJECTED_VAR"
73+
74+
# Create the baseline environment variables file.
75+
baseline_env = os.environ
76+
assert new_env_var not in baseline_env, "The test needs a new env var!"
77+
baseline = tmp_path / "baseline.env"
78+
baseline.write_text("A=1\nB=2\n")
79+
for k, v in baseline_env.items():
80+
baseline.write_text(f"{k}={v}")
81+
82+
# Now pretend we are mpirun, and we are adding a new env var.
83+
new_env = os.environ.copy()
84+
new_env[new_env_var] = "42"
85+
86+
wrapper = str(files("cwltool") / "singularity_wrapper.sh")
87+
command: list[str] = [
88+
wrapper,
89+
str(baseline),
90+
str(fake_singularity),
91+
"--cleanenv"
92+
]
93+
94+
result = subprocess.run(
95+
command,
96+
capture_output=True,
97+
text=True,
98+
env=new_env
99+
)
100+
101+
assert result.returncode == 0
102+
# There, now the wrapper just runs `env`, and the output must
103+
# contain the new environment variable. We know the wrapper
104+
# must have worked because we have thew new variable in the
105+
# output...
106+
assert new_env_var in result.stdout
107+
# And also because we have the new SINGULARITYENV_{new_env_var}!
108+
assert f"SINGULARITYENV_{new_env_var}" in result.stdout

0 commit comments

Comments
 (0)