2 # Copyright 2017 Christoph Reiter <reiter.christoph@gmail.com>
4 # This library is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU Lesser General Public
6 # License as published by the Free Software Foundation; either
7 # version 2.1 of the License, or (at your option) any later version.
9 # This library is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # Lesser General Public License for more details.
14 # You should have received a copy of the GNU Lesser General Public
15 # License along with this library; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
27 from email
import parser
30 from setuptools
import setup
, find_packages
31 from distutils
.core
import Extension
, Distribution
, Command
32 from distutils
.errors
import DistutilsSetupError
33 from distutils
.ccompiler
import new_compiler
34 from distutils
.sysconfig
import get_python_lib
35 from distutils
import dir_util
, log
38 def get_command_class(name
):
39 # Returns the right class for either distutils or setuptools
40 return Distribution({}).get_command_class(name
)
43 def get_pycairo_pkg_config_name():
44 return "py3cairo" if sys
.version_info
[0] == 3 else "pycairo"
47 def get_version_requirement(conf_dir
, pkg_config_name
):
48 """Given a pkg-config module name gets the minimum version required"""
50 if pkg_config_name
in ["cairo", "cairo-gobject"]:
54 "gobject-introspection-1.0": "introspection",
57 get_pycairo_pkg_config_name(): "pycairo",
60 assert pkg_config_name
in mapping
62 configure_ac
= os
.path
.join(conf_dir
, "configure.ac")
63 with io
.open(configure_ac
, "r", encoding
="utf-8") as h
:
65 conf_name
= mapping
[pkg_config_name
]
67 r
"%s_required_version,\s*([\d\.]+)\)" % conf_name
, text
)
72 def parse_versions(conf_dir
):
73 configure_ac
= os
.path
.join(conf_dir
, "configure.ac")
74 with io
.open(configure_ac
, "r", encoding
="utf-8") as h
:
75 version
= re
.findall(r
"pygobject_[^\s]+_version,\s*(\d+)\)", h
.read())
76 assert len(version
) == 3
79 "PYGOBJECT_MAJOR_VERSION": version
[0],
80 "PYGOBJECT_MINOR_VERSION": version
[1],
81 "PYGOBJECT_MICRO_VERSION": version
[2],
82 "VERSION": ".".join(version
),
87 def parse_pkg_info(conf_dir
):
88 """Returns an email.message.Message instance containing the content
89 of the PKG-INFO file. The version info is parsed from configure.ac
92 versions
= parse_versions(conf_dir
)
94 pkg_info
= os
.path
.join(conf_dir
, "PKG-INFO.in")
95 with io
.open(pkg_info
, "r", encoding
="utf-8") as h
:
97 for key
, value
in versions
.items():
98 text
= text
.replace("@%s@" % key
, value
)
101 message
= p
.parse(io
.StringIO(text
))
105 def _run_pkg_config(args
):
106 command
= ["pkg-config"] + args
109 return subprocess
.check_output(command
)
111 if e
.errno
== errno
.ENOENT
:
113 "%r not found.\nArguments: %r" % (command
[0], command
))
115 except subprocess
.CalledProcessError
as e
:
119 def pkg_config_version_check(pkg
, version
):
123 '%s >= %s' % (pkg
, version
),
127 def pkg_config_parse(opt
, pkg
):
128 ret
= _run_pkg_config([opt
, pkg
])
129 if sys
.version_info
[0] == 3:
130 output
= ret
.decode()
134 return [x
.lstrip(opt
) for x
in output
.split()]
137 du_sdist
= get_command_class("sdist")
140 class distcheck(du_sdist
):
141 """Creates a tarball and does some additional sanity checks such as
142 checking if the tarball includes all files, builds successfully and
143 the tests suite passes.
146 def _check_manifest(self
):
147 # make sure MANIFEST.in includes all tracked files
148 assert self
.get_archive_files()
150 if subprocess
.call(["git", "status"],
151 stdout
=subprocess
.PIPE
,
152 stderr
=subprocess
.PIPE
) != 0:
155 included_files
= self
.filelist
.files
156 assert included_files
158 process
= subprocess
.Popen(
159 ["git", "ls-tree", "-r", "HEAD", "--name-only"],
160 stdout
=subprocess
.PIPE
, universal_newlines
=True)
161 out
, err
= process
.communicate()
162 assert process
.returncode
== 0
164 tracked_files
= out
.splitlines()
165 for ignore
in [".gitignore"]:
166 tracked_files
.remove(ignore
)
168 diff
= set(tracked_files
) - set(included_files
)
170 "Not all tracked files included in tarball, check MANIFEST.in",
173 def _check_dist(self
):
174 # make sure the tarball builds
175 assert self
.get_archive_files()
177 distcheck_dir
= os
.path
.abspath(
178 os
.path
.join(self
.dist_dir
, "distcheck"))
179 if os
.path
.exists(distcheck_dir
):
180 dir_util
.remove_tree(distcheck_dir
)
181 self
.mkpath(distcheck_dir
)
183 archive
= self
.get_archive_files()[0]
184 tfile
= tarfile
.open(archive
, "r:gz")
185 tfile
.extractall(distcheck_dir
)
188 name
= self
.distribution
.get_fullname()
189 extract_dir
= os
.path
.join(distcheck_dir
, name
)
191 old_pwd
= os
.getcwd()
192 os
.chdir(extract_dir
)
194 self
.spawn([sys
.executable
, "setup.py", "build"])
195 self
.spawn([sys
.executable
, "setup.py", "install",
197 os
.path
.join(distcheck_dir
, "prefix"),
199 os
.path
.join(distcheck_dir
, "log.txt"),
201 self
.spawn([sys
.executable
, "setup.py", "test"])
207 self
._check
_manifest
()
211 class build_tests(Command
):
212 description
= "build test libraries and extensions"
214 ("force", "f", "force a rebuild"),
217 def initialize_options(self
):
218 self
.build_temp
= None
221 def finalize_options(self
):
222 self
.set_undefined_options(
224 ('build_temp', 'build_temp'))
226 def _newer_group(self
, sources
, *targets
):
229 from distutils
.dep_util
import newer_group
234 for target
in targets
:
235 if not newer_group(sources
, target
):
240 from distutils
.ccompiler
import new_compiler
241 from distutils
.sysconfig
import customize_compiler
243 gidatadir
= pkg_config_parse(
244 "--variable=gidatadir", "gobject-introspection-1.0")[0]
245 g_ir_scanner
= pkg_config_parse(
246 "--variable=g_ir_scanner", "gobject-introspection-1.0")[0]
247 g_ir_compiler
= pkg_config_parse(
248 "--variable=g_ir_compiler", "gobject-introspection-1.0")[0]
250 script_dir
= get_script_dir()
251 tests_dir
= os
.path
.join(script_dir
, "tests")
252 gi_tests_dir
= os
.path
.join(gidatadir
, "tests")
254 schema_xml
= os
.path
.join(tests_dir
, "org.gnome.test.gschema.xml")
255 schema_bin
= os
.path
.join(tests_dir
, "gschemas.compiled")
256 if self
._newer
_group
([schema_xml
], schema_bin
):
257 subprocess
.check_call([
258 "glib-compile-schemas",
259 "--targetdir=%s" % tests_dir
,
260 "--schema-file=%s" % schema_xml
,
263 compiler
= new_compiler()
264 customize_compiler(compiler
)
267 compiler
.shared_lib_extension
= ".dll"
269 if sys
.platform
== "darwin":
270 compiler
.shared_lib_extension
= ".dylib"
271 if "-bundle" in compiler
.linker_so
:
272 compiler
.linker_so
= list(compiler
.linker_so
)
273 i
= compiler
.linker_so
.index("-bundle")
274 compiler
.linker_so
[i
] = "-dynamiclib"
277 if compiler
.compiler_type
== "msvc":
278 raise Exception("MSVC support not implemented")
280 libname
= compiler
.shared_object_filename(ext
.name
)
281 ext_paths
= [os
.path
.join(tests_dir
, libname
)]
283 implibname
= libname
+ ".a"
284 ext_paths
.append(os
.path
.join(tests_dir
, implibname
))
286 if self
._newer
_group
(ext
.sources
+ ext
.depends
, *ext_paths
):
287 objects
= compiler
.compile(
289 output_dir
=self
.build_temp
,
290 include_dirs
=ext
.include_dirs
)
293 postargs
= ["-Wl,--out-implib=%s" %
294 os
.path
.join(tests_dir
, implibname
)]
298 compiler
.link_shared_object(
300 compiler
.shared_object_filename(ext
.name
),
301 output_dir
=tests_dir
,
302 libraries
=ext
.libraries
,
303 library_dirs
=ext
.library_dirs
,
304 extra_postargs
=postargs
)
309 name
='libgimarshallingtests',
311 os
.path
.join(gi_tests_dir
, "gimarshallingtests.c"),
312 os
.path
.join(tests_dir
, "gimarshallingtestsextra.c"),
319 os
.path
.join(gi_tests_dir
, "gimarshallingtests.h"),
320 os
.path
.join(tests_dir
, "gimarshallingtestsextra.h"),
323 add_ext_pkg_config_dep(ext
, compiler
.compiler_type
, "glib-2.0")
324 add_ext_pkg_config_dep(ext
, compiler
.compiler_type
, "gio-2.0")
325 ext_paths
= build_ext(ext
)
327 gir_path
= os
.path
.join(tests_dir
, "GIMarshallingTests-1.0.gir")
328 typelib_path
= os
.path
.join(
329 tests_dir
, "GIMarshallingTests-1.0.typelib")
331 if self
._newer
_group
(ext_paths
, gir_path
):
332 subprocess
.check_call([
336 "--namespace=GIMarshallingTests",
338 "--symbol-prefix=gi_marshalling_tests",
341 "--library-path=%s" % tests_dir
,
342 "--library=gimarshallingtests",
345 "--output=%s" % gir_path
,
346 ] + ext
.sources
+ ext
.depends
)
348 if self
._newer
_group
([gir_path
], typelib_path
):
349 subprocess
.check_call([
352 "--output=%s" % typelib_path
,
358 os
.path
.join(gi_tests_dir
, "regress.c"),
364 os
.path
.join(gi_tests_dir
, "regress.h"),
367 add_ext_pkg_config_dep(ext
, compiler
.compiler_type
, "glib-2.0")
368 add_ext_pkg_config_dep(ext
, compiler
.compiler_type
, "gio-2.0")
369 add_ext_pkg_config_dep(ext
, compiler
.compiler_type
, "cairo")
370 add_ext_pkg_config_dep(ext
, compiler
.compiler_type
, "cairo-gobject")
371 ext_paths
= build_ext(ext
)
373 gir_path
= os
.path
.join(tests_dir
, "Regress-1.0.gir")
374 typelib_path
= os
.path
.join(tests_dir
, "Regress-1.0.typelib")
376 if self
._newer
_group
(ext_paths
, gir_path
):
377 subprocess
.check_call([
380 "--include=cairo-1.0",
382 "--namespace=Regress",
386 "--library-path=%s" % tests_dir
,
391 "--pkg=cairo-gobject",
392 "--output=%s" % gir_path
,
393 ] + ext
.sources
+ ext
.depends
)
395 if self
._newer
_group
([gir_path
], typelib_path
):
396 subprocess
.check_call([
399 "--output=%s" % typelib_path
,
403 name
='tests.testhelper',
405 os
.path
.join(tests_dir
, "testhelpermodule.c"),
406 os
.path
.join(tests_dir
, "test-floating.c"),
407 os
.path
.join(tests_dir
, "test-thread.c"),
408 os
.path
.join(tests_dir
, "test-unknown.c"),
411 os
.path
.join(script_dir
, "gi"),
415 os
.path
.join(tests_dir
, "test-thread.h"),
416 os
.path
.join(tests_dir
, "test-unknown.h"),
417 os
.path
.join(tests_dir
, "test-floating.h"),
420 add_ext_pkg_config_dep(ext
, compiler
.compiler_type
, "glib-2.0")
421 add_ext_pkg_config_dep(ext
, compiler
.compiler_type
, "gio-2.0")
422 add_ext_pkg_config_dep(ext
, compiler
.compiler_type
, "cairo")
424 dist
= Distribution({"ext_modules": [ext
]})
425 cmd
= dist
.get_command_obj("build_ext")
427 cmd
.ensure_finalized()
434 def initialize_options(self
):
437 def finalize_options(self
):
441 cmd
= self
.reinitialize_command("build_ext")
443 cmd
.ensure_finalized()
446 cmd
= self
.reinitialize_command("build_tests")
447 cmd
.ensure_finalized()
450 env
= os
.environ
.copy()
451 env
.pop("MSYSTEM", None)
455 subprocess
.check_call(["dbus-run-session", "--", "true"])
456 except (EnvironmentError, subprocess
.CalledProcessError
):
457 # Spawning a bus failed, disable dbus instead of inheriting
459 env
["DBUS_SESSION_BUS_ADDRESS"] = ""
461 pre_args
= ["dbus-run-session", "--"]
463 tests_dir
= os
.path
.join(get_script_dir(), "tests")
464 subprocess
.check_call(pre_args
+ [
466 os
.path
.join(tests_dir
, "runtests.py"),
469 if not env
.get("TEST_NAMES"):
470 env
["TEST_NAMES"] = "compat_test_pygtk"
471 subprocess
.check_call(pre_args
+ [
473 os
.path
.join(tests_dir
, "runtests.py"),
477 class quality(Command
):
478 description
= "run code quality tests"
481 def initialize_options(self
):
484 def finalize_options(self
):
488 status
= subprocess
.call([
489 sys
.executable
, "-m", "flake8",
490 ], cwd
=get_script_dir())
492 raise SystemExit(status
)
495 def get_script_dir():
496 return os
.path
.dirname(os
.path
.realpath(__file__
))
499 def get_pycairo_include_dir():
500 """Returns the best guess at where to find the pycairo headers.
501 A bit convoluted because we have to deal with multiple pycairo
504 Raises if pycairo isn't found or it's too old.
507 script_dir
= get_script_dir()
508 pkg_config_name
= get_pycairo_pkg_config_name()
509 min_version
= get_version_requirement(script_dir
, pkg_config_name
)
511 def check_path(include_dir
):
512 log
.info("pycairo: trying include directory: %r" % include_dir
)
513 header_path
= os
.path
.join(include_dir
, "%s.h" % pkg_config_name
)
514 if os
.path
.exists(header_path
):
515 log
.info("pycairo: found %r" % header_path
)
517 log
.info("pycairo: header file (%r) not found" % header_path
)
520 def find_path(paths
):
521 for p
in reversed(paths
):
526 log
.info("pycairo: new API")
529 pkg_version
= pkg_resources
.parse_version(cairo
.version
)
530 pkg_min_version
= pkg_resources
.parse_version(min_version
)
531 if pkg_version
< pkg_min_version
:
532 raise DistutilsSetupError(
533 "pycairo >=%s required, %s found." % (
534 pkg_min_version
, pkg_version
))
536 if hasattr(cairo
, "get_include"):
537 return [cairo
.get_include()]
538 log
.info("pycairo: no get_include()")
542 log
.info("pycairo: old API")
543 dist
= pkg_resources
.get_distribution("pycairo>=%s" % min_version
)
544 log
.info("pycairo: found %r" % dist
)
546 def samefile(src
, dst
):
547 # Python 2 on Windows doesn't have os.path.samefile, so we have to
549 if hasattr(os
.path
, "samefile"):
550 return os
.path
.samefile(src
, dst
)
553 return (os
.path
.normcase(os
.path
.abspath(src
)) ==
554 os
.path
.normcase(os
.path
.abspath(dst
)))
556 def get_sys_path(dist
, name
):
557 # Returns the sysconfig path for a distribution, or None
558 location
= dist
.location
559 for scheme
in sysconfig
.get_scheme_names():
560 for path_type
in ["platlib", "purelib"]:
561 path
= sysconfig
.get_path(path_type
, scheme
)
563 if samefile(path
, location
):
564 return sysconfig
.get_path(name
, scheme
)
565 except EnvironmentError:
568 data_path
= get_sys_path(dist
, "data") or sys
.prefix
569 return [os
.path
.join(data_path
, "include", "pycairo")]
571 def find_pkg_config():
572 log
.info("pycairo: pkg-config")
573 pkg_config_version_check(pkg_config_name
, min_version
)
574 return pkg_config_parse("--cflags-only-I", pkg_config_name
)
576 # First the new get_include() API added in >1.15.6
577 include_dir
= find_path(find_new_api())
578 if include_dir
is not None:
581 # Then try to find it in the data prefix based on the module path.
582 # This works with many virtualenv/userdir setups, but not all apparently,
583 # see https://gitlab.gnome.org/GNOME/pygobject/issues/150
584 include_dir
= find_path(find_old_api())
585 if include_dir
is not None:
588 # Finally, fall back to pkg-config
589 include_dir
= find_path(find_pkg_config())
590 if include_dir
is not None:
593 raise DistutilsSetupError("Could not find pycairo headers")
596 def add_ext_pkg_config_dep(ext
, compiler_type
, name
):
597 script_dir
= get_script_dir()
600 "glib-2.0": ["glib-2.0"],
601 "gio-2.0": ["gio-2.0", "gobject-2.0", "glib-2.0"],
602 "gobject-introspection-1.0":
603 ["girepository-1.0", "gobject-2.0", "glib-2.0"],
606 ["cairo-gobject", "cairo", "gobject-2.0", "glib-2.0"],
610 fallback_libs
= msvc_libraries
[name
]
611 if compiler_type
== "msvc":
612 # assume that INCLUDE and LIB contains the right paths
613 ext
.libraries
+= fallback_libs
615 min_version
= get_version_requirement(script_dir
, name
)
616 pkg_config_version_check(name
, min_version
)
617 ext
.include_dirs
+= pkg_config_parse("--cflags-only-I", name
)
618 ext
.library_dirs
+= pkg_config_parse("--libs-only-L", name
)
619 ext
.libraries
+= pkg_config_parse("--libs-only-l", name
)
622 du_build_ext
= get_command_class("build_ext")
625 class build_ext(du_build_ext
):
627 def initialize_options(self
):
628 du_build_ext
.initialize_options(self
)
629 self
.compiler_type
= None
631 def finalize_options(self
):
632 du_build_ext
.finalize_options(self
)
633 self
.compiler_type
= new_compiler(compiler
=self
.compiler
).compiler_type
635 def _write_config_h(self
):
636 script_dir
= get_script_dir()
637 target
= os
.path
.join(script_dir
, "config.h")
638 versions
= parse_versions(script_dir
)
639 with io
.open(target
, 'w', encoding
="utf-8") as h
:
641 /* Configuration header created by setup.py - do not edit */
645 #define PYGOBJECT_MAJOR_VERSION %(PYGOBJECT_MAJOR_VERSION)s
646 #define PYGOBJECT_MINOR_VERSION %(PYGOBJECT_MINOR_VERSION)s
647 #define PYGOBJECT_MICRO_VERSION %(PYGOBJECT_MICRO_VERSION)s
648 #define VERSION "%(VERSION)s"
650 #endif /* _CONFIG_H */
653 def _setup_extensions(self
):
654 ext
= {e
.name
: e
for e
in self
.extensions
}
656 def add_dependency(ext
, name
):
657 add_ext_pkg_config_dep(ext
, self
.compiler_type
, name
)
659 def add_pycairo(ext
):
660 ext
.include_dirs
+= [get_pycairo_include_dir()]
662 gi_ext
= ext
["gi._gi"]
663 add_dependency(gi_ext
, "glib-2.0")
664 add_dependency(gi_ext
, "gio-2.0")
665 add_dependency(gi_ext
, "gobject-introspection-1.0")
666 add_dependency(gi_ext
, "libffi")
668 gi_cairo_ext
= ext
["gi._gi_cairo"]
669 add_dependency(gi_cairo_ext
, "glib-2.0")
670 add_dependency(gi_cairo_ext
, "gio-2.0")
671 add_dependency(gi_cairo_ext
, "gobject-introspection-1.0")
672 add_dependency(gi_cairo_ext
, "libffi")
673 add_dependency(gi_cairo_ext
, "cairo")
674 add_dependency(gi_cairo_ext
, "cairo-gobject")
675 add_pycairo(gi_cairo_ext
)
678 self
._write
_config
_h
()
679 self
._setup
_extensions
()
680 du_build_ext
.run(self
)
683 class install_pkgconfig(Command
):
684 description
= "install .pc file"
687 def initialize_options(self
):
688 self
.install_base
= None
689 self
.install_platbase
= None
690 self
.install_data
= None
691 self
.compiler_type
= None
694 def finalize_options(self
):
695 self
.set_undefined_options(
697 ('install_base', 'install_base'),
698 ('install_data', 'install_data'),
699 ('install_platbase', 'install_platbase'),
702 self
.set_undefined_options(
704 ('compiler_type', 'compiler_type'),
707 def get_outputs(self
):
710 def get_inputs(self
):
714 cmd
= self
.distribution
.get_command_obj("bdist_wheel", create
=False)
717 "Python wheels and pkg-config is not compatible. "
718 "No pkg-config file will be included in the wheel. Install "
719 "from source if you need one.")
722 if self
.compiler_type
== "msvc":
725 script_dir
= get_script_dir()
726 pkgconfig_in
= os
.path
.join(script_dir
, "pygobject-3.0.pc.in")
727 with io
.open(pkgconfig_in
, "r", encoding
="utf-8") as h
:
731 "prefix": self
.install_base
,
732 "exec_prefix": self
.install_platbase
,
733 "includedir": "${prefix}/include",
734 "datarootdir": "${prefix}/share",
735 "datadir": "${datarootdir}",
736 "VERSION": self
.distribution
.get_version(),
738 for key
, value
in config
.items():
739 content
= content
.replace("@%s@" % key
, value
)
741 libdir
= os
.path
.dirname(get_python_lib(True, True, self
.install_data
))
742 pkgconfig_dir
= os
.path
.join(libdir
, "pkgconfig")
743 self
.mkpath(pkgconfig_dir
)
744 target
= os
.path
.join(pkgconfig_dir
, "pygobject-3.0.pc")
745 with io
.open(target
, "w", encoding
="utf-8") as h
:
747 self
.outfiles
.append(target
)
750 du_install
= get_command_class("install")
753 class install(du_install
):
755 sub_commands
= du_install
.sub_commands
+ [
756 ("install_pkgconfig", lambda self
: True),
761 script_dir
= get_script_dir()
762 pkginfo
= parse_pkg_info(script_dir
)
763 gi_dir
= os
.path
.join(script_dir
, "gi")
766 os
.path
.join("gi", n
) for n
in os
.listdir(gi_dir
)
767 if os
.path
.splitext(n
)[-1] == ".c"
769 cairo_sources
= [os
.path
.join("gi", "pygi-foreign-cairo.c")]
770 for s
in cairo_sources
:
773 readme
= os
.path
.join(script_dir
, "README.rst")
774 with io
.open(readme
, encoding
="utf-8") as h
:
775 long_description
= h
.read()
780 include_dirs
=[script_dir
, gi_dir
],
781 define_macros
=[("HAVE_CONFIG_H", None)],
784 gi_cairo_ext
= Extension(
786 sources
=cairo_sources
,
787 include_dirs
=[script_dir
, gi_dir
],
788 define_macros
=[("HAVE_CONFIG_H", None)],
792 name
=pkginfo
["Name"],
793 version
=pkginfo
["Version"],
794 description
=pkginfo
["Summary"],
795 url
=pkginfo
["Home-page"],
796 author
=pkginfo
["Author"],
797 author_email
=pkginfo
["Author-email"],
798 maintainer
=pkginfo
["Maintainer"],
799 maintainer_email
=pkginfo
["Maintainer-email"],
800 license
=pkginfo
["License"],
801 long_description
=long_description
,
802 platforms
=pkginfo
.get_all("Platform"),
803 classifiers
=pkginfo
.get_all("Classifier"),
804 packages
=find_packages(script_dir
),
810 "build_ext": build_ext
,
811 "distcheck": distcheck
,
812 "build_tests": build_tests
,
816 "install_pkgconfig": install_pkgconfig
,
819 "pycairo>=%s" % get_version_requirement(
820 script_dir
, get_pycairo_pkg_config_name()),
823 ('include/pygobject-3.0', ['gi/pygobject.h']),
829 if __name__
== "__main__":