summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/spack/spack/environment.py159
-rw-r--r--lib/spack/spack/spec.py23
-rw-r--r--lib/spack/spack/test/cmd/env.py22
-rw-r--r--lib/spack/spack/test/spec_dag.py7
-rw-r--r--lib/spack/spack/user_environment.py2
-rw-r--r--lib/spack/spack/util/hash.py31
6 files changed, 172 insertions, 72 deletions
diff --git a/lib/spack/spack/environment.py b/lib/spack/spack/environment.py
index 14e349ff11..fbdcb67761 100644
--- a/lib/spack/spack/environment.py
+++ b/lib/spack/spack/environment.py
@@ -2,7 +2,6 @@
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
-
import collections
import os
import re
@@ -34,6 +33,7 @@ import spack.util.environment
from spack.spec import Spec
from spack.spec_list import SpecList, InvalidSpecConstraintError
from spack.variant import UnknownVariantError
+import spack.util.hash
import spack.util.lock as lk
from spack.util.path import substitute_path_variables
import spack.util.path
@@ -472,9 +472,11 @@ class ViewDescriptor(object):
self.link == other.link])
def to_dict(self):
- ret = {'root': self.root}
+ ret = syaml.syaml_dict([('root', self.root)])
if self.projections:
- ret['projections'] = self.projections
+ projections_dict = syaml.syaml_dict(
+ sorted(self.projections.items()))
+ ret['projections'] = projections_dict
if self.select:
ret['select'] = self.select
if self.exclude:
@@ -492,10 +494,66 @@ class ViewDescriptor(object):
d.get('exclude', []),
d.get('link', default_view_link))
- def view(self):
- root = self.root
- if not os.path.isabs(root):
- root = os.path.normpath(os.path.join(self.base, self.root))
+ @property
+ def _current_root(self):
+ if not os.path.exists(self.root):
+ return None
+
+ root = os.readlink(self.root)
+ if os.path.isabs(root):
+ return root
+
+ root_dir = os.path.dirname(self.root)
+ return os.path.join(root_dir, root)
+
+ def _next_root(self, specs):
+ content_hash = self.content_hash(specs)
+ root_dir = os.path.dirname(self.root)
+ root_name = os.path.basename(self.root)
+ return os.path.join(root_dir, '._%s' % root_name, content_hash)
+
+ def content_hash(self, specs):
+ d = syaml.syaml_dict([
+ ('descriptor', self.to_dict()),
+ ('specs', [(spec.full_hash(), spec.prefix) for spec in sorted(specs)])
+ ])
+ contents = sjson.dump(d)
+ return spack.util.hash.b32_hash(contents)
+
+ def get_projection_for_spec(self, spec):
+ """Get projection for spec relative to view root
+
+ Getting the projection from the underlying root will get the temporary
+ projection. This gives the permanent projection relative to the root
+ symlink.
+ """
+ view = self.view()
+ view_path = view.get_projection_for_spec(spec)
+ rel_path = os.path.relpath(view_path, self._current_root)
+ return os.path.join(self.root, rel_path)
+
+ def view(self, new=None):
+ """
+ Generate the FilesystemView object for this ViewDescriptor
+
+ By default, this method returns a FilesystemView object rooted at the
+ current underlying root of this ViewDescriptor (self._current_root)
+
+ Raise if new is None and there is no current view
+
+ Arguments:
+ new (string or None): If a string, create a FilesystemView
+ rooted at that path. Default None. This should only be used to
+ regenerate the view, and cannot be used to access specs.
+ """
+ root = self._current_root
+ if new:
+ root = new
+ if not root:
+ # This can only be hit if we write a future bug
+ msg = ("Attempting to get nonexistent view from environment. "
+ "View root is at %s" % self.root)
+ raise SpackEnvironmentViewError(msg)
return YamlFilesystemView(root, spack.store.layout,
ignore_conflicts=True,
projections=self.projections)
@@ -517,9 +575,10 @@ class ViewDescriptor(object):
return True
- def regenerate(self, all_specs, roots):
+ def specs_for_view(self, all_specs, roots):
specs_for_view = []
specs = all_specs if self.link == 'all' else roots
+
for spec in specs:
# The view does not store build deps, so if we want it to
# recognize environment specs (which do store build deps),
@@ -531,6 +590,10 @@ class ViewDescriptor(object):
spec_copy._hash = spec._hash
spec_copy._normal = spec._normal
specs_for_view.append(spec_copy)
+ return specs_for_view
+
+ def regenerate(self, all_specs, roots):
+ specs_for_view = self.specs_for_view(all_specs, roots)
# regeneration queries the database quite a bit; this read
# transaction ensures that we don't repeatedly lock/unlock.
@@ -540,36 +603,52 @@ class ViewDescriptor(object):
# To ensure there are no conflicts with packages being installed
# that cannot be resolved or have repos that have been removed
- # we always regenerate the view from scratch. We must first make
- # sure the root directory exists for the very first time though.
- root = os.path.normpath(
- self.root if os.path.isabs(self.root) else os.path.join(
- self.base, self.root)
- )
- fs.mkdirp(root)
-
- # The tempdir for the directory transaction must be in the same
- # filesystem mount as the view for symlinks to work. Provide
- # dirname(root) as the tempdir for the
- # replace_directory_transaction because it must be on the same
- # filesystem mount as the view itself. Otherwise it may be
- # impossible to construct the view in the tempdir even when it can
- # be constructed in-place.
- with fs.replace_directory_transaction(root, os.path.dirname(root)):
- view = self.view()
-
- view.clean()
- specs_in_view = set(view.get_all_specs())
- tty.msg("Updating view at {0}".format(self.root))
-
- rm_specs = specs_in_view - installed_specs_for_view
- add_specs = installed_specs_for_view - specs_in_view
-
- # pass all_specs in, as it's expensive to read all the
- # spec.yaml files twice.
- view.remove_specs(*rm_specs, with_dependents=False,
- all_specs=specs_in_view)
- view.add_specs(*add_specs, with_dependencies=False)
+ # we always regenerate the view from scratch.
+ # We will do this by hashing the view contents and putting the view
+ # in a directory by hash, and then having a symlink to the real
+ # view in the root. The real root for a view at /dirname/basename
+ # will be /dirname/._basename_<hash>.
+ # This allows for atomic swaps when we update the view
+
+ # cache the roots because the way we determine which is which does
+ # not work while we are updating
+ new_root = self._next_root(installed_specs_for_view)
+ old_root = self._current_root
+
+ if new_root == old_root:
+ tty.debug("View at %s does not need regeneration." % self.root)
+ return
+
+ # construct view at new_root
+ tty.msg("Updating view at {0}".format(self.root))
+
+ view = self.view(new=new_root)
+ fs.mkdirp(new_root)
+ view.add_specs(*installed_specs_for_view,
+ with_dependencies=False)
+
+ # create symlink from tmpname to new_root
+ root_dirname = os.path.dirname(self.root)
+ tmp_symlink_name = os.path.join(root_dirname, '._view_link')
+ if os.path.exists(tmp_symlink_name):
+ os.unlink(tmp_symlink_name)
+ os.symlink(new_root, tmp_symlink_name)
+
+ # mv symlink atomically over root symlink to old_root
+ if os.path.exists(self.root) and not os.path.islink(self.root):
+ msg = "Cannot create view: "
+ msg += "file already exists and is not a link: %s" % self.root
+ raise SpackEnvironmentViewError(msg)
+ os.rename(tmp_symlink_name, self.root)
+
+ # remove old_root
+ if old_root and os.path.exists(old_root):
+ try:
+ shutil.rmtree(old_root)
+ except (IOError, OSError) as e:
+ msg = "Failed to remove old view at %s\n" % old_root
+ msg += str(e)
+ tty.warn(msg)
class Environment(object):
@@ -2106,3 +2185,7 @@ def is_latest_format(manifest):
class SpackEnvironmentError(spack.error.SpackError):
"""Superclass for all errors to do with Spack environments."""
+
+
+class SpackEnvironmentViewError(SpackEnvironmentError):
+ """Class for errors regarding view generation."""
diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py
index 71971453f2..54ae03092e 100644
--- a/lib/spack/spack/spec.py
+++ b/lib/spack/spack/spec.py
@@ -76,10 +76,8 @@ thing. Spack uses ~variant in directory names and in the canonical form of
specs to avoid ambiguity. Both are provided because ~ can cause shell
expansion when it is the first character in an id typed on the command line.
"""
-import base64
import sys
import collections
-import hashlib
import itertools
import operator
import os
@@ -108,6 +106,7 @@ import spack.solver
import spack.store
import spack.util.crypto
import spack.util.executable
+import spack.util.hash
import spack.util.module_cmd as md
import spack.util.prefix
import spack.util.spack_json as sjson
@@ -1505,13 +1504,7 @@ class Spec(object):
# this when we move to using package hashing on all specs.
node_dict = self.to_node_dict(hash=hash)
yaml_text = syaml.dump(node_dict, default_flow_style=True)
- sha = hashlib.sha1(yaml_text.encode('utf-8'))
- b32_hash = base64.b32encode(sha.digest()).lower()
-
- if sys.version_info[0] >= 3:
- b32_hash = b32_hash.decode('utf-8')
-
- return b32_hash
+ return spack.util.hash.b32_hash(yaml_text)
def _cached_hash(self, hash, length=None):
"""Helper function for storing a cached hash on the spec.
@@ -1567,7 +1560,7 @@ class Spec(object):
def dag_hash_bit_prefix(self, bits):
"""Get the first <bits> bits of the DAG hash as an integer type."""
- return base32_prefix_bits(self.dag_hash(), bits)
+ return spack.util.hash.base32_prefix_bits(self.dag_hash(), bits)
def to_node_dict(self, hash=ht.dag_hash):
"""Create a dictionary representing the state of this Spec.
@@ -4764,16 +4757,6 @@ def save_dependency_spec_yamls(
fd.write(dep_spec.to_yaml(hash=ht.build_hash))
-def base32_prefix_bits(hash_string, bits):
- """Return the first <bits> bits of a base32 string as an integer."""
- if bits > len(hash_string) * 5:
- raise ValueError("Too many bits! Requested %d bit prefix of '%s'."
- % (bits, hash_string))
-
- hash_bytes = base64.b32decode(hash_string, casefold=True)
- return spack.util.crypto.prefix_bits(hash_bytes, bits)
-
-
class SpecParseError(spack.error.SpecError):
"""Wrapper for ParseError for when we're parsing specs."""
def __init__(self, parse_error):
diff --git a/lib/spack/spack/test/cmd/env.py b/lib/spack/spack/test/cmd/env.py
index 9358d0a3a9..f6933dc349 100644
--- a/lib/spack/spack/test/cmd/env.py
+++ b/lib/spack/spack/test/cmd/env.py
@@ -9,6 +9,7 @@ from six import StringIO
import pytest
import llnl.util.filesystem as fs
+import llnl.util.link_tree
import spack.hash_types as ht
import spack.modules
@@ -1097,7 +1098,7 @@ def test_store_different_build_deps():
def test_env_updates_view_install(
tmpdir, mock_stage, mock_fetch, install_mockery):
- view_dir = tmpdir.mkdir('view')
+ view_dir = tmpdir.join('view')
env('create', '--with-view=%s' % view_dir, 'test')
with ev.read('test'):
add('mpileaks')
@@ -1108,12 +1109,13 @@ def test_env_updates_view_install(
def test_env_view_fails(
tmpdir, mock_packages, mock_stage, mock_fetch, install_mockery):
- view_dir = tmpdir.mkdir('view')
+ view_dir = tmpdir.join('view')
env('create', '--with-view=%s' % view_dir, 'test')
with ev.read('test'):
add('libelf')
add('libelf cflags=-g')
- with pytest.raises(RuntimeError, match='merge blocked by file'):
+ with pytest.raises(llnl.util.link_tree.MergeConflictError,
+ match='merge blocked by file'):
install('--fake')
@@ -1126,7 +1128,7 @@ def test_env_without_view_install(
with pytest.raises(spack.environment.SpackEnvironmentError):
test_env.default_view
- view_dir = tmpdir.mkdir('view')
+ view_dir = tmpdir.join('view')
with ev.read('test'):
add('mpileaks')
@@ -1161,7 +1163,7 @@ env:
def test_env_updates_view_install_package(
tmpdir, mock_stage, mock_fetch, install_mockery):
- view_dir = tmpdir.mkdir('view')
+ view_dir = tmpdir.join('view')
env('create', '--with-view=%s' % view_dir, 'test')
with ev.read('test'):
install('--fake', 'mpileaks')
@@ -1171,7 +1173,7 @@ def test_env_updates_view_install_package(
def test_env_updates_view_add_concretize(
tmpdir, mock_stage, mock_fetch, install_mockery):
- view_dir = tmpdir.mkdir('view')
+ view_dir = tmpdir.join('view')
env('create', '--with-view=%s' % view_dir, 'test')
install('--fake', 'mpileaks')
with ev.read('test'):
@@ -1183,7 +1185,7 @@ def test_env_updates_view_add_concretize(
def test_env_updates_view_uninstall(
tmpdir, mock_stage, mock_fetch, install_mockery):
- view_dir = tmpdir.mkdir('view')
+ view_dir = tmpdir.join('view')
env('create', '--with-view=%s' % view_dir, 'test')
with ev.read('test'):
install('--fake', 'mpileaks')
@@ -1198,7 +1200,7 @@ def test_env_updates_view_uninstall(
def test_env_updates_view_uninstall_referenced_elsewhere(
tmpdir, mock_stage, mock_fetch, install_mockery):
- view_dir = tmpdir.mkdir('view')
+ view_dir = tmpdir.join('view')
env('create', '--with-view=%s' % view_dir, 'test')
install('--fake', 'mpileaks')
with ev.read('test'):
@@ -1215,7 +1217,7 @@ def test_env_updates_view_uninstall_referenced_elsewhere(
def test_env_updates_view_remove_concretize(
tmpdir, mock_stage, mock_fetch, install_mockery):
- view_dir = tmpdir.mkdir('view')
+ view_dir = tmpdir.join('view')
env('create', '--with-view=%s' % view_dir, 'test')
install('--fake', 'mpileaks')
with ev.read('test'):
@@ -1233,7 +1235,7 @@ def test_env_updates_view_remove_concretize(
def test_env_updates_view_force_remove(
tmpdir, mock_stage, mock_fetch, install_mockery):
- view_dir = tmpdir.mkdir('view')
+ view_dir = tmpdir.join('view')
env('create', '--with-view=%s' % view_dir, 'test')
with ev.read('test'):
install('--fake', 'mpileaks')
diff --git a/lib/spack/spack/test/spec_dag.py b/lib/spack/spack/test/spec_dag.py
index efee120ae5..ea16f655db 100644
--- a/lib/spack/spack/test/spec_dag.py
+++ b/lib/spack/spack/test/spec_dag.py
@@ -13,6 +13,7 @@ import spack.package
from spack.spec import Spec
from spack.dependency import all_deptypes, Dependency, canonical_deptype
from spack.util.mock_package import MockPackageMultiRepo
+import spack.util.hash as hashutil
def check_links(spec_to_check):
@@ -705,17 +706,17 @@ class TestSpecDag(object):
for c in test_hash])
for bits in (1, 2, 3, 4, 7, 8, 9, 16, 64, 117, 128, 160):
- actual_int = spack.spec.base32_prefix_bits(test_hash, bits)
+ actual_int = hashutil.base32_prefix_bits(test_hash, bits)
fmt = "#0%sb" % (bits + 2)
actual = format(actual_int, fmt).replace('0b', '')
assert expected[:bits] == actual
with pytest.raises(ValueError):
- spack.spec.base32_prefix_bits(test_hash, 161)
+ hashutil.base32_prefix_bits(test_hash, 161)
with pytest.raises(ValueError):
- spack.spec.base32_prefix_bits(test_hash, 256)
+ hashutil.base32_prefix_bits(test_hash, 256)
def test_traversal_directions(self):
"""Make sure child and parent traversals of specs work."""
diff --git a/lib/spack/spack/user_environment.py b/lib/spack/spack/user_environment.py
index f111aaedc3..7dd63dcdea 100644
--- a/lib/spack/spack/user_environment.py
+++ b/lib/spack/spack/user_environment.py
@@ -72,7 +72,7 @@ def environment_modifications_for_spec(spec, view=None):
the view."""
spec = spec.copy()
if view and not spec.external:
- spec.prefix = prefix.Prefix(view.view().get_projection_for_spec(spec))
+ spec.prefix = prefix.Prefix(view.get_projection_for_spec(spec))
# generic environment modifications determined by inspecting the spec
# prefix
diff --git a/lib/spack/spack/util/hash.py b/lib/spack/spack/util/hash.py
new file mode 100644
index 0000000000..da81284ea6
--- /dev/null
+++ b/lib/spack/spack/util/hash.py
@@ -0,0 +1,31 @@
+# Copyright 2013-2021 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)
+
+import base64
+import hashlib
+import sys
+
+import spack.util.crypto
+
+
+def b32_hash(content):
+ """Return the b32 encoded sha1 hash of the input string as a string."""
+ sha = hashlib.sha1(content.encode('utf-8'))
+ b32_hash = base64.b32encode(sha.digest()).lower()
+
+ if sys.version_info[0] >= 3:
+ b32_hash = b32_hash.decode('utf-8')
+
+ return b32_hash
+
+
+def base32_prefix_bits(hash_string, bits):
+ """Return the first <bits> bits of a base32 string as an integer."""
+ if bits > len(hash_string) * 5:
+ raise ValueError("Too many bits! Requested %d bit prefix of '%s'."
+ % (bits, hash_string))
+
+ hash_bytes = base64.b32decode(hash_string, casefold=True)
+ return spack.util.crypto.prefix_bits(hash_bytes, bits)