Skip to content

Commit 29259b3

Browse files
committed
Use a wrapper script to add MPI envs to singularity binary
1 parent bff8585 commit 29259b3

2 files changed

Lines changed: 126 additions & 10 deletions

File tree

cwltool/singularity.py

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Support for executing Docker format containers using Singularity {2,3}.x or Apptainer 1.x."""
22

3+
import atexit
34
import copy
45
import hashlib
56
import json
@@ -11,7 +12,9 @@
1112
import sys
1213
import threading
1314
from collections.abc import Callable, MutableMapping
15+
from importlib.resources import files
1416
from subprocess import check_call, check_output, run # nosec
17+
from tempfile import NamedTemporaryFile
1518
from typing import cast
1619

1720
from mypy_extensions import mypyc_attr
@@ -590,23 +593,50 @@ def create_runtime(
590593
"""Return the Singularity runtime list of commands and options."""
591594
any_path_okay = self.builder.get_requirement("DockerRequirement")[1] or False
592595

593-
runtime = [
594-
"singularity",
595-
"--quiet",
596-
"run" if (is_apptainer_1_1_or_newer() or is_version_3_10_or_newer()) else "exec",
597-
"--ipc",
598-
"--contain",
599-
]
600596
mpi_req, is_req = self.builder.get_requirement(MPIRequirementName)
601-
if not mpi_req or not is_req:
602-
runtime.append("--cleanenv")
603-
else:
597+
mpi_enabled = mpi_req and is_req
598+
mpi_env_vars_reference_file_name: str | None = None
599+
runtime: list[str] = []
600+
if mpi_enabled:
601+
# Save current environment variables. The ``cwl_singularity_wrapper.sh`` will
602+
# diff it against the env vars produced by mpirun/srun/etc., and use the new
603+
# env vars as Singularity ``--env`` arguments for MPI.
604+
with NamedTemporaryFile(mode="wb", delete=False) as f:
605+
for k, v in os.environ.items():
606+
f.write(f"{k}={v}\n")
607+
mpi_env_vars_reference_file_name = f.name
608+
609+
def delete_mpi_baseline_env() -> None:
610+
"""Clean up the MPI baseline environment variables file at exit."""
611+
try:
612+
os.remove(mpi_env_vars_reference_file_name)
613+
except FileNotFoundError:
614+
pass
615+
atexit.register(delete_mpi_baseline_env)
616+
617+
runtime.extend([
618+
str(files("cwltool") / "singularity_wrapper.sh"),
619+
mpi_env_vars_reference_file_name
620+
])
621+
622+
# MPI implementations like OpenMPI and MPICH use shared memory.
604623
self.append_volume(
605624
runtime,
606625
runtime_context.create_tmpdir(),
607626
"/dev/shm",
608627
writable=True,
609628
)
629+
else:
630+
runtime.append("singularity")
631+
632+
runtime.extend([
633+
"--quiet",
634+
"run" if (is_apptainer_1_1_or_newer() or is_version_3_10_or_newer()) else "exec",
635+
"--contain",
636+
"--ipc",
637+
"--cleanenv",
638+
])
639+
610640
if is_apptainer_1_1_or_newer() or is_version_3_10_or_newer():
611641
runtime.append("--no-eval")
612642

cwltool/singularity_wrapper.sh

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# singularity_wrapper.sh
5+
#
6+
# DESCRIPTION
7+
# Wrapper around Singularity/Apptainer for CWL + MPI + Singularity.
8+
#
9+
# 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).
12+
#
13+
# This allows CWL (which uses --cleanenv) to launch MPI + Singularity.
14+
#
15+
# USAGE
16+
# singularity_wrapper.sh <baseline-env-file> <singularity-bin> <args>
17+
#
18+
# ARGUMENTS
19+
# <baseline-env-file>
20+
# Path to the file containing KEY=VALUE pairs with the baseline env.
21+
#
22+
# <singularity-bin>
23+
# Path to singularity/apptainer executable.
24+
#
25+
# [args...]
26+
# Arguments passed to the singularity binary.
27+
#
28+
# EXAMPLE
29+
# singularity_wrapper.sh env.txt singularity --cleanenv exec image.sif
30+
#
31+
32+
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
39+
exit 1
40+
}
41+
42+
if [[ "${1:-}" == "--help" ]]; then
43+
grep '^#' "$0" | sed 's/^#//' >&2
44+
exit 0
45+
fi
46+
47+
[[ $# -ge 2 ]] || usage
48+
49+
BASELINE_FILE="$1"
50+
SINGULARITY_BIN="$2"
51+
shift 2
52+
53+
if [[ ! -f "$BASELINE_FILE" ]]; then
54+
echo "Error: baseline env file not found: $BASELINE_FILE" >&2
55+
exit 2
56+
fi
57+
58+
if [[ ! -x "$SINGULARITY_BIN" ]]; then
59+
echo "Error: singularity binary not executable: $SINGULARITY_BIN" >&2
60+
exit 3
61+
fi
62+
63+
# Read baseline env into an array.
64+
declare -A BASE_ENV
65+
while IFS='=' read -r k v; do
66+
[[ -n "$k" && -n "$v" ]] || continue
67+
BASE_ENV["$k"]="$v"
68+
done < "$BASELINE_FILE"
69+
70+
# Build new environment variables.
71+
ENV_ARGS=()
72+
while IFS='=' read -r k v; do
73+
[[ -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")
78+
fi
79+
done < <(env)
80+
81+
# Debug
82+
# echo "Adding env vars into Singularity command: ${#ENV_ARGS[@]}" >&2
83+
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}"

0 commit comments

Comments
 (0)