diff --git a/.appveyor.yml b/.appveyor.yml index 5b828b5d..47455ec8 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,5 +1,17 @@ environment: matrix: + - MSYS2_ARCH: x86_64 + MSYSTEM: MINGW64 + PYTHON: python2 + - MSYS2_ARCH: i686 + MSYSTEM: MINGW32 + PYTHON: python2 + - MSYS2_ARCH: x86_64 + MSYSTEM: MINGW64 + PYTHON: python3 + - MSYS2_ARCH: i686 + MSYSTEM: MINGW32 + PYTHON: python3 - MSVC_PLATFORM: x86 PYTHON_ROOT: Python27 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2013 @@ -18,24 +30,12 @@ environment: - MSVC_PLATFORM: x64 PYTHON_ROOT: Python36-x64 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 - - MSYS2_ARCH: x86_64 - MSYSTEM: MINGW64 - PYTHON: python2 - - MSYS2_ARCH: i686 - MSYSTEM: MINGW32 - PYTHON: python2 - - MSYS2_ARCH: x86_64 - MSYSTEM: MINGW64 - PYTHON: python3 - - MSYS2_ARCH: i686 - MSYSTEM: MINGW32 - PYTHON: python3 build_script: - IF DEFINED MSYSTEM set PATH=C:\msys64\%MSYSTEM%\bin;C:\msys64\usr\bin;%PATH% - IF DEFINED MSYSTEM set CHERE_INVOKING=yes - - IF DEFINED MSYSTEM bash -lc "bash .appveyor/msys2-pre.sh" - - IF DEFINED MSYSTEM bash -lc "bash .appveyor/msys2.sh" + - IF DEFINED MSYSTEM bash -lc "bash -x .appveyor/msys2-pre.sh" + - IF DEFINED MSYSTEM bash -lc "bash -x .appveyor/msys2.sh" - IF DEFINED MSVC_PLATFORM ".appveyor/msvc.bat" deploy: off diff --git a/.appveyor/msys2.sh b/.appveyor/msys2.sh index 6af80a64..6a99ddea 100644 --- a/.appveyor/msys2.sh +++ b/.appveyor/msys2.sh @@ -10,6 +10,7 @@ export CFLAGS="-std=c90 -Wall -Wno-long-long -Werror -coverage" $PYTHON -m coverage run --branch setup.py test $PYTHON -m codecov $PYTHON setup.py sdist +$PYTHON setup.py install --root="$(pwd)"/_root_abs $PYTHON -m pip install dist/* # Also test with older cairo diff --git a/.travis.yml b/.travis.yml index 5acbdcf7..dc2682aa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -67,5 +67,12 @@ script: - python -m codecov - python -m flake8 . - python setup.py sdist - - if [[ "$TRAVIS_PYTHON_VERSION" != "pypy" ]] && [[ "$TRAVIS_PYTHON_VERSION" != "pypy3" ]]; then python -m pip install "$(eval 'echo dist/*')"; fi + - python setup.py bdist + - python setup.py install --root=_root + - python setup.py install --root="$(pwd)"/_root_abs + - python setup.py install --user + - PYCAIRO_SETUPTOOLS=1 python setup.py bdist_egg + - PYCAIRO_SETUPTOOLS=1 python setup.py bdist_wheel + - PYCAIRO_SETUPTOOLS=1 python setup.py install --root=_root_setup + - if [[ "$TRAVIS_PYTHON_VERSION" != "pypy" ]] && [[ "$TRAVIS_PYTHON_VERSION" != "pypy3" ]]; then python -m pip install .; fi - if [[ "$TRAVIS_PYTHON_VERSION" != "3.3" ]]; then python -m sphinx -W -a -E -b html -n docs docs/_build; fi diff --git a/cairo/__init__.py b/cairo/__init__.py index dd86f6a5..e61c7a36 100755 --- a/cairo/__init__.py +++ b/cairo/__init__.py @@ -1 +1,25 @@ from ._cairo import * # noqa: F401,F403 + + +def get_include(): + """Returns a path to the directory containing the C header files""" + + import os + + def is_ok(path): + return os.path.exists(path) and os.path.isdir(path) + + package_path = os.path.dirname(os.path.realpath(__file__)) + install_path = os.path.join(package_path, "include") + + # in case we are installed + if is_ok(install_path): + return install_path + + # in case we are running from source + if is_ok(package_path): + return package_path + + # in case we are in an .egg + import pkg_resources + return pkg_resources.resource_filename(__name__, "include") diff --git a/docs/pycairo_c_api.rst b/docs/pycairo_c_api.rst index b1a16a15..0c4e8ea5 100644 --- a/docs/pycairo_c_api.rst +++ b/docs/pycairo_c_api.rst @@ -11,6 +11,41 @@ This manual documents the API used by C and C++ programmers who want to write extension modules that use pycairo. +Pycairo Compiler Flags +====================== + +To compile a Python extension using Pycairo you need to know where Pycairo and +cairo are located and what flags to pass to the compiler and linker. + +1. Variant: + + Only available since version 1.15.7. + + While Pycairo installs a pkg-config file, in case of virtualenvs, + installation to the user directory or when using wheels/eggs, pkg-config + will not be able to locate the .pc file. The :func:`get_include` function + should work in all cases, as long as Pycairo is in your Python search path. + + Compiler Flags: + * ``python -c "import cairo; print(cairo.get_include())"`` + * ``pkg-config --cflags cairo`` + + Linker Flags: + * ``pkg-config --libs cairo`` + +2. Variant: + + This works with older versions, but with the limitations mentioned above. + Use it as a fallback if you want to support older versions or if your + module does not require virtualenv/pip support. + + Compiler Flags: + * ``pkg-config --cflags pycairo`` or ``pkg-config --cflags py3cairo`` + + Linker Flags: + * ``pkg-config --libs pycairo`` or ``pkg-config --libs py3cairo`` + + .. _api-includes: To access the Pycairo C API under Python 2 @@ -21,7 +56,7 @@ Edit the client module file to add the following lines:: /* All function, type and macro definitions needed to use the Pycairo/C API * are included in your code by the following line */ - #include "Pycairo.h" + #include "pycairo.h" /* define a variable for the C API */ static Pycairo_CAPI_t *Pycairo_CAPI; @@ -52,21 +87,6 @@ Example showing how to import the pycairo API:: } -Pkg-Config Setup -================ - -pycairo installs "pycairo.pc" or "py3cairo.pc" in case of a Python 3 install: - -.. code-block:: console - - > pkg-config --libs --cflags pycairo - > pkg-config --libs --cflags py3cairo - -I/usr/include/cairo -I/usr/include/glib-2.0 - -I/usr/lib/x86_64-linux-gnu/glib-2.0/include -I/usr/include/pixman-1 - -I/usr/include/freetype2 -I/usr/include/libpng16 -I/usr/include/pycairo - -lcairo - - Misc Functions ============== diff --git a/docs/reference/constants.rst b/docs/reference/constants.rst index f9585d47..eb789155 100644 --- a/docs/reference/constants.rst +++ b/docs/reference/constants.rst @@ -26,6 +26,15 @@ Module Functions Returns the version of the underlying C cairo library as a human-readable string of the form "X.Y.Z". +.. function:: get_include() + + :returns: a path to the directory containing the C header files + :rtype: str + + Gives the include path which should be passed to the compiler. + + .. versionadded:: 1.15.7 + Module Constants ================ diff --git a/setup.py b/setup.py index ae4bc06f..c7701318 100755 --- a/setup.py +++ b/setup.py @@ -5,10 +5,15 @@ import sys import os import errno -import warnings + +if os.environ.get("PYCAIRO_SETUPTOOLS"): + # for testing + import setuptools + setuptools from distutils.core import Extension, setup, Command, Distribution from distutils.ccompiler import new_compiler +from distutils import log PYCAIRO_VERSION = '1.15.7' @@ -87,14 +92,22 @@ class install_pkgconfig(Command): def initialize_options(self): self.install_base = None self.install_data = None + self.install_lib = None + self.root = None self.compiler_type = None self.outfiles = [] def finalize_options(self): self.set_undefined_options( - 'install', + 'install_lib', ('install_base', 'install_base'), ('install_data', 'install_data'), + ('install_lib', 'install_lib'), + ) + + self.set_undefined_options( + 'install', + ('root', 'root'), ) self.set_undefined_options( @@ -116,13 +129,20 @@ def run(self): # wrong paths. So in case bdist_wheel is used, just skip this command. cmd = self.distribution.get_command_obj("bdist_wheel", create=False) if cmd is not None: - warnings.warn( - "Python wheels and pkg-config is not compatible. " - "No pkg-config file will be included in the wheel. Install " - "from source if you need one.") + log.info( + "Skipping install_pkgconfig, not supported with bdist_wheel") + return + + # same for bdist_egg + cmd = self.distribution.get_command_obj("bdist_egg", create=False) + if cmd is not None: + log.info( + "Skipping install_pkgconfig, not supported with bdist_egg") return if self.compiler_type == "msvc": + log.info( + "Skipping install_pkgconfig, not supported with MSVC") return pkgconfig_dir = os.path.join(self.install_data, "share", "pkgconfig") @@ -133,6 +153,17 @@ def run(self): else: target = os.path.join(pkgconfig_dir, "pycairo.pc") + # figure out the package path relative to the prefix + lib = self.install_lib + if self.root is not None: + lib = self.install_lib[len(self.root):] + rel_lib = os.path.relpath(lib, self.install_base) + + rel_include_dir = os.path.join(rel_lib, "cairo", "include") + + log.info("Writing %s" % target) + log.info("pkg-config prefix: %s" % self.install_base) + log.info("pkg-config include: ${prefix}/%s" % rel_include_dir) with open(target, "wb") as h: h.write((u"""\ prefix=%(prefix)s @@ -141,24 +172,97 @@ def run(self): Description: Python %(py_version)d bindings for cairo Version: %(version)s Requires: cairo -Cflags: -I${prefix}/include/pycairo +Cflags: -I${prefix}/%(include)s Libs: """ % { "prefix": self.install_base, "version": PYCAIRO_VERSION, - "py_version": sys.version_info[0]}).encode("utf-8")) + "py_version": sys.version_info[0], + "include": rel_include_dir}).encode("utf-8")) self.outfiles.append(target) -du_install = get_command_class("install") +class install_pycairo_header(Command): + description = "install pycairo header" + user_options = [] + + def initialize_options(self): + self.install_data = None + self.install_lib = None + self.force = None + self.outfiles = [] + + def finalize_options(self): + self.set_undefined_options( + 'install_lib', + ('install_data', 'install_data'), + ('install_lib', 'install_lib'), + ) + self.set_undefined_options( + 'install', + ('force', 'force'), + ) -class install(du_install): + def get_outputs(self): + return self.outfiles - sub_commands = du_install.sub_commands + [ - ("install_pkgconfig", lambda self: True), - ] + def get_inputs(self): + return [os.path.join('cairo', 'pycairo.h')] + + def run(self): + # https://github.com/pygobject/pycairo/issues/92 + hname = 'py3cairo.h' if sys.version_info[0] == 3 else 'pycairo.h' + source = self.get_inputs()[0] + + lib_hdir = os.path.join( + self.install_lib, "cairo", "include") + self.mkpath(lib_hdir) + header_path = os.path.join(lib_hdir, hname) + (out, _) = self.copy_file(source, header_path) + self.outfiles.append(out) + + # install a simple header including the new one in the old location, + # in case some code has hardcoded the old include path. + data_hdir = os.path.join(self.install_data, "include", "pycairo") + rel_include_path = os.path.normpath( + os.path.relpath(header_path, data_hdir)).replace("\\\\", "/") + back_comp_path = os.path.join(data_hdir, hname) + self.mkpath(data_hdir) + + log.info("Writing %s" % back_comp_path) + with io.open(back_comp_path, "w", encoding="utf-8") as h: + h.write(u"/* the header has moved, use pkg-config */\n") + h.write(u"#include \"%s\"\n" % rel_include_path) + self.outfiles.append(back_comp_path) + + +du_install_lib = get_command_class("install_lib") + + +class install_lib(du_install_lib): + + def initialize_options(self): + self.install_base = None + self.install_lib = None + self.install_data = None + du_install_lib.initialize_options(self) + + def finalize_options(self): + du_install_lib.finalize_options(self) + self.set_undefined_options( + 'install', + ('install_base', 'install_base'), + ('install_lib', 'install_lib'), + ('install_data', 'install_data'), + ) + + def run(self): + du_install_lib.run(self) + # bdist_egg doesn't run install, so run our commands here instead + self.run_command("install_pkgconfig") + self.run_command("install_pycairo_header") du_build_ext = get_command_class("build_ext") @@ -272,18 +376,6 @@ def finalize_options(self): self.enable_xpyb = bool(self.enable_xpyb) -du_install_data = get_command_class("install_data") - - -class install_data(du_install_data): - - def copy_file(self, src, dst, *args, **kwargs): - # XXX: rename target on the fly. ugly, but works - if os.path.basename(src) == "pycairo.h" and sys.version_info[0] == 3: - dst = os.path.join(dst, "py3cairo.h") - return du_install_data.copy_file(self, src, dst, *args, **kwargs) - - def main(): cairo_ext = Extension( @@ -325,9 +417,9 @@ def main(): cmdclass = { "build": build, "build_ext": build_ext, - "install": install, + "install_lib": install_lib, "install_pkgconfig": install_pkgconfig, - "install_data": install_data, + "install_pycairo_header": install_pycairo_header, "test": test_cmd, } @@ -355,10 +447,6 @@ def main(): 'GNU Lesser General Public License v2 (LGPLv2)'), 'License :: OSI Approved :: Mozilla Public License 1.1 (MPL 1.1)', ], - data_files=[ - ('include/pycairo', ['cairo/pycairo.h']), - ], - zip_safe=False, cmdclass=cmdclass, ) diff --git a/tests/test_api.py b/tests/test_api.py index d8902c54..31f5cbc9 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -17,6 +17,13 @@ import pytest +def test_get_include(): + include = cairo.get_include() + assert isinstance(include, str) + assert os.path.exists(include) + assert os.path.isdir(include) + + def test_version(): cairo.cairo_version() cairo.cairo_version_string()