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