From 8011fedd9ca47578e8da37a9060407c6784d7015 Mon Sep 17 00:00:00 2001 From: Todd Gamblin Date: Tue, 21 Jan 2020 23:50:59 -0800 Subject: 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. --- lib/spack/spack/test/cmd/gpg.py | 41 +++++++++++++++++++++++++++++++---- lib/spack/spack/test/conftest.py | 15 +++++++++++-- lib/spack/spack/util/gpg.py | 46 ++++++++++++++++++++++++++++++++++------ 3 files changed, 89 insertions(+), 13 deletions(-) diff --git a/lib/spack/spack/test/cmd/gpg.py b/lib/spack/spack/test/cmd/gpg.py index 2b63fbdb29..4333a38fe2 100644 --- a/lib/spack/spack/test/cmd/gpg.py +++ b/lib/spack/spack/test/cmd/gpg.py @@ -7,6 +7,9 @@ import os 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.main import SpackCommand 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')) diff --git a/lib/spack/spack/test/conftest.py b/lib/spack/spack/test/conftest.py index b40dd92f99..8b8d128d2c 100644 --- a/lib/spack/spack/test/conftest.py +++ b/lib/spack/spack/test/conftest.py @@ -11,6 +11,7 @@ import itertools import os import os.path import shutil +import tempfile import xml.etree.ElementTree import ordereddict_backport @@ -674,9 +675,19 @@ def module_configuration(monkeypatch, request): @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 diff --git a/lib/spack/spack/util/gpg.py b/lib/spack/spack/util/gpg.py index 93d79d1e11..29b2add852 100644 --- a/lib/spack/spack/util/gpg.py +++ b/lib/spack/spack/util/gpg.py @@ -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 @@ class Gpg(object): cls.gpg()('--list-public-keys') if signing: cls.gpg()('--list-secret-keys') + + +class SpackGPGError(spack.error.SpackError): + """Class raised when GPG errors are detected.""" -- cgit v1.2.3-70-g09d2