Repository: singularityhub/singularity-hpc (language: python)

Issue:
Change location of wrapper scripts
Hello, 

I was wondering if there is an option to change the location of wrapper script into another location. If we have many binaries in container.yaml file then many wrapper scripts are being created and Lmod issues a warning of having too many non-module files in modules folder.

Thanks,
Thekla


Discussion / hints:
We don't have support for that - but it's a great idea! I'll see if I can make some time soon to do a PR for you to test!
Thank you for your prompt reply!

Fix patch:
diff --git a/.github/dev-requirements.txt b/.github/dev-requirements.txt
index 0b29a8891..6116de386 100644
--- a/.github/dev-requirements.txt
+++ b/.github/dev-requirements.txt
@@ -1,4 +1,4 @@
 pre-commit
-black
+black==23.3.0
 isort
 flake8
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9b24f3334..b9c23ac4e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ and **Merged pull requests**. Critical items to know are:
 The versions coincide with releases on pip. Only major versions will be released as tags on Github.
 
 ## [0.0.x](https://github.com/singularityhub/singularity-hpc/tree/main) (0.0.x)
+ - Allow custom location for wrapper scripts (0.1.24)
  - Labels with newlines need additional parsing (0.1.23)
  - Do not write directly to output with shpc show (0.1.22)
  - Podman template bug (0.1.21)
diff --git a/docs/getting_started/user-guide.rst b/docs/getting_started/user-guide.rst
index 9b935a54f..49cf8cd3c 100644
--- a/docs/getting_started/user-guide.rst
+++ b/docs/getting_started/user-guide.rst
@@ -176,6 +176,9 @@ variable replacement. A summary table of variables is included below, and then f
    * - module_base
      - The install directory for modules
      - $root_dir/modules
+   * - wrapper_base
+     - The install directory for script wrappers
+     - $root_dir/modules
    * - container_base
      - Where to install containers. If not defined, they are installed in "containers" in the install root
      - $root_dir/containers
@@ -347,6 +350,20 @@ Singularity Registry HPC uses this simple directory structure to ensure
 a unique namespace.
 
 
+Wrapper Base
+------------
+
+By default, if you do not set a wrapper script base they will be stored alongside
+modules. However, for large installations, we recommend you customize this path
+to be somewhere else. This way, you can avoid warnings from your module software
+about having too many files.
+
+.. code-block:: console
+
+    # an absolute path
+    $ shpc config set wrapper_base /opt/lmod/wrappers
+
+
 Container Images Folder
 -----------------------
 
diff --git a/shpc/main/container/base.py b/shpc/main/container/base.py
index afabadff0..f7d383886 100644
--- a/shpc/main/container/base.py
+++ b/shpc/main/container/base.py
@@ -66,13 +66,16 @@ def add(self, sif, module_name, modulefile, template, **kwargs):
         """
         logger.warning("Add is not supported for %s" % self)
 
-    def add_environment(self, module_dir, envars, environment_file):
+    def add_environment(self, env_dir, envars, environment_file):
         """
         Given one or more environment variables in a dictionary, write to file.
+
+        The environment file goes in the wrapper directory, which can default
+        to the module directory if the value uses the default or is unset.
         """
         # Podman envars are written directly to the module file
         out = Template(shpc.main.templates.environment_file).render(envars=envars)
-        env_file = os.path.join(module_dir, environment_file)
+        env_file = os.path.join(env_dir, environment_file)
         shpc.utils.write_file(env_file, out)
 
     def delete(self, image):
diff --git a/shpc/main/container/docker.py b/shpc/main/container/docker.py
index 7130b5d33..a55fc1bfb 100644
--- a/shpc/main/container/docker.py
+++ b/shpc/main/container/docker.py
@@ -218,7 +218,7 @@ def install(self, module_path, template, module, features=None):
         if self.settings.wrapper_scripts["enabled"] is True:
             wrapper_scripts = shpc.main.wrappers.generate(
                 aliases=aliases,
-                module_dir=module.module_dir,
+                wrapper_dir=module.wrapper_dir,
                 features=features,
                 container=self,
                 image=module.container_path,
diff --git a/shpc/main/container/singularity.py b/shpc/main/container/singularity.py
index 46fb60adb..1d5a5d428 100644
--- a/shpc/main/container/singularity.py
+++ b/shpc/main/container/singularity.py
@@ -211,9 +211,9 @@ def install(self, module_path, template, module, features=None):
         if self.settings.wrapper_scripts["enabled"] is True:
             wrapper_scripts = shpc.main.wrappers.generate(
                 aliases=aliases,
-                module_dir=module.module_dir,
                 features=features,
                 container=self,
+                wrapper_dir=module.wrapper_dir,
                 image=module.container_path,
                 config=module.config,
             )
diff --git a/shpc/main/container/update/versions.py b/shpc/main/container/update/versions.py
index 20e69dba7..39ac93097 100644
--- a/shpc/main/container/update/versions.py
+++ b/shpc/main/container/update/versions.py
@@ -155,7 +155,7 @@ def _cmp(self, other):
             this_version = self.version[i]
             other_version = other.version[i]
 
-            if type(this_version) != type(other_version):
+            if type(this_version) is not type(other_version):
                 continue
 
             if this_version == other_version:
diff --git a/shpc/main/modules/base.py b/shpc/main/modules/base.py
index 8b0058c0d..52908c380 100644
--- a/shpc/main/modules/base.py
+++ b/shpc/main/modules/base.py
@@ -130,6 +130,14 @@ def uninstall(self, name, force=False):
                 "$module_base/%s" % module.name,
             )
 
+        # If we have a wrapper
+        if module.wrapper_dir != module.module_dir:
+            self._uninstall(
+                module.wrapper_dir,
+                self.settings.wrapper_base,
+                "$wrapper_base/%s" % module.name,
+            )
+
         # If uninstalling the entire module, clean up symbolic links in all views
         for view_name in views_with_module:
             self.views[view_name].uninstall(module.module_dir)
diff --git a/shpc/main/modules/module.py b/shpc/main/modules/module.py
index d95bf9a17..1a16b671b 100644
--- a/shpc/main/modules/module.py
+++ b/shpc/main/modules/module.py
@@ -36,7 +36,7 @@ def add_environment(self):
         Write the environment to the module directory.
         """
         self.container.add_environment(
-            self.module_dir,
+            self.wrapper_dir,
             envars=self.config.get_envars(),
             environment_file=self.settings.environment_file,
         )
@@ -165,6 +165,14 @@ def uri(self):
         """
         return self._uri
 
+    @property
+    def wrapper_dir(self):
+        """
+        Full path to the wrapper directory
+        """
+        wrapper_dir = self.settings.wrapper_base or self.settings.module_base
+        return os.path.join(wrapper_dir, self.module_basepath)
+
     @property
     def module_dir(self):
         """
diff --git a/shpc/main/modules/templates/docker.lua b/shpc/main/modules/templates/docker.lua
index 575cb6347..845ac1bf9 100644
--- a/shpc/main/modules/templates/docker.lua
+++ b/shpc/main/modules/templates/docker.lua
@@ -16,18 +16,18 @@ Container:
 Commands include:
 
  - {|module_name|}-run:
-       {{ command }} run -i{% if settings.enable_tty %}t{% endif %} -u `id -u`:`id -g` --rm {% if settings.environment_file %}--env-file <moduleDir>/{{ settings.environment_file }} {% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %}-v ${PWD} -w ${PWD} <container> "$@"
+       {{ command }} run -i{% if settings.enable_tty %}t{% endif %} -u `id -u`:`id -g` --rm {% if settings.environment_file %}--env-file <wrapperDir>/{{ settings.environment_file }} {% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %}-v ${PWD} -w ${PWD} <container> "$@"
  - {|module_name|}-shell:
-       {{ command }} run -i{% if settings.enable_tty %}t{% endif %} -u `id -u`:`id -g` --rm {% if settings.environment_file %}--env-file <moduleDir>/{{ settings.environment_file }} {% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %}--entrypoint {{ shell }} -v ${PWD} -w ${PWD}<container>
+       {{ command }} run -i{% if settings.enable_tty %}t{% endif %} -u `id -u`:`id -g` --rm {% if settings.environment_file %}--env-file <wrapperDir>/{{ settings.environment_file }} {% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %}--entrypoint {{ shell }} -v ${PWD} -w ${PWD}<container>
  - {|module_name|}-exec:
-       {{ command }} run -i{% if settings.enable_tty %}t{% endif %} -u `id -u`:`id -g` --rm --entrypoint "" {% if settings.environment_file %}--env-file <moduleDir>/{{ settings.environment_file }} {% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v ${PWD} -w ${PWD} <container> "$@"
+       {{ command }} run -i{% if settings.enable_tty %}t{% endif %} -u `id -u`:`id -g` --rm --entrypoint "" {% if settings.environment_file %}--env-file <wrapperDir>/{{ settings.environment_file }} {% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v ${PWD} -w ${PWD} <container> "$@"
  - {|module_name|}-inspect:
        {{ command }} inspect <container>
  - {|module_name|}-container:
        echo "$PODMAN_CONTAINER"
 
 {% if aliases %}{% for alias in aliases %} - {{ alias.name }}:
-       {{ command }} run -i{% if settings.enable_tty %}t{% endif %} -u `id -u`:`id -g` --rm --entrypoint {{ alias.entrypoint }} {% if settings.environment_file %}--env-file <moduleDir>/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %}{% if alias.docker_options %}{{ alias.docker_options }} {% endif %} -v ${PWD} -w ${PWD} <container> "{{ alias.args }}" "$@"
+       {{ command }} run -i{% if settings.enable_tty %}t{% endif %} -u `id -u`:`id -g` --rm --entrypoint {{ alias.entrypoint }} {% if settings.environment_file %}--env-file <wrapperDir>/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %}{% if alias.docker_options %}{{ alias.docker_options }} {% endif %} -v ${PWD} -w ${PWD} <container> "{{ alias.args }}" "$@"
 {% endfor %}{% endif %}
 
 For each of the above, you can export:
@@ -48,23 +48,26 @@ if not os.getenv("PODMAN_COMMAND_OPTS") then setenv ("PODMAN_COMMAND_OPTS", "")
 -- directory containing this modulefile, once symlinks resolved (dynamically defined)
 local moduleDir = subprocess("realpath " .. myFileName()):match("(.*[/])") or "."
 
+-- If we have wrapper base set, honor it, otherwise we use the moduleDir
+{% if settings.wrapper_base %}local wrapperDir = "{{ module.wrapper_dir }}"{% else %}local wrapperDir = moduleDir{% endif %}
+
 -- interactive shell to any container, plus exec for aliases
 local containerPath = '{{ module.container_path }}'
 
 -- service environment variable to access docker URI
 setenv("PODMAN_CONTAINER", containerPath)
 
-local shellCmd = "{{ command }} ${PODMAN_OPTS} run -i{% if settings.enable_tty %}t{% endif %} ${PODMAN_COMMAND_OPTS} -u `id -u`:`id -g` --rm --entrypoint {{ shell }} {% if settings.environment_file %}--env-file " .. moduleDir .. "/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v ${PWD} -w ${PWD} " .. containerPath
+local shellCmd = "{{ command }} ${PODMAN_OPTS} run -i{% if settings.enable_tty %}t{% endif %} ${PODMAN_COMMAND_OPTS} -u `id -u`:`id -g` --rm --entrypoint {{ shell }} {% if settings.environment_file %}--env-file " .. wrapperDir .. "/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v ${PWD} -w ${PWD} " .. containerPath
 -- execCmd needs entrypoint to be the executor
-local execCmd = "{{ command }} ${PODMAN_OPTS} run ${PODMAN_COMMAND_OPTS} -i{% if settings.enable_tty %}t{% endif %} -u `id -u`:`id -g` --rm {% if settings.environment_file %}--env-file " .. moduleDir .. "/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v ${PWD} -w ${PWD} "
-local runCmd = "{{ command }} ${PODMAN_OPTS} run ${PODMAN_COMMAND_OPTS} -i{% if settings.enable_tty %}t{% endif %} -u `id -u`:`id -g` --rm {% if settings.environment_file %}--env-file " .. moduleDir .. "/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v ${PWD} -w ${PWD} " .. containerPath
+local execCmd = "{{ command }} ${PODMAN_OPTS} run ${PODMAN_COMMAND_OPTS} -i{% if settings.enable_tty %}t{% endif %} -u `id -u`:`id -g` --rm {% if settings.environment_file %}--env-file " .. wrapperDir .. "/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v ${PWD} -w ${PWD} "
+local runCmd = "{{ command }} ${PODMAN_OPTS} run ${PODMAN_COMMAND_OPTS} -i{% if settings.enable_tty %}t{% endif %} -u `id -u`:`id -g` --rm {% if settings.environment_file %}--env-file " .. wrapperDir .. "/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v ${PWD} -w ${PWD} " .. containerPath
 local inspectCmd = "{{ command }} ${PODMAN_OPTS} inspect ${PODMAN_COMMAND_OPTS} " .. containerPath
 
 -- conflict with modules with the same name
 conflict("{{ parsed_name.tool }}"{% if name != parsed_name.tool %},"{{ module.name }}"{% endif %}{% if aliases %}{% for alias in aliases %}{% if alias.name != parsed_name.tool %},"{{ alias.name }}"{% endif %}{% endfor %}{% endif %})
 
 -- if we have any wrapper scripts, add the bin directory
-{% if wrapper_scripts %}prepend_path("PATH", pathJoin(moduleDir, "bin")){% endif %}
+{% if wrapper_scripts %}prepend_path("PATH", pathJoin(wrapperDir, "bin")){% endif %}
 
 -- "aliases" to module commands - generate only if not a wrapper script already generated
 {% if aliases %}{% for alias in aliases %}{% if alias.name not in wrapper_scripts %}set_shell_function("{{ alias.name }}", execCmd .. {% if alias.docker_options %} "{{ alias.docker_options }} " .. {% endif %} " --entrypoint {{ alias.entrypoint }} " .. containerPath .. " {{ alias.args }} \"$@\"", execCmd .. {% if alias.docker_options %} "{{ alias.docker_options }} " .. {% endif %} " --entrypoint {{ alias.entrypoint }} " .. containerPath .. " {{ alias.args }}"){% endif %}
diff --git a/shpc/main/modules/templates/docker.tcl b/shpc/main/modules/templates/docker.tcl
index d8f1e663b..531d801b6 100644
--- a/shpc/main/modules/templates/docker.tcl
+++ b/shpc/main/modules/templates/docker.tcl
@@ -15,18 +15,18 @@ proc ModulesHelp { } {
     puts stderr " - {{ module.container_path }}"
     puts stderr "Commands include:"
     puts stderr " - {|module_name|}-run:"
-    puts stderr "       {{ command }} run -i{% if settings.enable_tty %}t{% endif %} -u `id -u`:`id -g` --rm {% if settings.environment_file %}--env-file  <moduleDir>/{{ settings.environment_file }} {% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v . -w . <container> \"\$@\""
+    puts stderr "       {{ command }} run -i{% if settings.enable_tty %}t{% endif %} -u `id -u`:`id -g` --rm {% if settings.environment_file %}--env-file  <wrapperDir>/{{ settings.environment_file }} {% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v . -w . <container> \"\$@\""
     puts stderr " - {|module_name|}-shell:"
-    puts stderr "       {{ command }} run -i{% if settings.enable_tty %}t{% endif %} -u `id -u`:`id -g` --rm --entrypoint {{ shell }} {% if settings.environment_file %} --env-file <moduleDir>/{{ settings.environment_file }} {% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v . -w . <container>"
+    puts stderr "       {{ command }} run -i{% if settings.enable_tty %}t{% endif %} -u `id -u`:`id -g` --rm --entrypoint {{ shell }} {% if settings.environment_file %} --env-file <wrapperDir>/{{ settings.environment_file }} {% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v . -w . <container>"
     puts stderr " - {|module_name|}-exec:"
-    puts stderr "       {{ command }} run -i{% if settings.enable_tty %}t{% endif %} -u `id -u`:`id -g` --rm --entrypoint \"\" {% if settings.environment_file %} --env-file  <moduleDir>/{{ settings.environment_file }} {% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v . -w . <container> \"\$@\""
+    puts stderr "       {{ command }} run -i{% if settings.enable_tty %}t{% endif %} -u `id -u`:`id -g` --rm --entrypoint \"\" {% if settings.environment_file %} --env-file  <wrapperDir>/{{ settings.environment_file }} {% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v . -w . <container> \"\$@\""
     puts stderr " - {|module_name|}-inspect:"
     puts stderr "       {{ command }} inspect <container>"
     puts stderr " - {|module_name|}-container:"
     puts stderr "       echo \"\$PODMAN_CONTAINER\""
     puts stderr ""
 {% if aliases %}{% for alias in aliases %}    puts stderr " - {{ alias.name }}:"
-    puts stderr "       {{ command }} run -i{% if settings.enable_tty %}t{% endif %} --rm -u `id -u`:`id -g` --entrypoint {{ alias.entrypoint | replace("$", "\$") }} {% if settings.environment_file %}--settings.environment_file  <moduleDir>/{{ settings.environment_file }} {% endif %}{% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %}{% if alias.docker_options %}{{ alias.docker_options | replace("$", "\$") }} {% endif %} -v . -w . <container> {{ alias.args | replace("$", "\$") }} \"\$@\""
+    puts stderr "       {{ command }} run -i{% if settings.enable_tty %}t{% endif %} --rm -u `id -u`:`id -g` --entrypoint {{ alias.entrypoint | replace("$", "\$") }} {% if settings.environment_file %}--settings.environment_file  <wrapperDir>/{{ settings.environment_file }} {% endif %}{% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %}{% if alias.docker_options %}{{ alias.docker_options | replace("$", "\$") }} {% endif %} -v . -w . <container> {{ alias.args | replace("$", "\$") }} \"\$@\""
 {% endfor %}{% endif %}
     puts stderr ""
     puts stderr "For each of the above, you can export:"
@@ -62,6 +62,9 @@ set helpcommand "This module is a {{ docker }} container wrapper for {{ module.n
 # directory containing this modulefile, once symlinks resolved (dynamically defined)
 set moduleDir   [file dirname [expr { [string equal [file type ${ModulesCurrentModulefile}] "link"] ? [file readlink ${ModulesCurrentModulefile}] : ${ModulesCurrentModulefile} }]]
 
+# If we have wrapper base set, honor it, otherwise we use the moduleDir
+{% if settings.wrapper_base %}set wrapperDir "{{ module.wrapper_dir }}"{% else %}set wrapperDir "${moduleDir}"{% endif %}
+
 # conflict with modules with the same alias name
 conflict {{ parsed_name.tool }}
 {% if name != parsed_name.tool %}conflict {{ module.name }}{% endif %}
@@ -72,15 +75,15 @@ conflict {{ parsed_name.tool }}
 setenv PODMAN_CONTAINER "${containerPath}"
 
 # interactive shell to any container, plus exec for aliases
-set shellCmd "{{ command }} \${PODMAN_OPTS} run \${PODMAN_COMMAND_OPTS} -u `id -u`:`id -g` --rm -i{% if settings.enable_tty %}t{% endif %} --entrypoint {{ shell }} {% if settings.environment_file %}--env-file ${moduleDir}/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v $workdir -w $workdir ${containerPath}"
+set shellCmd "{{ command }} \${PODMAN_OPTS} run \${PODMAN_COMMAND_OPTS} -u `id -u`:`id -g` --rm -i{% if settings.enable_tty %}t{% endif %} --entrypoint {{ shell }} {% if settings.environment_file %}--env-file ${wrapperDir}/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v $workdir -w $workdir ${containerPath}"
 
 # execCmd needs entrypoint to be the executor
-set execCmd "{{ command }} \${PODMAN_OPTS} run -i{% if settings.enable_tty %}t{% endif %} \${PODMAN_COMMAND_OPTS} -u `id -u`:`id -g` --rm {% if settings.environment_file %} --env-file ${moduleDir}/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }}{% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v $workdir -w $workdir"
-set runCmd "{{ command }} \${PODMAN_OPTS} run -i{% if settings.enable_tty %}t{% endif %} \${PODMAN_COMMAND_OPTS} -u `id -u`:`id -g` --rm {% if settings.environment_file %}--env-file  ${moduleDir}/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v $workdir -w $workdir ${containerPath}"
+set execCmd "{{ command }} \${PODMAN_OPTS} run -i{% if settings.enable_tty %}t{% endif %} \${PODMAN_COMMAND_OPTS} -u `id -u`:`id -g` --rm {% if settings.environment_file %} --env-file ${wrapperDir}/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }}{% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v $workdir -w $workdir"
+set runCmd "{{ command }} \${PODMAN_OPTS} run -i{% if settings.enable_tty %}t{% endif %} \${PODMAN_COMMAND_OPTS} -u `id -u`:`id -g` --rm {% if settings.environment_file %}--env-file  ${wrapperDir}/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v $workdir -w $workdir ${containerPath}"
 set inspectCmd "{{ command }} \${PODMAN_OPTS} inspect ${containerPath}"
 
 # wrapper scripts? Add bin to path
-{% if wrapper_scripts %}prepend-path PATH "${moduleDir}/bin"{% endif %}
+{% if wrapper_scripts %}prepend-path PATH ${wrapperDir}/bin{% endif %}
 
 # "aliases" to module commands
 {% if aliases %}if { [ module-info shell bash ] } {
diff --git a/shpc/main/modules/templates/singularity.lua b/shpc/main/modules/templates/singularity.lua
index 97ec76395..ccade2938 100644
--- a/shpc/main/modules/templates/singularity.lua
+++ b/shpc/main/modules/templates/singularity.lua
@@ -16,11 +16,11 @@ Container (available through variable SINGULARITY_CONTAINER):
 Commands include:
 
  - {|module_name|}-run:
-       singularity run {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home }} --home {{ features.home }} {% endif %}{% if features.x11 %}-B {{ features.x11 }} {% endif %}{% if settings.environment_file %}-B <moduleDir>/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }} {% endif %}<container> "$@"
+       singularity run {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home }} --home {{ features.home }} {% endif %}{% if features.x11 %}-B {{ features.x11 }} {% endif %}{% if settings.environment_file %}-B <wrapperDir>/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }} {% endif %}<container> "$@"
  - {|module_name|}-shell:
-       singularity shell -s {{ settings.singularity_shell }} {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home }} --home {{ features.home }} {% endif %}{% if features.x11 %}-B {{ features.x11 }} {% endif %}{% if settings.environment_file %}-B <moduleDir>/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }} {% endif %}<container>
+       singularity shell -s {{ settings.singularity_shell }} {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home }} --home {{ features.home }} {% endif %}{% if features.x11 %}-B {{ features.x11 }} {% endif %}{% if settings.environment_file %}-B <wrapperDir>/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }} {% endif %}<container>
  - {|module_name|}-exec:
-       singularity exec {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home }} --home {{ features.home }} {% endif %}{% if features.x11 %}-B {{ features.x11 }} {% endif %}{% if settings.environment_file %}-B <moduleDir>/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }} {% endif %}<container> "$@"
+       singularity exec {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home }} --home {{ features.home }} {% endif %}{% if features.x11 %}-B {{ features.x11 }} {% endif %}{% if settings.environment_file %}-B <wrapperDir>/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }} {% endif %}<container> "$@"
  - {|module_name|}-inspect-runscript:
        singularity inspect -r <container>
  - {|module_name|}-inspect-deffile:
@@ -29,7 +29,7 @@ Commands include:
        echo "$SINGULARITY_CONTAINER"
 
 {% if aliases %}{% for alias in aliases %} - {{ alias.name }}:
-       singularity exec {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home }} --home {{ features.home }} {% endif %}{% if features.x11 %}-B {{ features.x11 }} {% endif %}{% if settings.environment_file %}-B <moduleDir>/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }} {% endif %}{% if alias.singularity_options %}{{ alias.singularity_options }} {% endif %}<container> {{ alias.command }} "$@"
+       singularity exec {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home }} --home {{ features.home }} {% endif %}{% if features.x11 %}-B {{ features.x11 }} {% endif %}{% if settings.environment_file %}-B <wrapperDir>/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }} {% endif %}{% if alias.singularity_options %}{{ alias.singularity_options }} {% endif %}<container> {{ alias.command }} "$@"
 {% endfor %}{% endif %}
 
 For each of the above, you can export:
@@ -46,6 +46,9 @@ For each of the above, you can export:
 -- directory containing this modulefile, once symlinks resolved (dynamically defined)
 local moduleDir = subprocess("realpath " .. myFileName()):match("(.*[/])") or "."
 
+-- If we have wrapper base set, honor it, otherwise we use the moduleDir
+{% if settings.wrapper_base %}local wrapperDir = "{{ module.wrapper_dir }}"{% else %}local wrapperDir = moduleDir{% endif %}
+
 -- singularity environment variable to set shell
 setenv("SINGULARITY_SHELL", "{{ settings.singularity_shell }}")
 
@@ -58,16 +61,16 @@ local containerPath = '{{ module.container_path }}'
 setenv("SINGULARITY_CONTAINER", containerPath)
 
 -- interactive shell to any container, plus exec for aliases
-local shellCmd = "singularity ${SINGULARITY_OPTS} shell ${SINGULARITY_COMMAND_OPTS} -s {{ settings.singularity_shell }} {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home }} --home {{ features.home }} {% endif %}{% if features.x11 %}-B {{ features.x11 }} {% endif %}{% if settings.environment_file %}-B " .. moduleDir .. "/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }}{% endif %} " .. containerPath
-local execCmd = "singularity ${SINGULARITY_OPTS} exec ${SINGULARITY_COMMAND_OPTS} {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home }} --home {{ features.home }} {% endif %}{% if features.x11 %}-B {{ features.x11 }} {% endif %}{% if settings.environment_file %}-B " .. moduleDir .. "/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }}{% endif %} "
-local runCmd = "singularity ${SINGULARITY_OPTS} run ${SINGULARITY_COMMAND_OPTS} {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home }} --home {{ features.home }} {% endif %}{% if features.x11 %}-B {{ features.x11 }} {% endif %}{% if settings.environment_file %}-B " .. moduleDir .. "/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }}{% endif %} " .. containerPath
+local shellCmd = "singularity ${SINGULARITY_OPTS} shell ${SINGULARITY_COMMAND_OPTS} -s {{ settings.singularity_shell }} {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home }} --home {{ features.home }} {% endif %}{% if features.x11 %}-B {{ features.x11 }} {% endif %}{% if settings.environment_file %}-B " .. wrapperDir .. "/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }}{% endif %} " .. containerPath
+local execCmd = "singularity ${SINGULARITY_OPTS} exec ${SINGULARITY_COMMAND_OPTS} {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home }} --home {{ features.home }} {% endif %}{% if features.x11 %}-B {{ features.x11 }} {% endif %}{% if settings.environment_file %}-B " .. wrapperDir .. "/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }}{% endif %} "
+local runCmd = "singularity ${SINGULARITY_OPTS} run ${SINGULARITY_COMMAND_OPTS} {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home }} --home {{ features.home }} {% endif %}{% if features.x11 %}-B {{ features.x11 }} {% endif %}{% if settings.environment_file %}-B " .. wrapperDir .. "/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }}{% endif %} " .. containerPath
 local inspectCmd = "singularity ${SINGULARITY_OPTS} inspect ${SINGULARITY_COMMAND_OPTS} "
 
 -- conflict with modules with the same name
 conflict("{{ parsed_name.tool }}"{% if name != parsed_name.tool %},"{{ module.name }}"{% endif %}{% if aliases %}{% for alias in aliases %}{% if alias.name != parsed_name.tool %},"{{ alias.name }}"{% endif %}{% endfor %}{% endif %})
 
 -- if we have any wrapper scripts, add bin to path
-{% if wrapper_scripts %}prepend_path("PATH", pathJoin(moduleDir, "bin")){% endif %}
+{% if wrapper_scripts %}prepend_path("PATH", pathJoin(wrapperDir, "bin")){% endif %}
 
 -- "aliases" to module commands
 {% if aliases %}{% for alias in aliases %}{% if alias.name not in wrapper_scripts %}set_shell_function("{{ alias.name }}", execCmd .. {% if alias.singularity_options %} "{{ alias.singularity_options }} " .. {% endif %} containerPath .. " {{ alias.command }} \"$@\"", execCmd .. {% if alias.singularity_options %} "{{ alias.singularity_options }} " .. {% endif %} containerPath .. " {{ alias.command }}"){% endif %}
diff --git a/shpc/main/modules/templates/singularity.tcl b/shpc/main/modules/templates/singularity.tcl
index bcf45ac07..f21ccb3ad 100644
--- a/shpc/main/modules/templates/singularity.tcl
+++ b/shpc/main/modules/templates/singularity.tcl
@@ -67,6 +67,9 @@ set helpcommand "This module is a singularity container wrapper for {{ module.na
 # directory containing this modulefile, once symlinks resolved (dynamically defined)
 set moduleDir   [file dirname [expr { [string equal [file type ${ModulesCurrentModulefile}] "link"] ? [file readlink ${ModulesCurrentModulefile}] : ${ModulesCurrentModulefile} }]]
 
+# If we have wrapper base set, honor it, otherwise we use the moduleDir
+{% if settings.wrapper_base %}set wrapperDir "{{ module.wrapper_dir }}"{% else %}set wrapperDir "${moduleDir}"{% endif %}
+
 # conflict with modules with the same alias name
 conflict {{ parsed_name.tool }}
 {% if name != parsed_name.tool %}conflict {{ module.name }}{% endif %}
@@ -80,13 +83,13 @@ setenv SINGULARITY_SHELL {{ settings.singularity_shell }}
 setenv SINGULARITY_CONTAINER "${containerPath}"
 
 # interactive shell to any container, plus exec for aliases
-set shellCmd "singularity \${SINGULARITY_OPTS} shell \${SINGULARITY_COMMAND_OPTS} -s {{ settings.singularity_shell }} {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home | replace("$", "\$") }} --home {{ features.home | replace("$", "\$") }} {% endif %}{% if features.x11 %}-B {{ features.x11 | replace("$", "\$") }} {% endif %}{% if settings.environment_file %}-B ${moduleDir}/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }}{% endif %} ${containerPath}"
-set execCmd "singularity \${SINGULARITY_OPTS} exec \${SINGULARITY_COMMAND_OPTS} {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home | replace("$", "\$") }} --home {{ features.home | replace("$", "\$") }} {% endif %}{% if features.x11 %}-B {{ features.x11 | replace("$", "\$") }} {% endif %}{% if settings.environment_file %}-B ${moduleDir}/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }}{% endif %} "
-set runCmd "singularity \${SINGULARITY_OPTS} run \${SINGULARITY_COMMAND_OPTS} {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home | replace("$", "\$") }} --home {{ features.home | replace("$", "\$") }} {% endif %}{% if features.x11 %}-B {{ features.x11 | replace("$", "\$") }} {% endif %}{% if settings.environment_file %}-B ${moduleDir}/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }}{% endif %} ${containerPath}"
+set shellCmd "singularity \${SINGULARITY_OPTS} shell \${SINGULARITY_COMMAND_OPTS} -s {{ settings.singularity_shell }} {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home | replace("$", "\$") }} --home {{ features.home | replace("$", "\$") }} {% endif %}{% if features.x11 %}-B {{ features.x11 | replace("$", "\$") }} {% endif %}{% if settings.environment_file %}-B ${wrapperDir}/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }}{% endif %} ${containerPath}"
+set execCmd "singularity \${SINGULARITY_OPTS} exec \${SINGULARITY_COMMAND_OPTS} {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home | replace("$", "\$") }} --home {{ features.home | replace("$", "\$") }} {% endif %}{% if features.x11 %}-B {{ features.x11 | replace("$", "\$") }} {% endif %}{% if settings.environment_file %}-B ${wrapperDir}/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }}{% endif %} "
+set runCmd "singularity \${SINGULARITY_OPTS} run \${SINGULARITY_COMMAND_OPTS} {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home | replace("$", "\$") }} --home {{ features.home | replace("$", "\$") }} {% endif %}{% if features.x11 %}-B {{ features.x11 | replace("$", "\$") }} {% endif %}{% if settings.environment_file %}-B ${wrapperDir}/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }}{% endif %} ${containerPath}"
 set inspectCmd "singularity \${SINGULARITY_OPTS} inspect \${SINGULARITY_COMMAND_OPTS} "
 
 # if we have any wrapper scripts, add bin to path
-{% if wrapper_scripts %}prepend-path PATH "${moduleDir}/bin"{% endif %}
+{% if wrapper_scripts %}prepend-path PATH ${wrapperDir}/bin{% endif %}
 
 # "aliases" to module commands
 {% if aliases %}if { [ module-info shell bash ] } {
diff --git a/shpc/main/schemas.py b/shpc/main/schemas.py
index 1efdc2381..cc234550b 100644
--- a/shpc/main/schemas.py
+++ b/shpc/main/schemas.py
@@ -137,6 +137,7 @@
 settingsProperties = {
     "registry": {"type": "array", "items": {"type": "string"}},
     "sync_registry": {"type": "string"},
+    "wrapper_base": {"type": ["string", "null"]},
     "module_base": {"type": "string"},
     "container_base": {"type": ["string", "null"]},
     "namespace": {"type": ["string", "null"]},
diff --git a/shpc/main/wrappers/base.py b/shpc/main/wrappers/base.py
index fdf978468..98e1e0414 100644
--- a/shpc/main/wrappers/base.py
+++ b/shpc/main/wrappers/base.py
@@ -37,11 +37,13 @@ def container_dest_dir(self):
         return os.path.dirname(self.image)
 
     @property
-    def module_dir(self):
+    def wrapper_dir(self):
         """
-        Get the module directory (should error if not provided in kwargs)
+        Get the wrapper directory (should error if not provided in kwargs)
+
+        This can be the module directory or a custom wrapper directory.
         """
-        return self.kwargs["module_dir"]
+        return self.kwargs["wrapper_dir"]
 
     def get_template_paths(self):
         """
@@ -121,8 +123,8 @@ def generate(self, wrapper_name, alias_definition=None):
         interaction wrappers (e.g., exec, shell, etc.)
         """
 
-        # Write scripts into container directory
-        wrapper_dir = os.path.join(self.module_dir, "bin")
+        # Write scripts into custom module directory
+        wrapper_dir = os.path.join(self.wrapper_dir, "bin")
         shpc.utils.mkdirp([wrapper_dir])
         wrapper_path = os.path.join(wrapper_dir, wrapper_name)
 
@@ -132,7 +134,7 @@ def generate(self, wrapper_name, alias_definition=None):
             settings=self.settings,
             image=self.image,
             config=self.config,
-            # includes module_dir, features, etc
+            # includes wrapper_dir, features, etc
             **self.kwargs
         )
         shpc.utils.write_file(wrapper_path, out, exec=True)
diff --git a/shpc/main/wrappers/templates/bases/shell-script-base.sh b/shpc/main/wrappers/templates/bases/shell-script-base.sh
index 111f736c4..5d335656b 100755
--- a/shpc/main/wrappers/templates/bases/shell-script-base.sh
+++ b/shpc/main/wrappers/templates/bases/shell-script-base.sh
@@ -1,7 +1,6 @@
 #!{{ settings.wrapper_shell }}
 
 {% if '/csh' in settings.wrapper_shell %}set {% endif %}script=`realpath $0`
-{% if '/csh' in settings.wrapper_shell %}set {% endif %}wrapper_bin=`dirname $script`
-{% if '/csh' in settings.wrapper_shell %}set {% endif %}moduleDir=`dirname $wrapper_bin`
+{% if '/csh' in settings.wrapper_shell %}set {% endif %}wrapperDir=`dirname $script`/..
 
 {% block content %}{% endblock %}
diff --git a/shpc/main/wrappers/templates/docker.sh b/shpc/main/wrappers/templates/docker.sh
index eda8b65f1..2e92bd1df 100755
--- a/shpc/main/wrappers/templates/docker.sh
+++ b/shpc/main/wrappers/templates/docker.sh
@@ -1,4 +1,4 @@
 {% extends "bases/shell-script-base.sh" %}
 
-{% block content %}{{ container.command }} ${PODMAN_OPTS} run ${PODMAN_COMMAND_OPTS} -i{% if settings.enable_tty %}t{% endif %} -u `id -u`:`id -g` --rm {% if settings.environment_file %}--env-file $moduleDir/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v ${PWD} -w ${PWD} {% if alias.docker_options %} {{ alias.docker_options }} {% endif %} --entrypoint {{ alias.entrypoint }} {{ image }} {{ alias.args }} {% if '/sh' in settings.wrapper_shell or '/bash' in settings.wrapper_shell %}"$@"{% elif '/csh' in settings.wrapper_shell %}$argv:q{% endif %}
+{% block content %}{{ container.command }} ${PODMAN_OPTS} run ${PODMAN_COMMAND_OPTS} -i{% if settings.enable_tty %}t{% endif %} -u `id -u`:`id -g` --rm {% if settings.environment_file %}--env-file $wrapperDir/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v ${PWD} -w ${PWD} {% if alias.docker_options %} {{ alias.docker_options }} {% endif %} --entrypoint {{ alias.entrypoint }} {{ image }} {{ alias.args }} {% if '/sh' in settings.wrapper_shell or '/bash' in settings.wrapper_shell %}"$@"{% elif '/csh' in settings.wrapper_shell %}$argv:q{% endif %}
 {% endblock %}
diff --git a/shpc/main/wrappers/templates/docker/exec.sh b/shpc/main/wrappers/templates/docker/exec.sh
index 9de392494..b15194adb 100755
--- a/shpc/main/wrappers/templates/docker/exec.sh
+++ b/shpc/main/wrappers/templates/docker/exec.sh
@@ -1,4 +1,4 @@
 {% extends "bases/shell-script-base.sh" %}
 
-{% block content %}{{ container.command }} ${PODMAN_OPTS} run ${PODMAN_COMMAND_OPTS} -i{% if settings.enable_tty %}t{% endif %} -u `id -u`:`id -g` --rm {% if settings.environment_file %}--env-file $moduleDir/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v ${PWD} -w ${PWD} {{ image }} {{ alias.args }} {% if '/sh' in settings.wrapper_shell or '/bash' in settings.wrapper_shell %}"$@"{% elif '/csh' in settings.wrapper_shell %}$argv:q{% endif %}
+{% block content %}{{ container.command }} ${PODMAN_OPTS} run ${PODMAN_COMMAND_OPTS} -i{% if settings.enable_tty %}t{% endif %} -u `id -u`:`id -g` --rm {% if settings.environment_file %}--env-file $wrapperDir/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v ${PWD} -w ${PWD} {{ image }} {{ alias.args }} {% if '/sh' in settings.wrapper_shell or '/bash' in settings.wrapper_shell %}"$@"{% elif '/csh' in settings.wrapper_shell %}$argv:q{% endif %}
 {% endblock %}
diff --git a/shpc/main/wrappers/templates/docker/run.sh b/shpc/main/wrappers/templates/docker/run.sh
index 9de392494..b15194adb 100755
--- a/shpc/main/wrappers/templates/docker/run.sh
+++ b/shpc/main/wrappers/templates/docker/run.sh
@@ -1,4 +1,4 @@
 {% extends "bases/shell-script-base.sh" %}
 
-{% block content %}{{ container.command }} ${PODMAN_OPTS} run ${PODMAN_COMMAND_OPTS} -i{% if settings.enable_tty %}t{% endif %} -u `id -u`:`id -g` --rm {% if settings.environment_file %}--env-file $moduleDir/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v ${PWD} -w ${PWD} {{ image }} {{ alias.args }} {% if '/sh' in settings.wrapper_shell or '/bash' in settings.wrapper_shell %}"$@"{% elif '/csh' in settings.wrapper_shell %}$argv:q{% endif %}
+{% block content %}{{ container.command }} ${PODMAN_OPTS} run ${PODMAN_COMMAND_OPTS} -i{% if settings.enable_tty %}t{% endif %} -u `id -u`:`id -g` --rm {% if settings.environment_file %}--env-file $wrapperDir/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v ${PWD} -w ${PWD} {{ image }} {{ alias.args }} {% if '/sh' in settings.wrapper_shell or '/bash' in settings.wrapper_shell %}"$@"{% elif '/csh' in settings.wrapper_shell %}$argv:q{% endif %}
 {% endblock %}
diff --git a/shpc/main/wrappers/templates/docker/shell.sh b/shpc/main/wrappers/templates/docker/shell.sh
index 4081f6ff6..00e26d64e 100755
--- a/shpc/main/wrappers/templates/docker/shell.sh
+++ b/shpc/main/wrappers/templates/docker/shell.sh
@@ -1,4 +1,4 @@
 {% extends "bases/shell-script-base.sh" %}
 
-{% block content %}{{ container.command }} ${PODMAN_OPTS} run ${PODMAN_COMMAND_OPTS} -i{% if settings.enable_tty %}t{% endif %} -u `id -u`:`id -g` --rm {% if settings.environment_file %}--env-file $moduleDir/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v ${PWD} -w ${PWD} --entrypoint {{ settings.wrapper_shell }} {{ image }} {% if '/sh' in settings.wrapper_shell or '/bash' in settings.wrapper_shell %}"$@"{% elif '/csh' in settings.wrapper_shell %}$argv:q{% endif %}
+{% block content %}{{ container.command }} ${PODMAN_OPTS} run ${PODMAN_COMMAND_OPTS} -i{% if settings.enable_tty %}t{% endif %} -u `id -u`:`id -g` --rm {% if settings.environment_file %}--env-file $wrapperDir/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-v {{ settings.bindpaths }} {% endif %}{% if features.home %}-v {{ features.home }} {% endif %} -v ${PWD} -w ${PWD} --entrypoint {{ settings.wrapper_shell }} {{ image }} {% if '/sh' in settings.wrapper_shell or '/bash' in settings.wrapper_shell %}"$@"{% elif '/csh' in settings.wrapper_shell %}$argv:q{% endif %}
 {% endblock %}
diff --git a/shpc/main/wrappers/templates/singularity.sh b/shpc/main/wrappers/templates/singularity.sh
index 98ab377c8..7c7f2aa3f 100755
--- a/shpc/main/wrappers/templates/singularity.sh
+++ b/shpc/main/wrappers/templates/singularity.sh
@@ -1,4 +1,4 @@
 {% extends "bases/shell-script-base.sh" %}
 
-{% block content %}singularity ${SINGULARITY_OPTS} exec ${SINGULARITY_COMMAND_OPTS} {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home }} --home {{ features.home }} {% endif %}{% if features.x11 %}-B {{ features.x11 }} {% endif %}{% if settings.environment_file %}-B $moduleDir/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }}{% endif %} {% if alias.singularity_options %} {{ alias.singularity_options }} {% endif %} {{ image }} {{ alias.command }} {% if '/sh' in settings.wrapper_shell or '/bash' in settings.wrapper_shell %}"$@"{% elif '/csh' in settings.wrapper_shell %}$argv:q{% endif %}
+{% block content %}singularity ${SINGULARITY_OPTS} exec ${SINGULARITY_COMMAND_OPTS} {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home }} --home {{ features.home }} {% endif %}{% if features.x11 %}-B {{ features.x11 }} {% endif %}{% if settings.environment_file %}-B $wrapperDir/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }}{% endif %} {% if alias.singularity_options %} {{ alias.singularity_options }} {% endif %} {{ image }} {{ alias.command }} {% if '/sh' in settings.wrapper_shell or '/bash' in settings.wrapper_shell %}"$@"{% elif '/csh' in settings.wrapper_shell %}$argv:q{% endif %}
 {% endblock %}
diff --git a/shpc/main/wrappers/templates/singularity/exec.sh b/shpc/main/wrappers/templates/singularity/exec.sh
index d2b70eaf1..3040e3c23 100644
--- a/shpc/main/wrappers/templates/singularity/exec.sh
+++ b/shpc/main/wrappers/templates/singularity/exec.sh
@@ -1,4 +1,4 @@
 {% extends "bases/shell-script-base.sh" %}
 
-{% block content %}singularity ${SINGULARITY_OPTS} exec ${SINGULARITY_COMMAND_OPTS} {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home }} --home {{ features.home }} {% endif %}{% if features.x11 %}-B {{ features.x11 }} {% endif %}{% if settings.environment_file %}-B $moduleDir/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }}{% endif %} {% if alias.singularity_options %} {{ alias.singularity_options }} {% endif %} {{ image }} {% if '/sh' in settings.wrapper_shell or '/bash' in settings.wrapper_shell %}"$@"{% elif '/csh' in settings.wrapper_shell %}$argv:q{% endif %}
+{% block content %}singularity ${SINGULARITY_OPTS} exec ${SINGULARITY_COMMAND_OPTS} {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home }} --home {{ features.home }} {% endif %}{% if features.x11 %}-B {{ features.x11 }} {% endif %}{% if settings.environment_file %}-B $wrapperDir/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }}{% endif %} {% if alias.singularity_options %} {{ alias.singularity_options }} {% endif %} {{ image }} {% if '/sh' in settings.wrapper_shell or '/bash' in settings.wrapper_shell %}"$@"{% elif '/csh' in settings.wrapper_shell %}$argv:q{% endif %}
 {% endblock %}
diff --git a/shpc/main/wrappers/templates/singularity/run.sh b/shpc/main/wrappers/templates/singularity/run.sh
index 5047be356..7da8a9da0 100644
--- a/shpc/main/wrappers/templates/singularity/run.sh
+++ b/shpc/main/wrappers/templates/singularity/run.sh
@@ -1,4 +1,4 @@
 {% extends "bases/shell-script-base.sh" %}
 
-{% block content %}singularity ${SINGULARITY_OPTS} run ${SINGULARITY_COMMAND_OPTS} {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home }} --home {{ features.home }} {% endif %}{% if features.x11 %}-B {{ features.x11 }} {% endif %}{% if settings.environment_file %}-B $moduleDir/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }}{% endif %} {% if alias.singularity_options %} {{ alias.singularity_options }} {% endif %} {{ image }} {% if '/sh' in settings.wrapper_shell or '/bash' in settings.wrapper_shell %}"$@"{% elif '/csh' in settings.wrapper_shell %}$argv:q{% endif %}
+{% block content %}singularity ${SINGULARITY_OPTS} run ${SINGULARITY_COMMAND_OPTS} {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home }} --home {{ features.home }} {% endif %}{% if features.x11 %}-B {{ features.x11 }} {% endif %}{% if settings.environment_file %}-B $wrapperDir/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }}{% endif %} {% if alias.singularity_options %} {{ alias.singularity_options }} {% endif %} {{ image }} {% if '/sh' in settings.wrapper_shell or '/bash' in settings.wrapper_shell %}"$@"{% elif '/csh' in settings.wrapper_shell %}$argv:q{% endif %}
 {% endblock %}
diff --git a/shpc/main/wrappers/templates/singularity/shell.sh b/shpc/main/wrappers/templates/singularity/shell.sh
index f1638686d..aac4116fa 100644
--- a/shpc/main/wrappers/templates/singularity/shell.sh
+++ b/shpc/main/wrappers/templates/singularity/shell.sh
@@ -1,4 +1,4 @@
 {% extends "bases/shell-script-base.sh" %}
 
-{% block content %}singularity ${SINGULARITY_OPTS} shell ${SINGULARITY_COMMAND_OPTS} {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home }} --home {{ features.home }} {% endif %}{% if features.x11 %}-B {{ features.x11 }} {% endif %}{% if settings.environment_file %}-B $moduleDir/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }}{% endif %} -s {{ settings.wrapper_shell }} {{ image }}
+{% block content %}singularity ${SINGULARITY_OPTS} shell ${SINGULARITY_COMMAND_OPTS} {% if features.gpu %}{{ features.gpu }} {% endif %}{% if features.home %}-B {{ features.home }} --home {{ features.home }} {% endif %}{% if features.x11 %}-B {{ features.x11 }} {% endif %}{% if settings.environment_file %}-B $wrapperDir/{{ settings.environment_file }}:/.singularity.d/env/{{ settings.environment_file }}{% endif %} {% if settings.bindpaths %}-B {{ settings.bindpaths }}{% endif %} -s {{ settings.wrapper_shell }} {{ image }}
 {% endblock %}
diff --git a/shpc/settings.yml b/shpc/settings.yml
index e9d9f76eb..d74743824 100644
--- a/shpc/settings.yml
+++ b/shpc/settings.yml
@@ -45,6 +45,8 @@ container_base: $root_dir/containers
 # When parsing labels, replace newlines with this string
 label_separator: ', '
 
+wrapper_base: $root_dir/modules
+
 # Default root directory to create views
 views_base: $root_dir/views
 
diff --git a/shpc/version.py b/shpc/version.py
index 311dbd49a..43fac3353 100644
--- a/shpc/version.py
+++ b/shpc/version.py
@@ -2,7 +2,7 @@
 __copyright__ = "Copyright 2021-2023, Vanessa Sochat"
 __license__ = "MPL 2.0"
 
-__version__ = "0.1.23"
+__version__ = "0.1.24"
 AUTHOR = "Vanessa Sochat"
 EMAIL = "vsoch@users.noreply.github.com"
 NAME = "singularity-hpc"


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 414eb3704..038c25258 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -97,6 +97,8 @@ jobs:
         printf "\n\nmodule help ============================================\n"
         module help python/3.9.5-alpine
 
+        script_path=$(which python-exec)
+        cat $script_path
         set -x
         python-exec echo donuts >test_output
         cat test_output
diff --git a/shpc/tests/test_client.py b/shpc/tests/test_client.py
index b920d8db7..6cdfc85cf 100644
--- a/shpc/tests/test_client.py
+++ b/shpc/tests/test_client.py
@@ -44,10 +44,14 @@ def test_install_get(tmp_path, module_sys, module_file, container_tech, remote):
     assert os.path.exists(client.settings.module_base)
 
     module_dir = os.path.join(client.settings.module_base, "python", "3.9.2-alpine")
+    wrapper_dir = os.path.join(client.settings.wrapper_base, "python", "3.9.2-alpine")
+
     assert os.path.exists(module_dir)
     module_file = os.path.join(module_dir, module_file)
     assert os.path.exists(module_file)
-    env_file = os.path.join(module_dir, client.settings.environment_file)
+
+    # Environment file is in wrapper directory
+    env_file = os.path.join(wrapper_dir, client.settings.environment_file)
     assert os.path.exists(env_file)
 
     assert client.get("python:3.9.2-alpine")
diff --git a/shpc/tests/test_wrappers.py b/shpc/tests/test_wrappers.py
index 4fe6cecb3..0bb421fc0 100644
--- a/shpc/tests/test_wrappers.py
+++ b/shpc/tests/test_wrappers.py
@@ -13,6 +13,7 @@
 import shpc.main.container as container
 import shpc.main.registry as registry
 import shpc.main.wrappers.base as wrappers_base
+import shpc.utils as utils
 
 from .helpers import here, init_client
 
@@ -108,10 +109,13 @@ def test_wrapper_install(tmp_path, module_sys, module_file, container_tech):
     # Install known tag
     client.install("python:3.9.2-alpine")
     module_dir = os.path.join(client.settings.module_base, "python", "3.9.2-alpine")
+    wrapper_dir = os.path.join(client.settings.wrapper_base, "python", "3.9.2-alpine")
     assert os.path.exists(module_dir)
-    assert "bin" in os.listdir(module_dir)
-    module_bin = os.path.join(module_dir, "bin")
-    assert os.path.exists(module_bin)
+
+    # wrappers are installed to wrapper directory
+    assert "bin" in os.listdir(wrapper_dir)
+    wrapper_bin = os.path.join(wrapper_dir, "bin")
+    assert os.path.exists(wrapper_bin)
 
     requireds = [
         "python-container",
@@ -124,7 +128,7 @@ def test_wrapper_install(tmp_path, module_sys, module_file, container_tech):
         requireds += ["python-inspect-runscript", "python-inspect-deffile"]
     else:
         requireds += ["python-inspect"]
-    binaries = os.listdir(module_bin)
+    binaries = os.listdir(wrapper_bin)
     for required in requireds:
         assert required in binaries
 
@@ -154,6 +158,103 @@ def test_disabled_wrapper_install(tmp_path, module_sys, module_file, container_t
     assert "bin" not in os.listdir(module_dir)
 
 
+@pytest.mark.parametrize(
+    "module_sys,module_file,container_tech,wrapper_type",
+    [
+        ("lmod", "module.lua", "singularity", "custom"),
+        ("lmod", "module.lua", "singularity", "default"),
+        ("lmod", "module.lua", "singularity", "unset"),
+        ("lmod", "module.lua", "singularity", "disabled"),
+        ("lmod", "module.lua", "podman", "custom"),
+        ("lmod", "module.lua", "podman", "default"),
+        ("lmod", "module.lua", "podman", "unset"),
+        ("lmod", "module.lua", "podman", "disabled"),
+        ("tcl", "module.tcl", "singularity", "custom"),
+        ("tcl", "module.tcl", "singularity", "default"),
+        ("tcl", "module.tcl", "singularity", "unset"),
+        ("tcl", "module.tcl", "singularity", "disabled"),
+        ("tcl", "module.tcl", "podman", "custom"),
+        ("tcl", "module.tcl", "podman", "default"),
+        ("tcl", "module.tcl", "podman", "unset"),
+        ("tcl", "module.tcl", "podman", "disabled"),
+    ],
+)
+def test_custom_wrapper_dir(
+    tmp_path, module_sys, module_file, container_tech, wrapper_type
+):
+    """
+    Test that we can customize the install location of wrapper scripts
+
+    There are three cases:
+      - unset: the user explicitly sets to None
+      - default: we use the default value
+      - custom: we use a custom location
+    """
+
+    client = init_client(str(tmp_path), module_sys, container_tech)
+
+    if wrapper_type == "custom":
+        wrapper_dir = os.path.join(str(tmp_path), "wrappers")
+        client.settings.set("wrapper_base", wrapper_dir)
+
+    elif wrapper_type == "unset":
+        client.settings.set("wrapper_base", None)
+
+    # Note that if you set enabled to false but don't set path to None,
+    # we will still generate them, which likely is not desired
+    elif wrapper_type == "disabled":
+        client.settings.set("wrapper_scripts:enabled", False)
+        client.settings.set("wrapper_base", None)
+
+    # Default to $root/modules
+    elif wrapper_type == "default":
+        pass
+
+    # A custom wrapper directory should not have a bin in the module base
+    client.install("python:3.9.2-alpine")
+    module_dir = os.path.join(client.settings.module_base, "python", "3.9.2-alpine")
+    if client.settings.wrapper_base:
+        wrapper_dir = os.path.join(
+            client.settings.wrapper_base, "python", "3.9.2-alpine"
+        )
+    else:
+        wrapper_dir = module_dir
+
+    module_filepath = os.path.join(module_dir, module_file)
+
+    # Sanity check module is created and exists
+    assert os.path.exists(module_dir)
+    assert os.path.exists(module_filepath)
+
+    # The content of the file
+    content = utils.read_file(module_filepath)
+
+    print(module_file)
+    print(wrapper_type)
+
+    # For custom, wrapper_dir will have wrappers. For default, wrapper_dir will be modules
+    # directory. We can test the same way.
+    if module_file.endswith("lua") and wrapper_type in ["custom", "default"]:
+        assert f'local wrapperDir = "{wrapper_dir}"' in content
+
+    elif module_file.endswith("tcl") and wrapper_type in ["custom", "default"]:
+        assert f'set wrapperDir "{wrapper_dir}"' in content
+
+    elif module_file.endswith("lua") and wrapper_type in ["unset", "disabled"]:
+        assert "local wrapperDir = moduleDir" in content
+
+    elif module_file.endswith("tcl") and wrapper_type in ["unset", "disabled"]:
+        assert 'set wrapperDir "${moduleDir}"' in content
+
+    # Wrappers always generated in either wrapper_dir or module_dir, unless disabled
+    # bin is generated no matter what, but is populated in one case only
+    if wrapper_type == "disabled":
+        assert "bin" not in os.listdir(wrapper_dir)
+    else:
+        assert "bin" in os.listdir(wrapper_dir)
+        assert os.listdir(os.path.join(wrapper_dir, "bin"))
+
+
 @pytest.mark.parametrize(
     "make_absolute,exists",
     [