Skip to content

Automatically initialize Apptainer #20447

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: dev
Choose a base branch
from
1 change: 1 addition & 0 deletions lib/galaxy/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ def _configure_toolbox(self):
involucro_path=self.config.involucro_path,
involucro_auto_init=self.config.involucro_auto_init,
mulled_channels=self.config.mulled_channels,
apptainer_prefix=self.config.apptainer_prefix,
)
mulled_resolution_cache = None
if self.config.mulled_resolution_cache_type:
Expand Down
8 changes: 8 additions & 0 deletions lib/galaxy/config/schemas/config_schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,14 @@ mapping:
This will prevent problems with some specific packages (perl, R), at the cost
of extra disk space usage and extra time spent copying packages.

apptainer_prefix:
type: str
default: _apptainer
path_resolves_to: tool_dependency_dir
required: false
desc: |
Apptainer installation prefix.

local_conda_mapping_file:
type: str
default: 'local_conda_mapping.yml'
Expand Down
17 changes: 17 additions & 0 deletions lib/galaxy/jobs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
from galaxy.util.bunch import Bunch
from galaxy.util.expressions import ExpressionContext
from galaxy.util.path import external_chown
from galaxy.util.properties import running_from_source
from galaxy.util.xml_macros import load
from galaxy.web_stack.handlers import ConfiguresHandlers
from galaxy.work.context import WorkRequestContext
Expand Down Expand Up @@ -1162,6 +1163,22 @@ def cleanup_job(self):
def requires_containerization(self):
return util.asbool(self.get_destination_configuration("require_container", "False"))

@property
def use_metadata_venv(self):
return util.asbool(self.get_destination_configuration("use_metadata_venv", not running_from_source))

@property
def create_metadata_venv(self):
return util.asbool(self.get_destination_configuration("create_metadata_venv", self.use_metadata_venv))

@property
def metadata_venv_python(self):
return self.get_destination_configuration("metadata_venv_python", sys.executable)

@property
def metadata_venv_path(self):
return util.asbool(self.get_destination_configuration("metadata_venv_path", "False")) or None

@property
def use_metadata_binary(self):
return util.asbool(self.get_destination_configuration("use_metadata_binary", "False"))
Expand Down
116 changes: 116 additions & 0 deletions lib/galaxy/tool_util/deps/apptainer_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import logging
import os
import platform
import tempfile
from typing import (
List,
Optional,
Union,
)

import packaging.version

from galaxy.util import (
commands,
download_to_file,
)
from . import installable

DEFAULT_APPTAINER_COMMAND = "apptainer"
APPTAINER_VERSION = "1.4.0"
APPTAINER_URL_TEMPLATE = "https://github.com/galaxyproject/apptainer-build-unprivileged/releases/download/v{version}/apptainer-{version}-{el}-{arch}.tar.gz"


log = logging.getLogger(__name__)


def _glibc_version() -> Optional[packaging.version.Version]:
version = None
try:
# First line should always be 'ldd (dist-specific) VERSION'
glibc_version = commands.execute(["ldd", "--version"]).splitlines()[0].split()[-1]
version = packaging.version.parse(glibc_version)
except Exception as exc:
log.warning(f"Unable to get glibc version: {str(exc)}")
return version


def apptainer_url() -> str:
glibc_version = _glibc_version()
el = "el8"
if glibc_version and glibc_version >= packaging.version.parse("2.34"):
el = "el9"
url = APPTAINER_URL_TEMPLATE.format(version=APPTAINER_VERSION, el=el, arch=platform.machine())
return url


class ApptainerContext(installable.InstallableContext):
installable_description = "Apptainer"

def __init__(
self,
apptainer_prefix: Optional[str] = None,
apptainer_exec: Optional[Union[str, List[str]]] = None,
) -> None:
self.apptainer_prefix = apptainer_prefix

if apptainer_exec and isinstance(apptainer_exec, str):
apptainer_exec = os.path.normpath(apptainer_exec)

if apptainer_exec is not None:
self.apptainer_exec = apptainer_exec
elif apptainer_prefix is not None:
self.apptainer_exec = os.path.join(apptainer_prefix, "bin", "apptainer")
else:
self.apptainer_exec = "apptainer"

def apptainer_version(self) -> str:
cmd = [self.apptainer_exec, "version"]
version_out = commands.execute(cmd).strip()
return version_out

def is_installed(self) -> bool:
try:
self.apptainer_version()
return True
except Exception:
return False

def can_install(self) -> bool:
if self.apptainer_prefix is None:
return False
if platform.system() != "Linux" or platform.machine() not in ("x86_64", "aarch64"):
return False
glibc_version = _glibc_version()
if not glibc_version or glibc_version < packaging.version.parse("2.28"):
return False
return True

@property
def parent_path(self) -> Optional[str]:
prefix = None
if self.apptainer_prefix:
prefix = os.path.dirname(os.path.abspath(self.apptainer_prefix))
return prefix


def install_apptainer(apptainer_context: ApptainerContext) -> int:
with tempfile.NamedTemporaryFile(suffix=".tar.gz", prefix="apptainer_install", delete=False) as temp:
tarball_path = temp.name
install_cmd = ["tar", "zxf", tarball_path, "-C", apptainer_context.apptainer_prefix, "--strip-components=1"]
fetch_url = apptainer_url()
log.info("Installing Apptainer, this may take several minutes.")
log.info(f"Fetching from: {fetch_url}")
assert apptainer_context.apptainer_prefix
try:
os.makedirs(apptainer_context.apptainer_prefix)
download_to_file(fetch_url, tarball_path)
commands.execute(install_cmd)
except Exception:
log.exception("Failed to fetch Apptainer tarball")
return 1
finally:
if os.path.exists(tarball_path):
os.remove(tarball_path)
log.info(f"Apptainer installed to: {apptainer_context.apptainer_prefix}")
return 0
5 changes: 4 additions & 1 deletion lib/galaxy/tool_util/deps/container_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -532,8 +532,11 @@ class SingularityContainer(Container, HasDockerLikeVolumes):
container_type = SINGULARITY_CONTAINER_TYPE

def get_singularity_target_kwds(self) -> Dict[str, Any]:
cmd_default = (
self.container_description and self.container_description.cmd
) or singularity_util.DEFAULT_SINGULARITY_COMMAND
return dict(
singularity_cmd=self.prop("cmd", singularity_util.DEFAULT_SINGULARITY_COMMAND),
singularity_cmd=self.prop("cmd", cmd_default),
sudo=asbool(self.prop("sudo", singularity_util.DEFAULT_SUDO)),
sudo_cmd=self.prop("sudo_cmd", singularity_util.DEFAULT_SUDO_COMMAND),
)
Expand Down
Loading
Loading