setup.py: Add a ".dev" suffix to the version for pre-releases. Fixes #190
[pygobject.git] / setup.py
blob1114b9e50cf2a71a73974a3506c5db6fff1c8afd
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 class test(Command):
606 user_options = [
607 ("valgrind", None, "run tests under valgrind"),
608 ("valgrind-log-file=", None, "save logs instead of printing them"),
609 ("gdb", None, "run tests under gdb"),
610 ("no-capture", "s", "don't capture test output"),
613 def initialize_options(self):
614 self.valgrind = None
615 self.valgrind_log_file = None
616 self.gdb = None
617 self.no_capture = None
619 def finalize_options(self):
620 self.valgrind = bool(self.valgrind)
621 if self.valgrind_log_file and not self.valgrind:
622 raise DistutilsOptionError("valgrind not enabled")
623 self.gdb = bool(self.gdb)
624 self.no_capture = bool(self.no_capture)
626 def run(self):
627 cmd = self.reinitialize_command("build_tests")
628 cmd.ensure_finalized()
629 cmd.run()
631 env = os.environ.copy()
632 env.pop("MSYSTEM", None)
634 if self.no_capture:
635 env["PYGI_TEST_VERBOSE"] = "1"
637 env["MALLOC_PERTURB_"] = "85"
638 env["MALLOC_CHECK_"] = "3"
639 env["G_SLICE"] = "debug-blocks"
641 def get_suppression_files():
642 files = []
643 if sys.version_info[0] == 2:
644 files.append(os.path.join(
645 sys.prefix, "lib", "valgrind", "python.supp"))
646 else:
647 files.append(os.path.join(
648 sys.prefix, "lib", "valgrind", "python3.supp"))
649 files.append(os.path.join(
650 sys.prefix, "share", "glib-2.0", "valgrind", "glib.supp"))
651 return [f for f in files if os.path.isfile(f)]
653 pre_args = []
655 if self.valgrind:
656 env["G_SLICE"] = "always-malloc"
657 env["G_DEBUG"] = "gc-friendly"
658 env["PYTHONMALLOC"] = "malloc"
660 pre_args += [
661 "valgrind", "--leak-check=full", "--show-possibly-lost=no",
662 "--num-callers=20", "--child-silent-after-fork=yes",
663 ] + ["--suppressions=" + f for f in get_suppression_files()]
665 if self.valgrind_log_file:
666 pre_args += ["--log-file=" + self.valgrind_log_file]
668 if self.gdb:
669 env["PYGI_TEST_GDB"] = "1"
670 pre_args += ["gdb", "--args"]
672 if pre_args:
673 log.info(" ".join(pre_args))
675 tests_dir = os.path.join(get_script_dir(), "tests")
676 sys.exit(subprocess.call(pre_args + [
677 sys.executable,
678 os.path.join(tests_dir, "runtests.py"),
679 ], env=env))
682 class quality(Command):
683 description = "run code quality tests"
684 user_options = []
686 def initialize_options(self):
687 pass
689 def finalize_options(self):
690 pass
692 def run(self):
693 status = subprocess.call([
694 sys.executable, "-m", "flake8",
695 ], cwd=get_script_dir())
696 if status != 0:
697 raise SystemExit(status)
700 def get_script_dir():
701 return os.path.dirname(os.path.realpath(__file__))
704 def get_pycairo_include_dir():
705 """Returns the best guess at where to find the pycairo headers.
706 A bit convoluted because we have to deal with multiple pycairo
707 versions.
709 Raises if pycairo isn't found or it's too old.
712 pkg_config_name = get_pycairo_pkg_config_name()
713 min_version = get_version_requirement(pkg_config_name)
714 min_version_info = tuple(int(p) for p in min_version.split("."))
716 def check_path(include_dir):
717 log.info("pycairo: trying include directory: %r" % include_dir)
718 header_path = os.path.join(include_dir, "%s.h" % pkg_config_name)
719 if os.path.exists(header_path):
720 log.info("pycairo: found %r" % header_path)
721 return True
722 log.info("pycairo: header file (%r) not found" % header_path)
723 return False
725 def find_path(paths):
726 for p in reversed(paths):
727 if check_path(p):
728 return p
730 def find_new_api():
731 log.info("pycairo: new API")
732 import cairo
734 if cairo.version_info < min_version_info:
735 raise DistutilsSetupError(
736 "pycairo >= %s required, %s found." % (
737 min_version, ".".join(map(str, cairo.version_info))))
739 if hasattr(cairo, "get_include"):
740 return [cairo.get_include()]
741 log.info("pycairo: no get_include()")
742 return []
744 def find_old_api():
745 log.info("pycairo: old API")
747 import cairo
749 if cairo.version_info < min_version_info:
750 raise DistutilsSetupError(
751 "pycairo >= %s required, %s found." % (
752 min_version, ".".join(map(str, cairo.version_info))))
754 location = os.path.dirname(os.path.abspath(cairo.__path__[0]))
755 log.info("pycairo: found %r" % location)
757 def samefile(src, dst):
758 # Python 2 on Windows doesn't have os.path.samefile, so we have to
759 # provide a fallback
760 if hasattr(os.path, "samefile"):
761 return os.path.samefile(src, dst)
762 os.stat(src)
763 os.stat(dst)
764 return (os.path.normcase(os.path.abspath(src)) ==
765 os.path.normcase(os.path.abspath(dst)))
767 def get_sys_path(location, name):
768 # Returns the sysconfig path for a distribution, or None
769 for scheme in sysconfig.get_scheme_names():
770 for path_type in ["platlib", "purelib"]:
771 path = sysconfig.get_path(path_type, scheme)
772 try:
773 if samefile(path, location):
774 return sysconfig.get_path(name, scheme)
775 except EnvironmentError:
776 pass
778 data_path = get_sys_path(location, "data") or sys.prefix
779 return [os.path.join(data_path, "include", "pycairo")]
781 def find_pkg_config():
782 log.info("pycairo: pkg-config")
783 pkg_config_version_check(pkg_config_name, min_version)
784 return pkg_config_parse("--cflags-only-I", pkg_config_name)
786 # First the new get_include() API added in >1.15.6
787 include_dir = find_path(find_new_api())
788 if include_dir is not None:
789 return include_dir
791 # Then try to find it in the data prefix based on the module path.
792 # This works with many virtualenv/userdir setups, but not all apparently,
793 # see https://gitlab.gnome.org/GNOME/pygobject/issues/150
794 include_dir = find_path(find_old_api())
795 if include_dir is not None:
796 return include_dir
798 # Finally, fall back to pkg-config
799 include_dir = find_path(find_pkg_config())
800 if include_dir is not None:
801 return include_dir
803 raise DistutilsSetupError("Could not find pycairo headers")
806 def add_ext_pkg_config_dep(ext, compiler_type, name):
807 msvc_libraries = {
808 "glib-2.0": ["glib-2.0"],
809 "gio-2.0": ["gio-2.0", "gobject-2.0", "glib-2.0"],
810 "gobject-introspection-1.0":
811 ["girepository-1.0", "gobject-2.0", "glib-2.0"],
812 "cairo": ["cairo"],
813 "cairo-gobject":
814 ["cairo-gobject", "cairo", "gobject-2.0", "glib-2.0"],
815 "libffi": ["ffi"],
818 fallback_libs = msvc_libraries[name]
819 if compiler_type == "msvc":
820 # assume that INCLUDE and LIB contains the right paths
821 ext.libraries += fallback_libs
822 else:
823 min_version = get_version_requirement(name)
824 pkg_config_version_check(name, min_version)
825 ext.include_dirs += pkg_config_parse("--cflags-only-I", name)
826 ext.library_dirs += pkg_config_parse("--libs-only-L", name)
827 ext.libraries += pkg_config_parse("--libs-only-l", name)
830 def add_ext_compiler_flags(ext, compiler, _cache={}):
831 cache_key = compiler.compiler[0]
832 if cache_key not in _cache:
834 args = [
835 "-Wall",
836 "-Warray-bounds",
837 # "-Wcast-align",
838 "-Wdeclaration-after-statement",
839 # "-Wdouble-promotion",
840 "-Wduplicated-branches",
841 # "-Wduplicated-cond",
842 "-Wextra",
843 "-Wformat=2",
844 "-Wformat-nonliteral",
845 "-Wformat-security",
846 "-Wimplicit-function-declaration",
847 "-Winit-self",
848 "-Winline",
849 "-Wjump-misses-init",
850 # "-Wlogical-op",
851 "-Wmissing-declarations",
852 "-Wmissing-format-attribute",
853 "-Wmissing-include-dirs",
854 "-Wmissing-noreturn",
855 "-Wmissing-prototypes",
856 "-Wnested-externs",
857 # "-Wnull-dereference",
858 "-Wold-style-definition",
859 "-Wpacked",
860 "-Wpointer-arith",
861 "-Wrestrict",
862 "-Wreturn-type",
863 "-Wshadow",
864 "-Wsign-compare",
865 "-Wstrict-aliasing",
866 "-Wstrict-prototypes",
867 "-Wswitch-default",
868 "-Wundef",
869 "-Wunused-but-set-variable",
870 "-Wwrite-strings",
871 "-Wconversion",
874 args += [
875 "-Wno-incompatible-pointer-types-discards-qualifiers",
876 "-Wno-missing-field-initializers",
877 "-Wno-unused-parameter",
878 "-Wno-discarded-qualifiers",
879 "-Wno-sign-conversion",
882 # silence clang for unused gcc CFLAGS added by Debian
883 args += [
884 "-Wno-unused-command-line-argument",
887 args += [
888 "-fno-strict-aliasing",
889 "-fvisibility=hidden",
892 _cache[cache_key] = filter_compiler_arguments(compiler, args)
894 ext.extra_compile_args += _cache[cache_key]
897 du_build_ext = get_command_class("build_ext")
900 class build_ext(du_build_ext):
902 def initialize_options(self):
903 du_build_ext.initialize_options(self)
904 self.compiler_type = None
906 def finalize_options(self):
907 du_build_ext.finalize_options(self)
908 self.compiler_type = new_compiler(compiler=self.compiler).compiler_type
910 def _write_config_h(self):
911 script_dir = get_script_dir()
912 target = os.path.join(script_dir, "config.h")
913 versions = get_versions()
914 content = u"""
915 /* Configuration header created by setup.py - do not edit */
916 #ifndef _CONFIG_H
917 #define _CONFIG_H 1
919 #define PYGOBJECT_MAJOR_VERSION %(PYGOBJECT_MAJOR_VERSION)s
920 #define PYGOBJECT_MINOR_VERSION %(PYGOBJECT_MINOR_VERSION)s
921 #define PYGOBJECT_MICRO_VERSION %(PYGOBJECT_MICRO_VERSION)s
922 #define VERSION "%(VERSION)s"
924 #endif /* _CONFIG_H */
925 """ % versions
927 try:
928 with io.open(target, 'r', encoding="utf-8") as h:
929 if h.read() == content:
930 return
931 except EnvironmentError:
932 pass
934 with io.open(target, 'w', encoding="utf-8") as h:
935 h.write(content)
937 def _setup_extensions(self):
938 ext = {e.name: e for e in self.extensions}
940 compiler = new_compiler(compiler=self.compiler)
941 customize_compiler(compiler)
943 def add_dependency(ext, name):
944 add_ext_pkg_config_dep(ext, compiler.compiler_type, name)
946 def add_pycairo(ext):
947 ext.include_dirs += [get_pycairo_include_dir()]
949 gi_ext = ext["gi._gi"]
950 add_dependency(gi_ext, "glib-2.0")
951 add_dependency(gi_ext, "gio-2.0")
952 add_dependency(gi_ext, "gobject-introspection-1.0")
953 add_dependency(gi_ext, "libffi")
954 add_ext_compiler_flags(gi_ext, compiler)
956 gi_cairo_ext = ext["gi._gi_cairo"]
957 add_dependency(gi_cairo_ext, "glib-2.0")
958 add_dependency(gi_cairo_ext, "gio-2.0")
959 add_dependency(gi_cairo_ext, "gobject-introspection-1.0")
960 add_dependency(gi_cairo_ext, "libffi")
961 add_dependency(gi_cairo_ext, "cairo")
962 add_dependency(gi_cairo_ext, "cairo-gobject")
963 add_pycairo(gi_cairo_ext)
964 add_ext_compiler_flags(gi_cairo_ext, compiler)
966 def run(self):
967 self._write_config_h()
968 self._setup_extensions()
969 du_build_ext.run(self)
972 class install_pkgconfig(Command):
973 description = "install .pc file"
974 user_options = []
976 def initialize_options(self):
977 self.install_base = None
978 self.install_platbase = None
979 self.install_data = None
980 self.compiler_type = None
981 self.outfiles = []
983 def finalize_options(self):
984 self.set_undefined_options(
985 'install',
986 ('install_base', 'install_base'),
987 ('install_data', 'install_data'),
988 ('install_platbase', 'install_platbase'),
991 self.set_undefined_options(
992 'build_ext',
993 ('compiler_type', 'compiler_type'),
996 def get_outputs(self):
997 return self.outfiles
999 def get_inputs(self):
1000 return []
1002 def run(self):
1003 cmd = self.distribution.get_command_obj("bdist_wheel", create=False)
1004 if cmd is not None:
1005 log.warn(
1006 "Python wheels and pkg-config is not compatible. "
1007 "No pkg-config file will be included in the wheel. Install "
1008 "from source if you need one.")
1009 return
1011 if self.compiler_type == "msvc":
1012 return
1014 script_dir = get_script_dir()
1015 pkgconfig_in = os.path.join(script_dir, "pygobject-3.0.pc.in")
1016 with io.open(pkgconfig_in, "r", encoding="utf-8") as h:
1017 content = h.read()
1019 config = {
1020 "prefix": self.install_base,
1021 "exec_prefix": self.install_platbase,
1022 "includedir": "${prefix}/include",
1023 "datarootdir": "${prefix}/share",
1024 "datadir": "${datarootdir}",
1025 "VERSION": PYGOBJECT_VERISON,
1027 for key, value in config.items():
1028 content = content.replace("@%s@" % key, value)
1030 libdir = os.path.dirname(get_python_lib(True, True, self.install_data))
1031 pkgconfig_dir = os.path.join(libdir, "pkgconfig")
1032 self.mkpath(pkgconfig_dir)
1033 target = os.path.join(pkgconfig_dir, "pygobject-3.0.pc")
1034 with io.open(target, "w", encoding="utf-8") as h:
1035 h.write(content)
1036 self.outfiles.append(target)
1039 du_install = get_command_class("install")
1042 class install(du_install):
1044 sub_commands = du_install.sub_commands + [
1045 ("install_pkgconfig", lambda self: True),
1049 def main():
1050 script_dir = get_script_dir()
1051 pkginfo = parse_pkg_info(script_dir)
1052 gi_dir = os.path.join(script_dir, "gi")
1054 sources = [
1055 os.path.join("gi", n) for n in os.listdir(gi_dir)
1056 if os.path.splitext(n)[-1] == ".c"
1058 cairo_sources = [os.path.join("gi", "pygi-foreign-cairo.c")]
1059 for s in cairo_sources:
1060 sources.remove(s)
1062 readme = os.path.join(script_dir, "README.rst")
1063 with io.open(readme, encoding="utf-8") as h:
1064 long_description = h.read()
1066 gi_ext = Extension(
1067 name='gi._gi',
1068 sources=sources,
1069 include_dirs=[script_dir, gi_dir],
1070 depends=list_headers(script_dir) + list_headers(gi_dir),
1071 define_macros=[("PY_SSIZE_T_CLEAN", None)],
1074 gi_cairo_ext = Extension(
1075 name='gi._gi_cairo',
1076 sources=cairo_sources,
1077 include_dirs=[script_dir, gi_dir],
1078 depends=list_headers(script_dir) + list_headers(gi_dir),
1079 define_macros=[("PY_SSIZE_T_CLEAN", None)],
1082 version = pkginfo["Version"]
1083 if is_dev_version():
1084 # This makes it a PEP 440 pre-release and pip will only install it from
1085 # PyPI in case --pre is passed.
1086 version += ".dev"
1088 setup(
1089 name=pkginfo["Name"],
1090 version=version,
1091 description=pkginfo["Summary"],
1092 url=pkginfo["Home-page"],
1093 author=pkginfo["Author"],
1094 author_email=pkginfo["Author-email"],
1095 maintainer=pkginfo["Maintainer"],
1096 maintainer_email=pkginfo["Maintainer-email"],
1097 license=pkginfo["License"],
1098 long_description=long_description,
1099 platforms=pkginfo.get_all("Platform"),
1100 classifiers=pkginfo.get_all("Classifier"),
1101 packages=[
1102 "pygtkcompat",
1103 "gi",
1104 "gi.repository",
1105 "gi.overrides",
1107 ext_modules=[
1108 gi_ext,
1109 gi_cairo_ext,
1111 cmdclass={
1112 "build_ext": build_ext,
1113 "distcheck": distcheck,
1114 "sdist_gnome": sdist_gnome,
1115 "build_tests": build_tests,
1116 "test": test,
1117 "quality": quality,
1118 "install": install,
1119 "install_pkgconfig": install_pkgconfig,
1121 install_requires=[
1122 "pycairo>=%s" % get_version_requirement(
1123 get_pycairo_pkg_config_name()),
1125 data_files=[
1126 ('include/pygobject-3.0', ['gi/pygobject.h']),
1128 zip_safe=False,
1132 if __name__ == "__main__":
1133 main()