Imported Upstream version 3.32.2
[platform/upstream/python-gobject.git] / setup.py
1 #!/usr/bin/env python3
2 # Copyright 2017 Christoph Reiter <reiter.christoph@gmail.com>
3 #
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.
8 #
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.
13 #
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
18
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 import posixpath
28
29 from email import parser
30
31 try:
32     from setuptools import setup
33 except ImportError:
34     from distutils.core import setup
35
36 from distutils.core import Extension, Distribution, Command
37 from distutils.errors import DistutilsSetupError, DistutilsOptionError
38 from distutils.ccompiler import new_compiler
39 from distutils.sysconfig import get_python_lib, customize_compiler
40 from distutils import dir_util, log
41 from distutils.spawn import find_executable
42
43
44 PYGOBJECT_VERSION = "3.32.2"
45 GLIB_VERSION_REQUIRED = "2.48.0"
46 GI_VERSION_REQUIRED = "1.46.0"
47 PYCAIRO_VERSION_REQUIRED = "1.11.1"
48 LIBFFI_VERSION_REQUIRED = "3.0"
49
50 WITH_CAIRO = not bool(os.environ.get("PYGOBJECT_WITHOUT_PYCAIRO"))
51 """Set PYGOBJECT_WITHOUT_PYCAIRO if you don't want to build with
52 cairo/pycairo support. Note that this option might get removed in the future.
53 """
54
55
56 def is_dev_version():
57     version = tuple(map(int, PYGOBJECT_VERSION.split(".")))
58     return version[1] % 2 != 0
59
60
61 def get_command_class(name):
62     # Returns the right class for either distutils or setuptools
63     return Distribution({}).get_command_class(name)
64
65
66 def get_pycairo_pkg_config_name():
67     return "py3cairo" if sys.version_info[0] == 3 else "pycairo"
68
69
70 def get_version_requirement(pkg_config_name):
71     """Given a pkg-config module name gets the minimum version required"""
72
73     versions = {
74         "gobject-introspection-1.0": GI_VERSION_REQUIRED,
75         "glib-2.0": GLIB_VERSION_REQUIRED,
76         "gio-2.0": GLIB_VERSION_REQUIRED,
77         get_pycairo_pkg_config_name(): PYCAIRO_VERSION_REQUIRED,
78         "libffi": LIBFFI_VERSION_REQUIRED,
79         "cairo": "0",
80         "cairo-gobject": "0",
81     }
82
83     return versions[pkg_config_name]
84
85
86 def get_versions():
87     version = PYGOBJECT_VERSION.split(".")
88     assert len(version) == 3
89
90     versions = {
91         "PYGOBJECT_MAJOR_VERSION": version[0],
92         "PYGOBJECT_MINOR_VERSION": version[1],
93         "PYGOBJECT_MICRO_VERSION": version[2],
94         "VERSION": ".".join(version),
95     }
96     return versions
97
98
99 def parse_pkg_info(conf_dir):
100     """Returns an email.message.Message instance containing the content
101     of the PKG-INFO file.
102     """
103
104     versions = get_versions()
105
106     pkg_info = os.path.join(conf_dir, "PKG-INFO.in")
107     with io.open(pkg_info, "r", encoding="utf-8") as h:
108         text = h.read()
109         for key, value in versions.items():
110             text = text.replace("@%s@" % key, value)
111
112     p = parser.Parser()
113     message = p.parse(io.StringIO(text))
114     return message
115
116
117 def pkg_config_get_install_hint():
118     """Returns an installation hint for installing pkg-config or None"""
119
120     if not sys.platform.startswith("linux"):
121         return
122
123     if find_executable("apt"):
124         return "sudo apt install pkg-config"
125     elif find_executable("dnf"):
126         return "sudo dnf install pkg-config"
127
128
129 def pkg_config_get_package_install_hint(pkg_name):
130     """Returns an installation hint for a pkg-config name or None"""
131
132     if not sys.platform.startswith("linux"):
133         return
134
135     if find_executable("apt"):
136         dev_packages = {
137             "gobject-introspection-1.0": "libgirepository1.0-dev",
138             "glib-2.0": "libglib2.0-dev",
139             "gio-2.0": "libglib2.0-dev",
140             "cairo": "libcairo2-dev",
141             "cairo-gobject": "libcairo2-dev",
142             "libffi": "libffi-dev",
143         }
144         if pkg_name in dev_packages:
145             return "sudo apt install %s" % dev_packages[pkg_name]
146     elif find_executable("dnf"):
147         dev_packages = {
148             "gobject-introspection-1.0": "gobject-introspection-devel",
149             "glib-2.0": "glib2-devel",
150             "gio-2.0": "glib2-devel",
151             "cairo": "cairo-devel",
152             "cairo-gobject": "cairo-gobject-devel",
153             "libffi": "libffi-devel",
154         }
155         if pkg_name in dev_packages:
156             return "sudo dnf install %s" % dev_packages[pkg_name]
157
158
159 class PkgConfigError(Exception):
160     pass
161
162
163 class PkgConfigMissingError(PkgConfigError):
164     pass
165
166
167 class PkgConfigMissingPackageError(PkgConfigError):
168     pass
169
170
171 def _run_pkg_config(pkg_name, args, _cache={}):
172     """Raises PkgConfigError"""
173
174     command = tuple(["pkg-config"] + args)
175
176     if command not in _cache:
177         try:
178             result = subprocess.check_output(command)
179         except OSError as e:
180             if e.errno == errno.ENOENT:
181                 raise PkgConfigMissingError(
182                     "%r not found.\nArguments: %r" % (command[0], command))
183             raise PkgConfigError(e)
184         except subprocess.CalledProcessError as e:
185             try:
186                 subprocess.check_output(["pkg-config", "--exists", pkg_name])
187             except (subprocess.CalledProcessError, OSError):
188                 raise PkgConfigMissingPackageError(e)
189             else:
190                 raise PkgConfigError(e)
191         else:
192             _cache[command] = result
193
194     return _cache[command]
195
196
197 def _run_pkg_config_or_exit(pkg_name, args):
198     try:
199         return _run_pkg_config(pkg_name, args)
200     except PkgConfigMissingError as e:
201         hint = pkg_config_get_install_hint()
202         if hint:
203             raise SystemExit(
204                 "%s\n\nTry installing it with: %r" % (e, hint))
205         else:
206             raise SystemExit(e)
207     except PkgConfigMissingPackageError as e:
208         hint = pkg_config_get_package_install_hint(pkg_name)
209         if hint:
210             raise SystemExit(
211                 "%s\n\nTry installing it with: %r" % (e, hint))
212         else:
213             raise SystemExit(e)
214     except PkgConfigError as e:
215         raise SystemExit(e)
216
217
218 def pkg_config_version_check(pkg_name, version):
219     _run_pkg_config_or_exit(pkg_name, [
220         "--print-errors",
221         "--exists",
222         '%s >= %s' % (pkg_name, version),
223     ])
224
225
226 def pkg_config_parse(opt, pkg_name):
227     ret = _run_pkg_config_or_exit(pkg_name, [opt, pkg_name])
228
229     if sys.version_info[0] == 3:
230         output = ret.decode()
231     else:
232         output = ret
233     opt = opt[-2:]
234     return [x.lstrip(opt) for x in output.split()]
235
236
237 def list_headers(d):
238     return [os.path.join(d, e) for e in os.listdir(d) if e.endswith(".h")]
239
240
241 def filter_compiler_arguments(compiler, args):
242     """Given a compiler instance and a list of compiler warning flags
243     returns the list of supported flags.
244     """
245
246     if compiler.compiler_type == "msvc":
247         # TODO, not much of need for now.
248         return []
249
250     extra = []
251
252     def check_arguments(compiler, args):
253         p = subprocess.Popen(
254             [compiler.compiler[0]] + args + extra + ["-x", "c", "-E", "-"],
255             stdin=subprocess.PIPE,
256             stdout=subprocess.PIPE,
257             stderr=subprocess.PIPE)
258         stdout, stderr = p.communicate(b"int i;\n")
259         if p.returncode != 0:
260             text = stderr.decode("ascii", "replace")
261             return False, [a for a in args if a in text]
262         else:
263             return True, []
264
265     def check_argument(compiler, arg):
266         return check_arguments(compiler, [arg])[0]
267
268     # clang doesn't error out for unknown options, force it to
269     if check_argument(compiler, '-Werror=unknown-warning-option'):
270         extra += ['-Werror=unknown-warning-option']
271     if check_argument(compiler, '-Werror=unused-command-line-argument'):
272         extra += ['-Werror=unused-command-line-argument']
273
274     # first try to remove all arguments contained in the error message
275     supported = list(args)
276     while 1:
277         ok, maybe_unknown = check_arguments(compiler, supported)
278         if ok:
279             return supported
280         elif not maybe_unknown:
281             break
282         for unknown in maybe_unknown:
283             if not check_argument(compiler, unknown):
284                 supported.remove(unknown)
285
286     # hm, didn't work, try each argument one by one
287     supported = []
288     for arg in args:
289         if check_argument(compiler, arg):
290             supported.append(arg)
291     return supported
292
293
294 class sdist_gnome(Command):
295     description = "Create a source tarball for GNOME"
296     user_options = []
297
298     def initialize_options(self):
299         pass
300
301     def finalize_options(self):
302         pass
303
304     def run(self):
305         # Don't use PEP 440 pre-release versions for GNOME releases
306         self.distribution.metadata.version = PYGOBJECT_VERSION
307
308         dist_dir = tempfile.mkdtemp()
309         try:
310             cmd = self.reinitialize_command("sdist")
311             cmd.dist_dir = dist_dir
312             cmd.ensure_finalized()
313             cmd.run()
314
315             base_name = self.distribution.get_fullname().lower()
316             cmd.make_release_tree(base_name, cmd.filelist.files)
317             try:
318                 self.make_archive(base_name, "xztar", base_dir=base_name)
319             finally:
320                 dir_util.remove_tree(base_name)
321         finally:
322             dir_util.remove_tree(dist_dir)
323
324
325 du_sdist = get_command_class("sdist")
326
327
328 class distcheck(du_sdist):
329     """Creates a tarball and does some additional sanity checks such as
330     checking if the tarball includes all files, builds successfully and
331     the tests suite passes.
332     """
333
334     def _check_manifest(self):
335         # make sure MANIFEST.in includes all tracked files
336         assert self.get_archive_files()
337
338         if subprocess.call(["git", "status"],
339                            stdout=subprocess.PIPE,
340                            stderr=subprocess.PIPE) != 0:
341             return
342
343         included_files = self.filelist.files
344         assert included_files
345
346         process = subprocess.Popen(
347             ["git", "ls-tree", "-r", "HEAD", "--name-only"],
348             stdout=subprocess.PIPE, universal_newlines=True)
349         out, err = process.communicate()
350         assert process.returncode == 0
351
352         tracked_files = out.splitlines()
353         tracked_files = [
354             f for f in tracked_files
355             if os.path.basename(f) not in [".gitignore"]]
356
357         diff = set(tracked_files) - set(included_files)
358         assert not diff, (
359             "Not all tracked files included in tarball, check MANIFEST.in",
360             diff)
361
362     def _check_dist(self):
363         # make sure the tarball builds
364         assert self.get_archive_files()
365
366         distcheck_dir = os.path.abspath(
367             os.path.join(self.dist_dir, "distcheck"))
368         if os.path.exists(distcheck_dir):
369             dir_util.remove_tree(distcheck_dir)
370         self.mkpath(distcheck_dir)
371
372         archive = self.get_archive_files()[0]
373         tfile = tarfile.open(archive, "r:gz")
374         tfile.extractall(distcheck_dir)
375         tfile.close()
376
377         name = self.distribution.get_fullname()
378         extract_dir = os.path.join(distcheck_dir, name)
379
380         old_pwd = os.getcwd()
381         os.chdir(extract_dir)
382         try:
383             self.spawn([sys.executable, "setup.py", "build"])
384             self.spawn([sys.executable, "setup.py", "install",
385                         "--root",
386                         os.path.join(distcheck_dir, "prefix"),
387                         "--record",
388                         os.path.join(distcheck_dir, "log.txt"),
389                         ])
390             self.spawn([sys.executable, "setup.py", "test"])
391         finally:
392             os.chdir(old_pwd)
393
394     def run(self):
395         du_sdist.run(self)
396         self._check_manifest()
397         self._check_dist()
398
399
400 class build_tests(Command):
401     description = "build test libraries and extensions"
402     user_options = [
403         ("force", "f", "force a rebuild"),
404     ]
405
406     def initialize_options(self):
407         self.build_temp = None
408         self.build_base = None
409         self.force = False
410
411     def finalize_options(self):
412         self.set_undefined_options(
413             'build_ext',
414             ('build_temp', 'build_temp'))
415         self.set_undefined_options(
416             'build',
417             ('build_base', 'build_base'))
418
419     def _newer_group(self, sources, *targets):
420         assert targets
421
422         from distutils.dep_util import newer_group
423
424         if self.force:
425             return True
426         else:
427             for target in targets:
428                 if not newer_group(sources, target):
429                     return False
430             return True
431
432     def run(self):
433         cmd = self.reinitialize_command("build_ext")
434         cmd.inplace = True
435         cmd.force = self.force
436         cmd.ensure_finalized()
437         cmd.run()
438
439         gidatadir = pkg_config_parse(
440             "--variable=gidatadir", "gobject-introspection-1.0")[0]
441         g_ir_scanner = pkg_config_parse(
442             "--variable=g_ir_scanner", "gobject-introspection-1.0")[0]
443         g_ir_compiler = pkg_config_parse(
444             "--variable=g_ir_compiler", "gobject-introspection-1.0")[0]
445
446         script_dir = get_script_dir()
447         gi_dir = os.path.join(script_dir, "gi")
448         tests_dir = os.path.join(script_dir, "tests")
449         gi_tests_dir = os.path.join(gidatadir, "tests")
450
451         schema_xml = os.path.join(tests_dir, "org.gnome.test.gschema.xml")
452         schema_bin = os.path.join(tests_dir, "gschemas.compiled")
453         if self._newer_group([schema_xml], schema_bin):
454             subprocess.check_call([
455                 "glib-compile-schemas",
456                 "--targetdir=%s" % tests_dir,
457                 "--schema-file=%s" % schema_xml,
458             ])
459
460         compiler = new_compiler()
461         customize_compiler(compiler)
462
463         if os.name == "nt":
464             compiler.shared_lib_extension = ".dll"
465         elif sys.platform == "darwin":
466             compiler.shared_lib_extension = ".dylib"
467             if "-bundle" in compiler.linker_so:
468                 compiler.linker_so = list(compiler.linker_so)
469                 i = compiler.linker_so.index("-bundle")
470                 compiler.linker_so[i] = "-dynamiclib"
471         else:
472             compiler.shared_lib_extension = ".so"
473
474         if compiler.compiler_type == "msvc":
475             g_ir_scanner_cmd = [sys.executable, g_ir_scanner]
476         else:
477             g_ir_scanner_cmd = [g_ir_scanner]
478
479         def build_ext(ext):
480
481             libname = compiler.shared_object_filename(ext.name)
482             ext_paths = [os.path.join(tests_dir, libname)]
483             if os.name == "nt":
484                 if compiler.compiler_type == "msvc":
485                     # MSVC: Get rid of the 'lib' prefix and the .dll
486                     #       suffix from libname, and append .lib so
487                     #       that we get the right .lib filename to
488                     #       pass to g-ir-scanner with --library
489                     implibname = libname[3:libname.rfind(".dll")] + '.lib'
490                 else:
491                     implibname = libname + ".a"
492                 ext_paths.append(os.path.join(tests_dir, implibname))
493
494             if self._newer_group(ext.sources + ext.depends, *ext_paths):
495                 # MSVC: We need to define _GI_EXTERN explcitly so that
496                 #       symbols get exported properly
497                 if compiler.compiler_type == "msvc":
498                     extra_defines = [('_GI_EXTERN',
499                                       '__declspec(dllexport)extern')]
500                 else:
501                     extra_defines = []
502                 objects = compiler.compile(
503                     ext.sources,
504                     output_dir=self.build_temp,
505                     include_dirs=ext.include_dirs,
506                     macros=ext.define_macros + extra_defines)
507
508                 if os.name == "nt":
509                     if compiler.compiler_type == "msvc":
510                         postargs = ["-implib:%s" %
511                                     os.path.join(tests_dir, implibname)]
512                     else:
513                         postargs = ["-Wl,--out-implib=%s" %
514                                     os.path.join(tests_dir, implibname)]
515                 else:
516                     postargs = []
517
518                 compiler.link_shared_object(
519                     objects,
520                     compiler.shared_object_filename(ext.name),
521                     output_dir=tests_dir,
522                     libraries=ext.libraries,
523                     library_dirs=ext.library_dirs,
524                     extra_postargs=postargs)
525
526             return ext_paths
527
528         ext = Extension(
529             name='libgimarshallingtests',
530             sources=[
531                 os.path.join(gi_tests_dir, "gimarshallingtests.c"),
532                 os.path.join(tests_dir, "gimarshallingtestsextra.c"),
533             ],
534             include_dirs=[
535                 gi_tests_dir,
536                 tests_dir,
537             ],
538             depends=[
539                 os.path.join(gi_tests_dir, "gimarshallingtests.h"),
540                 os.path.join(tests_dir, "gimarshallingtestsextra.h"),
541             ],
542         )
543         add_ext_pkg_config_dep(ext, compiler.compiler_type, "glib-2.0")
544         add_ext_pkg_config_dep(ext, compiler.compiler_type, "gio-2.0")
545         ext_paths = build_ext(ext)
546
547         # We want to always use POSIX-style paths for g-ir-compiler
548         # because it expects the input .gir file and .typelib file to use
549         # POSIX-style paths, otherwise it fails
550         gir_path = posixpath.join(
551             tests_dir, "GIMarshallingTests-1.0.gir")
552         typelib_path = posixpath.join(
553             tests_dir, "GIMarshallingTests-1.0.typelib")
554
555         gimarshal_g_ir_scanner_cmd = g_ir_scanner_cmd + [
556             "--no-libtool",
557             "--include=Gio-2.0",
558             "--namespace=GIMarshallingTests",
559             "--nsversion=1.0",
560             "--symbol-prefix=gi_marshalling_tests",
561             "--warn-all",
562             "--warn-error",
563             "--library-path=%s" % tests_dir,
564             "--library=gimarshallingtests",
565             "--pkg=glib-2.0",
566             "--pkg=gio-2.0",
567             "--cflags-begin",
568             "-I%s" % gi_tests_dir,
569             "--cflags-end",
570             "--output=%s" % gir_path,
571         ]
572
573         if self._newer_group(ext_paths, gir_path):
574             subprocess.check_call(gimarshal_g_ir_scanner_cmd +
575                                   ext.sources + ext.depends)
576
577         if self._newer_group([gir_path], typelib_path):
578             subprocess.check_call([
579                 g_ir_compiler,
580                 gir_path,
581                 "--output=%s" % typelib_path,
582             ])
583
584         regress_macros = []
585         if not WITH_CAIRO:
586             regress_macros.append(("_GI_DISABLE_CAIRO", "1"))
587
588         ext = Extension(
589             name='libregress',
590             sources=[
591                 os.path.join(gi_tests_dir, "regress.c"),
592                 os.path.join(tests_dir, "regressextra.c"),
593             ],
594             include_dirs=[
595                 gi_tests_dir,
596             ],
597             depends=[
598                 os.path.join(gi_tests_dir, "regress.h"),
599                 os.path.join(tests_dir, "regressextra.h"),
600             ],
601             define_macros=regress_macros,
602         )
603         add_ext_pkg_config_dep(ext, compiler.compiler_type, "glib-2.0")
604         add_ext_pkg_config_dep(ext, compiler.compiler_type, "gio-2.0")
605         if WITH_CAIRO:
606             add_ext_pkg_config_dep(ext, compiler.compiler_type, "cairo")
607             add_ext_pkg_config_dep(
608                 ext, compiler.compiler_type, "cairo-gobject")
609         ext_paths = build_ext(ext)
610
611         # We want to always use POSIX-style paths for g-ir-compiler
612         # because it expects the input .gir file and .typelib file to use
613         # POSIX-style paths, otherwise it fails
614         gir_path = posixpath.join(tests_dir, "Regress-1.0.gir")
615         typelib_path = posixpath.join(tests_dir, "Regress-1.0.typelib")
616         regress_g_ir_scanner_cmd = g_ir_scanner_cmd + [
617             "--no-libtool",
618             "--include=Gio-2.0",
619             "--namespace=Regress",
620             "--nsversion=1.0",
621             "--warn-all",
622             "--warn-error",
623             "--library-path=%s" % tests_dir,
624             "--library=regress",
625             "--pkg=glib-2.0",
626             "--pkg=gio-2.0"]
627
628         if self._newer_group(ext_paths, gir_path):
629             if WITH_CAIRO:
630                 regress_g_ir_scanner_cmd += ["--include=cairo-1.0"]
631                 # MSVC: We don't normally have the pkg-config files for
632                 # cairo and cairo-gobject, so use --extra-library
633                 # instead of --pkg to pass those to the linker, so that
634                 # g-ir-scanner won't fail due to linker errors
635                 if compiler.compiler_type == "msvc":
636                     regress_g_ir_scanner_cmd += [
637                         "--extra-library=cairo",
638                         "--extra-library=cairo-gobject"]
639
640                 else:
641                     regress_g_ir_scanner_cmd += [
642                         "--pkg=cairo",
643                         "--pkg=cairo-gobject"]
644             else:
645                 regress_g_ir_scanner_cmd += ["-D_GI_DISABLE_CAIRO"]
646
647             regress_g_ir_scanner_cmd += ["--output=%s" % gir_path]
648
649             subprocess.check_call(regress_g_ir_scanner_cmd +
650                                   ext.sources + ext.depends)
651
652         if self._newer_group([gir_path], typelib_path):
653             subprocess.check_call([
654                 g_ir_compiler,
655                 gir_path,
656                 "--output=%s" % typelib_path,
657             ])
658
659         ext = Extension(
660             name='tests.testhelper',
661             sources=[
662                 os.path.join(tests_dir, "testhelpermodule.c"),
663                 os.path.join(tests_dir, "test-floating.c"),
664                 os.path.join(tests_dir, "test-thread.c"),
665                 os.path.join(tests_dir, "test-unknown.c"),
666             ],
667             include_dirs=[
668                 gi_dir,
669                 tests_dir,
670             ],
671             depends=list_headers(gi_dir) + list_headers(tests_dir),
672             define_macros=[("PY_SSIZE_T_CLEAN", None)],
673         )
674         add_ext_pkg_config_dep(ext, compiler.compiler_type, "glib-2.0")
675         add_ext_pkg_config_dep(ext, compiler.compiler_type, "gio-2.0")
676         add_ext_compiler_flags(ext, compiler)
677
678         dist = Distribution({"ext_modules": [ext]})
679
680         build_cmd = dist.get_command_obj("build")
681         build_cmd.build_base = os.path.join(self.build_base, "pygobject_tests")
682         build_cmd.ensure_finalized()
683
684         cmd = dist.get_command_obj("build_ext")
685         cmd.inplace = True
686         cmd.force = self.force
687         cmd.ensure_finalized()
688         cmd.run()
689
690
691 def get_suppression_files_for_prefix(prefix):
692     """Returns a list of valgrind suppression files for a given prefix"""
693
694     # Most specific first (/usr/share/doc is Fedora, /usr/lib is Debian)
695     # Take the first one found
696     major = str(sys.version_info[0])
697     minor = str(sys.version_info[1])
698     pyfiles = []
699     pyfiles.append(
700         os.path.join(
701             prefix, "share", "doc", "python%s%s" % (major, minor),
702             "valgrind-python.supp"))
703     pyfiles.append(
704         os.path.join(prefix, "lib", "valgrind", "python%s.supp" % major))
705     pyfiles.append(
706         os.path.join(
707             prefix, "share", "doc", "python%s-devel" % major,
708             "valgrind-python.supp"))
709     pyfiles.append(os.path.join(prefix, "lib", "valgrind", "python.supp"))
710
711     files = []
712     for f in pyfiles:
713         if os.path.isfile(f):
714             files.append(f)
715             break
716
717     files.append(os.path.join(
718         prefix, "share", "glib-2.0", "valgrind", "glib.supp"))
719     return [f for f in files if os.path.isfile(f)]
720
721
722 def get_real_prefix():
723     """Returns the base Python prefix, even in a virtualenv/venv"""
724
725     return getattr(sys, "base_prefix", getattr(sys, "real_prefix", sys.prefix))
726
727
728 def get_suppression_files():
729     """Returns a list of valgrind suppression files"""
730
731     prefixes = [
732         sys.prefix,
733         get_real_prefix(),
734         pkg_config_parse("--variable=prefix", "glib-2.0")[0],
735     ]
736
737     files = []
738     for prefix in prefixes:
739         files.extend(get_suppression_files_for_prefix(prefix))
740
741     files.append(os.path.join(get_script_dir(), "tests", "valgrind.supp"))
742     return sorted(set(files))
743
744
745 class test(Command):
746     user_options = [
747         ("valgrind", None, "run tests under valgrind"),
748         ("valgrind-log-file=", None, "save logs instead of printing them"),
749         ("gdb", None, "run tests under gdb"),
750         ("no-capture", "s", "don't capture test output"),
751     ]
752
753     def initialize_options(self):
754         self.valgrind = None
755         self.valgrind_log_file = None
756         self.gdb = None
757         self.no_capture = None
758
759     def finalize_options(self):
760         self.valgrind = bool(self.valgrind)
761         if self.valgrind_log_file and not self.valgrind:
762             raise DistutilsOptionError("valgrind not enabled")
763         self.gdb = bool(self.gdb)
764         self.no_capture = bool(self.no_capture)
765
766     def run(self):
767         cmd = self.reinitialize_command("build_tests")
768         cmd.ensure_finalized()
769         cmd.run()
770
771         env = os.environ.copy()
772         env.pop("MSYSTEM", None)
773
774         if self.no_capture:
775             env["PYGI_TEST_VERBOSE"] = "1"
776
777         env["MALLOC_PERTURB_"] = "85"
778         env["MALLOC_CHECK_"] = "3"
779         env["G_SLICE"] = "debug-blocks"
780
781         pre_args = []
782
783         if self.valgrind:
784             env["G_SLICE"] = "always-malloc"
785             env["G_DEBUG"] = "gc-friendly"
786             env["PYTHONMALLOC"] = "malloc"
787
788             pre_args += [
789                 "valgrind", "--leak-check=full", "--show-possibly-lost=no",
790                 "--num-callers=20", "--child-silent-after-fork=yes",
791             ] + ["--suppressions=" + f for f in get_suppression_files()]
792
793             if self.valgrind_log_file:
794                 pre_args += ["--log-file=" + self.valgrind_log_file]
795
796         if self.gdb:
797             env["PYGI_TEST_GDB"] = "1"
798             pre_args += ["gdb", "--args"]
799
800         if pre_args:
801             log.info(" ".join(pre_args))
802
803         tests_dir = os.path.join(get_script_dir(), "tests")
804         sys.exit(subprocess.call(pre_args + [
805             sys.executable,
806             os.path.join(tests_dir, "runtests.py"),
807         ], env=env))
808
809
810 class quality(Command):
811     description = "run code quality tests"
812     user_options = []
813
814     def initialize_options(self):
815         pass
816
817     def finalize_options(self):
818         pass
819
820     def run(self):
821         status = subprocess.call([
822             sys.executable, "-m", "flake8",
823         ], cwd=get_script_dir())
824         if status != 0:
825             raise SystemExit(status)
826
827
828 def get_script_dir():
829     return os.path.dirname(os.path.realpath(__file__))
830
831
832 def get_pycairo_include_dir():
833     """Returns the best guess at where to find the pycairo headers.
834     A bit convoluted because we have to deal with multiple pycairo
835     versions.
836
837     Raises if pycairo isn't found or it's too old.
838     """
839
840     pkg_config_name = get_pycairo_pkg_config_name()
841     min_version = get_version_requirement(pkg_config_name)
842     min_version_info = tuple(int(p) for p in min_version.split("."))
843
844     def check_path(include_dir):
845         log.info("pycairo: trying include directory: %r" % include_dir)
846         header_path = os.path.join(include_dir, "%s.h" % pkg_config_name)
847         if os.path.exists(header_path):
848             log.info("pycairo: found %r" % header_path)
849             return True
850         log.info("pycairo: header file (%r) not found" % header_path)
851         return False
852
853     def find_path(paths):
854         for p in reversed(paths):
855             if check_path(p):
856                 return p
857
858     def find_new_api():
859         log.info("pycairo: new API")
860         import cairo
861
862         if cairo.version_info < min_version_info:
863             raise DistutilsSetupError(
864                 "pycairo >= %s required, %s found." % (
865                     min_version, ".".join(map(str, cairo.version_info))))
866
867         if hasattr(cairo, "get_include"):
868             return [cairo.get_include()]
869         log.info("pycairo: no get_include()")
870         return []
871
872     def find_old_api():
873         log.info("pycairo: old API")
874
875         import cairo
876
877         if cairo.version_info < min_version_info:
878             raise DistutilsSetupError(
879                 "pycairo >= %s required, %s found." % (
880                     min_version, ".".join(map(str, cairo.version_info))))
881
882         location = os.path.dirname(os.path.abspath(cairo.__path__[0]))
883         log.info("pycairo: found %r" % location)
884
885         def samefile(src, dst):
886             # Python 2 on Windows doesn't have os.path.samefile, so we have to
887             # provide a fallback
888             if hasattr(os.path, "samefile"):
889                 return os.path.samefile(src, dst)
890             os.stat(src)
891             os.stat(dst)
892             return (os.path.normcase(os.path.abspath(src)) ==
893                     os.path.normcase(os.path.abspath(dst)))
894
895         def get_sys_path(location, name):
896             # Returns the sysconfig path for a distribution, or None
897             for scheme in sysconfig.get_scheme_names():
898                 for path_type in ["platlib", "purelib"]:
899                     path = sysconfig.get_path(path_type, scheme)
900                     try:
901                         if samefile(path, location):
902                             return sysconfig.get_path(name, scheme)
903                     except EnvironmentError:
904                         pass
905
906         data_path = get_sys_path(location, "data") or sys.prefix
907         return [os.path.join(data_path, "include", "pycairo")]
908
909     def find_pkg_config():
910         log.info("pycairo: pkg-config")
911         pkg_config_version_check(pkg_config_name, min_version)
912         return pkg_config_parse("--cflags-only-I", pkg_config_name)
913
914     # First the new get_include() API added in >1.15.6
915     include_dir = find_path(find_new_api())
916     if include_dir is not None:
917         return include_dir
918
919     # Then try to find it in the data prefix based on the module path.
920     # This works with many virtualenv/userdir setups, but not all apparently,
921     # see https://gitlab.gnome.org/GNOME/pygobject/issues/150
922     include_dir = find_path(find_old_api())
923     if include_dir is not None:
924         return include_dir
925
926     # Finally, fall back to pkg-config
927     include_dir = find_path(find_pkg_config())
928     if include_dir is not None:
929         return include_dir
930
931     raise DistutilsSetupError("Could not find pycairo headers")
932
933
934 def add_ext_pkg_config_dep(ext, compiler_type, name):
935     msvc_libraries = {
936         "glib-2.0": ["glib-2.0"],
937         "gio-2.0": ["gio-2.0", "gobject-2.0", "glib-2.0"],
938         "gobject-introspection-1.0":
939             ["girepository-1.0", "gobject-2.0", "glib-2.0"],
940         "cairo": ["cairo"],
941         "cairo-gobject":
942             ["cairo-gobject", "cairo", "gobject-2.0", "glib-2.0"],
943         "libffi": ["ffi"],
944     }
945
946     def add(target, new):
947         for entry in new:
948             if entry not in target:
949                 target.append(entry)
950
951     fallback_libs = msvc_libraries[name]
952     if compiler_type == "msvc":
953         # assume that INCLUDE and LIB contains the right paths
954         add(ext.libraries, fallback_libs)
955     else:
956         min_version = get_version_requirement(name)
957         pkg_config_version_check(name, min_version)
958         add(ext.include_dirs, pkg_config_parse("--cflags-only-I", name))
959         add(ext.library_dirs, pkg_config_parse("--libs-only-L", name))
960         add(ext.libraries, pkg_config_parse("--libs-only-l", name))
961
962
963 def add_ext_compiler_flags(ext, compiler, _cache={}):
964     if compiler.compiler_type == "msvc":
965         # MSVC: Just force-include msvc_recommended_pragmas.h so that
966         #       we can look out for compiler warnings that we really
967         #       want to look out for, and filter out those that don't
968         #       really matter to us.
969         ext.extra_compile_args += ['-FImsvc_recommended_pragmas.h']
970     else:
971         cache_key = compiler.compiler[0]
972         if cache_key not in _cache:
973
974             args = [
975                 "-Wall",
976                 "-Warray-bounds",
977                 "-Wcast-align",
978                 "-Wduplicated-branches",
979                 "-Wextra",
980                 "-Wformat=2",
981                 "-Wformat-nonliteral",
982                 "-Wformat-security",
983                 "-Wimplicit-function-declaration",
984                 "-Winit-self",
985                 "-Wjump-misses-init",
986                 "-Wlogical-op",
987                 "-Wmissing-declarations",
988                 "-Wmissing-format-attribute",
989                 "-Wmissing-include-dirs",
990                 "-Wmissing-noreturn",
991                 "-Wmissing-prototypes",
992                 "-Wnested-externs",
993                 "-Wnull-dereference",
994                 "-Wold-style-definition",
995                 "-Wpacked",
996                 "-Wpointer-arith",
997                 "-Wrestrict",
998                 "-Wreturn-type",
999                 "-Wshadow",
1000                 "-Wsign-compare",
1001                 "-Wstrict-aliasing",
1002                 "-Wstrict-prototypes",
1003                 "-Wswitch-default",
1004                 "-Wundef",
1005                 "-Wunused-but-set-variable",
1006                 "-Wwrite-strings",
1007             ]
1008
1009             if sys.version_info[0] == 2:
1010                 args += [
1011                     "-Wdeclaration-after-statement",
1012                 ]
1013
1014             args += [
1015                 "-Wno-incompatible-pointer-types-discards-qualifiers",
1016                 "-Wno-missing-field-initializers",
1017                 "-Wno-unused-parameter",
1018                 "-Wno-discarded-qualifiers",
1019                 "-Wno-sign-conversion",
1020                 "-Wno-cast-function-type",
1021                 "-Wno-int-conversion",
1022             ]
1023
1024             # silence clang for unused gcc CFLAGS added by Debian
1025             args += [
1026                 "-Wno-unused-command-line-argument",
1027             ]
1028
1029             args += [
1030                 "-fno-strict-aliasing",
1031                 "-fvisibility=hidden",
1032             ]
1033
1034             # force GCC to use colors
1035             if hasattr(sys.stdout, "isatty") and sys.stdout.isatty():
1036                 args.append("-fdiagnostics-color")
1037
1038             _cache[cache_key] = filter_compiler_arguments(compiler, args)
1039
1040         ext.extra_compile_args += _cache[cache_key]
1041
1042
1043 du_build_ext = get_command_class("build_ext")
1044
1045
1046 class build_ext(du_build_ext):
1047
1048     def initialize_options(self):
1049         du_build_ext.initialize_options(self)
1050         self.compiler_type = None
1051
1052     def finalize_options(self):
1053         du_build_ext.finalize_options(self)
1054         self.compiler_type = new_compiler(compiler=self.compiler).compiler_type
1055
1056     def _write_config_h(self):
1057         script_dir = get_script_dir()
1058         target = os.path.join(script_dir, "config.h")
1059         versions = get_versions()
1060         content = u"""
1061 /* Configuration header created by setup.py - do not edit */
1062 #ifndef _CONFIG_H
1063 #define _CONFIG_H 1
1064
1065 #define PYGOBJECT_MAJOR_VERSION %(PYGOBJECT_MAJOR_VERSION)s
1066 #define PYGOBJECT_MINOR_VERSION %(PYGOBJECT_MINOR_VERSION)s
1067 #define PYGOBJECT_MICRO_VERSION %(PYGOBJECT_MICRO_VERSION)s
1068 #define VERSION "%(VERSION)s"
1069
1070 #endif /* _CONFIG_H */
1071 """ % versions
1072
1073         try:
1074             with io.open(target, 'r', encoding="utf-8") as h:
1075                 if h.read() == content:
1076                     return
1077         except EnvironmentError:
1078             pass
1079
1080         with io.open(target, 'w', encoding="utf-8") as h:
1081             h.write(content)
1082
1083     def _setup_extensions(self):
1084         ext = {e.name: e for e in self.extensions}
1085
1086         compiler = new_compiler(compiler=self.compiler)
1087         customize_compiler(compiler)
1088
1089         def add_dependency(ext, name):
1090             add_ext_pkg_config_dep(ext, compiler.compiler_type, name)
1091
1092         def add_pycairo(ext):
1093             ext.include_dirs += [get_pycairo_include_dir()]
1094
1095         gi_ext = ext["gi._gi"]
1096         add_dependency(gi_ext, "glib-2.0")
1097         add_dependency(gi_ext, "gio-2.0")
1098         add_dependency(gi_ext, "gobject-introspection-1.0")
1099         add_dependency(gi_ext, "libffi")
1100         add_ext_compiler_flags(gi_ext, compiler)
1101
1102         if WITH_CAIRO:
1103             gi_cairo_ext = ext["gi._gi_cairo"]
1104             add_dependency(gi_cairo_ext, "glib-2.0")
1105             add_dependency(gi_cairo_ext, "gio-2.0")
1106             add_dependency(gi_cairo_ext, "gobject-introspection-1.0")
1107             add_dependency(gi_cairo_ext, "libffi")
1108             add_dependency(gi_cairo_ext, "cairo")
1109             add_dependency(gi_cairo_ext, "cairo-gobject")
1110             add_pycairo(gi_cairo_ext)
1111             add_ext_compiler_flags(gi_cairo_ext, compiler)
1112
1113     def run(self):
1114         self._write_config_h()
1115         self._setup_extensions()
1116         du_build_ext.run(self)
1117
1118
1119 class install_pkgconfig(Command):
1120     description = "install .pc file"
1121     user_options = []
1122
1123     def initialize_options(self):
1124         self.install_base = None
1125         self.install_platbase = None
1126         self.install_data = None
1127         self.compiler_type = None
1128         self.outfiles = []
1129
1130     def finalize_options(self):
1131         self.set_undefined_options(
1132             'install',
1133             ('install_base', 'install_base'),
1134             ('install_data', 'install_data'),
1135             ('install_platbase', 'install_platbase'),
1136         )
1137
1138         self.set_undefined_options(
1139             'build_ext',
1140             ('compiler_type', 'compiler_type'),
1141         )
1142
1143     def get_outputs(self):
1144         return self.outfiles
1145
1146     def get_inputs(self):
1147         return []
1148
1149     def run(self):
1150         cmd = self.distribution.get_command_obj("bdist_wheel", create=False)
1151         if cmd is not None:
1152             log.warn(
1153                 "Python wheels and pkg-config is not compatible. "
1154                 "No pkg-config file will be included in the wheel. Install "
1155                 "from source if you need one.")
1156             return
1157
1158         if self.compiler_type == "msvc":
1159             return
1160
1161         script_dir = get_script_dir()
1162         pkgconfig_in = os.path.join(script_dir, "pygobject-3.0.pc.in")
1163         with io.open(pkgconfig_in, "r", encoding="utf-8") as h:
1164             content = h.read()
1165
1166         config = {
1167             "prefix": self.install_base,
1168             "exec_prefix": self.install_platbase,
1169             "includedir": "${prefix}/include",
1170             "datarootdir": "${prefix}/share",
1171             "datadir": "${datarootdir}",
1172             "VERSION": PYGOBJECT_VERSION,
1173         }
1174         for key, value in config.items():
1175             content = content.replace("@%s@" % key, value)
1176
1177         libdir = os.path.dirname(get_python_lib(True, True, self.install_data))
1178         pkgconfig_dir = os.path.join(libdir, "pkgconfig")
1179         self.mkpath(pkgconfig_dir)
1180         target = os.path.join(pkgconfig_dir, "pygobject-3.0.pc")
1181         with io.open(target, "w", encoding="utf-8") as h:
1182             h.write(content)
1183         self.outfiles.append(target)
1184
1185
1186 du_install = get_command_class("install")
1187
1188
1189 class install(du_install):
1190
1191     sub_commands = du_install.sub_commands + [
1192         ("install_pkgconfig", lambda self: True),
1193     ]
1194
1195
1196 def main():
1197     script_dir = get_script_dir()
1198     pkginfo = parse_pkg_info(script_dir)
1199     gi_dir = os.path.join(script_dir, "gi")
1200
1201     sources = [
1202         os.path.join("gi", n) for n in os.listdir(gi_dir)
1203         if os.path.splitext(n)[-1] == ".c"
1204     ]
1205     cairo_sources = [os.path.join("gi", "pygi-foreign-cairo.c")]
1206     for s in cairo_sources:
1207         sources.remove(s)
1208
1209     readme = os.path.join(script_dir, "README.rst")
1210     with io.open(readme, encoding="utf-8") as h:
1211         long_description = h.read()
1212
1213     ext_modules = []
1214     install_requires = []
1215
1216     gi_ext = Extension(
1217         name='gi._gi',
1218         sources=sorted(sources),
1219         include_dirs=[script_dir, gi_dir],
1220         depends=list_headers(script_dir) + list_headers(gi_dir),
1221         define_macros=[("PY_SSIZE_T_CLEAN", None)],
1222     )
1223     ext_modules.append(gi_ext)
1224
1225     if WITH_CAIRO:
1226         gi_cairo_ext = Extension(
1227             name='gi._gi_cairo',
1228             sources=cairo_sources,
1229             include_dirs=[script_dir, gi_dir],
1230             depends=list_headers(script_dir) + list_headers(gi_dir),
1231             define_macros=[("PY_SSIZE_T_CLEAN", None)],
1232         )
1233         ext_modules.append(gi_cairo_ext)
1234         install_requires.append(
1235             "pycairo>=%s" % get_version_requirement(
1236                 get_pycairo_pkg_config_name()))
1237
1238     version = pkginfo["Version"]
1239     if is_dev_version():
1240         # This makes it a PEP 440 pre-release and pip will only install it from
1241         # PyPI in case --pre is passed.
1242         version += ".dev0"
1243
1244     setup(
1245         name=pkginfo["Name"],
1246         version=version,
1247         description=pkginfo["Summary"],
1248         url=pkginfo["Home-page"],
1249         author=pkginfo["Author"],
1250         author_email=pkginfo["Author-email"],
1251         maintainer=pkginfo["Maintainer"],
1252         maintainer_email=pkginfo["Maintainer-email"],
1253         license=pkginfo["License"],
1254         long_description=long_description,
1255         platforms=pkginfo.get_all("Platform"),
1256         classifiers=pkginfo.get_all("Classifier"),
1257         packages=[
1258             "pygtkcompat",
1259             "gi",
1260             "gi.repository",
1261             "gi.overrides",
1262         ],
1263         ext_modules=ext_modules,
1264         cmdclass={
1265             "build_ext": build_ext,
1266             "distcheck": distcheck,
1267             "sdist_gnome": sdist_gnome,
1268             "build_tests": build_tests,
1269             "test": test,
1270             "quality": quality,
1271             "install": install,
1272             "install_pkgconfig": install_pkgconfig,
1273         },
1274         install_requires=install_requires,
1275         python_requires=(
1276             '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4'),
1277         data_files=[
1278             ('include/pygobject-3.0', ['gi/pygobject.h']),
1279         ],
1280         zip_safe=False,
1281     )
1282
1283
1284 if __name__ == "__main__":
1285     main()