diff --git a/docs/config_reference.rst b/docs/config_reference.rst
index 356a97cec9..d03e882f53 100644
--- a/docs/config_reference.rst
+++ b/docs/config_reference.rst
@@ -467,6 +467,7 @@ ReFrame can launch containerized applications, but you need to configure properl
The type of the container platform.
Available values are the following:
+ - ``Apptainer``: The `Apptainer `__ container runtime.
- ``Docker``: The `Docker `__ container runtime.
- ``Sarus``: The `Sarus `__ container runtime.
- ``Shifter``: The `Shifter `__ container runtime.
diff --git a/reframe/core/containers.py b/reframe/core/containers.py
index 15be3766f8..6182b3d817 100644
--- a/reframe/core/containers.py
+++ b/reframe/core/containers.py
@@ -243,6 +243,7 @@ class Singularity(ContainerPlatform):
def __init__(self):
super().__init__()
self.with_cuda = False
+ self._launch_command = 'singularity'
def emit_prepare_commands(self, stagedir):
return []
@@ -259,10 +260,23 @@ def launch_command(self, stagedir):
run_opts += self.options
if self.command:
- return (f'singularity exec {" ".join(run_opts)} '
+ return (f'{self._launch_command} exec {" ".join(run_opts)} '
f'{self.image} {self.command}')
- return f'singularity run {" ".join(run_opts)} {self.image}'
+ return f'{self._launch_command} run {" ".join(run_opts)} {self.image}'
+
+
+class Apptainer(Singularity):
+ '''Container platform backend for running containers with `Apptainer
+ `__.
+
+ .. versionadded:: 4.0.0
+
+ '''
+
+ def __init__(self):
+ super().__init__()
+ self._launch_command = 'apptainer'
class ContainerPlatformField(fields.TypedField):
diff --git a/unittests/test_containers.py b/unittests/test_containers.py
index ea7134652e..b4d30724ff 100644
--- a/unittests/test_containers.py
+++ b/unittests/test_containers.py
@@ -13,7 +13,8 @@
'Sarus', 'Sarus+nocommand', 'Sarus+nopull', 'Sarus+mpi', 'Sarus+load',
'Shifter', 'Shifter+nocommand', 'Shifter+mpi', 'Shifter+nopull',
'Shifter+load',
- 'Singularity', 'Singularity+nocommand', 'Singularity+cuda'
+ 'Singularity', 'Singularity+nocommand', 'Singularity+cuda',
+ 'Apptainer', 'Apptainer+nocommand', 'Apptainer+cuda'
])
def container_variant(request):
return request.param
@@ -101,7 +102,7 @@ def expected_cmd_mount_points(container_variant):
'--mount=type=bind,source="/path/two",destination="/two" '
'--mount=type=bind,source="/foo",destination="/rfm_workdir" '
'load/library/image:tag cmd')
- elif container_variant in {'Singularity', 'Singularity+nopull'}:
+ elif container_variant == 'Singularity':
return ('singularity exec -B"/path/one:/one" '
'-B"/path/two:/two" -B"/foo:/rfm_workdir" '
'--pwd /rfm_workdir image:tag cmd')
@@ -113,6 +114,19 @@ def expected_cmd_mount_points(container_variant):
return ('singularity run -B"/path/one:/one" '
'-B"/path/two:/two" -B"/foo:/rfm_workdir" '
'--pwd /rfm_workdir image:tag')
+ elif container_variant == 'Apptainer':
+ return ('apptainer exec -B"/path/one:/one" '
+ '-B"/path/two:/two" -B"/foo:/rfm_workdir" '
+ '--pwd /rfm_workdir image:tag cmd')
+ elif container_variant == 'Apptainer+cuda':
+ return ('apptainer exec -B"/path/one:/one" '
+ '-B"/path/two:/two" -B"/foo:/rfm_workdir" --nv '
+ '--pwd /rfm_workdir image:tag cmd')
+ elif container_variant == 'Apptainer+nocommand':
+ return ('apptainer run -B"/path/one:/one" '
+ '-B"/path/two:/two" -B"/foo:/rfm_workdir" '
+ '--pwd /rfm_workdir image:tag')
+
@pytest.fixture
@@ -180,7 +194,7 @@ def expected_cmd_run_opts(container_variant):
'--mount=type=bind,source="/path/one",destination="/one" '
'--mount=type=bind,source="/foo",destination="/rfm_workdir" '
'--mpi --foo --bar image:tag cmd')
- elif container_variant in {'Singularity'}:
+ elif container_variant == 'Singularity':
return ('singularity exec -B"/path/one:/one" -B"/foo:/rfm_workdir" '
'--foo --bar image:tag cmd')
elif container_variant == 'Singularity+cuda':
@@ -189,6 +203,15 @@ def expected_cmd_run_opts(container_variant):
elif container_variant == 'Singularity+nocommand':
return ('singularity run -B"/path/one:/one" -B"/foo:/rfm_workdir" '
'--foo --bar image:tag')
+ elif container_variant == 'Apptainer':
+ return ('apptainer exec -B"/path/one:/one" -B"/foo:/rfm_workdir" '
+ '--foo --bar image:tag cmd')
+ elif container_variant == 'Apptainer+cuda':
+ return ('apptainer exec -B"/path/one:/one" -B"/foo:/rfm_workdir" '
+ '--nv --foo --bar image:tag cmd')
+ elif container_variant == 'Apptainer+nocommand':
+ return ('apptainer run -B"/path/one:/one" -B"/foo:/rfm_workdir" '
+ '--foo --bar image:tag')
def test_mount_points(container_platform, expected_cmd_mount_points):
@@ -245,6 +268,9 @@ def expected_run_with_commands(container_variant_noopt):
elif container_variant_noopt == 'Singularity':
return ("singularity exec -B\"/foo:/rfm_workdir\" "
"--foo image:tag bash -c 'cmd1; cmd2'")
+ elif container_variant_noopt == 'Apptainer':
+ return ("apptainer exec -B\"/foo:/rfm_workdir\" "
+ "--foo image:tag bash -c 'cmd1; cmd2'")
@pytest.fixture
@@ -267,6 +293,9 @@ def expected_run_with_workdir(container_variant_noopt):
elif container_variant_noopt == 'Singularity':
return ('singularity exec -B\"/foo:/rfm_workdir\" --pwd foodir '
'--foo image:tag cmd1')
+ elif container_variant_noopt == 'Apptainer':
+ return ('apptainer exec -B\"/foo:/rfm_workdir\" --pwd foodir '
+ '--foo image:tag cmd1')
def test_run_with_workdir(container_platform_with_opts,