Enable -Wnull-dereference
[pygobject.git] / setup.py
blob6e012acccb146f858ec6673fce3009b41284aece
1 #!/usr/bin/env python3
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 import io
20 import os
21 import sys
22 import errno
23 import subprocess
24 import tarfile
25 import sysconfig
26 import tempfile
27 from email import parser
29 try:
30 from setuptools import setup
31 except ImportError:
32 from distutils.core import setup
34 from distutils.core import Extension, Distribution, Command
35 from distutils.errors import DistutilsSetupError, DistutilsOptionError
36 from distutils.ccompiler import new_compiler
37 from distutils.sysconfig import get_python_lib, customize_compiler
38 from distutils import dir_util, log
39 from distutils.spawn import find_executable
42 PYGOBJECT_VERISON = "3.29.0"
43 GLIB_VERSION_REQUIRED = "2.38.0"
44 GI_VERSION_REQUIRED = "1.46.0"
45 PYCAIRO_VERSION_REQUIRED = "1.11.1"
46 LIBFFI_VERSION_REQUIRED = "3.0"
49 def is_dev_version():
50 version = tuple(map(int, PYGOBJECT_VERISON.split(".")))
51 return version[1] % 2 != 0
54 def get_command_class(name):
55 # Returns the right class for either distutils or setuptools
56 return Distribution({}).get_command_class(name)
59 def get_pycairo_pkg_config_name():
60 return "py3cairo" if sys.version_info[0] == 3 else "pycairo"
63 def get_version_requirement(pkg_config_name):
64 """Given a pkg-config module name gets the minimum version required"""
66 versions = {
67 "gobject-introspection-1.0": GI_VERSION_REQUIRED,
68 "glib-2.0": GLIB_VERSION_REQUIRED,
69 "gio-2.0": GLIB_VERSION_REQUIRED,
70 get_pycairo_pkg_config_name(): PYCAIRO_VERSION_REQUIRED,
71 "libffi": LIBFFI_VERSION_REQUIRED,
72 "cairo": "0",
73 "cairo-gobject": "0",
76 return versions[pkg_config_name]
79 def get_versions():
80 version = PYGOBJECT_VERISON.split(".")
81 assert len(version) == 3
83 versions = {
84 "PYGOBJECT_MAJOR_VERSION": version[0],
85 "PYGOBJECT_MINOR_VERSION": version[1],
86 "PYGOBJECT_MICRO_VERSION": version[2],
87 "VERSION": ".".join(version),
89 return versions
92 def parse_pkg_info(conf_dir):
93 """Returns an email.message.Message instance containing the content
94 of the PKG-INFO file.
95 """
97 versions = get_versions()
99 pkg_info = os.path.join(conf_dir, "PKG-INFO.in")
100 with io.open(pkg_info, "r", encoding="utf-8") as h:
101 text = h.read()
102 for key, value in versions.items():
103 text = text.replace("@%s@" % key, value)
105 p = parser.Parser()
106 message = p.parse(io.StringIO(text))
107 return message
110 def pkg_config_get_install_hint(pkg_name):
111 """Returns an installation hint for a pkg-config name or None"""
113 if not sys.platform.startswith("linux"):
114 return
116 if find_executable("apt"):
117 dev_packages = {
118 "gobject-introspection-1.0": "libgirepository1.0-dev",
119 "glib-2.0": "libglib2.0-dev",
120 "gio-2.0": "libglib2.0-dev",
121 "cairo": "libcairo2-dev",
122 "cairo-gobject": "libcairo2-dev",
123 "libffi": "libffi-dev",
125 if pkg_name in dev_packages:
126 return "sudo apt install %s" % dev_packages[pkg_name]
127 elif find_executable("dnf"):
128 dev_packages = {
129 "gobject-introspection-1.0": "gobject-introspection-devel",
130 "glib-2.0": "glib2-devel",
131 "gio-2.0": "glib2-devel",
132 "cairo": "cairo-devel",
133 "cairo-gobject": "cairo-gobject-devel",
134 "libffi": "libffi-devel",
136 if pkg_name in dev_packages:
137 return "sudo dnf install %s" % dev_packages[pkg_name]
140 class PkgConfigError(Exception):
141 pass
144 class PkgConfigMissingPackageError(PkgConfigError):
145 pass
148 def _run_pkg_config(pkg_name, args, _cache={}):
149 """Raises PkgConfigError"""
151 command = tuple(["pkg-config"] + args)
153 if command not in _cache:
154 try:
155 result = subprocess.check_output(command)
156 except OSError as e:
157 if e.errno == errno.ENOENT:
158 raise PkgConfigError(
159 "%r not found.\nArguments: %r" % (command[0], command))
160 raise PkgConfigError(e)
161 except subprocess.CalledProcessError as e:
162 try:
163 subprocess.check_output(["pkg-config", "--exists", pkg_name])
164 except (subprocess.CalledProcessError, OSError):
165 raise PkgConfigMissingPackageError(e)
166 else:
167 raise PkgConfigError(e)
168 else:
169 _cache[command] = result
171 return _cache[command]
174 def _run_pkg_config_or_exit(pkg_name, args):
175 try:
176 return _run_pkg_config(pkg_name, args)
177 except PkgConfigMissingPackageError as e:
178 hint = pkg_config_get_install_hint(pkg_name)
179 if hint:
180 raise SystemExit(
181 "%s\n\nTry installing it with: %r" % (e, hint))
182 else:
183 raise SystemExit(e)
184 except PkgConfigError as e:
185 raise SystemExit(e)
188 def pkg_config_version_check(pkg_name, version):
189 _run_pkg_config_or_exit(pkg_name, [
190 "--print-errors",
191 "--exists",
192 '%s >= %s' % (pkg_name, version),
196 def pkg_config_parse(opt, pkg_name):
197 ret = _run_pkg_config_or_exit(pkg_name, [opt, pkg_name])
199 if sys.version_info[0] == 3:
200 output = ret.decode()
201 else:
202 output = ret
203 opt = opt[-2:]
204 return [x.lstrip(opt) for x in output.split()]
207 def list_headers(d):
208 return [os.path.join(d, e) for e in os.listdir(d) if e.endswith(".h")]
211 def filter_compiler_arguments(compiler, args):
212 """Given a compiler instance and a list of compiler warning flags
213 returns the list of supported flags.
216 if compiler.compiler_type == "msvc":
217 # TODO
218 return []
220 extra = []
222 def check_arguments(compiler, args):
223 p = subprocess.Popen(
224 [compiler.compiler[0]] + args + extra + ["-x", "c", "-E", "-"],
225 stdin=subprocess.PIPE,
226 stdout=subprocess.PIPE,
227 stderr=subprocess.PIPE)
228 stdout, stderr = p.communicate(b"int i;\n")
229 if p.returncode != 0:
230 text = stderr.decode("ascii", "replace")
231 return False, [a for a in args if a in text]
232 else:
233 return True, []
235 def check_argument(compiler, arg):
236 return check_arguments(compiler, [arg])[0]
238 # clang doesn't error out for unknown options, force it to
239 if check_argument(compiler, '-Werror=unknown-warning-option'):
240 extra += ['-Werror=unknown-warning-option']
241 if check_argument(compiler, '-Werror=unused-command-line-argument'):
242 extra += ['-Werror=unused-command-line-argument']
244 # first try to remove all arguments contained in the error message
245 supported = list(args)
246 while 1:
247 ok, maybe_unknown = check_arguments(compiler, supported)
248 if ok:
249 return supported
250 elif not maybe_unknown:
251 break
252 for unknown in maybe_unknown:
253 if not check_argument(compiler, unknown):
254 supported.remove(unknown)
256 # hm, didn't work, try each argument one by one
257 supported = []
258 for arg in args:
259 if check_argument(compiler, arg):
260 supported.append(arg)
261 return supported
264 class sdist_gnome(Command):
265 description = "Create a source tarball for GNOME"
266 user_options = []
268 def initialize_options(self):
269 pass
271 def finalize_options(self):
272 pass
274 def run(self):
275 # Don't use PEP 440 pre-release versions for GNOME releases
276 self.distribution.metadata.version = PYGOBJECT_VERISON
278 dist_dir = tempfile.mkdtemp()
279 try:
280 cmd = self.reinitialize_command("sdist")
281 cmd.dist_dir = dist_dir
282 cmd.ensure_finalized()
283 cmd.run()
285 base_name = self.distribution.get_fullname().lower()
286 cmd.make_release_tree(base_name, cmd.filelist.files)
287 try:
288 self.make_archive(base_name, "xztar", base_dir=base_name)
289 finally:
290 dir_util.remove_tree(base_name)
291 finally:
292 dir_util.remove_tree(dist_dir)
295 du_sdist = get_command_class("sdist")
298 class distcheck(du_sdist):
299 """Creates a tarball and does some additional sanity checks such as
300 checking if the tarball includes all files, builds successfully and
301 the tests suite passes.
304 def _check_manifest(self):
305 # make sure MANIFEST.in includes all tracked files
306 assert self.get_archive_files()
308 if subprocess.call(["git", "status"],
309 stdout=subprocess.PIPE,
310 stderr=subprocess.PIPE) != 0:
311 return
313 included_files = self.filelist.files
314 assert included_files
316 process = subprocess.Popen(
317 ["git", "ls-tree", "-r", "HEAD", "--name-only"],
318 stdout=subprocess.PIPE, universal_newlines=True)
319 out, err = process.communicate()
320 assert process.returncode == 0
322 tracked_files = out.splitlines()
323 for ignore in [".gitignore"]:
324 if ignore in tracked_files:
325 tracked_files.remove(ignore)
327 diff = set(tracked_files) - set(included_files)
328 assert not diff, (
329 "Not all tracked files included in tarball, check MANIFEST.in",
330 diff)
332 def _check_dist(self):
333 # make sure the tarball builds
334 assert self.get_archive_files()
336 distcheck_dir = os.path.abspath(
337 os.path.join(self.dist_dir, "distcheck"))
338 if os.path.exists(distcheck_dir):
339 dir_util.remove_tree(distcheck_dir)
340 self.mkpath(distcheck_dir)
342 archive = self.get_archive_files()[0]
343 tfile = tarfile.open(archive, "r:gz")
344 tfile.extractall(distcheck_dir)
345 tfile.close()
347 name = self.distribution.get_fullname()
348 extract_dir = os.path.join(distcheck_dir, name)
350 old_pwd = os.getcwd()
351 os.chdir(extract_dir)
352 try:
353 self.spawn([sys.executable, "setup.py", "build"])
354 self.spawn([sys.executable, "setup.py", "install",
355 "--root",
356 os.path.join(distcheck_dir, "prefix"),
357 "--record",
358 os.path.join(distcheck_dir, "log.txt"),
360 self.spawn([sys.executable, "setup.py", "test"])
361 finally:
362 os.chdir(old_pwd)
364 def run(self):
365 du_sdist.run(self)
366 self._check_manifest()
367 self._check_dist()
370 class build_tests(Command):
371 description = "build test libraries and extensions"
372 user_options = [
373 ("force", "f", "force a rebuild"),
376 def initialize_options(self):
377 self.build_temp = None
378 self.build_base = None
379 self.force = False
381 def finalize_options(self):
382 self.set_undefined_options(
383 'build_ext',
384 ('build_temp', 'build_temp'))
385 self.set_undefined_options(
386 'build',
387 ('build_base', 'build_base'))
389 def _newer_group(self, sources, *targets):
390 assert targets
392 from distutils.dep_util import newer_group
394 if self.force:
395 return True
396 else:
397 for target in targets:
398 if not newer_group(sources, target):
399 return False
400 return True
402 def run(self):
403 cmd = self.reinitialize_command("build_ext")
404 cmd.inplace = True
405 cmd.force = self.force
406 cmd.ensure_finalized()
407 cmd.run()
409 gidatadir = pkg_config_parse(
410 "--variable=gidatadir", "gobject-introspection-1.0")[0]
411 g_ir_scanner = pkg_config_parse(
412 "--variable=g_ir_scanner", "gobject-introspection-1.0")[0]
413 g_ir_compiler = pkg_config_parse(
414 "--variable=g_ir_compiler", "gobject-introspection-1.0")[0]
416 script_dir = get_script_dir()
417 gi_dir = os.path.join(script_dir, "gi")
418 tests_dir = os.path.join(script_dir, "tests")
419 gi_tests_dir = os.path.join(gidatadir, "tests")
421 schema_xml = os.path.join(tests_dir, "org.gnome.test.gschema.xml")
422 schema_bin = os.path.join(tests_dir, "gschemas.compiled")
423 if self._newer_group([schema_xml], schema_bin):
424 subprocess.check_call([
425 "glib-compile-schemas",
426 "--targetdir=%s" % tests_dir,
427 "--schema-file=%s" % schema_xml,
430 compiler = new_compiler()
431 customize_compiler(compiler)
433 if os.name == "nt":
434 compiler.shared_lib_extension = ".dll"
435 elif sys.platform == "darwin":
436 compiler.shared_lib_extension = ".dylib"
437 if "-bundle" in compiler.linker_so:
438 compiler.linker_so = list(compiler.linker_so)
439 i = compiler.linker_so.index("-bundle")
440 compiler.linker_so[i] = "-dynamiclib"
441 else:
442 compiler.shared_lib_extension = ".so"
444 def build_ext(ext):
445 if compiler.compiler_type == "msvc":
446 raise Exception("MSVC support not implemented")
448 libname = compiler.shared_object_filename(ext.name)
449 ext_paths = [os.path.join(tests_dir, libname)]
450 if os.name == "nt":
451 implibname = libname + ".a"
452 ext_paths.append(os.path.join(tests_dir, implibname))
454 if self._newer_group(ext.sources + ext.depends, *ext_paths):
455 objects = compiler.compile(
456 ext.sources,
457 output_dir=self.build_temp,
458 include_dirs=ext.include_dirs)
460 if os.name == "nt":
461 postargs = ["-Wl,--out-implib=%s" %
462 os.path.join(tests_dir, implibname)]
463 else:
464 postargs = []
466 compiler.link_shared_object(
467 objects,
468 compiler.shared_object_filename(ext.name),
469 output_dir=tests_dir,
470 libraries=ext.libraries,
471 library_dirs=ext.library_dirs,
472 extra_postargs=postargs)
474 return ext_paths
476 ext = Extension(
477 name='libgimarshallingtests',
478 sources=[
479 os.path.join(gi_tests_dir, "gimarshallingtests.c"),
480 os.path.join(tests_dir, "gimarshallingtestsextra.c"),
482 include_dirs=[
483 gi_tests_dir,
484 tests_dir,
486 depends=[
487 os.path.join(gi_tests_dir, "gimarshallingtests.h"),
488 os.path.join(tests_dir, "gimarshallingtestsextra.h"),
491 add_ext_pkg_config_dep(ext, compiler.compiler_type, "glib-2.0")
492 add_ext_pkg_config_dep(ext, compiler.compiler_type, "gio-2.0")
493 ext_paths = build_ext(ext)
495 gir_path = os.path.join(tests_dir, "GIMarshallingTests-1.0.gir")
496 typelib_path = os.path.join(
497 tests_dir, "GIMarshallingTests-1.0.typelib")
499 if self._newer_group(ext_paths, gir_path):
500 subprocess.check_call([
501 g_ir_scanner,
502 "--no-libtool",
503 "--include=Gio-2.0",
504 "--namespace=GIMarshallingTests",
505 "--nsversion=1.0",
506 "--symbol-prefix=gi_marshalling_tests",
507 "--warn-all",
508 "--warn-error",
509 "--library-path=%s" % tests_dir,
510 "--library=gimarshallingtests",
511 "--pkg=glib-2.0",
512 "--pkg=gio-2.0",
513 "--output=%s" % gir_path,
514 ] + ext.sources + ext.depends)
516 if self._newer_group([gir_path], typelib_path):
517 subprocess.check_call([
518 g_ir_compiler,
519 gir_path,
520 "--output=%s" % typelib_path,
523 ext = Extension(
524 name='libregress',
525 sources=[
526 os.path.join(gi_tests_dir, "regress.c"),
527 os.path.join(tests_dir, "regressextra.c"),
529 include_dirs=[
530 gi_tests_dir,
532 depends=[
533 os.path.join(gi_tests_dir, "regress.h"),
534 os.path.join(tests_dir, "regressextra.h"),
537 add_ext_pkg_config_dep(ext, compiler.compiler_type, "glib-2.0")
538 add_ext_pkg_config_dep(ext, compiler.compiler_type, "gio-2.0")
539 add_ext_pkg_config_dep(ext, compiler.compiler_type, "cairo")
540 add_ext_pkg_config_dep(ext, compiler.compiler_type, "cairo-gobject")
541 ext_paths = build_ext(ext)
543 gir_path = os.path.join(tests_dir, "Regress-1.0.gir")
544 typelib_path = os.path.join(tests_dir, "Regress-1.0.typelib")
546 if self._newer_group(ext_paths, gir_path):
547 subprocess.check_call([
548 g_ir_scanner,
549 "--no-libtool",
550 "--include=cairo-1.0",
551 "--include=Gio-2.0",
552 "--namespace=Regress",
553 "--nsversion=1.0",
554 "--warn-all",
555 "--warn-error",
556 "--library-path=%s" % tests_dir,
557 "--library=regress",
558 "--pkg=glib-2.0",
559 "--pkg=gio-2.0",
560 "--pkg=cairo",
561 "--pkg=cairo-gobject",
562 "--output=%s" % gir_path,
563 ] + ext.sources + ext.depends)
565 if self._newer_group([gir_path], typelib_path):
566 subprocess.check_call([
567 g_ir_compiler,
568 gir_path,
569 "--output=%s" % typelib_path,
572 ext = Extension(
573 name='tests.testhelper',
574 sources=[
575 os.path.join(tests_dir, "testhelpermodule.c"),
576 os.path.join(tests_dir, "test-floating.c"),
577 os.path.join(tests_dir, "test-thread.c"),
578 os.path.join(tests_dir, "test-unknown.c"),
580 include_dirs=[
581 gi_dir,
582 tests_dir,
584 depends=list_headers(gi_dir) + list_headers(tests_dir),
585 define_macros=[("PY_SSIZE_T_CLEAN", None)],
587 add_ext_pkg_config_dep(ext, compiler.compiler_type, "glib-2.0")
588 add_ext_pkg_config_dep(ext, compiler.compiler_type, "gio-2.0")
589 add_ext_pkg_config_dep(ext, compiler.compiler_type, "cairo")
590 add_ext_compiler_flags(ext, compiler)
592 dist = Distribution({"ext_modules": [ext]})
594 build_cmd = dist.get_command_obj("build")
595 build_cmd.build_base = os.path.join(self.build_base, "pygobject_tests")
596 build_cmd.ensure_finalized()
598 cmd = dist.get_command_obj("build_ext")
599 cmd.inplace = True
600 cmd.force = self.force
601 cmd.ensure_finalized()
602 cmd.run()
605 def get_suppression_files_for_prefix(prefix):
606 """Returns a list of valgrind suppression files for a given prefix"""
608 # Most specific first (/usr/share/doc is Fedora, /usr/lib is Debian)
609 # Take the first one found
610 major = str(sys.version_info[0])
611 minor = str(sys.version_info[1])
612 pyfiles = []
613 pyfiles.append(
614 os.path.join(
615 prefix, "share", "doc", "python%s%s" % (major, minor),
616 "valgrind-python.supp"))
617 pyfiles.append(
618 os.path.join(prefix, "lib", "valgrind", "python%s.supp" % major))
619 pyfiles.append(
620 os.path.join(
621 prefix, "share", "doc", "python%s-devel" % major,
622 "valgrind-python.supp"))
623 pyfiles.append(os.path.join(prefix, "lib", "valgrind", "python.supp"))
625 files = []
626 for f in pyfiles:
627 if os.path.isfile(f):
628 files.append(f)
629 break
631 files.append(os.path.join(
632 prefix, "share", "glib-2.0", "valgrind", "glib.supp"))
633 return [f for f in files if os.path.isfile(f)]
636 def get_real_prefix():
637 """Returns the base Python prefix, even in a virtualenv/venv"""
639 return getattr(sys, "base_prefix", getattr(sys, "real_prefix", sys.prefix))
642 def get_suppression_files():
643 """Returns a list of valgrind suppression files"""
645 prefixes = [
646 sys.prefix,
647 get_real_prefix(),
648 pkg_config_parse("--variable=prefix", "glib-2.0")[0],
651 files = []
652 for prefix in prefixes:
653 files.extend(get_suppression_files_for_prefix(prefix))
654 return sorted(set(files))
657 class test(Command):
658 user_options = [
659 ("valgrind", None, "run tests under valgrind"),
660 ("valgrind-log-file=", None, "save logs instead of printing them"),
661 ("gdb", None, "run tests under gdb"),
662 ("no-capture", "s", "don't capture test output"),
665 def initialize_options(self):
666 self.valgrind = None
667 self.valgrind_log_file = None
668 self.gdb = None
669 self.no_capture = None
671 def finalize_options(self):
672 self.valgrind = bool(self.valgrind)
673 if self.valgrind_log_file and not self.valgrind:
674 raise DistutilsOptionError("valgrind not enabled")
675 self.gdb = bool(self.gdb)
676 self.no_capture = bool(self.no_capture)
678 def run(self):
679 cmd = self.reinitialize_command("build_tests")
680 cmd.ensure_finalized()
681 cmd.run()
683 env = os.environ.copy()
684 env.pop("MSYSTEM", None)
686 if self.no_capture:
687 env["PYGI_TEST_VERBOSE"] = "1"
689 env["MALLOC_PERTURB_"] = "85"
690 env["MALLOC_CHECK_"] = "3"
691 env["G_SLICE"] = "debug-blocks"
693 pre_args = []
695 if self.valgrind:
696 env["G_SLICE"] = "always-malloc"
697 env["G_DEBUG"] = "gc-friendly"
698 env["PYTHONMALLOC"] = "malloc"
700 pre_args += [
701 "valgrind", "--leak-check=full", "--show-possibly-lost=no",
702 "--num-callers=20", "--child-silent-after-fork=yes",
703 ] + ["--suppressions=" + f for f in get_suppression_files()]
705 if self.valgrind_log_file:
706 pre_args += ["--log-file=" + self.valgrind_log_file]
708 if self.gdb:
709 env["PYGI_TEST_GDB"] = "1"
710 pre_args += ["gdb", "--args"]
712 if pre_args:
713 log.info(" ".join(pre_args))
715 tests_dir = os.path.join(get_script_dir(), "tests")
716 sys.exit(subprocess.call(pre_args + [
717 sys.executable,
718 os.path.join(tests_dir, "runtests.py"),
719 ], env=env))
722 class quality(Command):
723 description = "run code quality tests"
724 user_options = []
726 def initialize_options(self):
727 pass
729 def finalize_options(self):
730 pass
732 def run(self):
733 status = subprocess.call([
734 sys.executable, "-m", "flake8",
735 ], cwd=get_script_dir())
736 if status != 0:
737 raise SystemExit(status)
740 def get_script_dir():
741 return os.path.dirname(os.path.realpath(__file__))
744 def get_pycairo_include_dir():
745 """Returns the best guess at where to find the pycairo headers.
746 A bit convoluted because we have to deal with multiple pycairo
747 versions.
749 Raises if pycairo isn't found or it's too old.
752 pkg_config_name = get_pycairo_pkg_config_name()
753 min_version = get_version_requirement(pkg_config_name)
754 min_version_info = tuple(int(p) for p in min_version.split("."))
756 def check_path(include_dir):
757 log.info("pycairo: trying include directory: %r" % include_dir)
758 header_path = os.path.join(include_dir, "%s.h" % pkg_config_name)
759 if os.path.exists(header_path):
760 log.info("pycairo: found %r" % header_path)
761 return True
762 log.info("pycairo: header file (%r) not found" % header_path)
763 return False
765 def find_path(paths):
766 for p in reversed(paths):
767 if check_path(p):
768 return p
770 def find_new_api():
771 log.info("pycairo: new API")
772 import cairo
774 if cairo.version_info < min_version_info:
775 raise DistutilsSetupError(
776 "pycairo >= %s required, %s found." % (
777 min_version, ".".join(map(str, cairo.version_info))))
779 if hasattr(cairo, "get_include"):
780 return [cairo.get_include()]
781 log.info("pycairo: no get_include()")
782 return []
784 def find_old_api():
785 log.info("pycairo: old API")
787 import cairo
789 if cairo.version_info < min_version_info:
790 raise DistutilsSetupError(
791 "pycairo >= %s required, %s found." % (
792 min_version, ".".join(map(str, cairo.version_info))))
794 location = os.path.dirname(os.path.abspath(cairo.__path__[0]))
795 log.info("pycairo: found %r" % location)
797 def samefile(src, dst):
798 # Python 2 on Windows doesn't have os.path.samefile, so we have to
799 # provide a fallback
800 if hasattr(os.path, "samefile"):
801 return os.path.samefile(src, dst)
802 os.stat(src)
803 os.stat(dst)
804 return (os.path.normcase(os.path.abspath(src)) ==
805 os.path.normcase(os.path.abspath(dst)))
807 def get_sys_path(location, name):
808 # Returns the sysconfig path for a distribution, or None
809 for scheme in sysconfig.get_scheme_names():
810 for path_type in ["platlib", "purelib"]:
811 path = sysconfig.get_path(path_type, scheme)
812 try:
813 if samefile(path, location):
814 return sysconfig.get_path(name, scheme)
815 except EnvironmentError:
816 pass
818 data_path = get_sys_path(location, "data") or sys.prefix
819 return [os.path.join(data_path, "include", "pycairo")]
821 def find_pkg_config():
822 log.info("pycairo: pkg-config")
823 pkg_config_version_check(pkg_config_name, min_version)
824 return pkg_config_parse("--cflags-only-I", pkg_config_name)
826 # First the new get_include() API added in >1.15.6
827 include_dir = find_path(find_new_api())
828 if include_dir is not None:
829 return include_dir
831 # Then try to find it in the data prefix based on the module path.
832 # This works with many virtualenv/userdir setups, but not all apparently,
833 # see https://gitlab.gnome.org/GNOME/pygobject/issues/150
834 include_dir = find_path(find_old_api())
835 if include_dir is not None:
836 return include_dir
838 # Finally, fall back to pkg-config
839 include_dir = find_path(find_pkg_config())
840 if include_dir is not None:
841 return include_dir
843 raise DistutilsSetupError("Could not find pycairo headers")
846 def add_ext_pkg_config_dep(ext, compiler_type, name):
847 msvc_libraries = {
848 "glib-2.0": ["glib-2.0"],
849 "gio-2.0": ["gio-2.0", "gobject-2.0", "glib-2.0"],
850 "gobject-introspection-1.0":
851 ["girepository-1.0", "gobject-2.0", "glib-2.0"],
852 "cairo": ["cairo"],
853 "cairo-gobject":
854 ["cairo-gobject", "cairo", "gobject-2.0", "glib-2.0"],
855 "libffi": ["ffi"],
858 fallback_libs = msvc_libraries[name]
859 if compiler_type == "msvc":
860 # assume that INCLUDE and LIB contains the right paths
861 ext.libraries += fallback_libs
862 else:
863 min_version = get_version_requirement(name)
864 pkg_config_version_check(name, min_version)
865 ext.include_dirs += pkg_config_parse("--cflags-only-I", name)
866 ext.library_dirs += pkg_config_parse("--libs-only-L", name)
867 ext.libraries += pkg_config_parse("--libs-only-l", name)
870 def add_ext_compiler_flags(ext, compiler, _cache={}):
871 cache_key = compiler.compiler[0]
872 if cache_key not in _cache:
874 args = [
875 "-Wall",
876 "-Warray-bounds",
877 # "-Wcast-align",
878 "-Wdeclaration-after-statement",
879 # "-Wdouble-promotion",
880 "-Wduplicated-branches",
881 # "-Wduplicated-cond",
882 "-Wextra",
883 "-Wformat=2",
884 "-Wformat-nonliteral",
885 "-Wformat-security",
886 "-Wimplicit-function-declaration",
887 "-Winit-self",
888 "-Winline",
889 "-Wjump-misses-init",
890 # "-Wlogical-op",
891 "-Wmissing-declarations",
892 "-Wmissing-format-attribute",
893 "-Wmissing-include-dirs",
894 "-Wmissing-noreturn",
895 "-Wmissing-prototypes",
896 "-Wnested-externs",
897 "-Wnull-dereference",
898 "-Wold-style-definition",
899 "-Wpacked",
900 "-Wpointer-arith",
901 "-Wrestrict",
902 "-Wreturn-type",
903 "-Wshadow",
904 "-Wsign-compare",
905 "-Wstrict-aliasing",
906 "-Wstrict-prototypes",
907 "-Wundef",
908 "-Wunused-but-set-variable",
909 "-Wwrite-strings",
910 "-Wconversion",
913 if sys.version_info[:2] != (3, 4):
914 args += [
915 "-Wswitch-default",
918 args += [
919 "-Wno-incompatible-pointer-types-discards-qualifiers",
920 "-Wno-missing-field-initializers",
921 "-Wno-unused-parameter",
922 "-Wno-discarded-qualifiers",
923 "-Wno-sign-conversion",
926 # silence clang for unused gcc CFLAGS added by Debian
927 args += [
928 "-Wno-unused-command-line-argument",
931 args += [
932 "-fno-strict-aliasing",
933 "-fvisibility=hidden",
936 # force GCC to use colors
937 if hasattr(sys.stdout, "isatty") and sys.stdout.isatty():
938 args.append("-fdiagnostics-color")
940 _cache[cache_key] = filter_compiler_arguments(compiler, args)
942 ext.extra_compile_args += _cache[cache_key]
945 du_build_ext = get_command_class("build_ext")
948 class build_ext(du_build_ext):
950 def initialize_options(self):
951 du_build_ext.initialize_options(self)
952 self.compiler_type = None
954 def finalize_options(self):
955 du_build_ext.finalize_options(self)
956 self.compiler_type = new_compiler(compiler=self.compiler).compiler_type
958 def _write_config_h(self):
959 script_dir = get_script_dir()
960 target = os.path.join(script_dir, "config.h")
961 versions = get_versions()
962 content = u"""
963 /* Configuration header created by setup.py - do not edit */
964 #ifndef _CONFIG_H
965 #define _CONFIG_H 1
967 #define PYGOBJECT_MAJOR_VERSION %(PYGOBJECT_MAJOR_VERSION)s
968 #define PYGOBJECT_MINOR_VERSION %(PYGOBJECT_MINOR_VERSION)s
969 #define PYGOBJECT_MICRO_VERSION %(PYGOBJECT_MICRO_VERSION)s
970 #define VERSION "%(VERSION)s"
972 #endif /* _CONFIG_H */
973 """ % versions
975 try:
976 with io.open(target, 'r', encoding="utf-8") as h:
977 if h.read() == content:
978 return
979 except EnvironmentError:
980 pass
982 with io.open(target, 'w', encoding="utf-8") as h:
983 h.write(content)
985 def _setup_extensions(self):
986 ext = {e.name: e for e in self.extensions}
988 compiler = new_compiler(compiler=self.compiler)
989 customize_compiler(compiler)
991 def add_dependency(ext, name):
992 add_ext_pkg_config_dep(ext, compiler.compiler_type, name)
994 def add_pycairo(ext):
995 ext.include_dirs += [get_pycairo_include_dir()]
997 gi_ext = ext["gi._gi"]
998 add_dependency(gi_ext, "glib-2.0")
999 add_dependency(gi_ext, "gio-2.0")
1000 add_dependency(gi_ext, "gobject-introspection-1.0")
1001 add_dependency(gi_ext, "libffi")
1002 add_ext_compiler_flags(gi_ext, compiler)
1004 gi_cairo_ext = ext["gi._gi_cairo"]
1005 add_dependency(gi_cairo_ext, "glib-2.0")
1006 add_dependency(gi_cairo_ext, "gio-2.0")
1007 add_dependency(gi_cairo_ext, "gobject-introspection-1.0")
1008 add_dependency(gi_cairo_ext, "libffi")
1009 add_dependency(gi_cairo_ext, "cairo")
1010 add_dependency(gi_cairo_ext, "cairo-gobject")
1011 add_pycairo(gi_cairo_ext)
1012 add_ext_compiler_flags(gi_cairo_ext, compiler)
1014 def run(self):
1015 self._write_config_h()
1016 self._setup_extensions()
1017 du_build_ext.run(self)
1020 class install_pkgconfig(Command):
1021 description = "install .pc file"
1022 user_options = []
1024 def initialize_options(self):
1025 self.install_base = None
1026 self.install_platbase = None
1027 self.install_data = None
1028 self.compiler_type = None
1029 self.outfiles = []
1031 def finalize_options(self):
1032 self.set_undefined_options(
1033 'install',
1034 ('install_base', 'install_base'),
1035 ('install_data', 'install_data'),
1036 ('install_platbase', 'install_platbase'),
1039 self.set_undefined_options(
1040 'build_ext',
1041 ('compiler_type', 'compiler_type'),
1044 def get_outputs(self):
1045 return self.outfiles
1047 def get_inputs(self):
1048 return []
1050 def run(self):
1051 cmd = self.distribution.get_command_obj("bdist_wheel", create=False)
1052 if cmd is not None:
1053 log.warn(
1054 "Python wheels and pkg-config is not compatible. "
1055 "No pkg-config file will be included in the wheel. Install "
1056 "from source if you need one.")
1057 return
1059 if self.compiler_type == "msvc":
1060 return
1062 script_dir = get_script_dir()
1063 pkgconfig_in = os.path.join(script_dir, "pygobject-3.0.pc.in")
1064 with io.open(pkgconfig_in, "r", encoding="utf-8") as h:
1065 content = h.read()
1067 config = {
1068 "prefix": self.install_base,
1069 "exec_prefix": self.install_platbase,
1070 "includedir": "${prefix}/include",
1071 "datarootdir": "${prefix}/share",
1072 "datadir": "${datarootdir}",
1073 "VERSION": PYGOBJECT_VERISON,
1075 for key, value in config.items():
1076 content = content.replace("@%s@" % key, value)
1078 libdir = os.path.dirname(get_python_lib(True, True, self.install_data))
1079 pkgconfig_dir = os.path.join(libdir, "pkgconfig")
1080 self.mkpath(pkgconfig_dir)
1081 target = os.path.join(pkgconfig_dir, "pygobject-3.0.pc")
1082 with io.open(target, "w", encoding="utf-8") as h:
1083 h.write(content)
1084 self.outfiles.append(target)
1087 du_install = get_command_class("install")
1090 class install(du_install):
1092 sub_commands = du_install.sub_commands + [
1093 ("install_pkgconfig", lambda self: True),
1097 def main():
1098 script_dir = get_script_dir()
1099 pkginfo = parse_pkg_info(script_dir)
1100 gi_dir = os.path.join(script_dir, "gi")
1102 sources = [
1103 os.path.join("gi", n) for n in os.listdir(gi_dir)
1104 if os.path.splitext(n)[-1] == ".c"
1106 cairo_sources = [os.path.join("gi", "pygi-foreign-cairo.c")]
1107 for s in cairo_sources:
1108 sources.remove(s)
1110 readme = os.path.join(script_dir, "README.rst")
1111 with io.open(readme, encoding="utf-8") as h:
1112 long_description = h.read()
1114 gi_ext = Extension(
1115 name='gi._gi',
1116 sources=sources,
1117 include_dirs=[script_dir, gi_dir],
1118 depends=list_headers(script_dir) + list_headers(gi_dir),
1119 define_macros=[("PY_SSIZE_T_CLEAN", None)],
1122 gi_cairo_ext = Extension(
1123 name='gi._gi_cairo',
1124 sources=cairo_sources,
1125 include_dirs=[script_dir, gi_dir],
1126 depends=list_headers(script_dir) + list_headers(gi_dir),
1127 define_macros=[("PY_SSIZE_T_CLEAN", None)],
1130 version = pkginfo["Version"]
1131 if is_dev_version():
1132 # This makes it a PEP 440 pre-release and pip will only install it from
1133 # PyPI in case --pre is passed.
1134 version += ".dev0"
1136 setup(
1137 name=pkginfo["Name"],
1138 version=version,
1139 description=pkginfo["Summary"],
1140 url=pkginfo["Home-page"],
1141 author=pkginfo["Author"],
1142 author_email=pkginfo["Author-email"],
1143 maintainer=pkginfo["Maintainer"],
1144 maintainer_email=pkginfo["Maintainer-email"],
1145 license=pkginfo["License"],
1146 long_description=long_description,
1147 platforms=pkginfo.get_all("Platform"),
1148 classifiers=pkginfo.get_all("Classifier"),
1149 packages=[
1150 "pygtkcompat",
1151 "gi",
1152 "gi.repository",
1153 "gi.overrides",
1155 ext_modules=[
1156 gi_ext,
1157 gi_cairo_ext,
1159 cmdclass={
1160 "build_ext": build_ext,
1161 "distcheck": distcheck,
1162 "sdist_gnome": sdist_gnome,
1163 "build_tests": build_tests,
1164 "test": test,
1165 "quality": quality,
1166 "install": install,
1167 "install_pkgconfig": install_pkgconfig,
1169 install_requires=[
1170 "pycairo>=%s" % get_version_requirement(
1171 get_pycairo_pkg_config_name()),
1173 data_files=[
1174 ('include/pygobject-3.0', ['gi/pygobject.h']),
1176 zip_safe=False,
1180 if __name__ == "__main__":
1181 main()