The extension to install mixed-language projects using Python CFFI and CMake compared to Chapter 9, Mixed-language Projects, Recipe 6, Mixing C, C++, Fortran, and Python using Python CFFI consists of two additional steps:
- We require the setup.py layer.
- We install targets such that the header files and the shared library file(s) required by the CFFI layer are installed in the correct paths depending on the selected Python environment.
The structure of setup.py is almost identical to the previous recipe, and we refer you to the previous recipe for a discussion of this file. The only addition was a line containing install_requires=['cffi'] to make sure that installing our example package also fetches and installs the required Python CFFI. The setup.py script will automatically install __init__.py and version.py, since these are referenced from the setup.py script. MANIFEST.in is slightly changed to package not only README.rst and CMake files, but also the header and Fortran source files:
include README.rst CMakeLists.txt
recursive-include account *.h *.f90 CMakeLists.txt
We have three challenges in this recipe to package a CMake project that uses Python CFFI with setup.py:
- We need to copy the account.h and account_export.h header files as well as the shared library to the Python module location which depends on the Python environment.
- We need to tell __init__.py where to locate these header files and the library. In Chapter 9, Mixed-language Projects, Recipe 6, Mixing C, C++, Fortran, and Python using Python CFFI we have solved these using environment variables, but it would be unpractical to set these every time we plan to use the Python module.
- On the Python side, we don't know the exact name (suffix) of the shared library file, since it depends on the operating system.
Let us start with the last point: we don't know the exact name, but upon build system generation CMake does and therefore we use the generator expression in interface_file_names.cfg.in to expand the placeholder:
[configuration]
header_file_name = account.h
library_file_name = $<TARGET_FILE_NAME:account>
This input file is used to generate ${CMAKE_CURRENT_BINARY_DIR}/interface_file_names.cfg:
file(
GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/interface_file_names.cfg
INPUT ${CMAKE_CURRENT_SOURCE_DIR}/interface_file_names.cfg.in
)
We then define the two header files as PUBLIC_HEADER (see also Chapter 10, Writing an Installer) and the configuration file as RESOURCE:
set_target_properties(account
PROPERTIES
PUBLIC_HEADER "account.h;${CMAKE_CURRENT_BINARY_DIR}/account_export.h"
RESOURCE "${CMAKE_CURRENT_BINARY_DIR}/interface_file_names.cfg"
)
Finally, we install the library, header files, and the configuration file to a structure relative to a path defined by setup.py:
install(
TARGETS
account
LIBRARY
DESTINATION account/lib
RUNTIME
DESTINATION account/lib
PUBLIC_HEADER
DESTINATION account/include
RESOURCE
DESTINATION account
)
Note that we set DESTINATION for both LIBRARY and RUNTIME to point to account/lib. This is important for Windows, where shared libraries have executable entry points and therefore we have to specify both.
The Python package will be able to find these files thanks to this section in account/__init__.py:
# this interface requires the header file and library file
# and these can be either provided by interface_file_names.cfg
# in the same path as this file
# or if this is not found then using environment variables
_this_path = Path(os.path.dirname(os.path.realpath(__file__)))
_cfg_file = _this_path / 'interface_file_names.cfg'
if _cfg_file.exists():
config = ConfigParser()
config.read(_cfg_file)
header_file_name = config.get('configuration', 'header_file_name')
_header_file = _this_path / 'include' / header_file_name
_header_file = str(_header_file)
library_file_name = config.get('configuration', 'library_file_name')
_library_file = _this_path / 'lib' / library_file_name
_library_file = str(_library_file)
else:
_header_file = os.getenv('ACCOUNT_HEADER_FILE')
assert _header_file is not None
_library_file = os.getenv('ACCOUNT_LIBRARY_FILE')
assert _library_file is not None
In this case, _cfg_file will be found and parsed and setup.py will find the header file under include and the library under lib and pass these on to CFFI to construct the library object. This is also the reason why we have used lib as the install target DESTINATION and not CMAKE_INSTALL_LIBDIR, which otherwise might confuse account/__init__.py.