From 8dfc0cfa05ad7eec2d7740634336d83a28c6eb8d Mon Sep 17 00:00:00 2001 From: bcumming Date: Mon, 19 Jan 2026 17:47:34 +0100 Subject: [PATCH 1/8] remove activate.sh script --- stackinator/recipe.py | 1 - 1 file changed, 1 deletion(-) diff --git a/stackinator/recipe.py b/stackinator/recipe.py index 2827fc3..60c92ce 100644 --- a/stackinator/recipe.py +++ b/stackinator/recipe.py @@ -326,7 +326,6 @@ def environment_view_meta(self): view_meta[view["name"]] = { "root": view["config"]["root"], - "activate": view["config"]["root"] + "/activate.sh", "description": "", # leave the description empty for now "recipe_variables": env.as_dict(), } From 504ae0c3dc118ded148301a60f233c0e22af79c1 Mon Sep 17 00:00:00 2001 From: bcumming Date: Mon, 19 Jan 2026 19:06:20 +0100 Subject: [PATCH 2/8] substitute mount and view variables at configuration time --- stackinator/recipe.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/stackinator/recipe.py b/stackinator/recipe.py index 60c92ce..555286a 100644 --- a/stackinator/recipe.py +++ b/stackinator/recipe.py @@ -292,6 +292,16 @@ def environment_view_meta(self): view_meta = {} for _, env in self.environments.items(): for view in env["views"]: + # recipe authors can substitute the name of the view, the mount + # and view path into environment variables using '$@key@' where + # key is one of view_name, mount and view_path. + substitutions = { + "view_name": view["name"], + "mount": self.mount, + "view_path": view["config"]["root"] + } + fill = lambda s: re.sub(r"\$@(\w+)@", lambda m: substitutions.get(m.group(1), m.group(0)), s,) + ev_inputs = view["extra"]["env_vars"] env = envvars.EnvVarSet() @@ -302,6 +312,9 @@ def environment_view_meta(self): for v in ev_inputs["set"]: ((name, value),) = v.items() + if value is not None: + value = fill(value) + # insist that the only 'set' operation on prefix variables is to unset/reset them # this requires that users use append and prepend to build up the variables if envvars.is_list_var(name) and value is not None: @@ -313,12 +326,16 @@ def environment_view_meta(self): env.set_scalar(name, value) for v in ev_inputs["prepend_path"]: ((name, value),) = v.items() + if value is not None: + value = fill(value) if not envvars.is_list_var(name): raise RuntimeError(f"{name} in the {view['name']} view is not a known prefix path variable") env.set_list(name, [value], envvars.EnvVarOp.APPEND) for v in ev_inputs["append_path"]: ((name, value),) = v.items() + if value is not None: + value = fill(value) if not envvars.is_list_var(name): raise RuntimeError(f"{name} in the {view['name']} view is not a known prefix path variable") From 89befe056bccc7d015b038e608b1d16d58519abf Mon Sep 17 00:00:00 2001 From: bcumming Date: Tue, 20 Jan 2026 17:28:02 +0100 Subject: [PATCH 3/8] roll back to python-based stack-config --- .gitignore | 2 ++ bin/stack-config | 23 +++++++++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 4b471c5..be460d2 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ site # distribution/packaging files *.egg-info/ build/ +uv.lock +.venv* diff --git a/bin/stack-config b/bin/stack-config index 31a700e..6649699 100755 --- a/bin/stack-config +++ b/bin/stack-config @@ -1,6 +1,21 @@ -#!/usr/bin/env bash +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.12" +# dependencies = [ +# "jinja2", +# "jsonschema", +# "pyYAML", +# ] +# /// -export UV_PROJECT_ENVIRONMENT=.venv-`uname -m` +import pathlib +import sys -STACKINATOR_ROOT=$(dirname `realpath $0`)/.. -uv run --directory $STACKINATOR_ROOT --with . python -m stackinator.main $@ +prefix = pathlib.Path(__file__).parent.parent.resolve() +sys.path = [prefix.as_posix()] + sys.path + +from stackinator.main import main + +# Once we've set up the system path, run the tool's main method +if __name__ == "__main__": + sys.exit(main()) From ccbc2386d93fb1cea53dac9493bdfdd7930c9740 Mon Sep 17 00:00:00 2001 From: bcumming Date: Tue, 20 Jan 2026 17:28:49 +0100 Subject: [PATCH 4/8] values for variable substitution need to be strings --- stackinator/recipe.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stackinator/recipe.py b/stackinator/recipe.py index 555286a..9ec8655 100644 --- a/stackinator/recipe.py +++ b/stackinator/recipe.py @@ -296,9 +296,9 @@ def environment_view_meta(self): # and view path into environment variables using '$@key@' where # key is one of view_name, mount and view_path. substitutions = { - "view_name": view["name"], - "mount": self.mount, - "view_path": view["config"]["root"] + "view_name": str(view["name"]), + "mount": str(self.mount), + "view_path": str(view["config"]["root"]) } fill = lambda s: re.sub(r"\$@(\w+)@", lambda m: substitutions.get(m.group(1), m.group(0)), s,) From 7d332011c9d5abdd9dff81e5eda19277c349fc0a Mon Sep 17 00:00:00 2001 From: bcumming Date: Tue, 20 Jan 2026 17:38:31 +0100 Subject: [PATCH 5/8] update docs --- docs/recipes.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/recipes.md b/docs/recipes.md index 97dafe4..cbe7129 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -405,12 +405,24 @@ The `set` field is a list of environment variables key-value pairs that specify * It is not possible to set an initial value that is not `null` for a prefix path variable. Set such variables to `null` (unset it), then provide `append_path` and `prefix_path` operations below to set the individual paths. -!!! note "using `${@VAR@}` to use environment variables" +!!! info "use `${@VAR@}` to set environment variables at runtime" Sometimes you want to compose an environment variable **that has been set in the runtime environment** in your environment variable definition. For example, every user has a different `HOME` or `SCRATCH` value, and you might want to configure your view to store / read configuration from this path. The special syntax `${@VAR@}` will defer expanding the environment variable `VAR` until the view is loaded by uenv. The example above shows how to set the Juliaup install directory to be in the user's local scratch, i.e. a personalised private location for each user. +!!! info "use `$@var@` to configure environment variables at configure time" + The special syntax `$@var@` can be used to substitute information about the view when configuring the recipe. + This is useful if you want to set an environment variable that refers to the mount point or mounted location of the view. + + The following values are available: + + | key | description | + | --- | ----------- | + | `mount` | the mount point of the image, e.g. `/user-environment` | + | `view_name` | the name of the view, e.g. `cuda-env` in the example above | + | `view_path` | the prefix path of the view, e.g. `/user-environment/env/cuda-env` | + The `prepend_path` field takes a list of key-value pairs that define paths to prepend to a prefix path variable. * Each entry is a single path From 93c2ba071929bf32e492cbaa34c9169d08b94c8c Mon Sep 17 00:00:00 2001 From: bcumming Date: Tue, 20 Jan 2026 17:41:09 +0100 Subject: [PATCH 6/8] feed da linta --- stackinator/recipe.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/stackinator/recipe.py b/stackinator/recipe.py index 9ec8655..dcde525 100644 --- a/stackinator/recipe.py +++ b/stackinator/recipe.py @@ -296,11 +296,15 @@ def environment_view_meta(self): # and view path into environment variables using '$@key@' where # key is one of view_name, mount and view_path. substitutions = { - "view_name": str(view["name"]), - "mount": str(self.mount), - "view_path": str(view["config"]["root"]) + "view_name": str(view["name"]), + "mount": str(self.mount), + "view_path": str(view["config"]["root"]), } - fill = lambda s: re.sub(r"\$@(\w+)@", lambda m: substitutions.get(m.group(1), m.group(0)), s,) + fill = lambda s: re.sub( + r"\$@(\w+)@", + lambda m: substitutions.get(m.group(1), m.group(0)), + s, + ) ev_inputs = view["extra"]["env_vars"] env = envvars.EnvVarSet() From 8d7aa1610de5e1d1dbf73aede0c2f3b5ddbb91f6 Mon Sep 17 00:00:00 2001 From: bcumming Date: Tue, 20 Jan 2026 17:43:52 +0100 Subject: [PATCH 7/8] feed the checker --- stackinator/recipe.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/stackinator/recipe.py b/stackinator/recipe.py index dcde525..4fe6a5d 100644 --- a/stackinator/recipe.py +++ b/stackinator/recipe.py @@ -300,11 +300,14 @@ def environment_view_meta(self): "mount": str(self.mount), "view_path": str(view["config"]["root"]), } - fill = lambda s: re.sub( - r"\$@(\w+)@", - lambda m: substitutions.get(m.group(1), m.group(0)), - s, - ) + + def fill(s): + re.sub( + r"\$@(\w+)@", + lambda m: substitutions.get(m.group(1), m.group(0)), + s, + ) + return s ev_inputs = view["extra"]["env_vars"] env = envvars.EnvVarSet() From 2286fcabf35df4ed8087e77c36a55bfd6db2296a Mon Sep 17 00:00:00 2001 From: bcumming Date: Tue, 20 Jan 2026 17:57:52 +0100 Subject: [PATCH 8/8] fix lamda->def --- stackinator/recipe.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/stackinator/recipe.py b/stackinator/recipe.py index 4fe6a5d..ca8d2b3 100644 --- a/stackinator/recipe.py +++ b/stackinator/recipe.py @@ -302,12 +302,11 @@ def environment_view_meta(self): } def fill(s): - re.sub( + return re.sub( r"\$@(\w+)@", lambda m: substitutions.get(m.group(1), m.group(0)), s, ) - return s ev_inputs = view["extra"]["env_vars"] env = envvars.EnvVarSet()