diff --git a/lang/python/Makefile.am b/lang/python/Makefile.am index a18a014a..b9145f5b 100644 --- a/lang/python/Makefile.am +++ b/lang/python/Makefile.am @@ -1,125 +1,100 @@ # Makefile.am for the Python bindings. # Copyright (C) 2016 g10 Code GmbH # # This file is part of GPGME. # # GPGME is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 2.1 of the # License, or (at your option) any later version. # # GPGME is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General # Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, see . EXTRA_DIST = \ README \ MANIFEST.in \ gpgme.i \ helpers.c helpers.h private.h \ gpgme-h-clean.py \ examples \ gpg SUBDIRS = . tests -COPY_FILES = \ - $(srcdir)/gpgme.i \ - $(srcdir)/README \ - $(srcdir)/MANIFEST.in \ - $(srcdir)/gpgme-h-clean.py \ - $(srcdir)/examples \ - $(srcdir)/helpers.c $(srcdir)/helpers.h $(srcdir)/private.h - -COPY_FILES_GPG = \ - $(srcdir)/gpg/callbacks.py \ - $(srcdir)/gpg/constants \ - $(srcdir)/gpg/core.py \ - $(srcdir)/gpg/errors.py \ - $(srcdir)/gpg/__init__.py \ - $(srcdir)/gpg/results.py \ - $(srcdir)/gpg/util.py - .PHONY: prepare -prepare: - test -n "$(PREPAREDIR)" - $(MKDIR_P) "$(PREPAREDIR)/gpg" - cp -R $(COPY_FILES) "$(PREPAREDIR)" - cp setup.py "$(PREPAREDIR)" - cp gpg/version.py "$(PREPAREDIR)/gpg" - ln -sf "$(abs_top_srcdir)/src/data.h" "$(PREPAREDIR)" - ln -sf "$(abs_top_builddir)/config.h" "$(PREPAREDIR)" - cp -R $(COPY_FILES_GPG) "$(PREPAREDIR)/gpg" +prepare: copystamp # For VPATH builds we need to copy some files because Python's # distutils are not VPATH-aware. -copystamp: $(COPY_FILES) $(COPY_FILES_GPG) - set -e ; for VERSION in $(PYTHON_VERSIONS); do \ - $(MAKE) PREPAREDIR=python$${VERSION}-gpg prepare; \ - done +copystamp: + ln -sf "$(abs_top_srcdir)/src/data.h" . + ln -sf "$(abs_top_builddir)/config.h" . touch $@ all-local: copystamp set -e ; set $(PYTHONS); for VERSION in $(PYTHON_VERSIONS); do \ PYTHON="$$1" ; shift ; \ - cd python$${VERSION}-gpg && \ CFLAGS="$(CFLAGS)" \ abs_top_builddir="$(abs_top_builddir)" \ - $$PYTHON setup.py build --verbose ; \ - cd .. ; \ + $$PYTHON setup.py build --verbose --build-base=python$${VERSION}-gpg ; \ done -python$(PYTHON_VERSION)-gpg/dist/gpg-$(VERSION).tar.gz \ python$(PYTHON_VERSION)-gpg/dist/gpg-$(VERSION).tar.gz.asc: copystamp - cd python$(PYTHON_VERSION)-gpg && \ + $(MKDIR_P) python$(PYTHON_VERSION)-gpg-dist CFLAGS="$(CFLAGS)" \ abs_top_builddir="$(abs_top_builddir)" \ - $(PYTHON) setup.py sdist --verbose - gpg2 --detach-sign --armor python$(PYTHON_VERSION)-gpg/dist/gpg-$(VERSION).tar.gz + $(PYTHON) setup.py sdist --verbose --dist-dir=python$(PYTHON_VERSION)-gpg-dist \ + --manifest=python$(PYTHON_VERSION)-gpg-dist/MANIFEST + gpg2 --detach-sign --armor python$(PYTHON_VERSION)-gpg-dist/gpg-$(VERSION).tar.gz .PHONY: sdist -sdist: python$(PYTHON_VERSION)-gpg/dist/gpg-$(VERSION).tar.gz \ - python$(PYTHON_VERSION)-gpg/dist/gpg-$(VERSION).tar.gz.asc +sdist: python$(PYTHON_VERSION)-gpg/dist/gpg-$(VERSION).tar.gz.asc .PHONY: upload -upload: python$(PYTHON_VERSION)-gpg/dist/gpg-$(VERSION).tar.gz \ - python$(PYTHON_VERSION)-gpg/dist/gpg-$(VERSION).tar.gz.asc +upload: python$(PYTHON_VERSION)-gpg-dist/gpg-$(VERSION).tar.gz \ + python$(PYTHON_VERSION)-gpg-dist/gpg-$(VERSION).tar.gz.asc twine upload $^ -CLEANFILES = copystamp +CLEANFILES = copystamp \ + config.h \ + data.h \ + files.txt \ + install_files.txt # Remove the rest. # # 'make distclean' clears the write bit, breaking rm -rf. Fix the # permissions. clean-local: rm -rf -- build for VERSION in $(PYTHON_VERSIONS); do \ find python$${VERSION}-gpg -type d ! -perm -200 -exec chmod u+w {} ';' ; \ rm -rf -- python$${VERSION}-gpg ; \ done install-exec-local: - rm -f install_files.txt set -e ; set $(PYTHONS); for VERSION in $(PYTHON_VERSIONS); do \ PYTHON="$$1" ; shift ; \ - cd python$${VERSION}-gpg ; \ abs_top_builddir="$(abs_top_builddir)" \ - $$PYTHON setup.py install \ - --prefix $(DESTDIR)$(prefix) \ + $$PYTHON setup.py \ + build \ + --build-base=python$${VERSION}-gpg \ + install \ + --prefix "$(DESTDIR)$(prefix)" \ --record files.txt \ --verbose ; \ - cat files.txt >> ../install_files.txt ; \ + cat files.txt >> install_files.txt ; \ rm files.txt ; \ - cd .. ; \ done - $(MKDIR_P) $(DESTDIR)$(pythondir)/gpg - mv install_files.txt $(DESTDIR)$(pythondir)/gpg + $(MKDIR_P) "$(DESTDIR)$(pythondir)/gpg" + mv install_files.txt "$(DESTDIR)$(pythondir)/gpg" uninstall-local: - xargs <$(DESTDIR)$(pythondir)/gpg/install_files.txt -- rm -rf -- - rm -rf -- $(DESTDIR)$(pythondir)/gpg + xargs < "$(DESTDIR)$(pythondir)/gpg/install_files.txt" -- rm -rf -- + rm -rf -- "$(DESTDIR)$(pythondir)/gpg" diff --git a/lang/python/setup.py.in b/lang/python/setup.py.in index 8ddbf276..6692de6f 100755 --- a/lang/python/setup.py.in +++ b/lang/python/setup.py.in @@ -1,194 +1,220 @@ #!/usr/bin/env python # Copyright (C) 2016 g10 Code GmbH # Copyright (C) 2004 Igor Belyi # Copyright (C) 2002 John Goerzen # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from distutils.core import setup, Extension import os, os.path, sys import glob +import shutil import subprocess # Out-of-tree build of the gpg bindings. gpg_error_config = ["gpg-error-config"] gpgme_config_flags = ["--thread=pthread"] gpgme_config = ["gpgme-config"] + gpgme_config_flags gpgme_h = "" include_dirs = [os.getcwd()] library_dirs = [] in_tree = False extra_swig_opts = [] extra_macros = dict() abs_top_builddir = os.environ.get("abs_top_builddir") if abs_top_builddir: # In-tree build. in_tree = True gpgme_config = [os.path.join(abs_top_builddir, "src/gpgme-config")] + gpgme_config_flags gpgme_h = os.path.join(abs_top_builddir, "src/gpgme.h") library_dirs = [os.path.join(abs_top_builddir, "src/.libs")] # XXX uses libtool internals extra_macros.update( HAVE_CONFIG_H=1, HAVE_DATA_H=1, IN_TREE_BUILD=1, ) if hasattr(subprocess, "DEVNULL"): devnull = subprocess.DEVNULL else: devnull = open(os.devnull, "w") try: subprocess.check_call(gpg_error_config + ['--version'], stdout=devnull) except: sys.exit("Could not find gpg-error-config. " + "Please install the libgpg-error development package.") try: subprocess.check_call(gpgme_config + ['--version'], stdout=devnull) except: sys.exit("Could not find gpgme-config. " + "Please install the libgpgme development package.") def getconfig(what, config=gpgme_config): confdata = subprocess.Popen(config + ["--%s" % what], stdout=subprocess.PIPE).communicate()[0] return [x for x in confdata.decode('utf-8').split() if x != ''] version = version_raw = getconfig("version")[0] if '-' in version: version = version.split('-')[0] major, minor, patch = map(int, version.split('.')) if not (major > 1 or (major == 1 and minor >= 7)): sys.exit('Need at least GPGME version 1.7, found {}.'.format(version_raw)) if not gpgme_h: gpgme_h = os.path.join(getconfig("prefix")[0], "include", "gpgme.h") gpg_error_prefix = getconfig("prefix", config=gpg_error_config)[0] gpg_error_h = os.path.join(gpg_error_prefix, "include", "gpg-error.h") if not os.path.exists(gpg_error_h): gpg_error_h = \ glob.glob(os.path.join(gpg_error_prefix, "include", "*", "gpg-error.h"))[0] -print("Building python gpg module using {} and {}.".format(gpgme_h, gpg_error_h)) - -# Cleanup gpgme.h from deprecated functions and typedefs. -subprocess.check_call([sys.executable, "gpgme-h-clean.py", gpgme_h], - stdout=open("gpgme.h", "w")) -subprocess.check_call([sys.executable, "gpgme-h-clean.py", gpg_error_h], - stdout=open("errors.i", "w")) - define_macros = [] libs = getconfig('libs') # Define extra_macros for both the SWIG and C code for k, v in extra_macros.items(): extra_swig_opts.append("-D{0}={1}".format(k, v)) define_macros.append((k, str(v))) for item in getconfig('cflags'): if item.startswith("-I"): include_dirs.append(item[2:]) elif item.startswith("-D"): defitem = item[2:].split("=", 1) if len(defitem)==2: define_macros.append((defitem[0], defitem[1])) else: define_macros.append((defitem[0], None)) # Adjust include and library locations in case of win32 uname_s = os.popen("uname -s").read() if uname_s.startswith("MINGW32"): mnts = [x.split()[0:3:2] for x in os.popen("mount").read().split("\n") if x] tmplist = sorted([(len(x[1]), x[1], x[0]) for x in mnts]) tmplist.reverse() extra_dirs = [] for item in include_dirs: for ln, mnt, tgt in tmplist: if item.startswith(mnt): item = os.path.normpath(item[ln:]) while item[0] == os.path.sep: item = item[1:] extra_dirs.append(os.path.join(tgt, item)) break include_dirs += extra_dirs for item in [x[2:] for x in libs if x.startswith("-L")]: for ln, mnt, tgt in tmplist: if item.startswith(mnt): item = os.path.normpath(item[ln:]) while item[0] == os.path.sep: item = item[1:] library_dirs.append(os.path.join(tgt, item)) break # We build an Extension using SWIG, which generates a Python module. # By default, the 'build_py' step is run before 'build_ext', and # therefore the generated Python module is not copied into the build # directory. # Bug: http://bugs.python.org/issue1016626 # Workaround: # http://stackoverflow.com/questions/12491328/python-distutils-not-include-the-swig-generated-module from distutils.command.build import build class BuildExtFirstHack(build): + + def _generate(self): + print("Building python gpg module using {} and {}.".format(gpgme_h, gpg_error_h)) + + # Cleanup gpgme.h from deprecated functions and typedefs. + # Keep timestamp to avoid rebuild + if not os.path.exists(self.build_base): + os.makedirs(self.build_base) + + for src, dst in ( + (gpgme_h, os.path.join(self.build_base, "gpgme.h")), + (gpg_error_h, os.path.join(self.build_base, "errors.i")) + ): + subprocess.check_call([sys.executable, "gpgme-h-clean.py", src], + stdout=open(dst, "w")) + shutil.copystat(src, dst) + + # Copy due to http://bugs.python.org/issue2624 + # Avoid creating in srcdir + shutil.copy2("gpgme.i", self.build_base) + def run(self): + self._generate() + + # Append generated files via build_base + if not os.path.exists(os.path.join(self.build_lib, "gpg")): + os.makedirs(os.path.join(self.build_lib, "gpg")) + + swig_sources.append(os.path.join(self.build_base, 'gpgme.i')) + swig_opts.extend(['-I' + self.build_base, '-outdir', os.path.join(self.build_lib, 'gpg')]) + include_dirs.append(self.build_base) + self.run_command('build_ext') build.run(self) py3 = [] if sys.version_info.major < 3 else ['-py3'] -swige = Extension("gpg._gpgme", ["gpgme.i", "helpers.c"], - swig_opts = ['-threads', - '-outdir', 'gpg'] + py3 + extra_swig_opts, +swig_sources = ['helpers.c'] +swig_opts = ['-threads'] + py3 + extra_swig_opts +swige = Extension("gpg._gpgme", + sources = swig_sources, + swig_opts = swig_opts, include_dirs = include_dirs, define_macros = define_macros, library_dirs = library_dirs, extra_link_args = libs) setup(name="gpg", cmdclass={'build': BuildExtFirstHack}, version="@VERSION@", description='Python bindings for GPGME GnuPG cryptography library', # XXX add a long description #long_description=long_description, author='The GnuPG hackers', author_email='gnupg-devel@gnupg.org', url='https://www.gnupg.org', ext_modules=[swige], packages = ['gpg', 'gpg.constants', 'gpg.constants.data', 'gpg.constants.keylist', 'gpg.constants.sig', 'gpg.constants.tofu'], license="LGPL2.1+ (the library), GPL2+ (tests and examples)", classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Operating System :: POSIX', 'Operating System :: Microsoft :: Windows', 'Topic :: Communications :: Email', 'Topic :: Security :: Cryptography', ], ) diff --git a/lang/python/tests/run-tests.py b/lang/python/tests/run-tests.py index 9e061d83..9e2fb78c 100644 --- a/lang/python/tests/run-tests.py +++ b/lang/python/tests/run-tests.py @@ -1,106 +1,105 @@ #!/usr/bin/env python # Copyright (C) 2016 g10 Code GmbH # # This file is part of GPGME. # # GPGME is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # GPGME is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General # Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, see . from __future__ import absolute_import from __future__ import division from __future__ import print_function from __future__ import unicode_literals import argparse import glob import os import subprocess import sys class SplitAndAccumulate(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): current = getattr(namespace, self.dest, list()) current.extend(values.split()) setattr(namespace, self.dest, current) parser = argparse.ArgumentParser(description='Run tests.') parser.add_argument('tests', metavar='TEST', type=str, nargs='+', help='A test to run') parser.add_argument('-v', '--verbose', action="store_true", default=False, help='Be verbose.') parser.add_argument('-q', '--quiet', action="store_true", default=False, help='Be quiet.') parser.add_argument('--interpreters', metavar='PYTHON', type=str, default=[], action=SplitAndAccumulate, help='Use these interpreters to run the tests, ' + 'separated by spaces.') parser.add_argument('--srcdir', type=str, default=os.environ.get("srcdir", ""), help='Location of the tests.') parser.add_argument('--builddir', type=str, default=os.environ.get("abs_builddir", ""), help='Location of the tests.') parser.add_argument('--parallel', action="store_true", default=False, help='Ignored. For compatibility with run-tests.scm.') args = parser.parse_args() if not args.interpreters: args.interpreters = [sys.executable] out = sys.stdout if args.verbose else None err = sys.stderr if args.verbose else None def status_to_str(code): return {0: "PASS", 77: "SKIP", 99: "ERROR"}.get(code, "FAIL") results = list() for interpreter in args.interpreters: version = subprocess.check_output( [interpreter, "-c", "import sys; print('{0}.{1}'.format(sys.version_info[0], sys.version_info[1]))"]).strip().decode() pattern = os.path.join(args.builddir, "..", "python{0}-gpg".format(version), - "build", "lib*"+version) builddirs = glob.glob(pattern) if len(builddirs) == 0: sys.exit("Build directory matching {0!r} not found.".format(pattern)) elif len(builddirs) > 1: sys.exit("Multiple build directories matching {0!r} found: {1}".format( pattern, builddirs)) env = dict(os.environ) env["PYTHONPATH"] = builddirs[0] if not args.quiet: print("Running tests using {0} ({1})...".format(interpreter, version)) for test in args.tests: status = subprocess.call( [interpreter, os.path.join(args.srcdir, test)], env=env, stdout=out, stderr=err) if not args.quiet: print("{0}: {1}".format(status_to_str(status), test)) results.append(status) def count(status): return len(list(filter(lambda x: x == status, results))) def failed(): return len(list(filter(lambda x: x not in (0, 77, 99), results))) if not args.quiet: print("{0} tests run, {1} succeeded, {2} failed, {3} skipped.".format( len(results), count(0), failed(), count(77))) sys.exit(len(results) - count(0) - count(77)) sys.exit(results[0])