summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorMassimiliano Culpo <massimiliano.culpo@gmail.com>2021-11-03 07:15:24 +0100
committerGitHub <noreply@github.com>2021-11-02 23:15:24 -0700
commit78c08fccd56d073a336eeee3dd4548d81101c920 (patch)
tree3d81383f9575f2f7163fc1163129be4b33cf0e9a /lib
parent1a3747b2b3a8cd9416f1875ce0f72756630740e6 (diff)
downloadspack-78c08fccd56d073a336eeee3dd4548d81101c920.tar.gz
spack-78c08fccd56d073a336eeee3dd4548d81101c920.tar.bz2
spack-78c08fccd56d073a336eeee3dd4548d81101c920.tar.xz
spack-78c08fccd56d073a336eeee3dd4548d81101c920.zip
Bootstrap GnuPG (#24003)
* GnuPG: allow bootstrapping from buildcache and sources * Add a test to bootstrap GnuPG from binaries * Disable bootstrapping in tests * Add e2e test to bootstrap GnuPG from sources on Ubuntu * Add e2e test to bootstrap GnuPG on macOS
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/spack/bootstrap.py293
-rw-r--r--lib/spack/spack/cmd/buildcache.py14
-rw-r--r--lib/spack/spack/modules/common.py6
-rw-r--r--lib/spack/spack/test/cmd/gpg.py7
-rw-r--r--lib/spack/spack/util/gpg.py6
5 files changed, 251 insertions, 75 deletions
diff --git a/lib/spack/spack/bootstrap.py b/lib/spack/spack/bootstrap.py
index 78ed54429f..97a38d20a0 100644
--- a/lib/spack/spack/bootstrap.py
+++ b/lib/spack/spack/bootstrap.py
@@ -6,6 +6,7 @@ from __future__ import print_function
import contextlib
import fnmatch
+import functools
import json
import os
import os.path
@@ -34,11 +35,14 @@ import spack.platforms
import spack.repo
import spack.spec
import spack.store
-import spack.user_environment as uenv
+import spack.user_environment
import spack.util.executable
import spack.util.path
from spack.util.environment import EnvironmentModifications
+#: "spack buildcache" command, initialized lazily
+_buildcache_cmd = None
+
#: Map a bootstrapper type to the corresponding class
_bootstrap_methods = {}
@@ -171,6 +175,34 @@ def _fix_ext_suffix(candidate_spec):
os.symlink(abs_path, link_name)
+def _executables_in_store(executables, abstract_spec_str):
+ """Return True if at least one of the executables can be retrieved from
+ a spec in store, False otherwise.
+
+ The different executables must provide the same functionality and are
+ "alternate" to each other, i.e. the function will exit True on the first
+ executable found.
+
+ Args:
+ executables: list of executables to be searched
+ abstract_spec_str: abstract spec that may provide the executable
+ """
+ executables_str = ', '.join(executables)
+ msg = "[BOOTSTRAP EXECUTABLES {0}] Try installed specs with query '{1}'"
+ tty.debug(msg.format(executables_str, abstract_spec_str))
+ installed_specs = spack.store.db.query(abstract_spec_str, installed=True)
+ if installed_specs:
+ for concrete_spec in installed_specs:
+ bin_dir = concrete_spec.prefix.bin
+ # IF we have a "bin" directory and it contains
+ # the executables we are looking for
+ if (os.path.exists(bin_dir) and os.path.isdir(bin_dir) and
+ spack.util.executable.which_string(*executables, path=bin_dir)):
+ spack.util.environment.path_put_first('PATH', [bin_dir])
+ return True
+ return False
+
+
@_bootstrapper(type='buildcache')
class _BuildcacheBootstrapper(object):
"""Install the software needed during bootstrapping from a buildcache."""
@@ -178,93 +210,140 @@ class _BuildcacheBootstrapper(object):
self.name = conf['name']
self.url = conf['info']['url']
- def try_import(self, module, abstract_spec_str):
- if _try_import_from_store(module, abstract_spec_str):
- return True
+ @staticmethod
+ def _spec_and_platform(abstract_spec_str):
+ """Return the spec object and platform we need to use when
+ querying the buildcache.
- tty.info("Bootstrapping {0} from pre-built binaries".format(module))
+ Args:
+ abstract_spec_str: abstract spec string we are looking for
+ """
+ # This import is local since it is needed only on Cray
+ import spack.platforms.linux
# Try to install from an unsigned binary cache
- abstract_spec = spack.spec.Spec(
- abstract_spec_str + ' ^' + spec_for_current_python()
- )
-
+ abstract_spec = spack.spec.Spec(abstract_spec_str)
# On Cray we want to use Linux binaries if available from mirrors
bincache_platform = spack.platforms.real_host()
if str(bincache_platform) == 'cray':
bincache_platform = spack.platforms.Linux()
with spack.platforms.use_platform(bincache_platform):
- abstract_spec = spack.spec.Spec(
- abstract_spec_str + ' ^' + spec_for_current_python()
- )
+ abstract_spec = spack.spec.Spec(abstract_spec_str)
+ return abstract_spec, bincache_platform
- # Read information on verified clingo binaries
- json_filename = '{0}.json'.format(module)
+ def _read_metadata(self, package_name):
+ """Return metadata about the given package."""
+ json_filename = '{0}.json'.format(package_name)
json_path = os.path.join(
spack.paths.share_path, 'bootstrap', self.name, json_filename
)
with open(json_path) as f:
data = json.load(f)
-
- buildcache = spack.main.SpackCommand('buildcache')
+ return data
+
+ def _install_by_hash(self, pkg_hash, pkg_sha256, index, bincache_platform):
+ global _buildcache_cmd
+
+ if _buildcache_cmd is None:
+ _buildcache_cmd = spack.main.SpackCommand('buildcache')
+
+ index_spec = next(x for x in index if x.dag_hash() == pkg_hash)
+ # Reconstruct the compiler that we need to use for bootstrapping
+ compiler_entry = {
+ "modules": [],
+ "operating_system": str(index_spec.os),
+ "paths": {
+ "cc": "/dev/null",
+ "cxx": "/dev/null",
+ "f77": "/dev/null",
+ "fc": "/dev/null"
+ },
+ "spec": str(index_spec.compiler),
+ "target": str(index_spec.target.family)
+ }
+ with spack.platforms.use_platform(bincache_platform):
+ with spack.config.override(
+ 'compilers', [{'compiler': compiler_entry}]
+ ):
+ spec_str = '/' + pkg_hash
+ install_args = [
+ 'install',
+ '--sha256', pkg_sha256,
+ '--only-root',
+ '-a', '-u', '-o', '-f', spec_str
+ ]
+ _buildcache_cmd(*install_args, fail_on_error=False)
+
+ def _install_and_test(
+ self, abstract_spec, bincache_platform, bincache_data, test_fn
+ ):
# Ensure we see only the buildcache being used to bootstrap
- mirror_scope = spack.config.InternalConfigScope(
- 'bootstrap_buildcache', {'mirrors:': {self.name: self.url}}
- )
- with spack.config.override(mirror_scope):
+ with spack.config.override(self.mirror_scope):
# This index is currently needed to get the compiler used to build some
- # specs that wwe know by dag hash.
+ # specs that we know by dag hash.
spack.binary_distribution.binary_index.regenerate_spec_cache()
index = spack.binary_distribution.update_cache_and_get_specs()
if not index:
raise RuntimeError("The binary index is empty")
- for item in data['verified']:
+ for item in bincache_data['verified']:
candidate_spec = item['spec']
- python_spec = item['python']
+ # This will be None for things that don't depend on python
+ python_spec = item.get('python', None)
# Skip specs which are not compatible
if not abstract_spec.satisfies(candidate_spec):
continue
- if python_spec not in abstract_spec:
+ if python_spec is not None and python_spec not in abstract_spec:
continue
for pkg_name, pkg_hash, pkg_sha256 in item['binaries']:
- msg = ('[BOOTSTRAP MODULE {0}] Try installing "{1}" from binary '
- 'cache at "{2}"')
- tty.debug(msg.format(module, pkg_name, self.url))
- index_spec = next(x for x in index if x.dag_hash() == pkg_hash)
- # Reconstruct the compiler that we need to use for bootstrapping
- compiler_entry = {
- "modules": [],
- "operating_system": str(index_spec.os),
- "paths": {
- "cc": "/dev/null",
- "cxx": "/dev/null",
- "f77": "/dev/null",
- "fc": "/dev/null"
- },
- "spec": str(index_spec.compiler),
- "target": str(index_spec.target.family)
- }
- with spack.platforms.use_platform(bincache_platform):
- with spack.config.override(
- 'compilers', [{'compiler': compiler_entry}]
- ):
- spec_str = '/' + pkg_hash
- install_args = [
- 'install',
- '--sha256', pkg_sha256,
- '-a', '-u', '-o', '-f', spec_str
- ]
- buildcache(*install_args, fail_on_error=False)
- # TODO: undo installations that didn't complete?
-
- if _try_import_from_store(module, abstract_spec_str):
+ # TODO: undo installations that didn't complete?
+ self._install_by_hash(
+ pkg_hash, pkg_sha256, index, bincache_platform
+ )
+
+ if test_fn():
return True
return False
+ @property
+ def mirror_scope(self):
+ return spack.config.InternalConfigScope(
+ 'bootstrap', {'mirrors:': {self.name: self.url}}
+ )
+
+ def try_import(self, module, abstract_spec_str):
+ test_fn = functools.partial(_try_import_from_store, module, abstract_spec_str)
+ if test_fn():
+ return True
+
+ tty.info("Bootstrapping {0} from pre-built binaries".format(module))
+ abstract_spec, bincache_platform = self._spec_and_platform(
+ abstract_spec_str + ' ^' + spec_for_current_python()
+ )
+ data = self._read_metadata(module)
+ return self._install_and_test(
+ abstract_spec, bincache_platform, data, test_fn
+ )
+
+ def try_search_path(self, executables, abstract_spec_str):
+ test_fn = functools.partial(
+ _executables_in_store, executables, abstract_spec_str
+ )
+ if test_fn():
+ return True
+
+ abstract_spec, bincache_platform = self._spec_and_platform(
+ abstract_spec_str
+ )
+ tty.info("Bootstrapping {0} from pre-built binaries".format(abstract_spec.name))
+ data = self._read_metadata(abstract_spec.name)
+ return self._install_and_test(
+ abstract_spec, bincache_platform, data, test_fn
+ )
+
@_bootstrapper(type='install')
class _SourceBootstrapper(object):
@@ -307,6 +386,26 @@ class _SourceBootstrapper(object):
return _try_import_from_store(module, abstract_spec_str=abstract_spec_str)
+ def try_search_path(self, executables, abstract_spec_str):
+ if _executables_in_store(executables, abstract_spec_str):
+ return True
+
+ # If we compile code from sources detecting a few build tools
+ # might reduce compilation time by a fair amount
+ _add_externals_if_missing()
+
+ # Add hint to use frontend operating system on Cray
+ if str(spack.platforms.host()) == 'cray':
+ abstract_spec_str += ' os=fe'
+
+ concrete_spec = spack.spec.Spec(abstract_spec_str)
+ concrete_spec.concretize()
+
+ msg = "[BOOTSTRAP GnuPG] Try installing '{0}' from sources"
+ tty.debug(msg.format(abstract_spec_str))
+ concrete_spec.package.do_install()
+ return _executables_in_store(executables, abstract_spec_str)
+
def _make_bootstrapper(conf):
"""Return a bootstrap object built according to the
@@ -418,6 +517,44 @@ def ensure_module_importable_or_raise(module, abstract_spec=None):
raise ImportError(msg)
+def ensure_executables_in_path_or_raise(executables, abstract_spec):
+ """Ensure that some executables are in path or raise.
+
+ Args:
+ executables (list): list of executables to be searched in the PATH,
+ in order. The function exits on the first one found.
+ abstract_spec (str): abstract spec that provides the executables
+
+ Raises:
+ RuntimeError: if the executables cannot be ensured to be in PATH
+ """
+ if spack.util.executable.which_string(*executables):
+ return
+
+ executables_str = ', '.join(executables)
+ source_configs = spack.config.get('bootstrap:sources', [])
+ for current_config in source_configs:
+ if not _source_is_trusted(current_config):
+ msg = ('[BOOTSTRAP EXECUTABLES {0}] Skipping source "{1}" since it is '
+ 'not trusted').format(executables_str, current_config['name'])
+ tty.debug(msg)
+ continue
+
+ b = _make_bootstrapper(current_config)
+ try:
+ if b.try_search_path(executables, abstract_spec):
+ return
+ except Exception as e:
+ msg = '[BOOTSTRAP EXECUTABLES {0}] Unexpected error "{1}"'
+ tty.debug(msg.format(executables_str, str(e)))
+
+ # We couldn't import in any way, so raise an import error
+ msg = 'cannot bootstrap any of the {0} executables'.format(executables_str)
+ if abstract_spec:
+ msg += ' from spec "{0}"'.format(abstract_spec)
+ raise RuntimeError(msg)
+
+
def _python_import(module):
try:
__import__(module)
@@ -455,7 +592,9 @@ def get_executable(exe, spec=None, install=False):
ret = spack.util.executable.Executable(exe_path[0])
envmod = EnvironmentModifications()
for dep in ispec.traverse(root=True, order='post'):
- envmod.extend(uenv.environment_modifications_for_spec(dep))
+ envmod.extend(
+ spack.user_environment.environment_modifications_for_spec(dep)
+ )
ret.add_default_envmod(envmod)
return ret
else:
@@ -484,7 +623,9 @@ def get_executable(exe, spec=None, install=False):
ret = spack.util.executable.Executable(exe_path[0])
envmod = EnvironmentModifications()
for dep in spec.traverse(root=True, order='post'):
- envmod.extend(uenv.environment_modifications_for_spec(dep))
+ envmod.extend(
+ spack.user_environment.environment_modifications_for_spec(dep)
+ )
ret.add_default_envmod(envmod)
return ret
@@ -523,8 +664,11 @@ def _add_compilers_if_missing():
def _add_externals_if_missing():
search_list = [
+ # clingo
spack.repo.path.get('cmake'),
- spack.repo.path.get('bison')
+ spack.repo.path.get('bison'),
+ # GnuPG
+ spack.repo.path.get('gawk')
]
detected_packages = spack.detection.by_executable(search_list)
spack.detection.update_configuration(detected_packages, scope='bootstrap')
@@ -600,10 +744,12 @@ def _config_path():
)
-def clingo_root_spec():
- # Construct the root spec that will be used to bootstrap clingo
- spec_str = 'clingo-bootstrap@spack+python'
+def _root_spec(spec_str):
+ """Add a proper compiler and target to a spec used during bootstrapping.
+ Args:
+ spec_str (str): spec to be bootstrapped. Must be without compiler and target.
+ """
# Add a proper compiler hint to the root spec. We use GCC for
# everything but MacOS.
if str(spack.platforms.host()) == 'darwin':
@@ -611,17 +757,32 @@ def clingo_root_spec():
else:
spec_str += ' %gcc'
- # Add the generic target
- generic_target = archspec.cpu.host().family
- spec_str += ' target={0}'.format(str(generic_target))
-
- tty.debug('[BOOTSTRAP ROOT SPEC] clingo: {0}'.format(spec_str))
+ target = archspec.cpu.host().family
+ spec_str += ' target={0}'.format(target)
+ tty.debug('[BOOTSTRAP ROOT SPEC] {0}'.format(spec_str))
return spec_str
+def clingo_root_spec():
+ """Return the root spec used to bootstrap clingo"""
+ return _root_spec('clingo-bootstrap@spack+python')
+
+
def ensure_clingo_importable_or_raise():
"""Ensure that the clingo module is available for import."""
ensure_module_importable_or_raise(
module='clingo', abstract_spec=clingo_root_spec()
)
+
+
+def gnupg_root_spec():
+ """Return the root spec used to bootstrap GnuPG"""
+ return _root_spec('gnupg@2.3:')
+
+
+def ensure_gpg_in_path_or_raise():
+ """Ensure gpg or gpg2 are in the PATH or raise."""
+ ensure_executables_in_path_or_raise(
+ executables=['gpg2', 'gpg'], abstract_spec=gnupg_root_spec(),
+ )
diff --git a/lib/spack/spack/cmd/buildcache.py b/lib/spack/spack/cmd/buildcache.py
index 13b0158ff4..1fda884e9a 100644
--- a/lib/spack/spack/cmd/buildcache.py
+++ b/lib/spack/spack/cmd/buildcache.py
@@ -104,6 +104,9 @@ def setup_parser(subparser):
" instead of default platform and OS")
# This argument is needed by the bootstrapping logic to verify checksums
install.add_argument('--sha256', help=argparse.SUPPRESS)
+ install.add_argument(
+ '--only-root', action='store_true', help=argparse.SUPPRESS
+ )
arguments.add_common_arguments(install, ['specs'])
install.set_defaults(func=installtarball)
@@ -534,9 +537,14 @@ def install_tarball(spec, args):
if s.external or s.virtual:
tty.warn("Skipping external or virtual package %s" % spec.format())
return
- for d in s.dependencies(deptype=('link', 'run')):
- tty.msg("Installing buildcache for dependency spec %s" % d)
- install_tarball(d, args)
+
+ # This argument is used only for bootstrapping specs without signatures,
+ # since we need to check the sha256 of each tarball
+ if not args.only_root:
+ for d in s.dependencies(deptype=('link', 'run')):
+ tty.msg("Installing buildcache for dependency spec %s" % d)
+ install_tarball(d, args)
+
package = spack.repo.get(spec)
if s.concrete and package.installed and not args.force:
tty.warn("Package for spec %s already installed." % spec.format())
diff --git a/lib/spack/spack/modules/common.py b/lib/spack/spack/modules/common.py
index ce1b9115cd..8855e57e64 100644
--- a/lib/spack/spack/modules/common.py
+++ b/lib/spack/spack/modules/common.py
@@ -40,7 +40,7 @@ import llnl.util.filesystem
import llnl.util.tty as tty
from llnl.util.lang import dedupe
-import spack.build_environment as build_environment
+import spack.build_environment
import spack.config
import spack.environment
import spack.error
@@ -732,12 +732,12 @@ class BaseContext(tengine.Context):
# Let the extendee/dependency modify their extensions/dependencies
# before asking for package-specific modifications
env.extend(
- build_environment.modifications_from_dependencies(
+ spack.build_environment.modifications_from_dependencies(
spec, context='run'
)
)
# Package specific modifications
- build_environment.set_module_variables_for_package(spec.package)
+ spack.build_environment.set_module_variables_for_package(spec.package)
spec.package.setup_run_environment(env)
# Modifications required from modules.yaml
diff --git a/lib/spack/spack/test/cmd/gpg.py b/lib/spack/spack/test/cmd/gpg.py
index 6c9728d872..182773b6ce 100644
--- a/lib/spack/spack/test/cmd/gpg.py
+++ b/lib/spack/spack/test/cmd/gpg.py
@@ -9,6 +9,7 @@ import pytest
import llnl.util.filesystem as fs
+import spack.bootstrap
import spack.util.executable
import spack.util.gpg
from spack.main import SpackCommand
@@ -17,6 +18,7 @@ from spack.util.executable import ProcessError
#: spack command used by tests below
gpg = SpackCommand('gpg')
+bootstrap = SpackCommand('bootstrap')
# test gpg command detection
@@ -46,9 +48,10 @@ def test_find_gpg(cmd_name, version, tmpdir, mock_gnupghome, monkeypatch):
assert spack.util.gpg.GPGCONF is not None
-def test_no_gpg_in_path(tmpdir, mock_gnupghome, monkeypatch):
+def test_no_gpg_in_path(tmpdir, mock_gnupghome, monkeypatch, mutable_config):
monkeypatch.setitem(os.environ, "PATH", str(tmpdir))
- with pytest.raises(spack.util.gpg.SpackGPGError):
+ bootstrap('disable')
+ with pytest.raises(RuntimeError):
spack.util.gpg.init(force=True)
diff --git a/lib/spack/spack/util/gpg.py b/lib/spack/spack/util/gpg.py
index 87059d7d34..b0a4b4e430 100644
--- a/lib/spack/spack/util/gpg.py
+++ b/lib/spack/spack/util/gpg.py
@@ -8,6 +8,7 @@ import functools
import os
import re
+import spack.bootstrap
import spack.error
import spack.paths
import spack.util.executable
@@ -59,7 +60,10 @@ def init(gnupghome=None, force=False):
spack.paths.gpg_path)
# Set the executable objects for "gpg" and "gpgconf"
- GPG, GPGCONF = _gpg(), _gpgconf()
+ with spack.bootstrap.ensure_bootstrap_configuration():
+ spack.bootstrap.ensure_gpg_in_path_or_raise()
+ GPG, GPGCONF = _gpg(), _gpgconf()
+
GPG.add_default_env('GNUPGHOME', GNUPGHOME)
if GPGCONF:
GPGCONF.add_default_env('GNUPGHOME', GNUPGHOME)