Unified environment modifications in config files (#14372)

* Unified environment modifications in config files

fixes #13357

This commit factors all the code that is involved in
the validation (schema) and parsing of environment modifications
from configuration files in a single place. The factored out
code is then used for module files and compiler configuration.

Attributes were separated by dashes in `compilers.yaml` files and
by underscores in `modules.yaml` files. This PR unifies the syntax
on attributes separated by underscores.

Unit testing of environment modifications in compilers
has been refactored and simplified.
This commit is contained in:
Massimiliano Culpo 2020-01-27 17:40:47 +01:00 committed by Greg Becker
parent 0f3ae864a5
commit b9629c36f2
8 changed files with 170 additions and 184 deletions

View File

@ -929,11 +929,13 @@ in GNU Autotools. If all flags are set, the order is
Compiler environment variables and additional RPATHs Compiler environment variables and additional RPATHs
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In the exceptional case a compiler requires setting special environment Sometimes compilers require setting special environment variables to
variables, like an explicit library load path. These can bet set in an operate correctly. Spack handles these cases by allowing custom environment
extra section in the compiler configuration (the supported environment modifications in the ``environment`` attribute of the compiler configuration
modification commands are: ``set``, ``unset``, ``append-path``, and section. See also the :ref:`configuration_environment_variables` section
``prepend-path``). The user can also specify additional ``RPATHs`` that the of the configuration files docs for more information.
It is also possible to specify additional ``RPATHs`` that the
compiler will add to all executables generated by that compiler. This is compiler will add to all executables generated by that compiler. This is
useful for forcing certain compilers to RPATH their own runtime libraries, so useful for forcing certain compilers to RPATH their own runtime libraries, so
that executables will run without the need to set ``LD_LIBRARY_PATH``. that executables will run without the need to set ``LD_LIBRARY_PATH``.
@ -950,28 +952,19 @@ that executables will run without the need to set ``LD_LIBRARY_PATH``.
fc: /opt/gcc/bin/gfortran fc: /opt/gcc/bin/gfortran
environment: environment:
unset: unset:
BAD_VARIABLE: # The colon is required but the value must be empty - BAD_VARIABLE
set: set:
GOOD_VARIABLE_NUM: 1 GOOD_VARIABLE_NUM: 1
GOOD_VARIABLE_STR: good GOOD_VARIABLE_STR: good
prepend-path: prepend_path:
PATH: /path/to/binutils PATH: /path/to/binutils
append-path: append_path:
LD_LIBRARY_PATH: /opt/gcc/lib LD_LIBRARY_PATH: /opt/gcc/lib
extra_rpaths: extra_rpaths:
- /path/to/some/compiler/runtime/directory - /path/to/some/compiler/runtime/directory
- /path/to/some/other/compiler/runtime/directory - /path/to/some/other/compiler/runtime/directory
.. note::
The section `environment` is interpreted as an ordered dictionary, which
means two things. First, environment modification are applied in the order
they are specified in the configuration file. Second, you cannot express
environment modifications that require mixing different commands, i.e. you
cannot `set` one variable, than `prepend-path` to another one, and than
again `set` a third one.
^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^
Architecture specifiers Architecture specifiers
^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -427,6 +427,33 @@ home directory, and ``~user`` will expand to a specified user's home
directory. The ``~`` must appear at the beginning of the path, or Spack directory. The ``~`` must appear at the beginning of the path, or Spack
will not expand it. will not expand it.
.. _configuration_environment_variables:
-------------------------
Environment Modifications
-------------------------
Spack allows to prescribe custom environment modifications in a few places
within its configuration files. Every time these modifications are allowed
they are specified as a dictionary, like in the following example:
.. code-block:: yaml
environment:
set:
LICENSE_FILE: '/path/to/license'
unset:
- CPATH
- LIBRARY_PATH
append_path:
PATH: '/new/bin/dir'
The possible actions that are permitted are ``set``, ``unset``, ``append_path``,
``prepend_path`` and finally ``remove_path``. They all require a dictionary
of variable names mapped to the values used for the modification.
The only exception is ``unset`` that requires just a list of variable names.
No particular order is ensured on the execution of each of these modifications.
---------------------------- ----------------------------
Seeing Spack's Configuration Seeing Spack's Configuration
---------------------------- ----------------------------

View File

@ -39,7 +39,6 @@
import sys import sys
import traceback import traceback
import types import types
from six import iteritems
from six import StringIO from six import StringIO
import llnl.util.tty as tty import llnl.util.tty as tty
@ -52,6 +51,7 @@
import spack.config import spack.config
import spack.main import spack.main
import spack.paths import spack.paths
import spack.schema.environment
import spack.store import spack.store
from spack.util.string import plural from spack.util.string import plural
from spack.util.environment import ( from spack.util.environment import (
@ -342,21 +342,7 @@ def set_build_environment_variables(pkg, env, dirty):
# Set environment variables if specified for # Set environment variables if specified for
# the given compiler # the given compiler
compiler = pkg.compiler compiler = pkg.compiler
environment = compiler.environment env.extend(spack.schema.environment.parse(compiler.environment))
for command, variable in iteritems(environment):
if command == 'set':
for name, value in iteritems(variable):
env.set(name, value)
elif command == 'unset':
for name, _ in iteritems(variable):
env.unset(name)
elif command == 'prepend-path':
for name, value in iteritems(variable):
env.prepend_path(name, value)
elif command == 'append-path':
for name, value in iteritems(variable):
env.append_path(name, value)
if compiler.extra_rpaths: if compiler.extra_rpaths:
extra_rpaths = ':'.join(compiler.extra_rpaths) extra_rpaths = ':'.join(compiler.extra_rpaths)

View File

@ -28,26 +28,24 @@
Each of the four classes needs to be sub-classed when implementing a new Each of the four classes needs to be sub-classed when implementing a new
module type. module type.
""" """
import collections
import copy import copy
import datetime import datetime
import inspect import inspect
import os.path import os.path
import re import re
import collections
import six
import llnl.util.filesystem import llnl.util.filesystem
import llnl.util.tty as tty import llnl.util.tty as tty
import spack.paths
import spack.build_environment as build_environment import spack.build_environment as build_environment
import spack.util.environment
import spack.tengine as tengine
import spack.util.path
import spack.util.environment
import spack.error import spack.error
import spack.util.spack_yaml as syaml import spack.paths
import spack.schema.environment
import spack.tengine as tengine
import spack.util.environment
import spack.util.file_permissions as fp import spack.util.file_permissions as fp
import spack.util.path
import spack.util.spack_yaml as syaml
#: config section for this file #: config section for this file
@ -415,22 +413,7 @@ def env(self):
"""List of environment modifications that should be done in the """List of environment modifications that should be done in the
module. module.
""" """
env_mods = spack.util.environment.EnvironmentModifications() return spack.schema.environment.parse(self.conf.get('environment', {}))
actions = self.conf.get('environment', {})
def process_arglist(arglist):
if method == 'unset':
for x in arglist:
yield (x,)
else:
for x in six.iteritems(arglist):
yield x
for method, arglist in actions.items():
for args in process_arglist(arglist):
getattr(env_mods, method)(*args)
return env_mods
@property @property
def suffixes(self): def suffixes(self):

View File

@ -8,7 +8,7 @@
.. literalinclude:: _spack_root/lib/spack/spack/schema/compilers.py .. literalinclude:: _spack_root/lib/spack/spack/schema/compilers.py
:lines: 13- :lines: 13-
""" """
import spack.schema.environment
#: Properties for inclusion in other schemas #: Properties for inclusion in other schemas
properties = { properties = {
@ -68,50 +68,7 @@
{'type': 'boolean'} {'type': 'boolean'}
] ]
}, },
'environment': { 'environment': spack.schema.environment.definition,
'type': 'object',
'default': {},
'additionalProperties': False,
'properties': {
'set': {
'type': 'object',
'patternProperties': {
# Variable name
r'\w[\w-]*': {
'anyOf': [{'type': 'string'},
{'type': 'number'}]
}
}
},
'unset': {
'type': 'object',
'patternProperties': {
# Variable name
r'\w[\w-]*': {'type': 'null'}
}
},
'prepend-path': {
'type': 'object',
'patternProperties': {
# Variable name
r'\w[\w-]*': {
'anyOf': [{'type': 'string'},
{'type': 'number'}]
}
}
},
'append-path': {
'type': 'object',
'patternProperties': {
# Variable name
r'\w[\w-]*': {
'anyOf': [{'type': 'string'},
{'type': 'number'}]
}
}
}
}
},
'extra_rpaths': { 'extra_rpaths': {
'type': 'array', 'type': 'array',
'default': [], 'default': [],

View File

@ -0,0 +1,58 @@
# Copyright 2013-2019 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
"""Schema for environment modifications. Meant for inclusion in other
schemas.
"""
array_of_strings_or_num = {
'type': 'array', 'default': [], 'items':
{'anyOf': [{'type': 'string'}, {'type': 'number'}]}
}
dictionary_of_strings_or_num = {
'type': 'object', 'patternProperties':
{r'\w[\w-]*': {'anyOf': [{'type': 'string'}, {'type': 'number'}]}}
}
definition = {
'type': 'object',
'default': {},
'additionalProperties': False,
'properties': {
'set': dictionary_of_strings_or_num,
'unset': array_of_strings_or_num,
'prepend_path': dictionary_of_strings_or_num,
'append_path': dictionary_of_strings_or_num,
'remove_path': dictionary_of_strings_or_num
}
}
def parse(config_obj):
"""Returns an EnvironmentModifications object containing the modifications
parsed from input.
Args:
config_obj: a configuration dictionary conforming to the
schema definition for environment modifications
"""
import spack.util.environment as ev
try:
from collections import Sequence # novm
except ImportError:
from collections.abc import Sequence # novm
env = ev.EnvironmentModifications()
for command, variable in config_obj.items():
# Distinguish between commands that take only a name as argument
# (e.g. unset) and commands that take a name and a value.
if isinstance(variable, Sequence):
for name in variable:
getattr(env, command)(name)
else:
for name, value in variable.items():
getattr(env, command)(name, value)
return env

View File

@ -8,6 +8,8 @@
.. literalinclude:: _spack_root/lib/spack/spack/schema/modules.py .. literalinclude:: _spack_root/lib/spack/spack/schema/modules.py
:lines: 13- :lines: 13-
""" """
import spack.schema.environment
#: Matches a spec or a multi-valued variant but not another #: Matches a spec or a multi-valued variant but not another
#: valid keyword. #: valid keyword.
@ -66,17 +68,7 @@
} }
} }
}, },
'environment': { 'environment': spack.schema.environment.definition
'type': 'object',
'default': {},
'additionalProperties': False,
'properties': {
'set': dictionary_of_strings,
'unset': array_of_strings,
'prepend_path': dictionary_of_strings,
'append_path': dictionary_of_strings
}
}
} }
} }

View File

@ -14,7 +14,6 @@
from spack.paths import build_env_path from spack.paths import build_env_path
from spack.build_environment import dso_suffix, _static_to_shared_library from spack.build_environment import dso_suffix, _static_to_shared_library
from spack.util.executable import Executable from spack.util.executable import Executable
from spack.util.spack_yaml import syaml_dict, syaml_str
from spack.util.environment import EnvironmentModifications from spack.util.environment import EnvironmentModifications
from llnl.util.filesystem import LibraryList, HeaderList from llnl.util.filesystem import LibraryList, HeaderList
@ -65,6 +64,18 @@ def build_environment(working_env):
del os.environ[name] del os.environ[name]
@pytest.fixture
def ensure_env_variables(config, mock_packages, monkeypatch, working_env):
"""Returns a function that takes a dictionary and updates os.environ
for the test lifetime accordingly. Plugs-in mock config and repo.
"""
def _ensure(env_mods):
for name, value in env_mods.items():
monkeypatch.setenv(name, value)
return _ensure
def test_static_to_shared_library(build_environment): def test_static_to_shared_library(build_environment):
os.environ['SPACK_TEST_COMMAND'] = 'dump-args' os.environ['SPACK_TEST_COMMAND'] = 'dump-args'
@ -119,79 +130,58 @@ def _set_wrong_cc(x):
assert os.environ['ANOTHER_VAR'] == 'THIS_IS_SET' assert os.environ['ANOTHER_VAR'] == 'THIS_IS_SET'
@pytest.mark.usefixtures('config', 'mock_packages') @pytest.mark.parametrize('initial,modifications,expected', [
def test_compiler_config_modifications(monkeypatch, working_env): # Set and unset variables
s = spack.spec.Spec('cmake') ({'SOME_VAR_STR': '', 'SOME_VAR_NUM': '0'},
s.concretize() {'set': {'SOME_VAR_STR': 'SOME_STR', 'SOME_VAR_NUM': 1}},
pkg = s.package {'SOME_VAR_STR': 'SOME_STR', 'SOME_VAR_NUM': '1'}),
({'SOME_VAR_STR': ''},
{'unset': ['SOME_VAR_STR']},
{'SOME_VAR_STR': None}),
({}, # Set a variable that was not defined already
{'set': {'SOME_VAR_STR': 'SOME_STR'}},
{'SOME_VAR_STR': 'SOME_STR'}),
# Append and prepend to the same variable
({'EMPTY_PATH_LIST': '/path/middle'},
{'prepend_path': {'EMPTY_PATH_LIST': '/path/first'},
'append_path': {'EMPTY_PATH_LIST': '/path/last'}},
{'EMPTY_PATH_LIST': '/path/first:/path/middle:/path/last'}),
# Append and prepend from empty variables
({'EMPTY_PATH_LIST': '', 'SOME_VAR_STR': ''},
{'prepend_path': {'EMPTY_PATH_LIST': '/path/first'},
'append_path': {'SOME_VAR_STR': '/path/last'}},
{'EMPTY_PATH_LIST': '/path/first', 'SOME_VAR_STR': '/path/last'}),
({}, # Same as before but on variables that were not defined
{'prepend_path': {'EMPTY_PATH_LIST': '/path/first'},
'append_path': {'SOME_VAR_STR': '/path/last'}},
{'EMPTY_PATH_LIST': '/path/first', 'SOME_VAR_STR': '/path/last'}),
# Remove a path from a list
({'EMPTY_PATH_LIST': '/path/first:/path/middle:/path/last'},
{'remove_path': {'EMPTY_PATH_LIST': '/path/middle'}},
{'EMPTY_PATH_LIST': '/path/first:/path/last'}),
({'EMPTY_PATH_LIST': '/only/path'},
{'remove_path': {'EMPTY_PATH_LIST': '/only/path'}},
{'EMPTY_PATH_LIST': ''}),
])
def test_compiler_config_modifications(
initial, modifications, expected, ensure_env_variables, monkeypatch
):
# Set the environment as per prerequisites
ensure_env_variables(initial)
os.environ['SOME_VAR_STR'] = '' # Monkeypatch a pkg.compiler.environment with the required modifications
os.environ['SOME_VAR_NUM'] = '0' pkg = spack.spec.Spec('cmake').concretized().package
os.environ['PATH_LIST'] = '/path/third:/path/forth' monkeypatch.setattr(pkg.compiler, 'environment', modifications)
os.environ['EMPTY_PATH_LIST'] = ''
os.environ.pop('NEW_PATH_LIST', None)
env_mod = syaml_dict() # Trigger the modifications
set_cmd = syaml_dict()
env_mod[syaml_str('set')] = set_cmd
set_cmd[syaml_str('SOME_VAR_STR')] = syaml_str('SOME_STR')
set_cmd[syaml_str('SOME_VAR_NUM')] = 1
monkeypatch.setattr(pkg.compiler, 'environment', env_mod)
spack.build_environment.setup_package(pkg, False) spack.build_environment.setup_package(pkg, False)
assert os.environ['SOME_VAR_STR'] == 'SOME_STR'
assert os.environ['SOME_VAR_NUM'] == str(1)
env_mod = syaml_dict() # Check they were applied
unset_cmd = syaml_dict() for name, value in expected.items():
env_mod[syaml_str('unset')] = unset_cmd if value is not None:
assert os.environ[name] == value
unset_cmd[syaml_str('SOME_VAR_STR')] = None continue
assert name not in os.environ
monkeypatch.setattr(pkg.compiler, 'environment', env_mod)
assert 'SOME_VAR_STR' in os.environ
spack.build_environment.setup_package(pkg, False)
assert 'SOME_VAR_STR' not in os.environ
env_mod = syaml_dict()
set_cmd = syaml_dict()
env_mod[syaml_str('set')] = set_cmd
append_cmd = syaml_dict()
env_mod[syaml_str('append-path')] = append_cmd
unset_cmd = syaml_dict()
env_mod[syaml_str('unset')] = unset_cmd
prepend_cmd = syaml_dict()
env_mod[syaml_str('prepend-path')] = prepend_cmd
set_cmd[syaml_str('EMPTY_PATH_LIST')] = syaml_str('/path/middle')
append_cmd[syaml_str('PATH_LIST')] = syaml_str('/path/last')
append_cmd[syaml_str('EMPTY_PATH_LIST')] = syaml_str('/path/last')
append_cmd[syaml_str('NEW_PATH_LIST')] = syaml_str('/path/last')
unset_cmd[syaml_str('SOME_VAR_NUM')] = None
prepend_cmd[syaml_str('PATH_LIST')] = syaml_str('/path/first:/path/second')
prepend_cmd[syaml_str('EMPTY_PATH_LIST')] = syaml_str('/path/first')
prepend_cmd[syaml_str('NEW_PATH_LIST')] = syaml_str('/path/first')
prepend_cmd[syaml_str('SOME_VAR_NUM')] = syaml_str('/8')
assert 'SOME_VAR_NUM' in os.environ
monkeypatch.setattr(pkg.compiler, 'environment', env_mod)
spack.build_environment.setup_package(pkg, False)
# Check that the order of modifications is respected and the
# variable was unset before it was prepended.
assert os.environ['SOME_VAR_NUM'] == '/8'
expected = '/path/first:/path/second:/path/third:/path/forth:/path/last'
assert os.environ['PATH_LIST'] == expected
expected = '/path/first:/path/middle:/path/last'
assert os.environ['EMPTY_PATH_LIST'] == expected
expected = '/path/first:/path/last'
assert os.environ['NEW_PATH_LIST'] == expected
@pytest.mark.regression('9107') @pytest.mark.regression('9107')