Skip to content

Commit 11b3f46

Browse files
salexan2001mr-ckinow
authored
Enhanced detection of singularity version including a distribution detection (#1654)
Co-authored-by: Michael R. Crusoe <[email protected]> Co-authored-by: Bruno P. Kinoshita <[email protected]>
1 parent 61c13dc commit 11b3f46

File tree

3 files changed

+216
-21
lines changed

3 files changed

+216
-21
lines changed

cwltool/singularity.py

Lines changed: 74 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,40 +20,96 @@
2020
from .singularity_utils import singularity_supports_userns
2121
from .utils import CWLObjectType, create_tmp_dir, ensure_non_writable, ensure_writable
2222

23-
_SINGULARITY_VERSION = ""
24-
25-
26-
def get_version() -> str:
23+
# Cached version number of singularity
24+
# This is a list containing major and minor versions as integer.
25+
# (The number of minor version digits can vary among different distributions,
26+
# therefore we need a list here.)
27+
_SINGULARITY_VERSION: Optional[List[int]] = None
28+
# Cached flavor / distribution of singularity
29+
# Can be singularity, singularity-ce or apptainer
30+
_SINGULARITY_FLAVOR: str = ""
31+
32+
33+
def get_version() -> Tuple[List[int], str]:
34+
"""
35+
Parse the output of 'singularity --version' to determine the singularity flavor /
36+
distribution (singularity, singularity-ce or apptainer) and the singularity version.
37+
Both pieces of information will be cached.
38+
39+
Returns
40+
-------
41+
A tuple containing:
42+
- A tuple with major and minor version numbers as integer.
43+
- A string with the name of the singularity flavor.
44+
"""
2745
global _SINGULARITY_VERSION # pylint: disable=global-statement
28-
if _SINGULARITY_VERSION == "":
29-
_SINGULARITY_VERSION = check_output( # nosec
46+
global _SINGULARITY_FLAVOR # pylint: disable=global-statement
47+
if _SINGULARITY_VERSION is None:
48+
version_output = check_output( # nosec
3049
["singularity", "--version"], universal_newlines=True
3150
).strip()
32-
if _SINGULARITY_VERSION.startswith("singularity version "):
33-
_SINGULARITY_VERSION = _SINGULARITY_VERSION[20:]
34-
if _SINGULARITY_VERSION.startswith("singularity-ce version "):
35-
_SINGULARITY_VERSION = _SINGULARITY_VERSION[23:]
36-
_logger.debug(f"Singularity version: {_SINGULARITY_VERSION}.")
37-
return _SINGULARITY_VERSION
51+
52+
version_match = re.match(r"(.+) version ([0-9\.]+)", version_output)
53+
if version_match is None:
54+
raise RuntimeError("Output of 'singularity --version' not recognized.")
55+
56+
version_string = version_match.group(2)
57+
_SINGULARITY_VERSION = [int(i) for i in version_string.split(".")]
58+
_SINGULARITY_FLAVOR = version_match.group(1)
59+
60+
_logger.debug(
61+
f"Singularity version: {version_string}" " ({_SINGULARITY_FLAVOR}."
62+
)
63+
return (_SINGULARITY_VERSION, _SINGULARITY_FLAVOR)
64+
65+
66+
def is_apptainer_1_or_newer() -> bool:
67+
"""
68+
Check if apptainer singularity distribution is version 1.0 or higher.
69+
70+
Apptainer v1.0.0 is compatible with SingularityCE 3.9.5.
71+
See: https://github.com/apptainer/apptainer/releases
72+
"""
73+
v = get_version()
74+
if v[1] != "apptainer":
75+
return False
76+
return v[0][0] >= 1
3877

3978

4079
def is_version_2_6() -> bool:
41-
return get_version().startswith("2.6")
80+
"""
81+
Check if this singularity version is exactly version 2.6.
82+
83+
Also returns False if the flavor is not singularity or singularity-ce.
84+
"""
85+
v = get_version()
86+
if v[1] != "singularity" and v[1] != "singularity-ce":
87+
return False
88+
return v[0][0] == 2 and v[0][1] == 6
4289

4390

4491
def is_version_3_or_newer() -> bool:
45-
return int(get_version()[0]) >= 3
92+
"""Check if this version is singularity version 3 or newer or equivalent."""
93+
if is_apptainer_1_or_newer():
94+
return True # this is equivalent to singularity-ce > 3.9.5
95+
v = get_version()
96+
return v[0][0] >= 3
4697

4798

4899
def is_version_3_1_or_newer() -> bool:
49-
version = get_version().split(".")
50-
return int(version[0]) >= 4 or (int(version[0]) == 3 and int(version[1]) >= 1)
100+
"""Check if this version is singularity version 3.1 or newer or equivalent."""
101+
if is_apptainer_1_or_newer():
102+
return True # this is equivalent to singularity-ce > 3.9.5
103+
v = get_version()
104+
return v[0][0] >= 4 or (v[0][0] == 3 and v[0][1] >= 1)
51105

52106

53107
def is_version_3_4_or_newer() -> bool:
54108
"""Detect if Singularity v3.4+ is available."""
55-
version = get_version().split(".")
56-
return int(version[0]) >= 4 or (int(version[0]) == 3 and int(version[1]) >= 4)
109+
if is_apptainer_1_or_newer():
110+
return True # this is equivalent to singularity-ce > 3.9.5
111+
v = get_version()
112+
return v[0][0] >= 4 or (v[0][0] == 3 and v[0][1] >= 4)
57113

58114

59115
def _normalize_image_id(string: str) -> str:

tests/test_environment.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,10 @@ def PWD(v: str, env: Env) -> bool:
131131
}
132132

133133
# Singularity variables appear to be in flux somewhat.
134-
version = get_version().split(".")
135-
vmajor = int(version[0])
134+
version = get_version()[0]
135+
vmajor = version[0]
136136
assert vmajor == 3, "Tests only work for Singularity 3"
137-
vminor = int(version[1])
137+
vminor = version[1]
138138
sing_vars: EnvChecks = {
139139
"SINGULARITY_CONTAINER": None,
140140
"SINGULARITY_NAME": None,

tests/test_singularity_versions.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
"""Test singularity{,-ce} & apptainer versions."""
2+
import cwltool.singularity
3+
from cwltool.singularity import (
4+
get_version,
5+
is_apptainer_1_or_newer,
6+
is_version_2_6,
7+
is_version_3_or_newer,
8+
is_version_3_1_or_newer,
9+
is_version_3_4_or_newer,
10+
)
11+
12+
from subprocess import check_output # nosec
13+
14+
15+
def reset_singularity_version_cache() -> None:
16+
"""Reset the cache for testing."""
17+
cwltool.singularity._SINGULARITY_VERSION = None
18+
cwltool.singularity._SINGULARITY_FLAVOR = ""
19+
20+
21+
def set_dummy_check_output(name: str, version: str) -> None:
22+
"""Mock out subprocess.check_output."""
23+
cwltool.singularity.check_output = ( # type: ignore[attr-defined]
24+
lambda c, universal_newlines: name + " version " + version
25+
)
26+
27+
28+
def restore_check_output() -> None:
29+
"""Undo the mock of subprocess.check_output."""
30+
cwltool.singularity.check_output = check_output # type: ignore[attr-defined]
31+
32+
33+
def test_get_version() -> None:
34+
"""Confirm expected types of singularity.get_version()."""
35+
set_dummy_check_output("apptainer", "1.0.1")
36+
reset_singularity_version_cache()
37+
v = get_version()
38+
assert isinstance(v, tuple)
39+
assert isinstance(v[0], list)
40+
assert isinstance(v[1], str)
41+
assert (
42+
cwltool.singularity._SINGULARITY_VERSION is not None
43+
) # pylint: disable=protected-access
44+
assert (
45+
len(cwltool.singularity._SINGULARITY_FLAVOR) > 0
46+
) # pylint: disable=protected-access
47+
v_cached = get_version()
48+
assert v == v_cached
49+
50+
assert v[0][0] == 1
51+
assert v[0][1] == 0
52+
assert v[0][2] == 1
53+
assert v[1] == "apptainer"
54+
55+
set_dummy_check_output("singularity", "3.8.5")
56+
reset_singularity_version_cache()
57+
v = get_version()
58+
59+
assert v[0][0] == 3
60+
assert v[0][1] == 8
61+
assert v[0][2] == 5
62+
assert v[1] == "singularity"
63+
restore_check_output()
64+
65+
66+
def test_version_checks() -> None:
67+
"""Confirm logic in the various singularity version checks."""
68+
set_dummy_check_output("apptainer", "1.0.1")
69+
reset_singularity_version_cache()
70+
assert is_apptainer_1_or_newer()
71+
assert not is_version_2_6()
72+
assert is_version_3_or_newer()
73+
assert is_version_3_1_or_newer()
74+
assert is_version_3_4_or_newer()
75+
76+
set_dummy_check_output("apptainer", "0.0.1")
77+
reset_singularity_version_cache()
78+
assert not is_apptainer_1_or_newer()
79+
assert not is_version_2_6()
80+
assert not is_version_3_or_newer()
81+
assert not is_version_3_1_or_newer()
82+
assert not is_version_3_4_or_newer()
83+
84+
set_dummy_check_output("singularity", "0.0.1")
85+
reset_singularity_version_cache()
86+
assert not is_apptainer_1_or_newer()
87+
assert not is_version_2_6()
88+
assert not is_version_3_or_newer()
89+
assert not is_version_3_1_or_newer()
90+
assert not is_version_3_4_or_newer()
91+
92+
set_dummy_check_output("singularity", "0.1")
93+
reset_singularity_version_cache()
94+
assert not is_apptainer_1_or_newer()
95+
assert not is_version_2_6()
96+
assert not is_version_3_or_newer()
97+
assert not is_version_3_1_or_newer()
98+
assert not is_version_3_4_or_newer()
99+
100+
set_dummy_check_output("singularity", "2.6")
101+
reset_singularity_version_cache()
102+
assert not is_apptainer_1_or_newer()
103+
assert is_version_2_6()
104+
assert not is_version_3_or_newer()
105+
assert not is_version_3_1_or_newer()
106+
assert not is_version_3_4_or_newer()
107+
108+
set_dummy_check_output("singularity", "3.0")
109+
reset_singularity_version_cache()
110+
assert not is_apptainer_1_or_newer()
111+
assert not is_version_2_6()
112+
assert is_version_3_or_newer()
113+
assert not is_version_3_1_or_newer()
114+
assert not is_version_3_4_or_newer()
115+
116+
set_dummy_check_output("singularity", "3.1")
117+
reset_singularity_version_cache()
118+
assert not is_apptainer_1_or_newer()
119+
assert not is_version_2_6()
120+
assert is_version_3_or_newer()
121+
assert is_version_3_1_or_newer()
122+
assert not is_version_3_4_or_newer()
123+
124+
set_dummy_check_output("singularity", "3.4")
125+
reset_singularity_version_cache()
126+
assert not is_apptainer_1_or_newer()
127+
assert not is_version_2_6()
128+
assert is_version_3_or_newer()
129+
assert is_version_3_1_or_newer()
130+
assert is_version_3_4_or_newer()
131+
132+
set_dummy_check_output("singularity", "3.6.3")
133+
reset_singularity_version_cache()
134+
assert not is_apptainer_1_or_newer()
135+
assert not is_version_2_6()
136+
assert is_version_3_or_newer()
137+
assert is_version_3_1_or_newer()
138+
assert is_version_3_4_or_newer()
139+
restore_check_output()

0 commit comments

Comments
 (0)