setup.py: Fix the distcheck command on Windows
[pygobject.git] / setup.py
blobc8b9ca69d0854dce597cecd8759e65fd20b3fb1c
1 #!/usr/bin/env python
2 # Copyright 2017 Christoph Reiter <reiter.christoph@gmail.com>
4 # This library is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU Lesser General Public
6 # License as published by the Free Software Foundation; either
7 # version 2.1 of the License, or (at your option) any later version.
9 # This library is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # Lesser General Public License for more details.
14 # You should have received a copy of the GNU Lesser General Public
15 # License along with this library; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
17 # USA
19 """
20 ATTENTION DISTRO PACKAGERS: This is not a valid replacement for autotools.
21 It does not install headers, pkgconfig files and does not support running
22 tests. Its main use case atm is installation in virtualenvs and via pip.
23 """
25 import io
26 import os
27 import re
28 import sys
29 import errno
30 import subprocess
31 import tarfile
32 from email import parser
34 from setuptools import setup, find_packages
35 from distutils.core import Extension, Distribution
36 from distutils.ccompiler import new_compiler
37 from distutils import dir_util
40 def get_command_class(name):
41 # Returns the right class for either distutils or setuptools
42 return Distribution({}).get_command_class(name)
45 def get_pycairo_pkg_config_name():
46 return "py3cairo" if sys.version_info[0] == 3 else "pycairo"
49 def get_version_requirement(conf_dir, pkg_config_name):
50 """Given a pkg-config module name gets the minimum version required"""
52 if pkg_config_name in ["cairo", "cairo-gobject"]:
53 return "0"
55 mapping = {
56 "gobject-introspection-1.0": "introspection",
57 "glib-2.0": "glib",
58 "gio-2.0": "gio",
59 get_pycairo_pkg_config_name(): "pycairo",
60 "libffi": "libffi",
62 assert pkg_config_name in mapping
64 configure_ac = os.path.join(conf_dir, "configure.ac")
65 with io.open(configure_ac, "r", encoding="utf-8") as h:
66 text = h.read()
67 conf_name = mapping[pkg_config_name]
68 res = re.findall(
69 r"%s_required_version,\s*([\d\.]+)\)" % conf_name, text)
70 assert len(res) == 1
71 return res[0]
74 def parse_versions(conf_dir):
75 configure_ac = os.path.join(conf_dir, "configure.ac")
76 with io.open(configure_ac, "r", encoding="utf-8") as h:
77 version = re.findall(r"pygobject_[^\s]+_version,\s*(\d+)\)", h.read())
78 assert len(version) == 3
80 versions = {
81 "PYGOBJECT_MAJOR_VERSION": version[0],
82 "PYGOBJECT_MINOR_VERSION": version[1],
83 "PYGOBJECT_MICRO_VERSION": version[2],
84 "VERSION": ".".join(version),
86 return versions
89 def parse_pkg_info(conf_dir):
90 """Returns an email.message.Message instance containing the content
91 of the PKG-INFO file. The version info is parsed from configure.ac
92 """
94 versions = parse_versions(conf_dir)
96 pkg_info = os.path.join(conf_dir, "PKG-INFO.in")
97 with io.open(pkg_info, "r", encoding="utf-8") as h:
98 text = h.read()
99 for key, value in versions.items():
100 text = text.replace("@%s@" % key, value)
102 p = parser.Parser()
103 message = p.parse(io.StringIO(text))
104 return message
107 def _run_pkg_config(args):
108 command = ["pkg-config"] + args
110 # Add $prefix/share/pkgconfig to PKG_CONFIG_PATH so we use the right
111 # pycairo in case we are in a virtualenv
112 env = dict(os.environ)
113 paths = env.get("PKG_CONFIG_PATH", "").split(os.pathsep)
114 paths = [p for p in paths if p]
115 paths.insert(0, os.path.join(sys.prefix, "share", "pkgconfig"))
116 env["PKG_CONFIG_PATH"] = os.pathsep.join(paths)
118 try:
119 return subprocess.check_output(command, env=env)
120 except OSError as e:
121 if e.errno == errno.ENOENT:
122 raise SystemExit(
123 "%r not found.\nArguments: %r" % (command[0], command))
124 raise SystemExit(e)
125 except subprocess.CalledProcessError as e:
126 raise SystemExit(e)
129 def pkg_config_version_check(pkg, version):
130 _run_pkg_config([
131 "--print-errors",
132 "--exists",
133 '%s >= %s' % (pkg, version),
137 def pkg_config_parse(opt, pkg):
138 ret = _run_pkg_config([opt, pkg])
139 output = ret.decode()
140 opt = opt[-2:]
141 return [x.lstrip(opt) for x in output.split()]
144 du_sdist = get_command_class("sdist")
147 class distcheck(du_sdist):
148 """Creates a tarball and does some additional sanity checks such as
149 checking if the tarballs includes all files and builds.
152 def _check_manifest(self):
153 # make sure MANIFEST.in includes all tracked files
154 assert self.get_archive_files()
156 if subprocess.call(["git", "status"],
157 stdout=subprocess.PIPE,
158 stderr=subprocess.PIPE) != 0:
159 return
161 included_files = self.filelist.files
162 assert included_files
164 process = subprocess.Popen(
165 ["git", "ls-tree", "-r", "HEAD", "--name-only"],
166 stdout=subprocess.PIPE, universal_newlines=True)
167 out, err = process.communicate()
168 assert process.returncode == 0
170 tracked_files = out.splitlines()
171 for ignore in [".gitignore"]:
172 tracked_files.remove(ignore)
174 diff = set(tracked_files) - set(included_files)
175 assert not diff, (
176 "Not all tracked files included in tarball, check MANIFEST.in",
177 diff)
179 def _check_dist(self):
180 # make sure the tarball builds
181 assert self.get_archive_files()
183 distcheck_dir = os.path.abspath(
184 os.path.join(self.dist_dir, "distcheck"))
185 if os.path.exists(distcheck_dir):
186 dir_util.remove_tree(distcheck_dir)
187 self.mkpath(distcheck_dir)
189 archive = self.get_archive_files()[0]
190 tfile = tarfile.open(archive, "r:gz")
191 tfile.extractall(distcheck_dir)
192 tfile.close()
194 name = self.distribution.get_fullname()
195 extract_dir = os.path.join(distcheck_dir, name)
197 old_pwd = os.getcwd()
198 os.chdir(extract_dir)
199 try:
200 self.spawn([sys.executable, "setup.py", "build"])
201 self.spawn([sys.executable, "setup.py", "install",
202 "--root",
203 os.path.join(distcheck_dir, "prefix"),
204 "--record",
205 os.path.join(distcheck_dir, "log.txt"),
207 finally:
208 os.chdir(old_pwd)
210 def run(self):
211 du_sdist.run(self)
212 self._check_manifest()
213 self._check_dist()
216 du_build_ext = get_command_class("build_ext")
219 class build_ext(du_build_ext):
221 def initialize_options(self):
222 du_build_ext.initialize_options(self)
223 self.compiler_type = None
225 def finalize_options(self):
226 du_build_ext.finalize_options(self)
227 self.compiler_type = new_compiler(compiler=self.compiler).compiler_type
229 def _write_config_h(self):
230 script_dir = os.path.dirname(os.path.realpath(__file__))
231 target = os.path.join(script_dir, "config.h")
232 versions = parse_versions(script_dir)
233 with io.open(target, 'w', encoding="utf-8") as h:
234 h.write("""
235 /* Configuration header created by setup.py - do not edit */
236 #ifndef _CONFIG_H
237 #define _CONFIG_H 1
239 #define PYGOBJECT_MAJOR_VERSION %(PYGOBJECT_MAJOR_VERSION)s
240 #define PYGOBJECT_MINOR_VERSION %(PYGOBJECT_MINOR_VERSION)s
241 #define PYGOBJECT_MICRO_VERSION %(PYGOBJECT_MICRO_VERSION)s
242 #define VERSION "%(VERSION)s"
244 #endif /* _CONFIG_H */
245 """ % versions)
247 def _setup_extensions(self):
248 ext = {e.name: e for e in self.extensions}
249 script_dir = os.path.dirname(os.path.realpath(__file__))
251 msvc_libraries = {
252 "glib-2.0": ["glib-2.0"],
253 "gio-2.0": ["gio-2.0", "gobject-2.0", "glib-2.0"],
254 "gobject-introspection-1.0":
255 ["girepository-1.0", "gobject-2.0", "glib-2.0"],
256 get_pycairo_pkg_config_name(): ["cairo"],
257 "cairo": ["cairo"],
258 "cairo-gobject":
259 ["cairo-gobject", "cairo", "gobject-2.0", "glib-2.0"],
260 "libffi": ["ffi"],
263 def add_dependency(ext, name):
264 fallback_libs = msvc_libraries[name]
266 if self.compiler_type == "msvc":
267 # assume that INCLUDE and LIB contains the right paths
268 ext.libraries += fallback_libs
270 # The PyCairo header is installed in a subdir of the
271 # Python installation that we are building for, so
272 # deduce that include path here, and use it
273 ext.include_dirs += [
274 os.path.join(sys.prefix, "include", "pycairo")]
275 else:
276 min_version = get_version_requirement(script_dir, name)
277 pkg_config_version_check(name, min_version)
278 ext.include_dirs += pkg_config_parse("--cflags-only-I", name)
279 ext.library_dirs += pkg_config_parse("--libs-only-L", name)
280 ext.libraries += pkg_config_parse("--libs-only-l", name)
282 gi_ext = ext["gi._gi"]
283 add_dependency(gi_ext, "glib-2.0")
284 add_dependency(gi_ext, "gio-2.0")
285 add_dependency(gi_ext, "gobject-introspection-1.0")
286 add_dependency(gi_ext, "libffi")
288 gi_cairo_ext = ext["gi._gi_cairo"]
289 add_dependency(gi_cairo_ext, "glib-2.0")
290 add_dependency(gi_cairo_ext, "gio-2.0")
291 add_dependency(gi_cairo_ext, "gobject-introspection-1.0")
292 add_dependency(gi_cairo_ext, "libffi")
293 add_dependency(gi_cairo_ext, "cairo")
294 add_dependency(gi_cairo_ext, "cairo-gobject")
295 add_dependency(gi_cairo_ext, get_pycairo_pkg_config_name())
297 def run(self):
298 self._write_config_h()
299 self._setup_extensions()
300 du_build_ext.run(self)
303 def main():
304 script_dir = os.path.dirname(os.path.realpath(__file__))
305 pkginfo = parse_pkg_info(script_dir)
306 gi_dir = os.path.join(script_dir, "gi")
308 sources = [
309 os.path.join("gi", n) for n in os.listdir(gi_dir)
310 if os.path.splitext(n)[-1] == ".c"
312 cairo_sources = [os.path.join("gi", "pygi-foreign-cairo.c")]
313 for s in cairo_sources:
314 sources.remove(s)
316 gi_ext = Extension(
317 name='gi._gi',
318 sources=sources,
319 include_dirs=[script_dir, gi_dir],
320 define_macros=[("HAVE_CONFIG_H", None)],
323 gi_cairo_ext = Extension(
324 name='gi._gi_cairo',
325 sources=cairo_sources,
326 include_dirs=[script_dir, gi_dir],
327 define_macros=[("HAVE_CONFIG_H", None)],
330 setup(
331 name=pkginfo["Name"],
332 version=pkginfo["Version"],
333 description=pkginfo["Summary"],
334 url=pkginfo["Home-page"],
335 author=pkginfo["Author"],
336 author_email=pkginfo["Author-email"],
337 maintainer=pkginfo["Maintainer"],
338 maintainer_email=pkginfo["Maintainer-email"],
339 license=pkginfo["License"],
340 long_description=pkginfo["Description"],
341 platforms=pkginfo.get_all("Platform"),
342 classifiers=pkginfo.get_all("Classifier"),
343 packages=find_packages(script_dir),
344 ext_modules=[
345 gi_ext,
346 gi_cairo_ext,
348 cmdclass={
349 "build_ext": build_ext,
350 "distcheck": distcheck,
352 install_requires=[
353 "pycairo>=%s" % get_version_requirement(
354 script_dir, get_pycairo_pkg_config_name()),
359 if __name__ == "__main__":
360 main()