diff --git a/src/deploy_tools/app_builder.py b/src/deploy_tools/app_builder.py index 913ebc0b..60b6a86a 100644 --- a/src/deploy_tools/app_builder.py +++ b/src/deploy_tools/app_builder.py @@ -9,7 +9,7 @@ from .apptainer import create_sif_file from .errors import DeployToolsError from .layout import ModuleBuildLayout -from .models.apptainer_app import ApptainerApp +from .models.apptainer_app import ApptainerApp, EntrypointOptions from .models.module import Application, Module from .models.shell_app import ShellApp from .templater import Templater, TemplateType @@ -56,7 +56,9 @@ def _create_apptainer_files(self, app: ApptainerApp, module: Module) -> None: options = entrypoint.options entrypoint_file = entrypoints_folder / entrypoint.name - mounts = ",".join(chain(global_options.mounts, options.mounts)).strip() + mounts, optional_mounts = self._resolve_entrypoint_mounts( + options, global_options + ) host_binaries = " ".join( chain(global_options.host_binaries, options.host_binaries) ).strip() @@ -71,6 +73,7 @@ def _create_apptainer_files(self, app: ApptainerApp, module: Module) -> None: params = { "mounts": mounts, + "optional_mounts": optional_mounts, "host_binaries": host_binaries, "apptainer_args": apptainer_args, "relative_sif_file": relative_sif_file, @@ -86,6 +89,28 @@ def _create_apptainer_files(self, app: ApptainerApp, module: Module) -> None: create_parents=True, ) + @staticmethod + def _resolve_entrypoint_mounts( + options: EntrypointOptions, global_options: EntrypointOptions + ) -> tuple[str, str]: + """Allow entrypoint options to override mounts defined in global_options.""" + entrypoint_mounts = options.mounts.union(options.optional_mounts) + global_mounts = ( + mount for mount in global_options.mounts if mount not in entrypoint_mounts + ) + global_optional_mounts = ( + mount + for mount in global_options.optional_mounts + if mount not in entrypoint_mounts + ) + + mounts = ",".join(chain(global_mounts, options.mounts)).strip() + optional_mounts = ",".join( + chain(global_optional_mounts, options.optional_mounts) + ).strip() + + return mounts, optional_mounts + def _generate_sif_file(self, app: ApptainerApp, module: Module) -> None: sif_file_path = self._get_sif_file_path(app, module) create_sif_file(sif_file_path, app.container.url, create_parents=True) diff --git a/src/deploy_tools/models/apptainer_app.py b/src/deploy_tools/models/apptainer_app.py index 718e15b3..1a3d6eac 100644 --- a/src/deploy_tools/models/apptainer_app.py +++ b/src/deploy_tools/models/apptainer_app.py @@ -1,6 +1,6 @@ from typing import Annotated, Literal -from pydantic import AnyUrl, Field, StringConstraints, UrlConstraints +from pydantic import AnyUrl, Field, StringConstraints, UrlConstraints, model_validator from .app import ENTRYPOINT_NAME_REGEX from .parent import ParentModel @@ -25,13 +25,24 @@ class EntrypointOptions(ParentModel): ] = "" mounts: Annotated[ - list[MountPoint], + set[MountPoint], Field( - description="A list of mount points to add to the container in the form of " + description="A set of mount points that will result in an error if their " + "host paths cannot be found. This takes the form of " "'host_path[:container_path[:opts]]' where opts (mount options) can be " - "'ro' or 'rw' and defaults to 'rw'" + "'ro' or 'rw' and defaults to 'rw'. " ), - ] = [] + ] = set() + + optional_mounts: Annotated[ + set[MountPoint], + Field( + description="A set of mount points that will not be mounted if their host " + "paths cannot be found (avoiding an error). This takes the form of " + "'host_path[:container_path[:opts]]' where opts (mount options) " + "can be 'ro' or 'rw' and defaults to 'rw'." + ), + ] = set() host_binaries: Annotated[ list[str], @@ -42,6 +53,17 @@ class EntrypointOptions(ParentModel): ), ] = [] + @model_validator(mode="after") + def check_unique_mounts(self) -> "EntrypointOptions": + """Ensure that mounts and optional_mounts do not contain duplicates.""" + duplicate_mounts = self.mounts.intersection(self.optional_mounts) + if duplicate_mounts: + raise ValueError( + f"Duplicate paths found in mounts and optional_mounts: " + f"{duplicate_mounts}" + ) + return self + class Entrypoint(ParentModel): """Represents an entrypoint to a command on the Apptainer image. diff --git a/src/deploy_tools/models/schemas/deployment.json b/src/deploy_tools/models/schemas/deployment.json index 22fea3e2..96e225b5 100644 --- a/src/deploy_tools/models/schemas/deployment.json +++ b/src/deploy_tools/models/schemas/deployment.json @@ -28,6 +28,7 @@ "apptainer_args": "", "command_args": "", "mounts": [], + "optional_mounts": [], "host_binaries": [] }, "description": "Global options that apply to all Entrypoints" @@ -157,6 +158,7 @@ "apptainer_args": "", "command_args": "", "mounts": [], + "optional_mounts": [], "host_binaries": [] }, "description": "Options to apply for this entrypoint" @@ -186,12 +188,23 @@ }, "mounts": { "default": [], - "description": "A list of mount points to add to the container in the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'", + "description": "A set of mount points that will result in an error if their host paths cannot be found. This takes the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'. ", "items": { "$ref": "#/$defs/MountPoint" }, "title": "Mounts", - "type": "array" + "type": "array", + "uniqueItems": true + }, + "optional_mounts": { + "default": [], + "description": "A set of mount points that will not be mounted if their host paths cannot be found (avoiding an error). This takes the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'.", + "items": { + "$ref": "#/$defs/MountPoint" + }, + "title": "Optional Mounts", + "type": "array", + "uniqueItems": true }, "host_binaries": { "default": [], diff --git a/src/deploy_tools/models/schemas/module.json b/src/deploy_tools/models/schemas/module.json index ccac6706..bbe65816 100644 --- a/src/deploy_tools/models/schemas/module.json +++ b/src/deploy_tools/models/schemas/module.json @@ -28,6 +28,7 @@ "apptainer_args": "", "command_args": "", "mounts": [], + "optional_mounts": [], "host_binaries": [] }, "description": "Global options that apply to all Entrypoints" @@ -138,6 +139,7 @@ "apptainer_args": "", "command_args": "", "mounts": [], + "optional_mounts": [], "host_binaries": [] }, "description": "Options to apply for this entrypoint" @@ -167,12 +169,23 @@ }, "mounts": { "default": [], - "description": "A list of mount points to add to the container in the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'", + "description": "A set of mount points that will result in an error if their host paths cannot be found. This takes the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'. ", "items": { "$ref": "#/$defs/MountPoint" }, "title": "Mounts", - "type": "array" + "type": "array", + "uniqueItems": true + }, + "optional_mounts": { + "default": [], + "description": "A set of mount points that will not be mounted if their host paths cannot be found (avoiding an error). This takes the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'.", + "items": { + "$ref": "#/$defs/MountPoint" + }, + "title": "Optional Mounts", + "type": "array", + "uniqueItems": true }, "host_binaries": { "default": [], diff --git a/src/deploy_tools/models/schemas/release.json b/src/deploy_tools/models/schemas/release.json index c625f215..822c444e 100644 --- a/src/deploy_tools/models/schemas/release.json +++ b/src/deploy_tools/models/schemas/release.json @@ -28,6 +28,7 @@ "apptainer_args": "", "command_args": "", "mounts": [], + "optional_mounts": [], "host_binaries": [] }, "description": "Global options that apply to all Entrypoints" @@ -138,6 +139,7 @@ "apptainer_args": "", "command_args": "", "mounts": [], + "optional_mounts": [], "host_binaries": [] }, "description": "Options to apply for this entrypoint" @@ -167,12 +169,23 @@ }, "mounts": { "default": [], - "description": "A list of mount points to add to the container in the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'", + "description": "A set of mount points that will result in an error if their host paths cannot be found. This takes the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'. ", "items": { "$ref": "#/$defs/MountPoint" }, "title": "Mounts", - "type": "array" + "type": "array", + "uniqueItems": true + }, + "optional_mounts": { + "default": [], + "description": "A set of mount points that will not be mounted if their host paths cannot be found (avoiding an error). This takes the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'.", + "items": { + "$ref": "#/$defs/MountPoint" + }, + "title": "Optional Mounts", + "type": "array", + "uniqueItems": true }, "host_binaries": { "default": [], diff --git a/src/deploy_tools/templates/apptainer_entrypoint b/src/deploy_tools/templates/apptainer_entrypoint index bb9d350e..56c04d78 100644 --- a/src/deploy_tools/templates/apptainer_entrypoint +++ b/src/deploy_tools/templates/apptainer_entrypoint @@ -8,6 +8,7 @@ set -e # Mounts for container mounts="{{ mounts }}" +optional_mounts="{{ optional_mounts }}" # Additional arguments for apptainer apptainer_args="{{ apptainer_args }}" # Sif file path @@ -17,6 +18,23 @@ command="{{ command }}" # Options and arguments to pass to command command_args="{{ command_args }}" +{% if optional_mounts|length %} +# Filter out optional mounts that are non-existent +IFS="," read -ra optional_mount_paths <<< "$optional_mounts" + +validated_mounts=() + +for mount_path in "${optional_mount_paths[@]}"; do + host_path="${mount_path%%:*}" + + if [[ -d "$host_path" || -f "$host_path" ]]; then + validated_mounts+=("$mount_path") + fi +done + +mounts="${mounts},$(IFS=','; echo "${validated_mounts[*]}")" + +{% endif -%} # Raise an error if sif file does not exist if [[ ! -f ${sif_file} ]]; then echo "ERROR: sif file ${sif_file} does not exist" 1>&2 diff --git a/tests/samples/01-initial/deploy-tools-output/deployment.yaml b/tests/samples/01-initial/deploy-tools-output/deployment.yaml index 368ad142..46e25f73 100644 --- a/tests/samples/01-initial/deploy-tools-output/deployment.yaml +++ b/tests/samples/01-initial/deploy-tools-output/deployment.yaml @@ -17,11 +17,13 @@ releases: command_args: Hello host_binaries: [] mounts: [] + optional_mounts: [] global_options: apptainer_args: '' command_args: '' host_binaries: [] mounts: [] + optional_mounts: [] - app_type: shell name: test-echo-module-var script: diff --git a/tests/samples/01-initial/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello b/tests/samples/01-initial/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello index d43f0d44..190e97ee 100755 --- a/tests/samples/01-initial/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello +++ b/tests/samples/01-initial/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello @@ -8,6 +8,7 @@ set -e # Mounts for container mounts="" +optional_mounts="" # Additional arguments for apptainer apptainer_args="" # Sif file path diff --git a/tests/samples/01-initial/deploy-tools-output/modules/apps/0.1/module.yaml b/tests/samples/01-initial/deploy-tools-output/modules/apps/0.1/module.yaml index 60d6f708..86e1ca21 100644 --- a/tests/samples/01-initial/deploy-tools-output/modules/apps/0.1/module.yaml +++ b/tests/samples/01-initial/deploy-tools-output/modules/apps/0.1/module.yaml @@ -12,11 +12,13 @@ applications: command_args: Hello host_binaries: [] mounts: [] + optional_mounts: [] global_options: apptainer_args: '' command_args: '' host_binaries: [] mounts: [] + optional_mounts: [] - app_type: shell name: test-echo-module-var script: diff --git a/tests/samples/02-added/deploy-tools-output/deployment.yaml b/tests/samples/02-added/deploy-tools-output/deployment.yaml index 9270aa75..7d4b9130 100644 --- a/tests/samples/02-added/deploy-tools-output/deployment.yaml +++ b/tests/samples/02-added/deploy-tools-output/deployment.yaml @@ -17,11 +17,13 @@ releases: command_args: Hello host_binaries: [] mounts: [] + optional_mounts: [] global_options: apptainer_args: '' command_args: '' host_binaries: [] mounts: [] + optional_mounts: [] - app_type: shell name: test-echo-module-var script: diff --git a/tests/samples/02-added/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello b/tests/samples/02-added/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello index d43f0d44..190e97ee 100755 --- a/tests/samples/02-added/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello +++ b/tests/samples/02-added/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello @@ -8,6 +8,7 @@ set -e # Mounts for container mounts="" +optional_mounts="" # Additional arguments for apptainer apptainer_args="" # Sif file path diff --git a/tests/samples/02-added/deploy-tools-output/modules/apps/0.1/module.yaml b/tests/samples/02-added/deploy-tools-output/modules/apps/0.1/module.yaml index 60d6f708..86e1ca21 100644 --- a/tests/samples/02-added/deploy-tools-output/modules/apps/0.1/module.yaml +++ b/tests/samples/02-added/deploy-tools-output/modules/apps/0.1/module.yaml @@ -12,11 +12,13 @@ applications: command_args: Hello host_binaries: [] mounts: [] + optional_mounts: [] global_options: apptainer_args: '' command_args: '' host_binaries: [] mounts: [] + optional_mounts: [] - app_type: shell name: test-echo-module-var script: diff --git a/tests/samples/03-updated/deploy-tools-output/deployment.yaml b/tests/samples/03-updated/deploy-tools-output/deployment.yaml index d2064d23..4786e656 100644 --- a/tests/samples/03-updated/deploy-tools-output/deployment.yaml +++ b/tests/samples/03-updated/deploy-tools-output/deployment.yaml @@ -17,11 +17,13 @@ releases: command_args: Hello host_binaries: [] mounts: [] + optional_mounts: [] global_options: apptainer_args: '' command_args: '' host_binaries: [] mounts: [] + optional_mounts: [] - app_type: shell name: test-echo-module-var script: diff --git a/tests/samples/03-updated/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello b/tests/samples/03-updated/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello index d43f0d44..190e97ee 100755 --- a/tests/samples/03-updated/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello +++ b/tests/samples/03-updated/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello @@ -8,6 +8,7 @@ set -e # Mounts for container mounts="" +optional_mounts="" # Additional arguments for apptainer apptainer_args="" # Sif file path diff --git a/tests/samples/03-updated/deploy-tools-output/modules/apps/0.1/module.yaml b/tests/samples/03-updated/deploy-tools-output/modules/apps/0.1/module.yaml index 60d6f708..86e1ca21 100644 --- a/tests/samples/03-updated/deploy-tools-output/modules/apps/0.1/module.yaml +++ b/tests/samples/03-updated/deploy-tools-output/modules/apps/0.1/module.yaml @@ -12,11 +12,13 @@ applications: command_args: Hello host_binaries: [] mounts: [] + optional_mounts: [] global_options: apptainer_args: '' command_args: '' host_binaries: [] mounts: [] + optional_mounts: [] - app_type: shell name: test-echo-module-var script: diff --git a/tests/samples/04-deprecated/deploy-tools-output/deployment.yaml b/tests/samples/04-deprecated/deploy-tools-output/deployment.yaml index df83de8b..8f0bcd67 100644 --- a/tests/samples/04-deprecated/deploy-tools-output/deployment.yaml +++ b/tests/samples/04-deprecated/deploy-tools-output/deployment.yaml @@ -17,11 +17,13 @@ releases: command_args: Hello host_binaries: [] mounts: [] + optional_mounts: [] global_options: apptainer_args: '' command_args: '' host_binaries: [] mounts: [] + optional_mounts: [] - app_type: shell name: test-echo-module-var script: diff --git a/tests/samples/04-deprecated/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello b/tests/samples/04-deprecated/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello index d43f0d44..190e97ee 100755 --- a/tests/samples/04-deprecated/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello +++ b/tests/samples/04-deprecated/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello @@ -8,6 +8,7 @@ set -e # Mounts for container mounts="" +optional_mounts="" # Additional arguments for apptainer apptainer_args="" # Sif file path diff --git a/tests/samples/04-deprecated/deploy-tools-output/modules/apps/0.1/module.yaml b/tests/samples/04-deprecated/deploy-tools-output/modules/apps/0.1/module.yaml index 60d6f708..86e1ca21 100644 --- a/tests/samples/04-deprecated/deploy-tools-output/modules/apps/0.1/module.yaml +++ b/tests/samples/04-deprecated/deploy-tools-output/modules/apps/0.1/module.yaml @@ -12,11 +12,13 @@ applications: command_args: Hello host_binaries: [] mounts: [] + optional_mounts: [] global_options: apptainer_args: '' command_args: '' host_binaries: [] mounts: [] + optional_mounts: [] - app_type: shell name: test-echo-module-var script: diff --git a/tests/samples/05-restored/deploy-tools-output/deployment.yaml b/tests/samples/05-restored/deploy-tools-output/deployment.yaml index 869b8d29..598f926b 100644 --- a/tests/samples/05-restored/deploy-tools-output/deployment.yaml +++ b/tests/samples/05-restored/deploy-tools-output/deployment.yaml @@ -17,11 +17,13 @@ releases: command_args: Hello host_binaries: [] mounts: [] + optional_mounts: [] global_options: apptainer_args: '' command_args: '' host_binaries: [] mounts: [] + optional_mounts: [] - app_type: shell name: test-echo-module-var script: diff --git a/tests/samples/05-restored/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello b/tests/samples/05-restored/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello index d43f0d44..190e97ee 100755 --- a/tests/samples/05-restored/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello +++ b/tests/samples/05-restored/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello @@ -8,6 +8,7 @@ set -e # Mounts for container mounts="" +optional_mounts="" # Additional arguments for apptainer apptainer_args="" # Sif file path diff --git a/tests/samples/05-restored/deploy-tools-output/modules/apps/0.1/module.yaml b/tests/samples/05-restored/deploy-tools-output/modules/apps/0.1/module.yaml index 60d6f708..86e1ca21 100644 --- a/tests/samples/05-restored/deploy-tools-output/modules/apps/0.1/module.yaml +++ b/tests/samples/05-restored/deploy-tools-output/modules/apps/0.1/module.yaml @@ -12,11 +12,13 @@ applications: command_args: Hello host_binaries: [] mounts: [] + optional_mounts: [] global_options: apptainer_args: '' command_args: '' host_binaries: [] mounts: [] + optional_mounts: [] - app_type: shell name: test-echo-module-var script: diff --git a/tests/samples/06-removed/deploy-tools-output/deployment.yaml b/tests/samples/06-removed/deploy-tools-output/deployment.yaml index c2972e01..77ef38ad 100644 --- a/tests/samples/06-removed/deploy-tools-output/deployment.yaml +++ b/tests/samples/06-removed/deploy-tools-output/deployment.yaml @@ -17,11 +17,13 @@ releases: command_args: Hello host_binaries: [] mounts: [] + optional_mounts: [] global_options: apptainer_args: '' command_args: '' host_binaries: [] mounts: [] + optional_mounts: [] - app_type: shell name: test-echo-module-var script: diff --git a/tests/samples/06-removed/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello b/tests/samples/06-removed/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello index d43f0d44..190e97ee 100755 --- a/tests/samples/06-removed/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello +++ b/tests/samples/06-removed/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello @@ -8,6 +8,7 @@ set -e # Mounts for container mounts="" +optional_mounts="" # Additional arguments for apptainer apptainer_args="" # Sif file path diff --git a/tests/samples/06-removed/deploy-tools-output/modules/apps/0.1/module.yaml b/tests/samples/06-removed/deploy-tools-output/modules/apps/0.1/module.yaml index 60d6f708..86e1ca21 100644 --- a/tests/samples/06-removed/deploy-tools-output/modules/apps/0.1/module.yaml +++ b/tests/samples/06-removed/deploy-tools-output/modules/apps/0.1/module.yaml @@ -12,11 +12,13 @@ applications: command_args: Hello host_binaries: [] mounts: [] + optional_mounts: [] global_options: apptainer_args: '' command_args: '' host_binaries: [] mounts: [] + optional_mounts: [] - app_type: shell name: test-echo-module-var script: diff --git a/tests/test_module_options.py b/tests/test_module_options.py index d0f1f3c5..3a3d5e46 100644 --- a/tests/test_module_options.py +++ b/tests/test_module_options.py @@ -93,7 +93,7 @@ def test_apptainer_options_merge_global_and_entrypoint( version="latest", ), global_options=EntrypointOptions( - mounts=["/global/mount"], + mounts={"/global/mount"}, host_binaries=["globalbin"], apptainer_args="--global-arg", command_args="--global-cmd", @@ -103,7 +103,7 @@ def test_apptainer_options_merge_global_and_entrypoint( name="example-cmd", command="realcmd", options=EntrypointOptions( - mounts=["/local/mount"], + mounts={"/local/mount"}, host_binaries=["localbin"], apptainer_args="--local-arg", command_args="--local-cmd", @@ -129,6 +129,63 @@ def test_apptainer_options_merge_global_and_entrypoint( assert 'command="realcmd"' in entrypoint +def test_apptainer_entrypoint_mounts_take_precendence_over_global_mounts( + tmp_path: Path, stub_apptainer_pull: None +) -> None: + # The Apptainer entrypoint combines global_options with per-entrypoint options + release = Release( + module=Module( + name="example-apptainer-opts", + version="2.0", + description="Module exercising apptainer entrypoint option merge", + applications=[ + ApptainerApp( + app_type="apptainer", + container=ContainerImage( + path="docker://ghcr.io/apptainer/lolcow", # type: ignore[arg-type] + version="latest", + ), + global_options=EntrypointOptions( + mounts={"/global/mount"}, + optional_mounts={"/global/mount/optional"}, + host_binaries=["globalbin"], + apptainer_args="--global-arg", + command_args="--global-cmd", + ), + entrypoints=[ + Entrypoint( + name="example-cmd", + command="realcmd", + options=EntrypointOptions( + mounts={"/global/mount/optional"}, + optional_mounts={"/global/mount"}, + host_binaries=["localbin"], + apptainer_args="--local-arg", + command_args="--local-cmd", + ), + ) + ], + ) + ], + ) + ) + deployment_root = _sync(tmp_path, release) + + entrypoint = ( + deployment_root / "modules/example-apptainer-opts/2.0/entrypoints/example-cmd" + ).read_text() + + # When duplicate mounts are defined between global and entrypoint options, + # entrypoint options will take priority + print(entrypoint) + assert 'mounts="/global/mount/optional"' in entrypoint + assert 'optional_mounts="/global/mount"' in entrypoint + assert 'apptainer_args="--global-arg --local-arg"' in entrypoint + assert 'command_args="--global-cmd --local-cmd"' in entrypoint + assert "for i in globalbin localbin; do" in entrypoint + assert 'command="realcmd"' in entrypoint + + def test_dependencies_render_module_load_lines(tmp_path: Path) -> None: # The modulefile dependency block branches on whether a version is given: a pinned # dependency renders `name/version`, while a version-less one renders the bare name.