Skip to content

Commit

Permalink
automatically determine which mode to build in
Browse files Browse the repository at this point in the history
  • Loading branch information
georgeharker committed Dec 9, 2024
1 parent 50505a2 commit ec9ce32
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 166 deletions.
7 changes: 4 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ venv
.DS_store

# Build temporaries
_*.o
_*.so
_*.c
_pangocairocffi*.o
_pangocairocffi*.so
_pangocairocffi*.c
_pangocairocffi*.py
98 changes: 54 additions & 44 deletions pangocairocffi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,55 +14,65 @@
import cairocffi


api_mode = True
# Attempt api mode, then precompiled abi mode, then import time abi
cffi_mode = "(unknown)"
try:
from _pangocairocffi import ffi
from _pangocairocffi import lib as pangocairo
api_mode = True
# Note in ABI mode lib is already available, no dlopen() needed
from _pangocairocffi import ffi, lib as pangocairo
cffi_mode = "api"
except ImportError:
api_mode = False
try:
# Note in ABI mode lib will be missing
from _pangocffi import ffi
cffi_mode = "abi_precompiled"
except ImportError:
# Fall back to importing and parsing cffi defs
from .ffi_build import ffi_for_mode
ffi = ffi_for_mode("abi")
cffi_mode = "abi"

# Fall back to non api mode
if not api_mode:
def _dlopen(dl_name: str, generated_ffi, names: List[str]):
"""
:param dl_name:
The name of the dynamic library. This is also used to determine the
environment variable name to lookup. For example, if dl_name is
"pangocairo", this function will attempt to load "PANGOCAIRO_LOCATION".
:param generated_ffi:
The FFI for pangocairo, generated by pangocairocffi.
:param names:
An array of library names commonly used across different platforms.
:return:
A FFILibrary instance for the library.
"""

# Try environment locations if set
env_location = os.getenv(f'{dl_name.upper()}_LOCATION')
if env_location:
names.append(env_location)

def _dlopen(dl_name: str, generated_ffi, names: List[str]):
"""
:param dl_name:
The name of the dynamic library. This is also used to determine the
environment variable name to lookup. For example, if dl_name is
"pangocairo", this function will attempt to load "PANGOCAIRO_LOCATION".
:param generated_ffi:
The FFI for pangocairo, generated by pangocairocffi.
:param names:
An array of library names commonly used across different platforms.
:return:
A FFILibrary instance for the library.
"""

# Try environment locations if set
env_location = os.getenv(f'{dl_name.upper()}_LOCATION')
if env_location:
names.append(env_location)
try:
return generated_ffi.dlopen(env_location)
except OSError:
warnings.warn(f"dlopen() failed to load {dl_name} library:"
f" '{env_location}'. Falling back.")

# Try various names for the same library, for different platforms.
for name in names:
for lib_name in (name, 'lib' + name):
try:
return generated_ffi.dlopen(env_location)
path = ctypes.util.find_library(lib_name)
lib = generated_ffi.dlopen(path or lib_name)
if lib:
return lib
except OSError:
warnings.warn(f"dlopen() failed to load {dl_name} library:"
f" '{env_location}'. Falling back.")

# Try various names for the same library, for different platforms.
for name in names:
for lib_name in (name, 'lib' + name):
try:
path = ctypes.util.find_library(lib_name)
lib = generated_ffi.dlopen(path or lib_name)
if lib:
return lib
except OSError:
pass
raise OSError(
f"dlopen() failed to load {dl_name} library: {' / '.join(names)}"
)

from .ffi_build import ffi # noqa
pass
raise OSError(
f"dlopen() failed to load {dl_name} library: {' / '.join(names)}"
)


# Fall back to non api mode
if cffi_mode != "api":
pangocairo = _dlopen('pangocairo', ffi, ['pangocairo-1.0', 'pangocairo-1.0-0']) # noqa


Expand Down
175 changes: 68 additions & 107 deletions pangocairocffi/ffi_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,124 +5,85 @@
Build the cffi bindings
"""

import os
import sys
from distutils.errors import CCompilerError, DistutilsExecError, DistutilsPlatformError
from pathlib import Path
from warnings import warn


from cffi import FFI
from cffi.error import VerificationError


sys.path.append(str(Path(__file__).parent))


api_mode = False
if ('PANGOCAIROCFFI_API_MODE' in os.environ and
int(os.environ['PANGOCAIROCFFI_API_MODE']) == 1):
# Allow explicit disable of api_mode
api_mode = True

# Create an empty _generated folder if needed
if not api_mode:
(Path(__file__).parent / '_generated').mkdir(exist_ok=True)

# Read the CFFI definitions
c_definitions_cairo_file = open(
str(Path(__file__).parent / 'c_definitions_cairo.txt'),
'r'
)
c_definitions_cairo = c_definitions_cairo_file.read()
c_definitions_pangocairo_file = open(
str(Path(__file__).parent / 'c_definitions_pangocairo.txt'),
'r'
)
c_definitions_pangocairo = c_definitions_pangocairo_file.read()

# cffi definitions, in the order outlined in:
ffi = FFI()

xcffib_available = False
try:
from xcffib.ffi_build import ffi as xcb_ffi
except ImportError:
warn("xcfiib not available")
else:
xcffib_available = True

pangocffi_api_mode = False
try:
from pangocffi import ffi_build as ffi_pango
ffi.include(ffi_pango.ffi)
pangocffi_api_mode = True
except ImportError:
from pangocffi import ffi as ffi_pango
ffi.include(ffi_pango)
if api_mode:
warn("pangocffi is not available in API mode, disabling API mode")
api_mode = False

cairocffi_api_mode = False
try:
from cairocffi.ffi_build import ffi as ffi_cairo
ffi.include(ffi_cairo)
cairocffi_api_mode = True
except ImportError:
ffi.cdef(c_definitions_cairo)
if api_mode:
warn("cairocffi is not available in API mode, disabling API mode")
api_mode = False


ffi.cdef(c_definitions_pangocairo)

if api_mode:
ffi.set_source_pkgconfig(
'_pangocairocffi',
['pangocairo', 'pango', 'glib-2.0'],
r"""
#include "glib.h"
#include "glib-object.h"
#include "pango/pango.h"
#include "pango/pangocairo.h"
#include "cairo-pdf.h"
#include "cairo-svg.h"
#include "cairo-ps.h"
#if defined(_WIN64) || defined(_WIN32)
#include "cairo-win32.h"
#endif
""" +
(r"""
#include "xcb/xcb.h"
#include "xcb/xproto.h"
#include "xcb/xcbext.h"
#include "xcb/render.h"
#include "cairo-xcb.h"
""" if xcffib_available else "") +
r"""
/* Deal with some newer definitions for compatibility */
#if CAIRO_VERSION < 11702
#define CAIRO_FORMAT_RGBA128F 7
#define CAIRO_FORMAT_RGB96F 6
#endif
#if CAIRO_VERSION < 11800
#include <stdio.h>
#include <stdbool.h>
void cairo_set_hairline(cairo_t*, cairo_bool_t);
cairo_bool_t cairo_get_hairline(cairo_t*);
void cairo_set_hairline(cairo_t*, cairo_bool_t) {
fprintf(stderr, "Unimplemented!!\n");
}
cairo_bool_t cairo_get_hairline(cairo_t*) {
fprintf(stderr, "Unimplemented!!\n");
return false;
}
#endif
""",
sources=[]
def ffi_for_mode(mode):
# Read the CFFI definitions
c_definitions_cairo_file = open(
str(Path(__file__).parent / 'c_definitions_cairo.txt'),
'r'
)
c_definitions_cairo = c_definitions_cairo_file.read()
c_definitions_pangocairo_file = open(
str(Path(__file__).parent / 'c_definitions_pangocairo.txt'),
'r'
)
c_definitions_pangocairo = c_definitions_pangocairo_file.read()

# cffi definitions, in the order outlined in:
ffi = FFI()

from pangocffi import cffi_mode as pango_cffi_mode
from pangocffi.ffi_build import ffi_for_mode as pango_ffi_for_mode
pango_ffi = pango_ffi_for_mode(mode)
ffi.include(pango_ffi)

from cairocffi import cffi_mode as cairo_cffi_mode
from cairocffi.ffi_build import ffi_for_mode as cairo_ffi_for_mode
from cairocffi.ffi_build import c_source_cairo, c_source_cairo_compat
cairo_ffi = cairo_ffi_for_mode(mode)
ffi.include(cairo_ffi)

ffi.cdef(c_definitions_pangocairo)

if mode == "api":
ffi.set_source_pkgconfig(
'_pangocairocffi',
['pangocairo', 'pango', 'glib-2.0'],
c_source_cairo +
r"""
#include "glib.h"
#include "glib-object.h"
#include "pango/pango.h"
#include "pango/pangocairo.h"
""" + c_source_cairo_compat,
sources=[]
)

else:
ffi.set_source('_pangocairocffi', None)
return ffi


def build_ffi():
"""
This will be called from setup() to return an FFI
which it will compile - work out here which type is
possible and return it.
"""
try:
ffi_api = ffi_for_mode("api")
ffi_api.compile(verbose=True)
return ffi_api
except (CCompilerError, DistutilsExecError, DistutilsPlatformError,
VerificationError) as e:
warn("Falling back to precompiled python mode: {}".format(str(e)))

ffi_abi = ffi_for_mode("abi")
ffi_abi.compile(verbose=True)
return ffi_abi

else:
ffi.set_source('pangocairocffi._generated.ffi', None)

if __name__ == '__main__':
ffi.compile()
build_ffi()
20 changes: 8 additions & 12 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,11 @@
'pangocairocffi does not support Python 2.x. Please use Python 3.'
)

if ('PANGOCAIROCFFI_API_MODE' in os.environ and
int(os.environ['PANGOCAIROCFFI_API_MODE']) == 1):
setup(
name='pangocairocffi',
# when cairocffi, pangocffi are updated, bump to include API mode
install_requires=['cffi >= 1.1.0', 'cairocffi >= 1.7.1', 'pangocffi >= 0.13.0'],
setup_requires=['cffi >= 1.1.0', 'cairocffi >= 1.7.1', 'pangocffi >= 0.13.0'],
cffi_modules=['pangocairocffi/ffi_build.py:ffi'],
packages=['pangocairocffi']
)
else:
setup()
setup(
name='pangocairocffi',
# when cairocffi, pangocffi are updated, bump to include API mode
install_requires=['cffi >= 1.1.0', 'cairocffi >= 1.7.1', 'pangocffi >= 0.13.0'],
setup_requires=['cffi >= 1.1.0', 'cairocffi >= 1.7.1', 'pangocffi >= 0.13.0'],
cffi_modules=['pangocairocffi/ffi_build.py:build_ffi'],
packages=['pangocairocffi']
)

0 comments on commit ec9ce32

Please sign in to comment.