Skip to content
Snippets Groups Projects
Unverified Commit 7cdb241f authored by Greg Becker's avatar Greg Becker Committed by Todd Gamblin
Browse files

environments: only write when necessary (#13546)

This changes Spack environments so that the YAML file associated with the environment is *only* written when necessary (i.e., if it is changed *by spack*).  The lockfile is still written out as before.

There is a larger question here of which part of Spack should be responsible for setting defaults in config files, and how we can get rid of empty lists and data structures currently cluttering files like `compilers.yaml`.  But that probably requires a rework of the default-setting validator in `spack.config`, as well as the code that uses `spack.config`.  This will at least help for `spack.yaml`.
parent d670765b
No related branches found
No related tags found
No related merge requests found
...@@ -409,9 +409,7 @@ def find_matching_config(spec, ci_mappings): ...@@ -409,9 +409,7 @@ def find_matching_config(spec, ci_mappings):
def release_jobs(parser, args): def release_jobs(parser, args):
env = ev.get_env(args, 'release-jobs', required=True) env = ev.get_env(args, 'release-jobs', required=True)
# FIXME: What's the difference between one that opens with 'spack' yaml_root = ev.config_dict(env.yaml)
# and one that opens with 'env'? This will only handle the former.
yaml_root = env.yaml['spack']
if 'gitlab-ci' not in yaml_root: if 'gitlab-ci' not in yaml_root:
tty.die('Environment yaml does not have "gitlab-ci" section') tty.die('Environment yaml does not have "gitlab-ci" section')
......
...@@ -70,8 +70,7 @@ ...@@ -70,8 +70,7 @@
# configuration settings. # configuration settings.
spack: spack:
# add package specs to the `specs` list # add package specs to the `specs` list
specs: specs: []
-
view: true view: true
""" """
#: regex for validating enviroment names #: regex for validating enviroment names
...@@ -391,20 +390,31 @@ def all_environments(): ...@@ -391,20 +390,31 @@ def all_environments():
def validate(data, filename=None): def validate(data, filename=None):
# validating changes data by adding defaults. Return validated data
validate_data = copy.deepcopy(data)
# HACK to fully copy ruamel CommentedMap that doesn't provide copy method
import ruamel.yaml as yaml
setattr(
validate_data,
yaml.comments.Comment.attrib,
getattr(data, yaml.comments.Comment.attrib, yaml.comments.Comment())
)
import jsonschema import jsonschema
try: try:
spack.schema.Validator(spack.schema.env.schema).validate(data) spack.schema.Validator(spack.schema.env.schema).validate(validate_data)
except jsonschema.ValidationError as e: except jsonschema.ValidationError as e:
raise spack.config.ConfigFormatError( raise spack.config.ConfigFormatError(
e, data, filename, e.instance.lc.line + 1) e, data, filename, e.instance.lc.line + 1)
return validate_data
def _read_yaml(str_or_file): def _read_yaml(str_or_file):
"""Read YAML from a file for round-trip parsing.""" """Read YAML from a file for round-trip parsing."""
data = syaml.load_config(str_or_file) data = syaml.load_config(str_or_file)
filename = getattr(str_or_file, 'name', None) filename = getattr(str_or_file, 'name', None)
validate(data, filename) default_data = validate(data, filename)
return data return (data, default_data)
def _write_yaml(data, str_or_file): def _write_yaml(data, str_or_file):
...@@ -444,6 +454,13 @@ def __init__(self, root, projections={}, select=[], exclude=[], ...@@ -444,6 +454,13 @@ def __init__(self, root, projections={}, select=[], exclude=[],
for e in self.exclude) for e in self.exclude)
self.link = link self.link = link
def __eq__(self, other):
return all([self.root == other.root,
self.projections == other.projections,
self.select == other.select,
self.exclude == other.exclude,
self.link == other.link])
def to_dict(self): def to_dict(self):
ret = {'root': self.root} ret = {'root': self.root}
if self.projections: if self.projections:
...@@ -540,7 +557,7 @@ def __init__(self, path, init_file=None, with_view=None): ...@@ -540,7 +557,7 @@ def __init__(self, path, init_file=None, with_view=None):
self._read_lockfile(f) self._read_lockfile(f)
self._set_user_specs_from_lockfile() self._set_user_specs_from_lockfile()
else: else:
self._read_manifest(f) self._read_manifest(f, raw_yaml=default_manifest_yaml)
else: else:
default_manifest = not os.path.exists(self.manifest_path) default_manifest = not os.path.exists(self.manifest_path)
if default_manifest: if default_manifest:
...@@ -573,9 +590,13 @@ def __init__(self, path, init_file=None, with_view=None): ...@@ -573,9 +590,13 @@ def __init__(self, path, init_file=None, with_view=None):
# If with_view is None, then defer to the view settings determined by # If with_view is None, then defer to the view settings determined by
# the manifest file # the manifest file
def _read_manifest(self, f): def _read_manifest(self, f, raw_yaml=None):
"""Read manifest file and set up user specs.""" """Read manifest file and set up user specs."""
self.yaml = _read_yaml(f) if raw_yaml:
_, self.yaml = _read_yaml(f)
self.raw_yaml, _ = _read_yaml(raw_yaml)
else:
self.raw_yaml, self.yaml = _read_yaml(f)
self.spec_lists = OrderedDict() self.spec_lists = OrderedDict()
...@@ -1311,6 +1332,9 @@ def write(self): ...@@ -1311,6 +1332,9 @@ def write(self):
# ensure path in var/spack/environments # ensure path in var/spack/environments
fs.mkdirp(self.path) fs.mkdirp(self.path)
yaml_dict = config_dict(self.yaml)
raw_yaml_dict = config_dict(self.raw_yaml)
if self.specs_by_hash: if self.specs_by_hash:
# ensure the prefix/.env directory exists # ensure the prefix/.env directory exists
fs.mkdirp(self.env_subdir_path) fs.mkdirp(self.env_subdir_path)
...@@ -1345,8 +1369,7 @@ def write(self): ...@@ -1345,8 +1369,7 @@ def write(self):
# The primary list is handled differently # The primary list is handled differently
continue continue
conf = config_dict(self.yaml) active_yaml_lists = [l for l in yaml_dict.get('definitions', [])
active_yaml_lists = [l for l in conf.get('definitions', [])
if name in l and if name in l and
_eval_conditional(l.get('when', 'True'))] _eval_conditional(l.get('when', 'True'))]
...@@ -1370,10 +1393,11 @@ def write(self): ...@@ -1370,10 +1393,11 @@ def write(self):
# put the new user specs in the YAML. # put the new user specs in the YAML.
# This can be done directly because there can't be multiple definitions # This can be done directly because there can't be multiple definitions
# nor when clauses for `specs` list. # nor when clauses for `specs` list.
yaml_spec_list = config_dict(self.yaml).setdefault(user_speclist_name, yaml_spec_list = yaml_dict.setdefault(user_speclist_name,
[]) [])
yaml_spec_list[:] = self.user_specs.yaml_list yaml_spec_list[:] = self.user_specs.yaml_list
# Construct YAML representation of view
default_name = default_view_name default_name = default_view_name
if self.views and len(self.views) == 1 and default_name in self.views: if self.views and len(self.views) == 1 and default_name in self.views:
path = self.default_view.root path = self.default_view.root
...@@ -1390,18 +1414,26 @@ def write(self): ...@@ -1390,18 +1414,26 @@ def write(self):
else: else:
view = False view = False
yaml_dict = config_dict(self.yaml) yaml_dict['view'] = view
if view is not True:
# The default case is to keep an active view inside of the # Remove yaml sections that are shadowing defaults
# Spack environment directory. To avoid cluttering the config, # construct garbage path to ensure we don't find a manifest by accident
# we omit the setting in this case. bare_env = Environment(os.path.join(self.manifest_path, 'garbage'),
yaml_dict['view'] = view with_view=self.view_path_default)
elif 'view' in yaml_dict: keys_present = list(yaml_dict.keys())
del yaml_dict['view'] for key in keys_present:
if yaml_dict[key] == config_dict(bare_env.yaml).get(key, None):
if key not in raw_yaml_dict:
del yaml_dict[key]
# if all that worked, write out the manifest file at the top level # if all that worked, write out the manifest file at the top level
with fs.write_tmp_and_move(self.manifest_path) as f: # Only actually write if it has changed or was never written
_write_yaml(self.yaml, f) changed = self.yaml != self.raw_yaml
written = os.path.exists(self.manifest_path)
if changed or not written:
self.raw_yaml = copy.deepcopy(self.yaml)
with fs.write_tmp_and_move(self.manifest_path) as f:
_write_yaml(self.yaml, f)
# TODO: for operations that just add to the env (install etc.) this # TODO: for operations that just add to the env (install etc.) this
# could just call update_view # could just call update_view
......
...@@ -63,6 +63,7 @@ ...@@ -63,6 +63,7 @@
{ {
'include': { 'include': {
'type': 'array', 'type': 'array',
'default': [],
'items': { 'items': {
'type': 'string' 'type': 'string'
}, },
......
...@@ -1749,3 +1749,13 @@ def test_duplicate_packages_raise_when_concretizing_together(): ...@@ -1749,3 +1749,13 @@ def test_duplicate_packages_raise_when_concretizing_together():
with pytest.raises(ev.SpackEnvironmentError, match=r'cannot contain more'): with pytest.raises(ev.SpackEnvironmentError, match=r'cannot contain more'):
e.concretize() e.concretize()
def test_env_write_only_non_default():
print(env('create', 'test'))
e = ev.read('test')
with open(e.manifest_path, 'r') as f:
yaml = f.read()
assert yaml == ev.default_manifest_yaml
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment