From 20fbc353745ef436f03bbfea2fc3a62372c57b63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavol=20=C5=BD=C3=A1=C4=8Dik?= Date: Sun, 1 Sep 2024 19:37:37 +0200 Subject: [PATCH] Python: Refactor the build of the bindings When building the binding, link the dynamic C library as built by `make` instead of linking the system-installed C library. This simplifies packaging in Linux distributions and also the build of the live docs. --- .readthedocs.yml | 7 +- python/pyproject.toml | 2 +- python/src/libcpuid/_ffi_build.py | 116 +++++++++++++++++++++--- python/src/libcpuid/_ffi_build_rtd.py | 31 ------- python/src/libcpuid/_ffi_build_utils.py | 105 --------------------- 5 files changed, 105 insertions(+), 156 deletions(-) delete mode 100644 python/src/libcpuid/_ffi_build_rtd.py delete mode 100644 python/src/libcpuid/_ffi_build_utils.py diff --git a/.readthedocs.yml b/.readthedocs.yml index 0866584..8aa71c8 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -14,13 +14,10 @@ build: pre_install: - libtoolize - autoreconf --install - - mkdir ./install - - ./configure --prefix=`pwd`/install + - ./configure - make - - make install - pip install cffi - - python ./python/src/libcpuid/_ffi_build_rtd.py ./libcpuid/libcpuid.h ./install - + - python python/src/libcpuid/_ffi_build.py --runtime-link sphinx: configuration: python/docs/conf.py diff --git a/python/pyproject.toml b/python/pyproject.toml index 637b0e6..580a4c7 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools", "cffi"] +requires = ["setuptools", "cffi", "wheel"] build-backend = "setuptools.build_meta" [project] diff --git a/python/src/libcpuid/_ffi_build.py b/python/src/libcpuid/_ffi_build.py index 6e24d12..e3a7ea0 100644 --- a/python/src/libcpuid/_ffi_build.py +++ b/python/src/libcpuid/_ffi_build.py @@ -3,23 +3,111 @@ Module for compiling the C FFI. """ import os -import sys +import subprocess +import tempfile +import re +import argparse +from pathlib import Path from cffi import FFI -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -from _ffi_build_utils import ( # pylint: disable=import-error, wrong-import-position - get_include_flags, - find_header_file, - preprocess_header, - eval_sizeofs, +class FFIBuildException(Exception): + """Generic exception for errors occuring during the CFFI build.""" + + +def preprocess_header(header_path): + """ + Preprocesses the header file (python-cffi only accepts preprocessed C definitions) + at the given path and returns it as a string. + """ + try: + return subprocess.check_output( + ["gcc", "-U __GNUC__", "-E", header_path] + ).decode() + except subprocess.CalledProcessError as e: + if e.returncode == 127: + raise FFIBuildException( + "The gcc compiler is necessary to build python-libcpuid." + ) from e + raise FFIBuildException( + f"Error preprocessing the libcpuid header file: {e.stderr}" + ) from e + + +def _get_sizeof_eval_source(sizeof): + return f""" +#include +#include + +int main() {{ + printf("%ld", {sizeof}); + return 0; +}} +""" + + +def eval_sizeofs(header, cflags): + """ + Evaluates each sizeof found in the given C header and replaces all + occurences of the sizeof with its computed value. + """ + sizeofs = set(re.findall(r"sizeof\([^\)]*\)", header)) + tmp_dir = tempfile.mkdtemp() + c_program_path = Path(tmp_dir, "sizeof.c") + executable_path = Path(tmp_dir, "sizeof") + + for sizeof in sizeofs: + with open(c_program_path, "w", encoding="UTF-8") as c_program_file: + c_program_file.write(_get_sizeof_eval_source(sizeof)) + subprocess.check_call(["gcc", c_program_path, *cflags, "-o", executable_path]) + size = subprocess.check_output([executable_path]).decode() + header = header.replace(sizeof, size) + + os.remove(c_program_path) + os.remove(executable_path) + os.rmdir(tmp_dir) + return header + + +LIBCPUID_DIR = str(Path(*(Path(os.path.abspath(__file__)).parts[:-4]))) +LIBCPUID_INCLUDE_DIR = str(Path(LIBCPUID_DIR, "libcpuid")) +LIBCPUID_LIBRARY_DIR = str(Path(LIBCPUID_DIR, "libcpuid", ".libs")) +LIBCPUID_MAIN_HEADER_FILENAME = "libcpuid.h" +LIBCPUID_MAIN_HEADER_PATH = str( + Path(LIBCPUID_INCLUDE_DIR, LIBCPUID_MAIN_HEADER_FILENAME) ) +LIBCPUID_LIBRARY_NAME = "cpuid" +PYTHON_SRC_DIR = str(Path(LIBCPUID_DIR, "python", "src")) + +PREPROCESSED_HEADER = preprocess_header(LIBCPUID_MAIN_HEADER_PATH) +EVAL_SIZEOF_CFLAGS = [ + f"-I{LIBCPUID_INCLUDE_DIR}", + f"-L{LIBCPUID_LIBRARY_DIR}", + f"-l{LIBCPUID_LIBRARY_NAME}", + f"-Wl,-rpath={LIBCPUID_LIBRARY_DIR}", +] + +NO_SIZEOF_HEADER = eval_sizeofs(PREPROCESSED_HEADER, EVAL_SIZEOF_CFLAGS) -include_flags = get_include_flags() -preprocessed_header = preprocess_header(find_header_file(include_flags)) -no_sizeof_header = eval_sizeofs(preprocessed_header, include_flags) ffibuilder = FFI() -ffibuilder.cdef(no_sizeof_header) -ffibuilder.set_source_pkgconfig( - "libcpuid._libcpuid_cffi", ["libcpuid"], "#include " -) +ffibuilder.cdef(NO_SIZEOF_HEADER) + +set_source_kwargs = { + "module_name": "libcpuid._libcpuid_cffi", + "source": f"#include <{LIBCPUID_MAIN_HEADER_FILENAME}>", + "libraries": [LIBCPUID_LIBRARY_NAME], + "include_dirs": [LIBCPUID_INCLUDE_DIR], + "library_dirs": [LIBCPUID_LIBRARY_DIR], +} + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("-r", "--runtime-link", action="store_true") + args = parser.parse_args() + if args.runtime_link: + set_source_kwargs["extra_link_args"] = [f"-Wl,-rpath={LIBCPUID_LIBRARY_DIR}"] + +ffibuilder.set_source(**set_source_kwargs) + +if __name__ == "__main__": + ffibuilder.compile(PYTHON_SRC_DIR) diff --git a/python/src/libcpuid/_ffi_build_rtd.py b/python/src/libcpuid/_ffi_build_rtd.py deleted file mode 100644 index 09d09ba..0000000 --- a/python/src/libcpuid/_ffi_build_rtd.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -Script for compiling the C FFI for the live documentation. -""" - -import sys -import os -from cffi import FFI - -sys.path.append(os.path.dirname(os.path.abspath(__file__))) - -from _ffi_build_utils import ( # pylint: disable=import-error, wrong-import-position - preprocess_header, - eval_sizeofs, -) - -if __name__ == "__main__": - header_path = sys.argv[1] - install_dir = sys.argv[2] - library_dir = os.path.join(os.getcwd(), install_dir, "lib") - include_dir = os.path.join(install_dir, "include", "libcpuid") - ffibuilder = FFI() - ffibuilder.cdef(eval_sizeofs(preprocess_header(header_path), [f"-I{include_dir}"])) - ffibuilder.set_source( - "python.src.libcpuid._libcpuid_cffi", - "#include ", - libraries=["cpuid"], - library_dirs=[library_dir], - include_dirs=[include_dir], - extra_link_args=[f"-Wl,-rpath={library_dir}"], - ) - ffibuilder.compile(verbose=True) diff --git a/python/src/libcpuid/_ffi_build_utils.py b/python/src/libcpuid/_ffi_build_utils.py deleted file mode 100644 index 410bd8c..0000000 --- a/python/src/libcpuid/_ffi_build_utils.py +++ /dev/null @@ -1,105 +0,0 @@ -""" -Utility functions for building the FFI. -""" - -import subprocess -import os -import re -import tempfile - - -class FFIBuildException(Exception): - """Generic exception for errors occuring during the CFFI build.""" - - -def get_include_flags(): - """ - Obtains libcpuid include flags via pkg-config. - """ - try: - cflags = ( - subprocess.check_output(["pkg-config", "libcpuid", "--cflags-only-I"]) - .decode() - .strip() - .split() - ) - return cflags - except subprocess.CalledProcessError as e: - if e.returncode == 127: - raise FFIBuildException( - "The pkg-config command is necessary to build python-libcpuid." - ) from e - if e.returncode == 1: - raise FFIBuildException( - "The libcpuid C library (devel) was not found." - ) from e - raise FFIBuildException("Error looking for the libcpuid library") from e - - -def find_header_file(include_flags): - """ - Obtains main libcpuid header file location from include flags. - """ - header_path = None # pylint: disable=invalid-name - for cflag in include_flags: - header_candidate = os.path.join(cflag[2:], "libcpuid.h") - if os.path.isfile(header_candidate): - header_path = header_candidate - break - if header_path is None: - raise FFIBuildException("Could not find header file of the libcpuid library.") - return header_path - - -def preprocess_header(header_path): - """ - Preprocesses the header file (python-cffi only accepts preprocessed C definitions) - at the given path and returns it as a string. - """ - try: - return subprocess.check_output( - ["gcc", "-U __GNUC__", "-E", header_path] - ).decode() - except subprocess.CalledProcessError as e: - if e.returncode == 127: - raise FFIBuildException( - "The gcc compiler is necessary to build python-libcpuid." - ) from e - raise FFIBuildException( - f"Error preprocessing the libcpuid header file: {e.stderr}" - ) from e - - -def _get_sizeof_eval_source(sizeof): - return f""" -#include -#include - -int main() {{ - printf("%ld", {sizeof}); - return 0; -}} -""" - - -def eval_sizeofs(header, cflags): - """ - Evaluates each sizeof found in the given C header and replaces all - occurences of the sizeof with its computed value. - """ - sizeofs = set(re.findall(r"sizeof\([^\)]*\)", header)) - tmp_dir = tempfile.mkdtemp() - c_program_path = os.path.join(tmp_dir, "sizeof.c") - executable_path = os.path.join(tmp_dir, "sizeof") - - for sizeof in sizeofs: - with open(c_program_path, "w", encoding="UTF-8") as c_program_file: - c_program_file.write(_get_sizeof_eval_source(sizeof)) - subprocess.check_call(["gcc", c_program_path, *cflags, "-o", executable_path]) - size = subprocess.check_output([executable_path]).decode() - header = header.replace(sizeof, size) - - os.remove(c_program_path) - os.remove(executable_path) - os.rmdir(tmp_dir) - return header