Skip to content

Commit 0715b51

Browse files
scheibelpbecker33
authored andcommitted
env: environments index specs by full DAG w/build deps
- ensure that Spec._build_hash attr is defined - add logic to compute _build_hash when it is not already computed (e.g. for specs prior to this PR) - add test to ensure that different instance of a build dep are preserved - test conversion of old env lockfile format to new format - tests: avoid view creation, DAG display in tests using MockPackage - add regression test for more-general bug also fixed by this PR - update lockfile version since the way we are maintaining hashes has changed - write out backup for version-1 lockfiles and test that
1 parent db7127c commit 0715b51

3 files changed

Lines changed: 257 additions & 30 deletions

File tree

lib/spack/spack/environment.py

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
valid_environment_name_re = r'^\w[\w-]*$'
7373

7474
#: version of the lockfile format. Must increase monotonically.
75-
lockfile_format_version = 1
75+
lockfile_format_version = 2
7676

7777
#: legal first keys in the spack.yaml manifest file
7878
env_schema_keys = ('spack', 'env')
@@ -533,11 +533,17 @@ def __init__(self, path, init_file=None, with_view=None):
533533

534534
if os.path.exists(self.lock_path):
535535
with open(self.lock_path) as f:
536-
self._read_lockfile(f)
536+
read_lock_version = self._read_lockfile(f)
537537
if default_manifest:
538538
# No manifest, set user specs from lockfile
539539
self._set_user_specs_from_lockfile()
540540

541+
if read_lock_version == 1:
542+
tty.debug(
543+
"Storing backup of old lockfile {0} at {1}".format(
544+
self.lock_path, self._lock_backup_v1_path))
545+
shutil.copy(self.lock_path, self._lock_backup_v1_path)
546+
541547
if with_view is False:
542548
self.views = {}
543549
elif with_view is True:
@@ -638,6 +644,11 @@ def lock_path(self):
638644
"""Path to spack.lock file in this environment."""
639645
return os.path.join(self.path, lockfile_name)
640646

647+
@property
648+
def _lock_backup_v1_path(self):
649+
"""Path to backup of v1 lockfile before conversion to v2"""
650+
return self.lock_path + '.backup.v1'
651+
641652
@property
642653
def env_subdir_path(self):
643654
"""Path to directory where the env stores repos, logs, views."""
@@ -809,7 +820,7 @@ def remove(self, query_spec, list_name=user_speclist_name, force=False):
809820
del self.concretized_order[i]
810821
del self.specs_by_hash[dag_hash]
811822

812-
def concretize(self, force=False):
823+
def concretize(self, force=False, _display=True):
813824
"""Concretize user_specs in this environment.
814825
815826
Only concretizes specs that haven't been concretized yet unless
@@ -850,12 +861,13 @@ def concretize(self, force=False):
850861
concrete = _concretize_from_constraints(uspec_constraints)
851862
self._add_concrete_spec(uspec, concrete)
852863

853-
# Display concretized spec to the user
854-
sys.stdout.write(concrete.tree(
855-
recurse_dependencies=True,
856-
status_fn=spack.spec.Spec.install_status,
857-
hashlen=7, hashes=True)
858-
)
864+
if _display:
865+
# Display concretized spec to the user
866+
sys.stdout.write(concrete.tree(
867+
recurse_dependencies=True,
868+
status_fn=spack.spec.Spec.install_status,
869+
hashlen=7, hashes=True)
870+
)
859871

860872
def install(self, user_spec, concrete_spec=None, **install_args):
861873
"""Install a single spec into an environment.
@@ -872,7 +884,7 @@ def install(self, user_spec, concrete_spec=None, **install_args):
872884
# spec might be in the user_specs, but not installed.
873885
# TODO: Redo name-based comparison for old style envs
874886
spec = next(s for s in self.user_specs if s.satisfies(user_spec))
875-
concrete = self.specs_by_hash.get(spec.dag_hash())
887+
concrete = self.specs_by_hash.get(spec.dag_hash(all_deps=True))
876888
if not concrete:
877889
concrete = spec.concretized()
878890
self._add_concrete_spec(spec, concrete)
@@ -984,7 +996,7 @@ def _add_concrete_spec(self, spec, concrete, new=True):
984996
# update internal lists of specs
985997
self.concretized_user_specs.append(spec)
986998

987-
h = concrete.dag_hash()
999+
h = concrete.dag_hash(all_deps=True)
9881000
self.concretized_order.append(h)
9891001
self.specs_by_hash[h] = concrete
9901002

@@ -1013,6 +1025,10 @@ def install_all(self, args=None):
10131025

10141026
def all_specs_by_hash(self):
10151027
"""Map of hashes to spec for all specs in this environment."""
1028+
# Note this uses dag-hashes calculated without build deps as keys,
1029+
# whereas the environment tracks specs based on dag-hashes calculated
1030+
# with all dependencies. This function should not be used by an
1031+
# Environment object for management of its own data structures
10161032
hashes = {}
10171033
for h in self.concretized_order:
10181034
specs = self.specs_by_hash[h].traverse(deptype=('link', 'run'))
@@ -1095,9 +1111,11 @@ def _to_lockfile_dict(self):
10951111
concrete_specs = {}
10961112
for spec in self.specs_by_hash.values():
10971113
for s in spec.traverse():
1098-
dag_hash = s.dag_hash()
1099-
if dag_hash not in concrete_specs:
1100-
concrete_specs[dag_hash] = s.to_node_dict(all_deps=True)
1114+
dag_hash_all = s.dag_hash(all_deps=True)
1115+
if dag_hash_all not in concrete_specs:
1116+
spec_dict = s.to_node_dict(all_deps=True)
1117+
spec_dict[s.name]['hash'] = s.dag_hash()
1118+
concrete_specs[dag_hash_all] = spec_dict
11011119

11021120
hash_spec_list = zip(
11031121
self.concretized_order, self.concretized_user_specs)
@@ -1126,6 +1144,7 @@ def _read_lockfile(self, file_or_json):
11261144
"""Read a lockfile from a file or from a raw string."""
11271145
lockfile_dict = sjson.load(file_or_json)
11281146
self._read_lockfile_dict(lockfile_dict)
1147+
return lockfile_dict['_meta']['lockfile-version']
11291148

11301149
def _read_lockfile_dict(self, d):
11311150
"""Read a lockfile dictionary into this environment."""
@@ -1146,8 +1165,25 @@ def _read_lockfile_dict(self, d):
11461165
specs_by_hash[dag_hash]._add_dependency(
11471166
specs_by_hash[dep_hash], deptypes)
11481167

1149-
self.specs_by_hash = dict(
1150-
(x, y) for x, y in specs_by_hash.items() if x in root_hashes)
1168+
# If we are reading an older lockfile format (which uses dag hashes
1169+
# that exclude build deps), we use this to convert the old
1170+
# concretized_order to the full hashes (preserving the order)
1171+
old_hash_to_new = {}
1172+
self.specs_by_hash = {}
1173+
for _, spec in specs_by_hash.items():
1174+
dag_hash = spec.dag_hash()
1175+
build_hash = spec.dag_hash(all_deps=True)
1176+
if dag_hash in root_hashes:
1177+
old_hash_to_new[dag_hash] = build_hash
1178+
1179+
if (dag_hash in root_hashes or build_hash in root_hashes):
1180+
self.specs_by_hash[build_hash] = spec
1181+
1182+
if old_hash_to_new:
1183+
# Replace any older hashes in concretized_order with hashes
1184+
# that include build deps
1185+
self.concretized_order = [
1186+
old_hash_to_new.get(h, h) for h in self.concretized_order]
11511187

11521188
def write(self):
11531189
"""Writes an in-memory environment to its location on disk.

lib/spack/spack/spec.py

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,7 @@ def __init__(self, spec_like=None,
927927
self.namespace = None
928928

929929
self._hash = None
930+
self._build_hash = None
930931
self._cmp_key_cache = None
931932
self._package = None
932933

@@ -1285,22 +1286,35 @@ def prefix(self):
12851286
def prefix(self, value):
12861287
self._prefix = Prefix(value)
12871288

1288-
def dag_hash(self, length=None):
1289+
def dag_hash(self, length=None, all_deps=False):
12891290
"""Return a hash of the entire spec DAG, including connectivity."""
1290-
if self._hash:
1291-
return self._hash[:length]
1292-
else:
1293-
yaml_text = syaml.dump(
1294-
self.to_node_dict(), default_flow_style=True, width=maxint)
1295-
sha = hashlib.sha1(yaml_text.encode('utf-8'))
1291+
if not self.concrete:
1292+
h = self._dag_hash(all_deps=all_deps)
1293+
# An upper bound of None is equivalent to len(h). An upper bound of
1294+
# 0 produces the empty string
1295+
return h[:length]
12961296

1297-
b32_hash = base64.b32encode(sha.digest()).lower()
1298-
if sys.version_info[0] >= 3:
1299-
b32_hash = b32_hash.decode('utf-8')
1297+
if not self._hash:
1298+
self._hash = self._dag_hash(all_deps=False)
13001299

1301-
if self.concrete:
1302-
self._hash = b32_hash
1303-
return b32_hash[:length]
1300+
if not self._build_hash:
1301+
self._build_hash = self._dag_hash(all_deps=True)
1302+
1303+
h = self._build_hash if all_deps else self._hash
1304+
return h[:length]
1305+
1306+
def _dag_hash(self, all_deps=False):
1307+
yaml_text = syaml.dump(
1308+
self.to_node_dict(all_deps=all_deps),
1309+
default_flow_style=True,
1310+
width=maxint)
1311+
sha = hashlib.sha1(yaml_text.encode('utf-8'))
1312+
1313+
b32_hash = base64.b32encode(sha.digest()).lower()
1314+
if sys.version_info[0] >= 3:
1315+
b32_hash = b32_hash.decode('utf-8')
1316+
1317+
return b32_hash
13041318

13051319
def dag_hash_bit_prefix(self, bits):
13061320
"""Get the first <bits> bits of the DAG hash as an integer type."""
@@ -1373,7 +1387,7 @@ def to_node_dict(self, hash_function=None, all_deps=False):
13731387
deps = self.dependencies_dict(deptype=deptypes)
13741388
if deps:
13751389
if hash_function is None:
1376-
hash_function = lambda s: s.dag_hash()
1390+
hash_function = lambda s: s.dag_hash(all_deps=all_deps)
13771391
d['dependencies'] = syaml_dict([
13781392
(name,
13791393
syaml_dict([
@@ -1393,6 +1407,8 @@ def to_dict(self, all_deps=False):
13931407
for s in self.traverse(order='pre', deptype=deptypes):
13941408
node = s.to_node_dict(all_deps=all_deps)
13951409
node[s.name]['hash'] = s.dag_hash()
1410+
if all_deps:
1411+
node[s.name]['build_hash'] = s.dag_hash(all_deps=True)
13961412
node_list.append(node)
13971413

13981414
return syaml_dict([('spec', node_list)])
@@ -1412,6 +1428,7 @@ def from_node_dict(node):
14121428
spec = Spec(name, full_hash=node.get('full_hash', None))
14131429
spec.namespace = node.get('namespace', None)
14141430
spec._hash = node.get('hash', None)
1431+
spec._build_hash = node.get('build_hash', None)
14151432

14161433
if 'version' in node or 'versions' in node:
14171434
spec.versions = VersionList.from_dict(node)
@@ -2812,11 +2829,13 @@ def _dup(self, other, deps=True, cleardeps=True, caches=None):
28122829

28132830
if caches:
28142831
self._hash = other._hash
2832+
self._build_hash = other._build_hash
28152833
self._cmp_key_cache = other._cmp_key_cache
28162834
self._normal = other._normal
28172835
self._full_hash = other._full_hash
28182836
else:
28192837
self._hash = None
2838+
self._build_hash = None
28202839
self._cmp_key_cache = None
28212840
self._normal = False
28222841
self._full_hash = None

0 commit comments

Comments
 (0)