summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristoph Reiter <creiter@src.gnome.org>2017-12-04 15:33:00 +0100
committerChristoph Reiter <creiter@src.gnome.org>2017-12-06 15:50:13 +0100
commit3e455944f5835c750911d3178a0607201f23f1a8 (patch)
treeccc78028794ea8f65cfafc8cfbae848be1d4c4d2
parent58f677bfaa0f117465a9e2146c5d83768b5a76ac (diff)
downloadpygobject-3e455944f5835c750911d3178a0607201f23f1a8.tar.gz
setup.py: Port to distutils/setuptools
Instead of wrapping autotools add a proper setuptools based build system. Compared to the autotools one this does not install .pc files or headers and does not allow running tests. It uses pkg-config for discovering dependencies and explictely searches for .pc files in the Python prefix so that pycairo installations in a virtualenv are discovered. When using MSVC, pkg-config is skipped and it is assumend that INCLUDE and LIB is properly set up. Version information and requirements are parsed from configure.ac, package metadata is parsed from PKG-INFO.in. Also adds a "setup.py distcheck" command which makes sure all tracked files end up in the tarball and that the tarball builds (no tests are run atm). https://bugzilla.gnome.org/show_bug.cgi?id=789211
-rw-r--r--.gitignore1
-rw-r--r--MANIFEST.in20
-rw-r--r--Makefile.am3
-rw-r--r--PKG-INFO.in8
-rw-r--r--configure.ac3
-rw-r--r--[-rwxr-xr-x]setup.py409
6 files changed, 359 insertions, 85 deletions
diff --git a/.gitignore b/.gitignore
index 03f742a1..aba77a16 100644
--- a/.gitignore
+++ b/.gitignore
@@ -76,3 +76,4 @@ Makefile.in
/build/
/dist/
/pygobject.egg-info/
+/MANIFEST
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 00000000..26b2abd0
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,20 @@
+include *.am
+include AUTHORS
+include autogen.sh
+include configure.ac
+include COPYING
+include HACKING
+include *.in
+include INSTALL
+include m4/introspection.m4
+include m4/python.m4
+include NEWS
+include pre-commit.hook
+include pygi-convert.sh
+include pygobject.doap
+include README
+recursive-include demos *.py *.png *.css *.ui *.gif *.gresource *.jpg *.xml
+recursive-include examples *.py *.am
+recursive-include gi *.am *.h
+recursive-include pygtkcompat *.am
+recursive-include tests *.py *.c *.h *.xml *.supp *nouppera *.am
diff --git a/Makefile.am b/Makefile.am
index 80183393..ffa4b461 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -20,7 +20,8 @@ EXTRA_DIST = \
pygi-convert.sh \
m4/python.m4 \
m4/introspection.m4 \
- setup.py
+ setup.py \
+ MANIFEST.in
MAINTAINERCLEANFILES = \
$(srcdir)/INSTALL \
diff --git a/PKG-INFO.in b/PKG-INFO.in
index 651dabeb..bebaf080 100644
--- a/PKG-INFO.in
+++ b/PKG-INFO.in
@@ -1,15 +1,15 @@
Metadata-Version: 1.0
-Name: PyGObject
+Name: pygobject
Version: @VERSION@
-Summary: Python bindings for GObject
-Home-page: http://www.pygtk.org/
+Summary: Python bindings for GObject Introspection
+Home-page: https://pygobject.readthedocs.io
Author: James Henstridge
Author-email: james@daa.com.au
Maintainer: Simon Feltman
Maintainer-email: sfeltman@src.gnome.org
License: GNU LGPL
Download-url: ftp://ftp.gnome.org/pub/GNOME/sources/pygobject/@PYGOBJECT_MAJOR_VERSION@.@PYGOBJECT_MINOR_VERSION@/pygobject-@VERSION@.tar.gz
-Description: Python bindings for GLib and GObject
+Description: Python bindings for GObject Introspection
Platform: POSIX, Windows
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Linux
diff --git a/configure.ac b/configure.ac
index 8ccccb8e..876681dd 100644
--- a/configure.ac
+++ b/configure.ac
@@ -26,6 +26,7 @@ m4_define(introspection_required_version, 1.46.0)
m4_define(pycairo_required_version, 1.11.1)
m4_define(glib_required_version, 2.38.0)
m4_define(gio_required_version, 2.38.0)
+m4_define(libffi_required_version, 3.0)
AC_INIT([pygobject],[pygobject_version],
[http://bugzilla.gnome.org/enter_bug.cgi?product=pygobject],
@@ -131,7 +132,7 @@ PYTHON_VALGRIND_SUPP=`$PYTHON -c "import sys; sys.stdout.write('python' + sys.ve
AC_SUBST([PYTHON_VALGRIND_SUPP])
dnl libffi
-PKG_CHECK_MODULES(FFI, libffi >= 3.0)
+PKG_CHECK_MODULES(FFI, libffi >= libffi_required_version)
LIBFFI_PC=libffi
AC_SUBST(FFI_CFLAGS)
diff --git a/setup.py b/setup.py
index 0da9ed95..af3aa5c6 100755..100644
--- a/setup.py
+++ b/setup.py
@@ -1,105 +1,356 @@
#!/usr/bin/env python
+# Copyright 2017 Christoph Reiter <reiter.christoph@gmail.com>
+#
+# 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
+# USA
+"""
+ATTENTION DISTRO PACKAGERS: This is not a valid replacement for autotools.
+It does not install headers, pkgconfig files and does not support running
+tests. Its main use case atm is installation in virtualenvs and via pip.
+"""
+import io
import os
import re
-import subprocess
import sys
+import errno
+import subprocess
+import tarfile
+from email import parser
+
+from setuptools import setup, find_packages
+from distutils.core import Extension, Distribution
+from distutils.ccompiler import new_compiler
+from distutils import dir_util
+
+
+def get_command_class(name):
+ # Returns the right class for either distutils or setuptools
+ return Distribution({}).get_command_class(name)
+
+
+def get_pycairo_pkg_config_name():
+ return "py3cairo" if sys.version_info[0] == 3 else "pycairo"
+
-from distutils.command.build import build as orig_build
-from setuptools.command.build_ext import build_ext as orig_build_ext
-from setuptools.command.build_py import build_py as orig_build_py
-from setuptools import setup, Extension
+def get_version_requirement(conf_dir, pkg_config_name):
+ """Given a pkg-config module name gets the minimum version required"""
+ if pkg_config_name in ["cairo", "cairo-gobject"]:
+ return "0"
-with open("configure.ac", "r") as h:
- version = ".".join(re.findall("pygobject_[^\s]+_version,\s*(\d+)\)", h.read()))
+ mapping = {
+ "gobject-introspection-1.0": "introspection",
+ "glib-2.0": "glib",
+ "gio-2.0": "gio",
+ get_pycairo_pkg_config_name(): "pycairo",
+ "libffi": "libffi",
+ }
+ assert pkg_config_name in mapping
+ configure_ac = os.path.join(conf_dir, "configure.ac")
+ with io.open(configure_ac, "r", encoding="utf-8") as h:
+ text = h.read()
+ conf_name = mapping[pkg_config_name]
+ res = re.findall(
+ r"%s_required_version,\s*([\d\.]+)\)" % conf_name, text)
+ assert len(res) == 1
+ return res[0]
-def makedirs(dirpath):
- """Safely make directories
- By default, os.makedirs fails if the directory already exists.
+def parse_versions(conf_dir):
+ configure_ac = os.path.join(conf_dir, "configure.ac")
+ with io.open(configure_ac, "r", encoding="utf-8") as h:
+ version = re.findall(r"pygobject_[^\s]+_version,\s*(\d+)\)", h.read())
+ assert len(version) == 3
- Python 3.2 introduced the `exist_ok` argument, but we can't use it because
- we want to keep supporting Python 2 for some time.
+ versions = {
+ "PYGOBJECT_MAJOR_VERSION": version[0],
+ "PYGOBJECT_MINOR_VERSION": version[1],
+ "PYGOBJECT_MICRO_VERSION": version[2],
+ "VERSION": ".".join(version),
+ }
+ return versions
+
+
+def parse_pkg_info(conf_dir):
+ """Returns an email.message.Message instance containing the content
+ of the PKG-INFO file. The version info is parsed from configure.ac
"""
- import errno
- try:
- os.makedirs(dirpath)
+ versions = parse_versions(conf_dir)
+
+ pkg_info = os.path.join(conf_dir, "PKG-INFO.in")
+ with io.open(pkg_info, "r", encoding="utf-8") as h:
+ text = h.read()
+ for key, value in versions.items():
+ text = text.replace("@%s@" % key, value)
+
+ p = parser.Parser()
+ message = p.parse(io.StringIO(text))
+ return message
+
+
+def _run_pkg_config(args):
+ command = ["pkg-config"] + args
+
+ # Add $prefix/share/pkgconfig to PKG_CONFIG_PATH so we use the right
+ # pycairo in case we are in a virtualenv
+ env = dict(os.environ)
+ paths = env.get("PKG_CONFIG_PATH", "").split(os.pathsep)
+ paths = [p for p in paths if p]
+ paths.insert(0, os.path.join(sys.prefix, "share", "pkgconfig"))
+ env["PKG_CONFIG_PATH"] = os.pathsep.join(paths)
+ try:
+ return subprocess.check_output(command, env=env)
except OSError as e:
- if e.errno == errno.EEXIST:
- return
+ if e.errno == errno.ENOENT:
+ raise SystemExit(
+ "%r not found.\nArguments: %r" % (command[0], command))
+ raise SystemExit(e)
+ except subprocess.CalledProcessError as e:
+ raise SystemExit(e)
+
+
+def pkg_config_version_check(pkg, version):
+ _run_pkg_config([
+ "--print-errors",
+ "--exists",
+ '%s >= %s' % (pkg, version),
+ ])
- raise
+def pkg_config_parse(opt, pkg):
+ ret = _run_pkg_config([opt, pkg])
+ output = ret.decode()
+ opt = opt[-2:]
+ return [x.lstrip(opt) for x in output.split()]
-class Build(orig_build):
- """Dummy version of distutils build which runs an Autotools build system
- instead.
+
+du_sdist = get_command_class("sdist")
+
+
+class distcheck(du_sdist):
+ """Creates a tarball and does some additional sanity checks such as
+ checking if the tarballs includes all files and builds.
"""
+
+ def _check_manifest(self):
+ # make sure MANIFEST.in includes all tracked files
+ assert self.get_archive_files()
+
+ if subprocess.call(["git", "status"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE) != 0:
+ return
+
+ included_files = self.filelist.files
+ assert included_files
+
+ process = subprocess.Popen(
+ ["git", "ls-tree", "-r", "HEAD", "--name-only"],
+ stdout=subprocess.PIPE, universal_newlines=True)
+ out, err = process.communicate()
+ assert process.returncode == 0
+
+ tracked_files = out.splitlines()
+ for ignore in [".gitignore"]:
+ tracked_files.remove(ignore)
+
+ diff = set(tracked_files) - set(included_files)
+ assert not diff, (
+ "Not all tracked files included in tarball, check MANIFEST.in",
+ diff)
+
+ def _check_dist(self):
+ # make sure the tarball builds
+ assert self.get_archive_files()
+
+ distcheck_dir = os.path.join(self.dist_dir, "distcheck")
+ if os.path.exists(distcheck_dir):
+ dir_util.remove_tree(distcheck_dir)
+ self.mkpath(distcheck_dir)
+
+ archive = self.get_archive_files()[0]
+ tfile = tarfile.open(archive, "r:gz")
+ tfile.extractall(distcheck_dir)
+ tfile.close()
+
+ name = self.distribution.get_fullname()
+ extract_dir = os.path.join(distcheck_dir, name)
+
+ old_pwd = os.getcwd()
+ os.chdir(extract_dir)
+ try:
+ self.spawn([sys.executable, "setup.py", "build"])
+ self.spawn([sys.executable, "setup.py", "install",
+ "--root", "../prefix", "--record", "../log.txt"])
+ finally:
+ os.chdir(old_pwd)
+
def run(self):
- srcdir = os.getcwd()
- builddir = os.path.join(srcdir, self.build_temp)
- makedirs(builddir)
- configure = os.path.join(srcdir, 'configure')
-
- if not os.path.exists(configure):
- configure = os.path.join(srcdir, 'autogen.sh')
-
- subprocess.check_call([
- configure,
- 'PYTHON=%s' % sys.executable,
- # Put the documentation, etc. out of the way: we only want
- # the Python code and extensions
- '--prefix=' + os.path.join(builddir, 'prefix'),
- ],
- cwd=builddir)
- make_args = [
- 'pythondir=%s' % os.path.join(srcdir, self.build_lib),
- 'pyexecdir=%s' % os.path.join(srcdir, self.build_lib),
- ]
- subprocess.check_call(['make', '-C', builddir] + make_args)
- subprocess.check_call(['make', '-C', builddir, 'install'] + make_args)
-
-
-class BuildExt(orig_build_ext):
- def run(self):
- pass
+ du_sdist.run(self)
+ self._check_manifest()
+ self._check_dist()
+
+
+du_build_ext = get_command_class("build_ext")
+
+
+class build_ext(du_build_ext):
+
+ def initialize_options(self):
+ du_build_ext.initialize_options(self)
+ self.compiler_type = None
+
+ def finalize_options(self):
+ du_build_ext.finalize_options(self)
+ self.compiler_type = new_compiler(compiler=self.compiler).compiler_type
+
+ def _write_config_h(self):
+ script_dir = os.path.dirname(os.path.realpath(__file__))
+ target = os.path.join(script_dir, "config.h")
+ versions = parse_versions(script_dir)
+ with io.open(target, 'w', encoding="utf-8") as h:
+ h.write("""
+/* Configuration header created by setup.py - do not edit */
+#ifndef _CONFIG_H
+#define _CONFIG_H 1
+
+#define PYGOBJECT_MAJOR_VERSION %(PYGOBJECT_MAJOR_VERSION)s
+#define PYGOBJECT_MINOR_VERSION %(PYGOBJECT_MINOR_VERSION)s
+#define PYGOBJECT_MICRO_VERSION %(PYGOBJECT_MICRO_VERSION)s
+#define VERSION "%(VERSION)s"
+
+#endif /* _CONFIG_H */
+""" % versions)
+ def _setup_extensions(self):
+ ext = {e.name: e for e in self.extensions}
+ script_dir = os.path.dirname(os.path.realpath(__file__))
+
+ msvc_libraries = {
+ "glib-2.0": ["glib-2.0"],
+ "gio-2.0": ["gio-2.0", "gobject-2.0", "glib-2.0"],
+ "gobject-introspection-1.0":
+ ["girepository-1.0", "gobject-2.0", "glib-2.0"],
+ get_pycairo_pkg_config_name(): ["cairo"],
+ "cairo": ["cairo"],
+ "cairo-gobject":
+ ["cairo-gobject", "cairo", "gobject-2.0", "glib-2.0"],
+ "libffi": ["ffi"],
+ }
+
+ def add_dependency(ext, name):
+ fallback_libs = msvc_libraries[name]
+
+ if self.compiler_type == "msvc":
+ # assume that INCLUDE and LIB contains the right paths
+ ext.libraries += fallback_libs
+
+ # The PyCairo header is installed in a subdir of the
+ # Python installation that we are building for, so
+ # deduce that include path here, and use it
+ ext.include_dirs += [
+ os.path.join(sys.prefix, "include", "pycairo")]
+ else:
+ min_version = get_version_requirement(script_dir, name)
+ pkg_config_version_check(name, min_version)
+ ext.include_dirs += pkg_config_parse("--cflags-only-I", name)
+ ext.library_dirs += pkg_config_parse("--libs-only-L", name)
+ ext.libraries += pkg_config_parse("--libs-only-l", name)
+
+ gi_ext = ext["gi._gi"]
+ add_dependency(gi_ext, "glib-2.0")
+ add_dependency(gi_ext, "gio-2.0")
+ add_dependency(gi_ext, "gobject-introspection-1.0")
+ add_dependency(gi_ext, "libffi")
+
+ gi_cairo_ext = ext["gi._gi_cairo"]
+ add_dependency(gi_cairo_ext, "glib-2.0")
+ add_dependency(gi_cairo_ext, "gio-2.0")
+ add_dependency(gi_cairo_ext, "gobject-introspection-1.0")
+ add_dependency(gi_cairo_ext, "libffi")
+ add_dependency(gi_cairo_ext, "cairo")
+ add_dependency(gi_cairo_ext, "cairo-gobject")
+ add_dependency(gi_cairo_ext, get_pycairo_pkg_config_name())
-class BuildPy(orig_build_py):
def run(self):
- pass
-
-
-setup(
- name='pygobject',
- version=version,
- description='Python bindings for GObject Introspection',
- maintainer='The pygobject maintainers',
- maintainer_email='http://mail.gnome.org/mailman/listinfo/python-hackers-list',
- download_url='http://download.gnome.org/sources/pygobject/',
- url='https://wiki.gnome.org/Projects/PyGObject',
- packages=['gi', 'pygtkcompat'],
- ext_modules=[
- Extension(
- '_gi', sources=['gi/gimodule.c'])
+ self._write_config_h()
+ self._setup_extensions()
+ du_build_ext.run(self)
+
+
+def main():
+ script_dir = os.path.dirname(os.path.realpath(__file__))
+ pkginfo = parse_pkg_info(script_dir)
+ gi_dir = os.path.join(script_dir, "gi")
+
+ sources = [
+ os.path.join("gi", n) for n in os.listdir(gi_dir)
+ if os.path.splitext(n)[-1] == ".c"
+ ]
+ cairo_sources = [os.path.join("gi", "pygi-foreign-cairo.c")]
+ for s in cairo_sources:
+ sources.remove(s)
+
+ gi_ext = Extension(
+ name='gi._gi',
+ sources=sources,
+ include_dirs=[script_dir, gi_dir],
+ define_macros=[("HAVE_CONFIG_H", None)],
+ )
+
+ gi_cairo_ext = Extension(
+ name='gi._gi_cairo',
+ sources=cairo_sources,
+ include_dirs=[script_dir, gi_dir],
+ define_macros=[("HAVE_CONFIG_H", None)],
+ )
+
+ setup(
+ name=pkginfo["Name"],
+ version=pkginfo["Version"],
+ description=pkginfo["Summary"],
+ url=pkginfo["Home-page"],
+ author=pkginfo["Author"],
+ author_email=pkginfo["Author-email"],
+ maintainer=pkginfo["Maintainer"],
+ maintainer_email=pkginfo["Maintainer-email"],
+ license=pkginfo["License"],
+ download_url=pkginfo["Download-url"],
+ long_description=pkginfo["Description"],
+ platforms=pkginfo.get_all("Platform"),
+ classifiers=pkginfo.get_all("Classifier"),
+ packages=find_packages(script_dir),
+ ext_modules=[
+ gi_ext,
+ gi_cairo_ext,
],
- license='LGPL',
- classifiers=[
- 'Development Status :: 5 - Production/Stable',
- 'License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)',
- 'Programming Language :: C',
- 'Programming Language :: Python :: 2',
- 'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: Implementation :: CPython',
- ],
- cmdclass={
- 'build': Build,
- 'build_py': BuildPy,
- 'build_ext': BuildExt,
- },
-)
+ cmdclass={
+ "build_ext": build_ext,
+ "distcheck": distcheck,
+ },
+ install_requires=[
+ "pycairo>=%s" % get_version_requirement(
+ script_dir, get_pycairo_pkg_config_name()),
+ ],
+ )
+
+
+if __name__ == "__main__":
+ main()