release 3.27.1
[pygobject.git] / setup.py
blob95669dba9b3ca27668f0c4e75b3205d30055a8c2
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 import sysconfig
33 from email import parser
35 import pkg_resources
36 from setuptools import setup, find_packages
37 from distutils.core import Extension, Distribution
38 from distutils.ccompiler import new_compiler
39 from distutils import dir_util
42 def get_command_class(name):
43 # Returns the right class for either distutils or setuptools
44 return Distribution({}).get_command_class(name)
47 def get_pycairo_pkg_config_name():
48 return "py3cairo" if sys.version_info[0] == 3 else "pycairo"
51 def get_version_requirement(conf_dir, pkg_config_name):
52 """Given a pkg-config module name gets the minimum version required"""
54 if pkg_config_name in ["cairo", "cairo-gobject"]:
55 return "0"
57 mapping = {
58 "gobject-introspection-1.0": "introspection",
59 "glib-2.0": "glib",
60 "gio-2.0": "gio",
61 get_pycairo_pkg_config_name(): "pycairo",
62 "libffi": "libffi",
64 assert pkg_config_name in mapping
66 configure_ac = os.path.join(conf_dir, "configure.ac")
67 with io.open(configure_ac, "r", encoding="utf-8") as h:
68 text = h.read()
69 conf_name = mapping[pkg_config_name]
70 res = re.findall(
71 r"%s_required_version,\s*([\d\.]+)\)" % conf_name, text)
72 assert len(res) == 1
73 return res[0]
76 def parse_versions(conf_dir):
77 configure_ac = os.path.join(conf_dir, "configure.ac")
78 with io.open(configure_ac, "r", encoding="utf-8") as h:
79 version = re.findall(r"pygobject_[^\s]+_version,\s*(\d+)\)", h.read())
80 assert len(version) == 3
82 versions = {
83 "PYGOBJECT_MAJOR_VERSION": version[0],
84 "PYGOBJECT_MINOR_VERSION": version[1],
85 "PYGOBJECT_MICRO_VERSION": version[2],
86 "VERSION": ".".join(version),
88 return versions
91 def parse_pkg_info(conf_dir):
92 """Returns an email.message.Message instance containing the content
93 of the PKG-INFO file. The version info is parsed from configure.ac
94 """
96 versions = parse_versions(conf_dir)
98 pkg_info = os.path.join(conf_dir, "PKG-INFO.in")
99 with io.open(pkg_info, "r", encoding="utf-8") as h:
100 text = h.read()
101 for key, value in versions.items():
102 text = text.replace("@%s@" % key, value)
104 p = parser.Parser()
105 message = p.parse(io.StringIO(text))
106 return message
109 def _run_pkg_config(args):
110 command = ["pkg-config"] + args
112 try:
113 return subprocess.check_output(command)
114 except OSError as e:
115 if e.errno == errno.ENOENT:
116 raise SystemExit(
117 "%r not found.\nArguments: %r" % (command[0], command))
118 raise SystemExit(e)
119 except subprocess.CalledProcessError as e:
120 raise SystemExit(e)
123 def pkg_config_version_check(pkg, version):
124 _run_pkg_config([
125 "--print-errors",
126 "--exists",
127 '%s >= %s' % (pkg, version),
131 def pkg_config_parse(opt, pkg):
132 ret = _run_pkg_config([opt, pkg])
133 output = ret.decode()
134 opt = opt[-2:]
135 return [x.lstrip(opt) for x in output.split()]
138 def samefile(src, dst):
139 # Python 2 on Windows doesn't have os.path.samefile, so we have to provide
140 # a fallback
142 if hasattr(os.path, "samefile"):
143 return os.path.samefile(src, dst)
145 os.stat(src)
146 os.stat(dst)
147 return (os.path.normcase(os.path.abspath(src)) ==
148 os.path.normcase(os.path.abspath(dst)))
151 du_sdist = get_command_class("sdist")
154 class distcheck(du_sdist):
155 """Creates a tarball and does some additional sanity checks such as
156 checking if the tarballs includes all files and builds.
159 def _check_manifest(self):
160 # make sure MANIFEST.in includes all tracked files
161 assert self.get_archive_files()
163 if subprocess.call(["git", "status"],
164 stdout=subprocess.PIPE,
165 stderr=subprocess.PIPE) != 0:
166 return
168 included_files = self.filelist.files
169 assert included_files
171 process = subprocess.Popen(
172 ["git", "ls-tree", "-r", "HEAD", "--name-only"],
173 stdout=subprocess.PIPE, universal_newlines=True)
174 out, err = process.communicate()
175 assert process.returncode == 0
177 tracked_files = out.splitlines()
178 for ignore in [".gitignore"]:
179 tracked_files.remove(ignore)
181 diff = set(tracked_files) - set(included_files)
182 assert not diff, (
183 "Not all tracked files included in tarball, check MANIFEST.in",
184 diff)
186 def _check_dist(self):
187 # make sure the tarball builds
188 assert self.get_archive_files()
190 distcheck_dir = os.path.abspath(
191 os.path.join(self.dist_dir, "distcheck"))
192 if os.path.exists(distcheck_dir):
193 dir_util.remove_tree(distcheck_dir)
194 self.mkpath(distcheck_dir)
196 archive = self.get_archive_files()[0]
197 tfile = tarfile.open(archive, "r:gz")
198 tfile.extractall(distcheck_dir)
199 tfile.close()
201 name = self.distribution.get_fullname()
202 extract_dir = os.path.join(distcheck_dir, name)
204 old_pwd = os.getcwd()
205 os.chdir(extract_dir)
206 try:
207 self.spawn([sys.executable, "setup.py", "build"])
208 self.spawn([sys.executable, "setup.py", "install",
209 "--root",
210 os.path.join(distcheck_dir, "prefix"),
211 "--record",
212 os.path.join(distcheck_dir, "log.txt"),
214 finally:
215 os.chdir(old_pwd)
217 def run(self):
218 du_sdist.run(self)
219 self._check_manifest()
220 self._check_dist()
223 du_build_ext = get_command_class("build_ext")
226 class build_ext(du_build_ext):
228 def initialize_options(self):
229 du_build_ext.initialize_options(self)
230 self.compiler_type = None
232 def finalize_options(self):
233 du_build_ext.finalize_options(self)
234 self.compiler_type = new_compiler(compiler=self.compiler).compiler_type
236 def _write_config_h(self):
237 script_dir = os.path.dirname(os.path.realpath(__file__))
238 target = os.path.join(script_dir, "config.h")
239 versions = parse_versions(script_dir)
240 with io.open(target, 'w', encoding="utf-8") as h:
241 h.write("""
242 /* Configuration header created by setup.py - do not edit */
243 #ifndef _CONFIG_H
244 #define _CONFIG_H 1
246 #define PYGOBJECT_MAJOR_VERSION %(PYGOBJECT_MAJOR_VERSION)s
247 #define PYGOBJECT_MINOR_VERSION %(PYGOBJECT_MINOR_VERSION)s
248 #define PYGOBJECT_MICRO_VERSION %(PYGOBJECT_MICRO_VERSION)s
249 #define VERSION "%(VERSION)s"
251 #endif /* _CONFIG_H */
252 """ % versions)
254 def _setup_extensions(self):
255 ext = {e.name: e for e in self.extensions}
256 script_dir = os.path.dirname(os.path.realpath(__file__))
258 msvc_libraries = {
259 "glib-2.0": ["glib-2.0"],
260 "gio-2.0": ["gio-2.0", "gobject-2.0", "glib-2.0"],
261 "gobject-introspection-1.0":
262 ["girepository-1.0", "gobject-2.0", "glib-2.0"],
263 "cairo": ["cairo"],
264 "cairo-gobject":
265 ["cairo-gobject", "cairo", "gobject-2.0", "glib-2.0"],
266 "libffi": ["ffi"],
269 def add_dependency(ext, name):
270 fallback_libs = msvc_libraries[name]
272 if self.compiler_type == "msvc":
273 # assume that INCLUDE and LIB contains the right paths
274 ext.libraries += fallback_libs
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 def add_pycairo(ext):
283 min_version = get_version_requirement(
284 script_dir, get_pycairo_pkg_config_name())
286 dist = pkg_resources.get_distribution("pycairo>=%s" % min_version)
288 def get_sys_path(dist, name):
289 """The sysconfig path for a distribution, or None"""
291 location = dist.location
292 for scheme in sysconfig.get_scheme_names():
293 for path_type in ["platlib", "purelib"]:
294 path = sysconfig.get_path(path_type, scheme)
295 try:
296 if samefile(path, location):
297 return sysconfig.get_path(name, scheme)
298 except EnvironmentError:
299 pass
301 data_path = get_sys_path(dist, "data") or sys.prefix
302 include_dir = os.path.join(data_path, "include", "pycairo")
303 ext.include_dirs += [include_dir]
305 gi_ext = ext["gi._gi"]
306 add_dependency(gi_ext, "glib-2.0")
307 add_dependency(gi_ext, "gio-2.0")
308 add_dependency(gi_ext, "gobject-introspection-1.0")
309 add_dependency(gi_ext, "libffi")
311 gi_cairo_ext = ext["gi._gi_cairo"]
312 add_dependency(gi_cairo_ext, "glib-2.0")
313 add_dependency(gi_cairo_ext, "gio-2.0")
314 add_dependency(gi_cairo_ext, "gobject-introspection-1.0")
315 add_dependency(gi_cairo_ext, "libffi")
316 add_dependency(gi_cairo_ext, "cairo")
317 add_dependency(gi_cairo_ext, "cairo-gobject")
318 add_pycairo(gi_cairo_ext)
320 def run(self):
321 self._write_config_h()
322 self._setup_extensions()
323 du_build_ext.run(self)
326 def main():
327 script_dir = os.path.dirname(os.path.realpath(__file__))
328 pkginfo = parse_pkg_info(script_dir)
329 gi_dir = os.path.join(script_dir, "gi")
331 sources = [
332 os.path.join("gi", n) for n in os.listdir(gi_dir)
333 if os.path.splitext(n)[-1] == ".c"
335 cairo_sources = [os.path.join("gi", "pygi-foreign-cairo.c")]
336 for s in cairo_sources:
337 sources.remove(s)
339 readme = os.path.join(script_dir, "README.rst")
340 with io.open(readme, encoding="utf-8") as h:
341 long_description = h.read()
343 gi_ext = Extension(
344 name='gi._gi',
345 sources=sources,
346 include_dirs=[script_dir, gi_dir],
347 define_macros=[("HAVE_CONFIG_H", None)],
350 gi_cairo_ext = Extension(
351 name='gi._gi_cairo',
352 sources=cairo_sources,
353 include_dirs=[script_dir, gi_dir],
354 define_macros=[("HAVE_CONFIG_H", None)],
357 setup(
358 name=pkginfo["Name"],
359 version=pkginfo["Version"],
360 description=pkginfo["Summary"],
361 url=pkginfo["Home-page"],
362 author=pkginfo["Author"],
363 author_email=pkginfo["Author-email"],
364 maintainer=pkginfo["Maintainer"],
365 maintainer_email=pkginfo["Maintainer-email"],
366 license=pkginfo["License"],
367 long_description=long_description,
368 platforms=pkginfo.get_all("Platform"),
369 classifiers=pkginfo.get_all("Classifier"),
370 packages=find_packages(script_dir),
371 ext_modules=[
372 gi_ext,
373 gi_cairo_ext,
375 cmdclass={
376 "build_ext": build_ext,
377 "distcheck": distcheck,
379 install_requires=[
380 "pycairo>=%s" % get_version_requirement(
381 script_dir, get_pycairo_pkg_config_name()),
386 if __name__ == "__main__":
387 main()