diff --git a/docs/recipes.md b/docs/recipes.md index 5d6e69d..7561a32 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -359,14 +359,73 @@ cuda-env: add_compilers: true prefix_paths: LD_LIBRARY_PATH: [lib, lib64] + env_vars: + set: + - WOMBAT: null + - NOCOLOR: "1" + - JULIAUP_INSTALLDIR: "${@SCRATCH@}/.julia/gh200/juliaup" + - PKG_CONFIG_PATH: null + prepend_path: + - PATH: "${@HOME@}/.local/x86_4/bin/" + append_path: + - PKG_CONFIG_PATH: /usr/lib/pkgconfig + - PKG_CONFIG_PATH: /opt/cray/libfabric/1.15.2.0/lib64/pkgconfig ``` * `add_compilers` (default `true`): by default Spack will not add compilers to the `PATH` variable. Stackinator automatically adds the `gcc` and/or `nvhpc` to path. This option can be used to explicitly disable or enable this feature. * `prefix_paths` (default empty): this option can be used to customise prefix style environment variables (`PATH`, `LD_LIBRARY_PATH`, `PKG_CONFIG_PATH`, `PYTHONPATH`, etc). * the key is the environment variable, and the value is a list of paths to search for in the environment view. All paths that match an entry in the list will be prepended to the prefix path environment variable. * the main use for this feature is to opt-in to setting `LD_LIBRARY_PATH`. By default Spack does not add `lib` and `lib64` to `LD_LIBRARY_PATH` because that can break system installed applications that depend on `LD_LIBRARY_PATH` or finding their dependencies in standard locations like `/usr/lib`. +* `env_vars`: see below for a more detailed explanation of how to fine tune environment variables in the view. -See the [interfaces documentation](interfaces.md#environment-views) for more information about how the environment views are provided to users of a stack. +!!! info + See the [interfaces documentation](interfaces.md#environment-views) for more information about how the environment views are provided. + +#### Setting environment variables with `env_vars` + +The `views::uenv:env_vars` field can be used to further fine-tune the environment variables that are set when the view is started. +There are three environment variable "operation" that can be specified - `set`, `prepend_path`, and `append_path` - as demonstrated in the example above. + +The `set` field is a list of environment variables key-value pairs that specify the variable name and the value to set it to. + +* Setting a field to `null` will unset the variable if it was set by the parent environment. +* `set` is a list, and values will be applied in the order that they are provided. + It is possible to provide two values for a variable, and the last value will be the one used. +* 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" + 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. + +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 +* To prepend more than one path to a variable, pass multiple values (see `PKG_CONFIG_PATH` in `append_path` above) +* The order in the list matters: paths will be prepended in the order that they appear in the list. + +The `append_path` field is the same as `prepend_path`, except it appends instead of prepending. + +!!! question "What are prefix path variables?" + Prefix path variables are environment variables that are `:`-separated list of paths, like `PATH`, `LD_LIBRARY_PATH`, `PYTHONPATH` etc that provide a list of paths that are typically searched in order. + + Currently the set of supported prefix path variables is hard coded in stackinator. + If you need to set a prefix path that isn't on this list, contact the stackinator devs and we can implement support for user-defined prefix paths. + + ??? info "the hard-coded prefix paths" + + * `ACLOCAL_PATH` + * `CMAKE_PREFIX_PATH` + * `CPATH` + * `LD_LIBRARY_PATH` + * `LIBRARY_PATH` + * `MANPATH` + * `MODULEPATH` + * `PATH` + * `PKG_CONFIG_PATH` + * `PYTHONPATH` ## Modules diff --git a/stackinator/builder.py b/stackinator/builder.py index f7fffb3..53de2ba 100644 --- a/stackinator/builder.py +++ b/stackinator/builder.py @@ -139,6 +139,9 @@ def environment_meta(self, recipe): "root": /user-environment/env/default, "activate": /user-environment/env/default/activate.sh, "description": "simple devolpment env: compilers, MPI, python, cmake." + "env_vars": { + ... + } }, "tools": { "root": /user-environment/env/tools, diff --git a/stackinator/etc/envvars.py b/stackinator/etc/envvars.py index 8447c69..ea15038 100755 --- a/stackinator/etc/envvars.py +++ b/stackinator/etc/envvars.py @@ -443,6 +443,7 @@ def view_impl(args): # force all prefix path style variables (list vars) to use PREPEND the first operation. envvars.make_dirty() + # remove all prefix path variable values that point to a location inside the build path. envvars.remove_root(args.build_path) if args.compilers is not None: @@ -527,11 +528,24 @@ def meta_impl(args): print(f"error - meta data file '{json_path}' does not exist.") exit(1) + # load the environment variable state that based on the activate.sh + # script generated by spack with open(json_path, "r") as fid: - envvar_dict = json.load(fid) + spack_vars = json.load(fid) + + # load the environment variable state changes specified in the + # view:uenv:env_vars field in the recipe environments.yaml file + recipe_vars = data["recipe_variables"] + + # update the view environment variables by appending variables from the recipe + for var_name, value in recipe_vars["scalar"].items(): + spack_vars["values"]["scalar"][var_name] = value + for var_name, updates in recipe_vars["list"].items(): + spack_vars["values"]["list"].setdefault(name, []) + spack_vars["values"]["list"][var_name] += updates # update the global meta data to include the environment variable state - meta["views"][name]["env"] = envvar_dict + meta["views"][name]["env"] = spack_vars meta["views"][name]["type"] = "spack-view" # process spack and modules diff --git a/stackinator/recipe.py b/stackinator/recipe.py index 91b111c..4b974a3 100644 --- a/stackinator/recipe.py +++ b/stackinator/recipe.py @@ -6,6 +6,7 @@ import yaml from . import cache, root_logger, schema, spack_util +from .etc import envvars class Recipe: @@ -270,10 +271,43 @@ def environment_view_meta(self): view_meta = {} for _, env in self.environments.items(): for view in env["views"]: + ev_inputs = view["extra"]["env_vars"] + env = envvars.EnvVarSet() + + # TODO: one day this code will be revisited because we need to append_path + # or prepend_path to a variable that isn't in envvars.is_list_var + # On that day, extend the environments.yaml views:uenv:env_vars field + # to also accept a list of env var names to add to the blessed list of prefix paths + + for v in ev_inputs["set"]: + ((name, value),) = v.items() + # 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: + raise RuntimeError(f"{name} in the {view['name']} view is a prefix variable.") + else: + if envvars.is_list_var(name): + env.set_list(name, [], envvars.EnvVarOp.SET) + else: + env.set_scalar(name, value) + for v in ev_inputs["prepend_path"]: + ((name, value),) = v.items() + 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 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.PREPEND) + 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(), } return view_meta @@ -377,12 +411,19 @@ def generate_environment_specs(self, raw): # ["link"] = "roots" # ["uenv"]["add_compilers"] = True # ["uenv"]["prefix_paths"] = {} + # ["uenv"]["env_vars"] = {"set": [], "unset": [], "prepend_path": [], "append_path": []} if view_config is None: view_config = {} view_config.setdefault("link", "roots") view_config.setdefault("uenv", {}) view_config["uenv"].setdefault("add_compilers", True) view_config["uenv"].setdefault("prefix_paths", {}) + view_config["uenv"].setdefault("env_vars", {}) + view_config["uenv"]["env_vars"].setdefault("set", []) + view_config["uenv"]["env_vars"].setdefault("unset", []) + view_config["uenv"]["env_vars"].setdefault("prepend_path", []) + view_config["uenv"]["env_vars"].setdefault("append_path", []) + prefix_string = ",".join( [f"{pname}={':'.join(paths)}" for pname, paths in view_config["uenv"]["prefix_paths"].items()] ) @@ -505,6 +546,7 @@ def environment_files(self): files["config"] = {} for env, config in self.environments.items(): spack_yaml_template = jenv.get_template("environments.spack.yaml") + # generate the spack.yaml file files["config"][env] = spack_yaml_template.render(config=config, name=env, store=self.mount) return files diff --git a/stackinator/schema/environments.json b/stackinator/schema/environments.json index 8a9fd6a..74f5e76 100644 --- a/stackinator/schema/environments.json +++ b/stackinator/schema/environments.json @@ -3,6 +3,35 @@ "title": "Schema for Spack Stack environments.yaml recipe file", "type": "object", "additionalProperties": false, + "defs": { + "gcc_version_spec": { + "type": "string", + "pattern": "^gcc@\\d{1,2}(\\.\\d{1}(\\.\\d{1})?)?$" + }, + "env_var_def": { + "type": "object", + "patternProperties": { + "^[A-Za-z_][A-Za-z0-9_]*$": { "type": "string" } + }, + "minProperties": 1, + "maxProperties": 1, + "additionalProperties": false + }, + "env_var_def_nullable": { + "type": "object", + "patternProperties": { + "^[A-Za-z_][A-Za-z0-9_]*$": { + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + } + }, + "minProperties": 1, + "maxProperties": 1, + "additionalProperties": false + } + }, "patternProperties": { "\\w[\\w-]*": { "type": "object", @@ -114,6 +143,24 @@ "items": {"type": "string"} } } + }, + "env_vars": { + "type": "object", + "additionalProperties": false, + "properties": { + "set": { + "type": "array", + "items": {"$ref": "#/defs/env_var_def_nullable"} + }, + "prepend_path": { + "type": "array", + "items": {"$ref": "#/defs/env_var_def"} + }, + "append_path": { + "type": "array", + "items": {"$ref": "#/defs/env_var_def"} + } + } } } }