Move header into the package and add a get_include() function. See #92

Pycairo installs .pc files which work quite well where the default
prefix is used, like with distro packaging etc. In virtualenvs
the pkg-config look up path needs to be set manually, and in pip
wheels are involved, where we had to disable installing .pc files
as they can get reused for a different prefix.

To make it easier for other python modules to use the C API introduce
a new function get_include() (similar ot what numpy has) which returns
the include path that needs to be passed to the compiler.

Since we can't really get the old header install path from the module
(one could walk up the tree and look for matching files, but that's ugly)
move the header file into the package itself, so that the path can be
dervived from the package path.

To prevent code from breaking which hardcodes the old include path
install a header to the old location which includes the new header
location.
This commit is contained in:
Christoph Reiter 2018-01-31 19:28:32 +01:00
parent 47aebd2073
commit fa0707e565
8 changed files with 218 additions and 62 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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
==============

View File

@ -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
================

150
setup.py
View File

@ -5,10 +5,15 @@ import subprocess
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 @@ class install_pkgconfig(Command):
# 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 @@ class install_pkgconfig(Command):
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 @@ Name: Pycairo
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'),
)
def get_outputs(self):
return self.outfiles
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)
class install(du_install):
du_install_lib = get_command_class("install_lib")
sub_commands = du_install.sub_commands + [
("install_pkgconfig", lambda self: True),
]
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 @@ class build(du_build):
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,
)

View File

@ -17,6 +17,13 @@ import cairo
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()