Merge branch 'gio-lists-tore-splice-fallback' into 'master'
[pygobject.git] / setup.py
blob38bc2e46f5af69e85438de09256595dce4f59f8d
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.3"
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 tracked_files = [
324 f for f in tracked_files
325 if os.path.basename(f) not in [".gitignore"]]
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 "--cflags-begin",
514 "-I%s" % gi_tests_dir,
515 "--cflags-end",
516 "--output=%s" % gir_path,
517 ] + ext.sources + ext.depends)
519 if self._newer_group([gir_path], typelib_path):
520 subprocess.check_call([
521 g_ir_compiler,
522 gir_path,
523 "--output=%s" % typelib_path,
526 ext = Extension(
527 name='libregress',
528 sources=[
529 os.path.join(gi_tests_dir, "regress.c"),
530 os.path.join(tests_dir, "regressextra.c"),
532 include_dirs=[
533 gi_tests_dir,
535 depends=[
536 os.path.join(gi_tests_dir, "regress.h"),
537 os.path.join(tests_dir, "regressextra.h"),
540 add_ext_pkg_config_dep(ext, compiler.compiler_type, "glib-2.0")
541 add_ext_pkg_config_dep(ext, compiler.compiler_type, "gio-2.0")
542 add_ext_pkg_config_dep(ext, compiler.compiler_type, "cairo")
543 add_ext_pkg_config_dep(ext, compiler.compiler_type, "cairo-gobject")
544 ext_paths = build_ext(ext)
546 gir_path = os.path.join(tests_dir, "Regress-1.0.gir")
547 typelib_path = os.path.join(tests_dir, "Regress-1.0.typelib")
549 if self._newer_group(ext_paths, gir_path):
550 subprocess.check_call([
551 g_ir_scanner,
552 "--no-libtool",
553 "--include=cairo-1.0",
554 "--include=Gio-2.0",
555 "--namespace=Regress",
556 "--nsversion=1.0",
557 "--warn-all",
558 "--warn-error",
559 "--library-path=%s" % tests_dir,
560 "--library=regress",
561 "--pkg=glib-2.0",
562 "--pkg=gio-2.0",
563 "--pkg=cairo",
564 "--pkg=cairo-gobject",
565 "--output=%s" % gir_path,
566 ] + ext.sources + ext.depends)
568 if self._newer_group([gir_path], typelib_path):
569 subprocess.check_call([
570 g_ir_compiler,
571 gir_path,
572 "--output=%s" % typelib_path,
575 ext = Extension(
576 name='tests.testhelper',
577 sources=[
578 os.path.join(tests_dir, "testhelpermodule.c"),
579 os.path.join(tests_dir, "test-floating.c"),
580 os.path.join(tests_dir, "test-thread.c"),
581 os.path.join(tests_dir, "test-unknown.c"),
583 include_dirs=[
584 gi_dir,
585 tests_dir,
587 depends=list_headers(gi_dir) + list_headers(tests_dir),
588 define_macros=[("PY_SSIZE_T_CLEAN", None)],
590 add_ext_pkg_config_dep(ext, compiler.compiler_type, "glib-2.0")
591 add_ext_pkg_config_dep(ext, compiler.compiler_type, "gio-2.0")
592 add_ext_pkg_config_dep(ext, compiler.compiler_type, "cairo")
593 add_ext_compiler_flags(ext, compiler)
595 dist = Distribution({"ext_modules": [ext]})
597 build_cmd = dist.get_command_obj("build")
598 build_cmd.build_base = os.path.join(self.build_base, "pygobject_tests")
599 build_cmd.ensure_finalized()
601 cmd = dist.get_command_obj("build_ext")
602 cmd.inplace = True
603 cmd.force = self.force
604 cmd.ensure_finalized()
605 cmd.run()
608 def get_suppression_files_for_prefix(prefix):
609 """Returns a list of valgrind suppression files for a given prefix"""
611 # Most specific first (/usr/share/doc is Fedora, /usr/lib is Debian)
612 # Take the first one found
613 major = str(sys.version_info[0])
614 minor = str(sys.version_info[1])
615 pyfiles = []
616 pyfiles.append(
617 os.path.join(
618 prefix, "share", "doc", "python%s%s" % (major, minor),
619 "valgrind-python.supp"))
620 pyfiles.append(
621 os.path.join(prefix, "lib", "valgrind", "python%s.supp" % major))
622 pyfiles.append(
623 os.path.join(
624 prefix, "share", "doc", "python%s-devel" % major,
625 "valgrind-python.supp"))
626 pyfiles.append(os.path.join(prefix, "lib", "valgrind", "python.supp"))
628 files = []
629 for f in pyfiles:
630 if os.path.isfile(f):
631 files.append(f)
632 break
634 files.append(os.path.join(
635 prefix, "share", "glib-2.0", "valgrind", "glib.supp"))
636 return [f for f in files if os.path.isfile(f)]
639 def get_real_prefix():
640 """Returns the base Python prefix, even in a virtualenv/venv"""
642 return getattr(sys, "base_prefix", getattr(sys, "real_prefix", sys.prefix))
645 def get_suppression_files():
646 """Returns a list of valgrind suppression files"""
648 prefixes = [
649 sys.prefix,
650 get_real_prefix(),
651 pkg_config_parse("--variable=prefix", "glib-2.0")[0],
654 files = []
655 for prefix in prefixes:
656 files.extend(get_suppression_files_for_prefix(prefix))
658 files.append(os.path.join(get_script_dir(), "tests", "valgrind.supp"))
659 return sorted(set(files))
662 class test(Command):
663 user_options = [
664 ("valgrind", None, "run tests under valgrind"),
665 ("valgrind-log-file=", None, "save logs instead of printing them"),
666 ("gdb", None, "run tests under gdb"),
667 ("no-capture", "s", "don't capture test output"),
670 def initialize_options(self):
671 self.valgrind = None
672 self.valgrind_log_file = None
673 self.gdb = None
674 self.no_capture = None
676 def finalize_options(self):
677 self.valgrind = bool(self.valgrind)
678 if self.valgrind_log_file and not self.valgrind:
679 raise DistutilsOptionError("valgrind not enabled")
680 self.gdb = bool(self.gdb)
681 self.no_capture = bool(self.no_capture)
683 def run(self):
684 cmd = self.reinitialize_command("build_tests")
685 cmd.ensure_finalized()
686 cmd.run()
688 env = os.environ.copy()
689 env.pop("MSYSTEM", None)
691 if self.no_capture:
692 env["PYGI_TEST_VERBOSE"] = "1"
694 env["MALLOC_PERTURB_"] = "85"
695 env["MALLOC_CHECK_"] = "3"
696 env["G_SLICE"] = "debug-blocks"
698 pre_args = []
700 if self.valgrind:
701 env["G_SLICE"] = "always-malloc"
702 env["G_DEBUG"] = "gc-friendly"
703 env["PYTHONMALLOC"] = "malloc"
705 pre_args += [
706 "valgrind", "--leak-check=full", "--show-possibly-lost=no",
707 "--num-callers=20", "--child-silent-after-fork=yes",
708 ] + ["--suppressions=" + f for f in get_suppression_files()]
710 if self.valgrind_log_file:
711 pre_args += ["--log-file=" + self.valgrind_log_file]
713 if self.gdb:
714 env["PYGI_TEST_GDB"] = "1"
715 pre_args += ["gdb", "--args"]
717 if pre_args:
718 log.info(" ".join(pre_args))
720 tests_dir = os.path.join(get_script_dir(), "tests")
721 sys.exit(subprocess.call(pre_args + [
722 sys.executable,
723 os.path.join(tests_dir, "runtests.py"),
724 ], env=env))
727 class quality(Command):
728 description = "run code quality tests"
729 user_options = []
731 def initialize_options(self):
732 pass
734 def finalize_options(self):
735 pass
737 def run(self):
738 status = subprocess.call([
739 sys.executable, "-m", "flake8",
740 ], cwd=get_script_dir())
741 if status != 0:
742 raise SystemExit(status)
745 def get_script_dir():
746 return os.path.dirname(os.path.realpath(__file__))
749 def get_pycairo_include_dir():
750 """Returns the best guess at where to find the pycairo headers.
751 A bit convoluted because we have to deal with multiple pycairo
752 versions.
754 Raises if pycairo isn't found or it's too old.
757 pkg_config_name = get_pycairo_pkg_config_name()
758 min_version = get_version_requirement(pkg_config_name)
759 min_version_info = tuple(int(p) for p in min_version.split("."))
761 def check_path(include_dir):
762 log.info("pycairo: trying include directory: %r" % include_dir)
763 header_path = os.path.join(include_dir, "%s.h" % pkg_config_name)
764 if os.path.exists(header_path):
765 log.info("pycairo: found %r" % header_path)
766 return True
767 log.info("pycairo: header file (%r) not found" % header_path)
768 return False
770 def find_path(paths):
771 for p in reversed(paths):
772 if check_path(p):
773 return p
775 def find_new_api():
776 log.info("pycairo: new API")
777 import cairo
779 if cairo.version_info < min_version_info:
780 raise DistutilsSetupError(
781 "pycairo >= %s required, %s found." % (
782 min_version, ".".join(map(str, cairo.version_info))))
784 if hasattr(cairo, "get_include"):
785 return [cairo.get_include()]
786 log.info("pycairo: no get_include()")
787 return []
789 def find_old_api():
790 log.info("pycairo: old API")
792 import cairo
794 if cairo.version_info < min_version_info:
795 raise DistutilsSetupError(
796 "pycairo >= %s required, %s found." % (
797 min_version, ".".join(map(str, cairo.version_info))))
799 location = os.path.dirname(os.path.abspath(cairo.__path__[0]))
800 log.info("pycairo: found %r" % location)
802 def samefile(src, dst):
803 # Python 2 on Windows doesn't have os.path.samefile, so we have to
804 # provide a fallback
805 if hasattr(os.path, "samefile"):
806 return os.path.samefile(src, dst)
807 os.stat(src)
808 os.stat(dst)
809 return (os.path.normcase(os.path.abspath(src)) ==
810 os.path.normcase(os.path.abspath(dst)))
812 def get_sys_path(location, name):
813 # Returns the sysconfig path for a distribution, or None
814 for scheme in sysconfig.get_scheme_names():
815 for path_type in ["platlib", "purelib"]:
816 path = sysconfig.get_path(path_type, scheme)
817 try:
818 if samefile(path, location):
819 return sysconfig.get_path(name, scheme)
820 except EnvironmentError:
821 pass
823 data_path = get_sys_path(location, "data") or sys.prefix
824 return [os.path.join(data_path, "include", "pycairo")]
826 def find_pkg_config():
827 log.info("pycairo: pkg-config")
828 pkg_config_version_check(pkg_config_name, min_version)
829 return pkg_config_parse("--cflags-only-I", pkg_config_name)
831 # First the new get_include() API added in >1.15.6
832 include_dir = find_path(find_new_api())
833 if include_dir is not None:
834 return include_dir
836 # Then try to find it in the data prefix based on the module path.
837 # This works with many virtualenv/userdir setups, but not all apparently,
838 # see https://gitlab.gnome.org/GNOME/pygobject/issues/150
839 include_dir = find_path(find_old_api())
840 if include_dir is not None:
841 return include_dir
843 # Finally, fall back to pkg-config
844 include_dir = find_path(find_pkg_config())
845 if include_dir is not None:
846 return include_dir
848 raise DistutilsSetupError("Could not find pycairo headers")
851 def add_ext_pkg_config_dep(ext, compiler_type, name):
852 msvc_libraries = {
853 "glib-2.0": ["glib-2.0"],
854 "gio-2.0": ["gio-2.0", "gobject-2.0", "glib-2.0"],
855 "gobject-introspection-1.0":
856 ["girepository-1.0", "gobject-2.0", "glib-2.0"],
857 "cairo": ["cairo"],
858 "cairo-gobject":
859 ["cairo-gobject", "cairo", "gobject-2.0", "glib-2.0"],
860 "libffi": ["ffi"],
863 def add(target, new):
864 for entry in new:
865 if entry not in target:
866 target.append(entry)
868 fallback_libs = msvc_libraries[name]
869 if compiler_type == "msvc":
870 # assume that INCLUDE and LIB contains the right paths
871 add(ext.libraries, fallback_libs)
872 else:
873 min_version = get_version_requirement(name)
874 pkg_config_version_check(name, min_version)
875 add(ext.include_dirs, pkg_config_parse("--cflags-only-I", name))
876 add(ext.library_dirs, pkg_config_parse("--libs-only-L", name))
877 add(ext.libraries, pkg_config_parse("--libs-only-l", name))
880 def add_ext_compiler_flags(ext, compiler, _cache={}):
881 cache_key = compiler.compiler[0]
882 if cache_key not in _cache:
884 args = [
885 "-Wall",
886 "-Warray-bounds",
887 "-Wcast-align",
888 "-Wdeclaration-after-statement",
889 "-Wduplicated-branches",
890 "-Wextra",
891 "-Wformat=2",
892 "-Wformat-nonliteral",
893 "-Wformat-security",
894 "-Wimplicit-function-declaration",
895 "-Winit-self",
896 "-Wjump-misses-init",
897 "-Wlogical-op",
898 "-Wmissing-declarations",
899 "-Wmissing-format-attribute",
900 "-Wmissing-include-dirs",
901 "-Wmissing-noreturn",
902 "-Wmissing-prototypes",
903 "-Wnested-externs",
904 "-Wnull-dereference",
905 "-Wold-style-definition",
906 "-Wpacked",
907 "-Wpointer-arith",
908 "-Wrestrict",
909 "-Wreturn-type",
910 "-Wshadow",
911 "-Wsign-compare",
912 "-Wstrict-aliasing",
913 "-Wstrict-prototypes",
914 "-Wundef",
915 "-Wunused-but-set-variable",
916 "-Wwrite-strings",
917 "-Wconversion",
920 if sys.version_info[:2] != (3, 4):
921 args += [
922 "-Wswitch-default",
925 args += [
926 "-Wno-incompatible-pointer-types-discards-qualifiers",
927 "-Wno-missing-field-initializers",
928 "-Wno-unused-parameter",
929 "-Wno-discarded-qualifiers",
930 "-Wno-sign-conversion",
931 "-Wno-cast-function-type",
934 # silence clang for unused gcc CFLAGS added by Debian
935 args += [
936 "-Wno-unused-command-line-argument",
939 args += [
940 "-fno-strict-aliasing",
941 "-fvisibility=hidden",
944 # force GCC to use colors
945 if hasattr(sys.stdout, "isatty") and sys.stdout.isatty():
946 args.append("-fdiagnostics-color")
948 _cache[cache_key] = filter_compiler_arguments(compiler, args)
950 ext.extra_compile_args += _cache[cache_key]
953 du_build_ext = get_command_class("build_ext")
956 class build_ext(du_build_ext):
958 def initialize_options(self):
959 du_build_ext.initialize_options(self)
960 self.compiler_type = None
962 def finalize_options(self):
963 du_build_ext.finalize_options(self)
964 self.compiler_type = new_compiler(compiler=self.compiler).compiler_type
966 def _write_config_h(self):
967 script_dir = get_script_dir()
968 target = os.path.join(script_dir, "config.h")
969 versions = get_versions()
970 content = u"""
971 /* Configuration header created by setup.py - do not edit */
972 #ifndef _CONFIG_H
973 #define _CONFIG_H 1
975 #define PYGOBJECT_MAJOR_VERSION %(PYGOBJECT_MAJOR_VERSION)s
976 #define PYGOBJECT_MINOR_VERSION %(PYGOBJECT_MINOR_VERSION)s
977 #define PYGOBJECT_MICRO_VERSION %(PYGOBJECT_MICRO_VERSION)s
978 #define VERSION "%(VERSION)s"
980 #endif /* _CONFIG_H */
981 """ % versions
983 try:
984 with io.open(target, 'r', encoding="utf-8") as h:
985 if h.read() == content:
986 return
987 except EnvironmentError:
988 pass
990 with io.open(target, 'w', encoding="utf-8") as h:
991 h.write(content)
993 def _setup_extensions(self):
994 ext = {e.name: e for e in self.extensions}
996 compiler = new_compiler(compiler=self.compiler)
997 customize_compiler(compiler)
999 def add_dependency(ext, name):
1000 add_ext_pkg_config_dep(ext, compiler.compiler_type, name)
1002 def add_pycairo(ext):
1003 ext.include_dirs += [get_pycairo_include_dir()]
1005 gi_ext = ext["gi._gi"]
1006 add_dependency(gi_ext, "glib-2.0")
1007 add_dependency(gi_ext, "gio-2.0")
1008 add_dependency(gi_ext, "gobject-introspection-1.0")
1009 add_dependency(gi_ext, "libffi")
1010 add_ext_compiler_flags(gi_ext, compiler)
1012 gi_cairo_ext = ext["gi._gi_cairo"]
1013 add_dependency(gi_cairo_ext, "glib-2.0")
1014 add_dependency(gi_cairo_ext, "gio-2.0")
1015 add_dependency(gi_cairo_ext, "gobject-introspection-1.0")
1016 add_dependency(gi_cairo_ext, "libffi")
1017 add_dependency(gi_cairo_ext, "cairo")
1018 add_dependency(gi_cairo_ext, "cairo-gobject")
1019 add_pycairo(gi_cairo_ext)
1020 add_ext_compiler_flags(gi_cairo_ext, compiler)
1022 def run(self):
1023 self._write_config_h()
1024 self._setup_extensions()
1025 du_build_ext.run(self)
1028 class install_pkgconfig(Command):
1029 description = "install .pc file"
1030 user_options = []
1032 def initialize_options(self):
1033 self.install_base = None
1034 self.install_platbase = None
1035 self.install_data = None
1036 self.compiler_type = None
1037 self.outfiles = []
1039 def finalize_options(self):
1040 self.set_undefined_options(
1041 'install',
1042 ('install_base', 'install_base'),
1043 ('install_data', 'install_data'),
1044 ('install_platbase', 'install_platbase'),
1047 self.set_undefined_options(
1048 'build_ext',
1049 ('compiler_type', 'compiler_type'),
1052 def get_outputs(self):
1053 return self.outfiles
1055 def get_inputs(self):
1056 return []
1058 def run(self):
1059 cmd = self.distribution.get_command_obj("bdist_wheel", create=False)
1060 if cmd is not None:
1061 log.warn(
1062 "Python wheels and pkg-config is not compatible. "
1063 "No pkg-config file will be included in the wheel. Install "
1064 "from source if you need one.")
1065 return
1067 if self.compiler_type == "msvc":
1068 return
1070 script_dir = get_script_dir()
1071 pkgconfig_in = os.path.join(script_dir, "pygobject-3.0.pc.in")
1072 with io.open(pkgconfig_in, "r", encoding="utf-8") as h:
1073 content = h.read()
1075 config = {
1076 "prefix": self.install_base,
1077 "exec_prefix": self.install_platbase,
1078 "includedir": "${prefix}/include",
1079 "datarootdir": "${prefix}/share",
1080 "datadir": "${datarootdir}",
1081 "VERSION": PYGOBJECT_VERISON,
1083 for key, value in config.items():
1084 content = content.replace("@%s@" % key, value)
1086 libdir = os.path.dirname(get_python_lib(True, True, self.install_data))
1087 pkgconfig_dir = os.path.join(libdir, "pkgconfig")
1088 self.mkpath(pkgconfig_dir)
1089 target = os.path.join(pkgconfig_dir, "pygobject-3.0.pc")
1090 with io.open(target, "w", encoding="utf-8") as h:
1091 h.write(content)
1092 self.outfiles.append(target)
1095 du_install = get_command_class("install")
1098 class install(du_install):
1100 sub_commands = du_install.sub_commands + [
1101 ("install_pkgconfig", lambda self: True),
1105 def main():
1106 script_dir = get_script_dir()
1107 pkginfo = parse_pkg_info(script_dir)
1108 gi_dir = os.path.join(script_dir, "gi")
1110 sources = [
1111 os.path.join("gi", n) for n in os.listdir(gi_dir)
1112 if os.path.splitext(n)[-1] == ".c"
1114 cairo_sources = [os.path.join("gi", "pygi-foreign-cairo.c")]
1115 for s in cairo_sources:
1116 sources.remove(s)
1118 readme = os.path.join(script_dir, "README.rst")
1119 with io.open(readme, encoding="utf-8") as h:
1120 long_description = h.read()
1122 gi_ext = Extension(
1123 name='gi._gi',
1124 sources=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 gi_cairo_ext = Extension(
1131 name='gi._gi_cairo',
1132 sources=cairo_sources,
1133 include_dirs=[script_dir, gi_dir],
1134 depends=list_headers(script_dir) + list_headers(gi_dir),
1135 define_macros=[("PY_SSIZE_T_CLEAN", None)],
1138 version = pkginfo["Version"]
1139 if is_dev_version():
1140 # This makes it a PEP 440 pre-release and pip will only install it from
1141 # PyPI in case --pre is passed.
1142 version += ".dev0"
1144 setup(
1145 name=pkginfo["Name"],
1146 version=version,
1147 description=pkginfo["Summary"],
1148 url=pkginfo["Home-page"],
1149 author=pkginfo["Author"],
1150 author_email=pkginfo["Author-email"],
1151 maintainer=pkginfo["Maintainer"],
1152 maintainer_email=pkginfo["Maintainer-email"],
1153 license=pkginfo["License"],
1154 long_description=long_description,
1155 platforms=pkginfo.get_all("Platform"),
1156 classifiers=pkginfo.get_all("Classifier"),
1157 packages=[
1158 "pygtkcompat",
1159 "gi",
1160 "gi.repository",
1161 "gi.overrides",
1163 ext_modules=[
1164 gi_ext,
1165 gi_cairo_ext,
1167 cmdclass={
1168 "build_ext": build_ext,
1169 "distcheck": distcheck,
1170 "sdist_gnome": sdist_gnome,
1171 "build_tests": build_tests,
1172 "test": test,
1173 "quality": quality,
1174 "install": install,
1175 "install_pkgconfig": install_pkgconfig,
1177 install_requires=[
1178 "pycairo>=%s" % get_version_requirement(
1179 get_pycairo_pkg_config_name()),
1181 data_files=[
1182 ('include/pygobject-3.0', ['gi/pygobject.h']),
1184 zip_safe=False,
1188 if __name__ == "__main__":
1189 main()