bugfix: gpg2 is called 'gpg' on macOS

The gpg2 command isn't always around; it's sometimes called gpg.  This is
the case with the brew-installed version, and it's breaking our tests.

- [x] Look for both 'gpg2' and 'gpg' when finding the command
- [x] If we find 'gpg', ensure the version is 2 or higher
- [x] Add tests for version detection.
This commit is contained in:
Todd Gamblin 2020-01-21 23:50:59 -08:00
parent 910df8cb4e
commit 8011fedd9c
3 changed files with 89 additions and 13 deletions

View file

@ -7,6 +7,9 @@
import pytest
import llnl.util.filesystem as fs
import spack.util.executable
import spack.util.gpg
from spack.paths import mock_gpg_data_path, mock_gpg_keys_path
@ -14,15 +17,45 @@
from spack.util.executable import ProcessError
@pytest.fixture(scope='function')
def gpg():
return SpackCommand('gpg')
#: spack command used by tests below
gpg = SpackCommand('gpg')
# test gpg command detection
@pytest.mark.parametrize('cmd_name,version', [
('gpg', 'undetectable'), # undetectable version
('gpg', 'gpg (GnuPG) 1.3.4'), # insufficient version
('gpg', 'gpg (GnuPG) 2.2.19'), # sufficient version
('gpg2', 'gpg (GnuPG) 2.2.19'), # gpg2 command
])
def test_find_gpg(cmd_name, version, tmpdir, mock_gnupghome, monkeypatch):
with tmpdir.as_cwd():
with open(cmd_name, 'w') as f:
f.write("""\
#!/bin/sh
echo "{version}"
""".format(version=version))
fs.set_executable(cmd_name)
monkeypatch.setitem(os.environ, "PATH", str(tmpdir))
if version == 'undetectable' or version.endswith('1.3.4'):
with pytest.raises(spack.util.gpg.SpackGPGError):
exe = spack.util.gpg.Gpg.gpg()
else:
exe = spack.util.gpg.Gpg.gpg()
assert isinstance(exe, spack.util.executable.Executable)
def test_no_gpg_in_path(tmpdir, mock_gnupghome, monkeypatch):
monkeypatch.setitem(os.environ, "PATH", str(tmpdir))
with pytest.raises(spack.util.gpg.SpackGPGError):
spack.util.gpg.Gpg.gpg()
@pytest.mark.maybeslow
@pytest.mark.skipif(not spack.util.gpg.Gpg.gpg(),
reason='These tests require gnupg2')
def test_gpg(gpg, tmpdir, mock_gnupghome):
def test_gpg(tmpdir, mock_gnupghome):
# Verify a file with an empty keyring.
with pytest.raises(ProcessError):
gpg('verify', os.path.join(mock_gpg_data_path, 'content.txt'))

View file

@ -11,6 +11,7 @@
import os
import os.path
import shutil
import tempfile
import xml.etree.ElementTree
import ordereddict_backport
@ -674,9 +675,19 @@ def writer_key_function():
@pytest.fixture()
def mock_gnupghome(tmpdir, monkeypatch):
monkeypatch.setattr(spack.util.gpg, 'GNUPGHOME', str(tmpdir.join('gpg')))
def mock_gnupghome(monkeypatch):
# GNU PGP can't handle paths longer than 108 characters (wtf!@#$) so we
# have to make our own tmpdir with a shorter name than pytest's.
# This comes up because tmp paths on macOS are already long-ish, and
# pytest makes them longer.
short_name_tmpdir = tempfile.mkdtemp()
monkeypatch.setattr(spack.util.gpg, 'GNUPGHOME', short_name_tmpdir)
monkeypatch.setattr(spack.util.gpg.Gpg, '_gpg', None)
yield
# clean up, since we are doing this manually
shutil.rmtree(short_name_tmpdir)
##########
# Fake archives and repositories

View file

@ -4,10 +4,14 @@
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import os
import re
import spack.error
import spack.paths
from spack.util.executable import Executable
import spack.version
from spack.util.executable import which
_gnupg_version_re = r"^gpg \(GnuPG\) (.*)$"
GNUPGHOME = spack.paths.gpg_path
@ -28,15 +32,39 @@ def parse_keys_output(output):
class Gpg(object):
_gpg = None
@staticmethod
def gpg():
# TODO: Support loading up a GPG environment from a built gpg.
gpg = Executable('gpg2')
if not os.path.exists(GNUPGHOME):
os.makedirs(GNUPGHOME)
os.chmod(GNUPGHOME, 0o700)
gpg.add_default_env('GNUPGHOME', GNUPGHOME)
return gpg
if Gpg._gpg is None:
gpg = which('gpg2', 'gpg')
if not gpg:
raise SpackGPGError("Spack requires gpg version 2 or higher.")
# ensure that the version is actually >= 2 if we find 'gpg'
if gpg.name == 'gpg':
output = gpg('--version', output=str)
match = re.search(_gnupg_version_re, output, re.M)
if not match:
raise SpackGPGError("Couldn't determine version of gpg")
v = spack.version.Version(match.group(1))
if v < spack.version.Version('2'):
raise SpackGPGError("Spack requires GPG version >= 2")
# make the GNU PG path if we need to
# TODO: does this need to be in the spack directory?
# we should probably just use GPG's regular conventions
if not os.path.exists(GNUPGHOME):
os.makedirs(GNUPGHOME)
os.chmod(GNUPGHOME, 0o700)
gpg.add_default_env('GNUPGHOME', GNUPGHOME)
Gpg._gpg = gpg
return Gpg._gpg
@classmethod
def create(cls, **kwargs):
@ -112,3 +140,7 @@ def list(cls, trusted, signing):
cls.gpg()('--list-public-keys')
if signing:
cls.gpg()('--list-secret-keys')
class SpackGPGError(spack.error.SpackError):
"""Class raised when GPG errors are detected."""