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