Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 60 additions & 1 deletion docs/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:<view_name>: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

Expand Down
3 changes: 3 additions & 0 deletions stackinator/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
18 changes: 16 additions & 2 deletions stackinator/etc/envvars.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions stackinator/recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import yaml

from . import cache, root_logger, schema, spack_util
from .etc import envvars


class Recipe:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()]
)
Expand Down Expand Up @@ -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
47 changes: 47 additions & 0 deletions stackinator/schema/environments.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"}
}
}
}
}
}
Expand Down