From 9fb5c61c4b57cdcc0be14aa7b0c1945aa4098825 Mon Sep 17 00:00:00 2001 From: George Harker Date: Mon, 9 Dec 2024 12:06:30 -0800 Subject: [PATCH] simplify api vs abi mode and auto pick --- pangocffi/__init__.py | 97 ++++++++++++++------------ pangocffi/ffi_build.py | 112 +++++++++++++++++++++++------- pangocffi/ffi_instance_builder.py | 80 --------------------- setup.py | 7 +- 4 files changed, 140 insertions(+), 156 deletions(-) delete mode 100644 pangocffi/ffi_instance_builder.py diff --git a/pangocffi/__init__.py b/pangocffi/__init__.py index 3f87949..3d82832 100644 --- a/pangocffi/__init__.py +++ b/pangocffi/__init__.py @@ -4,65 +4,74 @@ from typing import List -# Attempt api mode if installed -api_mode = True +# Attempt api mode, then precompiled abi mode, then import time abi +cffi_mode = "(unknown)" try: - from _pangocffi import ffi - from _pangocffi import lib as pango + # Note in ABI mode lib is already available, no dlopen() needed + from _pangocffi import ffi, lib as pango gobject = pango glib = pango - api_mode = True + 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 "glib", this function will attempt to load "GLIB_LOCATION". - :param generated_ffi: - The FFI for pango/gobject/glib, generated by pangocffi. - :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 "glib", this function will attempt to load "GLIB_LOCATION". + :param generated_ffi: + The FFI for pango/gobject/glib, generated by pangocffi. + :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.") + pass + raise OSError( + f"dlopen() failed to load {dl_name} library: {' / '.join(names)}" + ) - # 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 +# Fall back to non api mode +if cffi_mode != "api": pango = _dlopen('pango', ffi, ['pango', 'pango-1', 'pango-1.0', 'pango-1.0-0']) gobject = _dlopen('gobject', ffi, ['gobject-2.0', 'gobject-2.0-0']) glib = _dlopen('glib', ffi, ['glib-2.0', 'glib-2.0-0']) + # Imports are normally always put at the top of the file. # But the wrapper API requires that the pango library be loaded first. # Therefore, we have to disable linting rules for these lines. diff --git a/pangocffi/ffi_build.py b/pangocffi/ffi_build.py index ce155e4..7c5e148 100644 --- a/pangocffi/ffi_build.py +++ b/pangocffi/ffi_build.py @@ -6,38 +6,98 @@ """ -import importlib.util -import os +import platform 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 ('PANGOCFFI_API_MODE' in os.environ and - int(os.environ['PANGOCFFI_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) - -# Because we can't directly load the instance builder (it would run -# ``__init__.py`` for any module import) we have to do this dubious import. -spec = importlib.util.spec_from_file_location( - 'ffi_instance_builder', - str(Path(__file__).parent / 'ffi_instance_builder.py') -) -ffi_instance_builder = importlib.util.module_from_spec(spec) -spec.loader.exec_module(ffi_instance_builder) - -# Generate the bindings -ffiBuilder = ffi_instance_builder.FFIInstanceBuilder( - source='pangocffi._generated.ffi' if not api_mode else None -) -ffi = ffiBuilder.generate() + +def ffi_for_mode(mode): + # Read the C definitions + c_definitions_glib_file = open( + str(Path(__file__).parent / 'c_definitions_glib.txt'), + 'r' + ) + c_definitions_pango_file = open( + str(Path(__file__).parent / 'c_definitions_pango.txt'), + 'r' + ) + c_definitions_glib = c_definitions_glib_file.read() + c_definitions_pango = c_definitions_pango_file.read() + + ffi = FFI() + # Mirror the GType setup in gobject/gtypes.h + if ffi.sizeof('void*') > ffi.sizeof('long'): + ffi.cdef('typedef unsigned int* GType;') + else: + ffi.cdef('typedef unsigned long GType;') + ffi.cdef(c_definitions_glib) + ffi.cdef(c_definitions_pango) + if mode == "api": + ffi.set_source_pkgconfig( + "_pangocffi", + ['pango', 'glib-2.0', 'pangoft2'] + + (['pangoxft'] if platform.system() == 'Linux' else []), + r""" + #include "glib.h" + #include "glib-object.h" + #include "pango/pango.h" + #include "pango/pango-fontmap.h" + + #include + #if PANGO_VERSION < G_ENCODE_VERSION(1, 54) + int pango_item_get_char_offset ( + PangoItem* item) { + fprintf(stderr, "Unimplemented!!\n"); + return -1; + } + #endif + #if PANGO_VERSION < G_ENCODE_VERSION(1, 52) + PangoFont* + pango_font_map_reload_font ( + PangoFontMap* fontmap, + PangoFont* font, + double scale, + PangoContext* context, + const char* variations) { + fprintf(stderr, "Unimplemented!!\n"); + return NULL; + } + + #endif + """, + sources=[] + ) + else: + ffi.set_source("_pangocffi", 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 + if __name__ == '__main__': + ffi = build_ffi() ffi.compile() diff --git a/pangocffi/ffi_instance_builder.py b/pangocffi/ffi_instance_builder.py deleted file mode 100644 index 94909de..0000000 --- a/pangocffi/ffi_instance_builder.py +++ /dev/null @@ -1,80 +0,0 @@ -""" - pangocffi.ffi_instance_builder - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - generates an FFI for pangocffi -""" - -import os -import platform -from pathlib import Path -from cffi import FFI -from typing import Optional - - -class FFIInstanceBuilder: - - def __init__(self, source: Optional[str] = None): - self.source = source - - def generate(self) -> FFI: - # Read the C definitions - c_definitions_glib_file = open( - str(Path(__file__).parent / 'c_definitions_glib.txt'), - 'r' - ) - c_definitions_pango_file = open( - str(Path(__file__).parent / 'c_definitions_pango.txt'), - 'r' - ) - c_definitions_glib = c_definitions_glib_file.read() - c_definitions_pango = c_definitions_pango_file.read() - - ffi = FFI() - # Mirror the GType setup in gobject/gtypes.h - if ffi.sizeof('void*') > ffi.sizeof('long'): - ffi.cdef('typedef unsigned int* GType;') - else: - ffi.cdef('typedef unsigned long GType;') - ffi.cdef(c_definitions_glib) - ffi.cdef(c_definitions_pango) - if ('PANGOCFFI_API_MODE' in os.environ and - int(os.environ['PANGOCFFI_API_MODE']) == 1): - ffi.set_source_pkgconfig( - '_pangocffi', - ['pango', 'glib-2.0', 'pangoft2'] + - (['pangoxft'] if platform.system() == 'Linux' else []), - r""" - #include "glib.h" - #include "glib-object.h" - #include "pango/pango.h" - #include "pango/pango-fontmap.h" - - #include - #if PANGO_VERSION < G_ENCODE_VERSION(1, 54) - int pango_item_get_char_offset ( - PangoItem* item) { - fprintf(stderr, "Unimplemented!!\n"); - return -1; - } - #endif - #if PANGO_VERSION < G_ENCODE_VERSION(1, 52) - PangoFont* - pango_font_map_reload_font ( - PangoFontMap* fontmap, - PangoFont* font, - double scale, - PangoContext* context, - const char* variations) { - fprintf(stderr, "Unimplemented!!\n"); - return NULL; - } - - #endif - """, - sources=[] - ) - else: - ffi.set_source(self.source, None) - - return ffi diff --git a/setup.py b/setup.py index e8b80da..ca0ee1e 100644 --- a/setup.py +++ b/setup.py @@ -9,17 +9,12 @@ 'pangocffi does not support Python 2.x. Please use Python 3.' ) -api_mode = False -if ('PANGOCFFI_API_MODE' in os.environ and - int(os.environ['PANGOCFFI_API_MODE']) == 1): - api_mode = True - setup( name='pangocffi', use_scm_version=True, install_requires=['cffi >= 1.1.0'], setup_requires=['cffi >= 1.1.0'], packages=['pangocffi'], - cffi_modules=['pangocffi/ffi_build.py:ffi'] if api_mode else [] + cffi_modules=['pangocffi/ffi_build.py:build_ffi'] )