|
| 1 | +# Copyright 2022-2023 Ghent University |
| 2 | +# |
| 3 | +# This file is part of EasyBuild, |
| 4 | +# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en), |
| 5 | +# with support of Ghent University (http://ugent.be/hpc), |
| 6 | +# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be), |
| 7 | +# Flemish Research Foundation (FWO) (http://www.fwo.be/en) |
| 8 | +# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en). |
| 9 | +# |
| 10 | +# https://github.com/easybuilders/easybuild |
| 11 | +# |
| 12 | +# EasyBuild is free software: you can redistribute it and/or modify |
| 13 | +# it under the terms of the GNU General Public License as published by |
| 14 | +# the Free Software Foundation v2. |
| 15 | +# |
| 16 | +# EasyBuild is distributed in the hope that it will be useful, |
| 17 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 18 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 19 | +# GNU General Public License for more details. |
| 20 | +# |
| 21 | +# You should have received a copy of the GNU General Public License |
| 22 | +# along with EasyBuild. If not, see <http://www.gnu.org/licenses/>. |
| 23 | +# |
| 24 | +""" |
| 25 | +Support for generating Apptainer container recipes and creating container images |
| 26 | +
|
| 27 | +:author: Kenneth Hoste (HPC-UGent) |
| 28 | +""" |
| 29 | +import os |
| 30 | +import re |
| 31 | + |
| 32 | +from easybuild.tools.build_log import EasyBuildError, print_msg |
| 33 | +from easybuild.tools.containers.singularity import SingularityContainer |
| 34 | +from easybuild.tools.config import CONT_IMAGE_FORMAT_EXT3, CONT_IMAGE_FORMAT_SANDBOX |
| 35 | +from easybuild.tools.config import CONT_IMAGE_FORMAT_SIF, CONT_IMAGE_FORMAT_SQUASHFS |
| 36 | +from easybuild.tools.config import build_option, container_path |
| 37 | +from easybuild.tools.filetools import remove_file, which |
| 38 | +from easybuild.tools.run import run_cmd |
| 39 | + |
| 40 | + |
| 41 | +class ApptainerContainer(SingularityContainer): |
| 42 | + |
| 43 | + TOOLS = {'apptainer': '1.0', 'sudo': None} |
| 44 | + |
| 45 | + RECIPE_FILE_NAME = 'Apptainer' |
| 46 | + |
| 47 | + @staticmethod |
| 48 | + def apptainer_version(): |
| 49 | + """Get Apptainer version.""" |
| 50 | + version_cmd = "apptainer --version" |
| 51 | + out, ec = run_cmd(version_cmd, simple=False, trace=False, force_in_dry_run=True) |
| 52 | + if ec: |
| 53 | + raise EasyBuildError("Error running '%s': %s for tool {1} with output: {2}" % (version_cmd, out)) |
| 54 | + |
| 55 | + res = re.search(r"\d+\.\d+(\.\d+)?", out.strip()) |
| 56 | + if not res: |
| 57 | + raise EasyBuildError("Error parsing Apptainer version: %s" % out) |
| 58 | + |
| 59 | + return res.group(0) |
| 60 | + |
| 61 | + def build_image(self, recipe_path): |
| 62 | + """Build container image by calling out to 'sudo apptainer build'.""" |
| 63 | + |
| 64 | + cont_path = container_path() |
| 65 | + def_file = os.path.basename(recipe_path) |
| 66 | + |
| 67 | + # use --imagename if specified, otherwise derive based on filename of recipe |
| 68 | + img_name = self.img_name |
| 69 | + if img_name is None: |
| 70 | + # definition file Apptainer.<app>-<version, container name <app>-<version>.<img|simg> |
| 71 | + img_name = def_file.split('.', 1)[1] |
| 72 | + |
| 73 | + cmd_opts = '' |
| 74 | + |
| 75 | + image_format = self.image_format |
| 76 | + |
| 77 | + # singularity image format (default for Apptainer) |
| 78 | + if image_format in [None, CONT_IMAGE_FORMAT_SQUASHFS, CONT_IMAGE_FORMAT_SIF]: |
| 79 | + img_path = os.path.join(cont_path, img_name + '.sif') |
| 80 | + |
| 81 | + # ext3 image format, creating as writable container |
| 82 | + elif image_format == CONT_IMAGE_FORMAT_EXT3: |
| 83 | + raise EasyBuildError("ext3 image format is not supported with Apptainer") |
| 84 | + |
| 85 | + # sandbox image format, creates as a directory but acts like a container |
| 86 | + elif image_format == CONT_IMAGE_FORMAT_SANDBOX: |
| 87 | + img_path = os.path.join(cont_path, img_name) |
| 88 | + cmd_opts = '--sandbox' |
| 89 | + |
| 90 | + else: |
| 91 | + raise EasyBuildError("Unknown container image format specified for Apptainer: %s" % image_format) |
| 92 | + |
| 93 | + if os.path.exists(img_path): |
| 94 | + if build_option('force'): |
| 95 | + print_msg("WARNING: overwriting existing container image at %s due to --force" % img_path) |
| 96 | + remove_file(img_path) |
| 97 | + else: |
| 98 | + raise EasyBuildError("Container image already exists at %s, not overwriting it without --force", |
| 99 | + img_path) |
| 100 | + |
| 101 | + # resolve full path to 'apptainer' binary, since it may not be available via $PATH under sudo... |
| 102 | + apptainer = which('apptainer') |
| 103 | + cmd_env = '' |
| 104 | + |
| 105 | + apptainer_tmpdir = self.tmpdir |
| 106 | + if apptainer_tmpdir: |
| 107 | + cmd_env += 'APPTAINER_TMPDIR=%s' % apptainer_tmpdir |
| 108 | + |
| 109 | + cmd = ' '.join(['sudo', cmd_env, apptainer, 'build', cmd_opts, img_path, recipe_path]) |
| 110 | + print_msg("Running '%s', you may need to enter your 'sudo' password..." % cmd) |
| 111 | + run_cmd(cmd, stream_output=True) |
| 112 | + print_msg("Apptainer image created at %s" % img_path, log=self.log) |
0 commit comments