Repository: spack/spack

Issue:
Allow for environment modifications by the external packages
It would be nice to have an option to apply additional modifications to the environment when an external package is in use. We already have it for compilers, so the required logic is already more or less implemented.

### Rationale

This would help to customize the existing modules. For example, some packages need to run `srun` either at the configure time (to make sure that the provided MPI implementation does not suffer some particular bugs) or when running tests. A common assumption is that `srun -n 4 ./executable` does the job. There are cases, though, when this does not work. On one of the systems I have to specify an account, a constraint and a partition:
```console
srun -A some_account -C some_constraint -p some_partition -n 4 ./executable
```

This could be solved with the requested feature as follows:
```yaml
packages:
  cray-mpich:
    buildable: false
    externals:
    - spec: cray-mpich@7.7.16
      modules:
      - cray-mpich/7.7.16
      environment:
        set:
          SLURM_ACCOUNT: some_account
          SLURM_CONSTRAINT: some_constraint
          SLURM_PARTITION: some_partition
```

and `srun -n 4 ./executable` would work.

### Description

The current way to solve this problem is to create an additional module and append it to the list of loaded modules. This is not very convenient though.

### Additional information
We need to discuss whether the environment modifications should affect the hash. This makes sense for the modules since we normally get the prefix from them but I don't think we should try to guess the prefix based on the environment modifications.


### General information

- [ ] I have run `spack --version` and reported the version of Spack
- [x] I have searched the issues of this repo and believe this is not a duplicate


Discussion:
There is probably one more thing to consider. Imagine that `mpi` is not an external package. Say we build `openmpi`. We still have to set additional environment variables to make `srun` work. Therefore, it would be nice to have an additional `environment` section for the whole package, not only for externals:

```yaml
packages:
  openmpi:
    buildable: true
    environment:
# we want to set SLURM variables regardless of whether an external version is used
      set:
        SLURM_ACCOUNT: some_account
        SLURM_CONSTRAINT: some_constraint
        SLURM_PARTITION: some_partition
    externals:
    - spec: openmpi@4.0.5%gcc
      modules:
      - openmpi/4.0.5-gcc
      environment:
# the openmpi/4.0.5-gcc module sets some custom BAD_VAR variable, which we don't want in the build environment
        unset: BAD_VAR
```

Resolving patch:
diff --git a/lib/spack/spack/build_environment.py b/lib/spack/spack/build_environment.py
--- a/lib/spack/spack/build_environment.py
+++ b/lib/spack/spack/build_environment.py
@@ -1032,6 +1032,11 @@ def get_env_modifications(self) -> EnvironmentModifications:
                     if id(spec) in self.nodes_in_subdag:
                         pkg.setup_dependent_run_environment(run_env_mods, spec)
                 pkg.setup_run_environment(run_env_mods)
+
+                external_env = (dspec.extra_attributes or {}).get("environment", {})
+                if external_env:
+                    run_env_mods.extend(spack.schema.environment.parse(external_env))
+
                 if self.context == Context.BUILD:
                     # Don't let the runtime environment of comiler like dependencies leak into the
                     # build env
diff --git a/lib/spack/spack/schema/packages.py b/lib/spack/spack/schema/packages.py
--- a/lib/spack/spack/schema/packages.py
+++ b/lib/spack/spack/schema/packages.py
@@ -7,6 +7,7 @@
 .. literalinclude:: _spack_root/lib/spack/spack/schema/packages.py
    :lines: 13-
 """
+import spack.schema.environment
 
 permissions = {
     "type": "object",
@@ -155,7 +156,13 @@
                                 "spec": {"type": "string"},
                                 "prefix": {"type": "string"},
                                 "modules": {"type": "array", "items": {"type": "string"}},
-                                "extra_attributes": {"type": "object"},
+                                "extra_attributes": {
+                                    "type": "object",
+                                    "additionalProperties": True,
+                                    "properties": {
+                                        "environment": spack.schema.environment.definition
+                                    },
+                                },
                             },
                             "additionalProperties": True,
                             "required": ["spec"],


Test patch:
diff --git a/lib/spack/spack/test/build_environment.py b/lib/spack/spack/test/build_environment.py
--- a/lib/spack/spack/test/build_environment.py
+++ b/lib/spack/spack/test/build_environment.py
@@ -285,6 +285,24 @@ def platform_pathsep(pathlist):
         assert name not in os.environ
 
 
+def test_external_config_env(mock_packages, mutable_config, working_env):
+    cmake_config = {
+        "externals": [
+            {
+                "spec": "cmake@1.0",
+                "prefix": "/fake/path",
+                "extra_attributes": {"environment": {"set": {"TEST_ENV_VAR_SET": "yes it's set"}}},
+            }
+        ]
+    }
+    spack.config.set("packages:cmake", cmake_config)
+
+    cmake_client = spack.spec.Spec("cmake-client").concretized()
+    spack.build_environment.setup_package(cmake_client.package, False)
+
+    assert os.environ["TEST_ENV_VAR_SET"] == "yes it's set"
+
+
 @pytest.mark.regression("9107")
 def test_spack_paths_before_module_paths(config, mock_packages, monkeypatch, working_env):
     s = spack.spec.Spec("cmake")