diff --git a/.gitignore b/.gitignore index 4b471c52..be460d29 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 31a700ed..66496991 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()) diff --git a/docs/recipes.md b/docs/recipes.md index 97dafe49..cbe71290 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 diff --git a/stackinator/recipe.py b/stackinator/recipe.py index 2827fc3e..ca8d2b3d 100644 --- a/stackinator/recipe.py +++ b/stackinator/recipe.py @@ -292,6 +292,22 @@ 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": str(view["name"]), + "mount": str(self.mount), + "view_path": str(view["config"]["root"]), + } + + def fill(s): + return 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 +318,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 +332,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") @@ -326,7 +349,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(), }