Skip to content
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