diff options
author | Stephen Kelly <steveire@gmail.com> | 2016-04-23 17:24:11 +0200 |
---|---|---|
committer | Stephen Kelly <steveire@gmail.com> | 2016-10-31 15:38:08 +0000 |
commit | 64eb5f8e1320feb78c56ec0acb7399ee6085770d (patch) | |
tree | 6ced89f400b6f4208fb5ad2fbd8501a20f1751bd /find-modules | |
parent | e052fc95db845a5e0f0b450c8fbffb35f0bbc638 (diff) | |
download | extra-cmake-modules-64eb5f8e1320feb78c56ec0acb7399ee6085770d.tar.gz extra-cmake-modules-64eb5f8e1320feb78c56ec0acb7399ee6085770d.tar.bz2 |
Add the PythonModuleGeneration module
This can be used by KF5 libraries to generate python 2 and 3 bindings.
Diffstat (limited to 'find-modules')
-rw-r--r-- | find-modules/FindPythonModuleGeneration.cmake | 436 |
1 files changed, 436 insertions, 0 deletions
diff --git a/find-modules/FindPythonModuleGeneration.cmake b/find-modules/FindPythonModuleGeneration.cmake new file mode 100644 index 00000000..ef47fed2 --- /dev/null +++ b/find-modules/FindPythonModuleGeneration.cmake @@ -0,0 +1,436 @@ +#.rst: +# FindPythonModuleGeneration +# -------------------------- +# +# This module is experimental and internal. The interface will likely +# change in the coming releases. +# +# Tools and macros for generating python bindings +# +# This will define the following public function: +# +# ecm_generate_python_binding(TARGET <target> +# PYTHONNAMESPACE <namespace> +# MODULENAME <modulename> +# HEADERS <headers>) +# +# Invoking the function will create bindings for the <target> for python 2 and 3, +# if available. The bindings will be put in the namespace <namespace> in python, +# and will be available from the module <modulename>. +# +# The optional rules file specifies the rules for creating the bindings +# +# A simple invokation would be: +# +# ecm_generate_python_binding(KMyTarget +# PYTHONNAMESPACE PyKF5 +# MODULENAME MyTarget +# SIP_DEPENDS QtCore/QtCoremod.sip +# HEADERS ${myTargetHeaders} +# ) +# +# which can then be used from python as +# +# import PyKF5.MyTarget +# + +#============================================================================= +# Copyright 2016 Stephen Kelly <steveire@gmail.com> +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#============================================================================= + + +macro(_find_python version minor_version) + set(_CURRENT_VERSION ${version}.${minor_version}) + find_library(GPB_PYTHON${version}_LIBRARY + NAMES + python${_CURRENT_VERSION}mu + python${_CURRENT_VERSION}m + python${_CURRENT_VERSION}u + python${_CURRENT_VERSION} + PATHS + [HKEY_LOCAL_MACHINE\\SOFTWARE\\Python\\PythonCore\\${_CURRENT_VERSION}\\InstallPath]/libs + [HKEY_CURRENT_USER\\SOFTWARE\\Python\\PythonCore\\${_CURRENT_VERSION}\\InstallPath]/libs + # Avoid finding the .dll in the PATH. We want the .lib. + NO_SYSTEM_ENVIRONMENT_PATH + ) + + if(GPB_PYTHON${version}_LIBRARY) + # Use the library's install prefix as a hint + set(_Python_INCLUDE_PATH_HINT) + get_filename_component(_Python_PREFIX ${GPB_PYTHON${version}_LIBRARY} PATH) + get_filename_component(_Python_PREFIX ${_Python_PREFIX} PATH) + if(_Python_PREFIX) + set(_Python_INCLUDE_PATH_HINT ${_Python_PREFIX}/include) + endif() + unset(_Python_PREFIX) + + find_path(GPB_PYTHON${version}_INCLUDE_DIR + NAMES Python.h + HINTS + ${_Python_INCLUDE_PATH_HINT} + PATHS + [HKEY_LOCAL_MACHINE\\SOFTWARE\\Python\\PythonCore\\${_CURRENT_VERSION}\\InstallPath]/include + [HKEY_CURRENT_USER\\SOFTWARE\\Python\\PythonCore\\${_CURRENT_VERSION}\\InstallPath]/include + PATH_SUFFIXES + python${_CURRENT_VERSION}mu + python${_CURRENT_VERSION}m + python${_CURRENT_VERSION}u + python${_CURRENT_VERSION} + ) + endif() + + find_program(GPB_PYTHON${version}_COMMAND python${version}) +endmacro() + +macro(_create_imported_python_target version) + if(GPB_PYTHON${version}_LIBRARY AND GPB_PYTHON${version}_INCLUDE_DIR AND EXISTS "${GPB_PYTHON${version}_INCLUDE_DIR}/patchlevel.h") + list(APPEND _pyversions ${version}) + + file(STRINGS "${GPB_PYTHON${version}_INCLUDE_DIR}/patchlevel.h" python_version_define + REGEX "^#define[ \t]+PY_MINOR_VERSION[ \t]+[0-9]+") + string(REGEX REPLACE "^#define[ \t]+PY_MINOR_VERSION[ \t]+([0-9]+)" "\\1" + min_ver "${python_version_define}") + unset(python_version_define) + + set(pyversion${version}_maj_min ${version}.${min_ver}) + + add_library(Python::Libs${version} UNKNOWN IMPORTED) + set_property(TARGET Python::Libs${version} PROPERTY IMPORTED_LOCATION ${GPB_PYTHON${version}_LIBRARY}) + set_property(TARGET Python::Libs${version} PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${GPB_PYTHON${version}_INCLUDE_DIR}) + endif() +endmacro() + +macro(_report_NOT_FOUND message) + if(NOT ${CMAKE_FIND_PACKAGE_NAME}_FIND_QUIETLY) + set(_GPB_MESSAGE_TYPE STATUS) + if(${CMAKE_FIND_PACKAGE_NAME}_FIND_REQUIRED) + set(_GPB_MESSAGE_TYPE FATAL_ERROR) + endif() + message(${_GPB_MESSAGE_TYPE} ${message}) + endif() + set(PythonModuleGeneration_FOUND FALSE) + return() +endmacro() + +if (NOT Qt5Core_FOUND) + _report_NOT_FOUND("Qt 5 must be found before finding ${CMAKE_FIND_PACKAGE_NAME}.") +endif() + +if (NOT GPB_PYTHON3_LIBRARY) + set(_PYTHON3_MIN_VERSION 4) + set(_PYTHON3_MAX_VERSION 10) + + _find_python(3 ${_PYTHON3_MAX_VERSION}) # Canary check + + if (GPB_PYTHON3_LIBRARY) + message(FATAL_ERROR "The max python version in ${CMAKE_FIND_PACKAGE_NAME} must be updated.") + endif() + + set(_PYTHON3_FIND_VERSION ${_PYTHON3_MAX_VERSION}) + + while(NOT GPB_PYTHON3_LIBRARY + AND NOT GPB_PYTHON3_INCLUDE_DIR + AND NOT EXISTS "${GPB_PYTHON3_INCLUDE_DIR}/patchlevel.h" + AND NOT _PYTHON3_FIND_VERSION EQUAL ${_PYTHON3_MIN_VERSION}) + math(EXPR _PYTHON3_FIND_VERSION "${_PYTHON3_FIND_VERSION} - 1") + _find_python(3 ${_PYTHON3_FIND_VERSION}) + endwhile() +endif() +_create_imported_python_target(3) + +_find_python(2 7) +_create_imported_python_target(2) + +if (NOT _pyversions) + _report_NOT_FOUND("At least one python version must be available to use ${CMAKE_FIND_PACKAGE_NAME}.") +endif() + +find_program(GBP_SIP_COMMAND sip) + +if (NOT GBP_SIP_COMMAND) + _report_NOT_FOUND("The sip executable must be available to use ${CMAKE_FIND_PACKAGE_NAME}.") +endif() + +if (NOT GPB_PYTHON2_COMMAND) + _report_NOT_FOUND("The python2 executable is required by clang-python for the ${CMAKE_FIND_PACKAGE_NAME} Module.") +endif() + +if (NOT libclang_LIBRARY) + set(_LIBCLANG3_MAX_VERSION 16) + set(_LIBCLANG3_MIN_VERSION 8) + + find_library(libclang_LIBRARY clang-3.${_LIBCLANG3_MAX_VERSION}) + + if (libclang_LIBRARY) + message(FATAL_ERROR "The max python version in ${CMAKE_FIND_PACKAGE_NAME} must be updated.") + endif() + + set(_LIBCLANG3_FIND_VERSION ${_LIBCLANG3_MAX_VERSION}) + while(NOT libclang_LIBRARY AND NOT _LIBCLANG3_FIND_VERSION EQUAL _LIBCLANG3_MIN_VERSION) + math(EXPR _LIBCLANG3_FIND_VERSION "${_LIBCLANG3_FIND_VERSION} - 1") + set(_GPB_CLANG_SUFFIX 3.${_LIBCLANG3_FIND_VERSION}) + find_library(libclang_LIBRARY clang-3.${_LIBCLANG3_FIND_VERSION}) + endwhile() +endif() + +if (NOT libclang_LIBRARY) + _report_NOT_FOUND("Could not find libclang version 3.8 or greater.") +endif() + +execute_process( + COMMAND ${GPB_PYTHON2_COMMAND} ${CMAKE_CURRENT_LIST_DIR}/sip_generator.py --self-check ${libclang_LIBRARY} + RESULT_VARIABLE selfCheckErrors +) + +if (selfCheckErrors) + _report_NOT_FOUND("sip_generator failed a self-check for the ${CMAKE_FIND_PACKAGE_NAME} Module.") +endif() + +get_filename_component(libclang_file "${libclang_file}" REALPATH) + +find_file(SIP_Qt5Core_Mod_FILE + NAMES QtCoremod.sip + PATH_SUFFIXES share/sip/PyQt5/QtCore +) + +if(NOT SIP_Qt5Core_Mod_FILE) + _report_NOT_FOUND("PyQt module files not found for the ${CMAKE_FIND_PACKAGE_NAME} Module.") +endif() + +file(STRINGS "${SIP_Qt5Core_Mod_FILE}" _SIP_Qt5_VERSIONS + REGEX "^%Timeline" +) + +string(REGEX MATCHALL "Qt_5_[^ }]+" _SIP_Qt5_VERSIONS "${_SIP_Qt5_VERSIONS}") + +set(GPB_Qt5_Tag Qt_5_${Qt5Core_VERSION_MINOR}_${Qt5Core_VERSION_PATCH}) + +list(FIND _SIP_Qt5_VERSIONS ${GPB_Qt5_Tag} _SIP_Qt5_Version_Index) + +if(_SIP_Qt5_Version_Index EQUAL -1) + _report_NOT_FOUND("PyQt module does not support Qt version 5.${Qt5Core_VERSION_MINOR}.${Qt5Core_VERSION_PATCH} for the ${CMAKE_FIND_PACKAGE_NAME} Module. Found available Qt5 tags: \"${_SIP_Qt5_VERSIONS}\".") +endif() + +set(PythonModuleGeneration_FOUND TRUE) + +include(CMakeParseArguments) + +set(GPB_MODULE_DIR ${CMAKE_CURRENT_LIST_DIR}) + +function(_compute_implicit_include_dirs) + set(CLANG_CXX_DRIVER clang++${_GPB_CLANG_SUFFIX}) + if (NOT EXISTS ${CLANG_CXX_DRIVER}) + set(CLANG_CXX_DRIVER clang++) + endif() + execute_process(COMMAND ${CLANG_CXX_DRIVER} -v -E -x c++ - + ERROR_VARIABLE _compilerOutput + OUTPUT_VARIABLE _compilerStdout + INPUT_FILE /dev/null) + + if( "${_compilerOutput}" MATCHES "> search starts here[^\n]+\n *(.+ *\n) *End of (search) list" ) + + # split the output into lines and then remove leading and trailing spaces from each of them: + string(REGEX MATCHALL "[^\n]+\n" _includeLines "${CMAKE_MATCH_1}") + foreach(nextLine ${_includeLines}) + # on OSX, gcc says things like this: "/System/Library/Frameworks (framework directory)", strip the last part + string(REGEX REPLACE "\\(framework directory\\)" "" nextLineNoFramework "${nextLine}") + # strip spaces at the beginning and the end + string(STRIP "${nextLineNoFramework}" _includePath) + list(APPEND _resultIncludeDirs "${_includePath}") + endforeach() + endif() + + set(_GPB_IMPLICIT_INCLUDE_DIRS ${_resultIncludeDirs} PARENT_SCOPE) +endfunction() + +function(ecm_generate_python_binding + target_keyword target_value + pythonnamespace_keyword pythonnamespace_value + modulename_keyword modulename_value + ) + + cmake_parse_arguments(GPB "" "RULES_FILE" "SIP_DEPENDS;SIP_INCLUDES;HEADERS" ${ARGN}) + + file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/sip/${pythonnamespace_value}/${modulename_value}/${modulename_value}mod.sip" + " +%Module ${pythonnamespace_value}.${modulename_value} + +%ModuleHeaderCode +#pragma GCC visibility push(default) +%End\n\n") + + set(generator_depends "${GPB_MODULE_DIR}/sip_generator.py" + "${GPB_MODULE_DIR}/rules_engine.py" + "${GPB_MODULE_DIR}/FindPythonModuleGeneration.cmake" + ) + + if (NOT _GPB_IMPLICIT_INCLUDE_DIRS) + _compute_implicit_include_dirs() + endif() + + foreach(dep ${GPB_SIP_DEPENDS}) + if (IS_ABSOLUTE ${dep}) + list(APPEND generator_depends "${dep}") + endif() + file(APPEND "${CMAKE_CURRENT_BINARY_DIR}/sip/${pythonnamespace_value}/${modulename_value}/${modulename_value}mod.sip" + "%Import ${dep}\n\n") + endforeach() + + set(sip_files) + set(commands) + + if (NOT GPB_RULES_FILE) + set(GPB_RULES_FILE "${GPB_MODULE_DIR}/Qt5Ruleset.py") + endif() + + + list(APPEND generator_depends ${GPB_RULES_FILE}) + + foreach(hdr ${GPB_HEADERS}) + + get_filename_component(hdr_file ${hdr} ABSOLUTE) + + get_filename_component(hdr ${hdr} NAME_WE) + string(TOLOWER ${hdr}.h hdr_filename) + + if (NOT EXISTS "${hdr_file}") + message(FATAL_ERROR "File not found: ${hdr_file}") + endif() + + set(sip_file "${CMAKE_CURRENT_BINARY_DIR}/sip/${pythonnamespace_value}/${modulename_value}/${hdr}.sip") + list(APPEND sip_files ${sip_file}) + + set(inc_dirs "-I$<JOIN:$<TARGET_PROPERTY:${target_value},INTERFACE_INCLUDE_DIRECTORIES>,;-I>") + set(sys_inc_dirs) + foreach(d ${_GPB_IMPLICIT_INCLUDE_DIRS}) + list(APPEND sys_inc_dirs "-isystem" "${d}") + endforeach() + set(comp_defs "-D$<JOIN:$<TARGET_PROPERTY:${target_value},INTERFACE_COMPILE_DEFINITIONS>,;-D>") + + foreach(stdVar 11 14) + set(stdFlag "$<$<STREQUAL:$<TARGET_PROPERTY:${target_value},CXX_STANDARD>,${stdVar}>:${CMAKE_CXX${stdVar}_EXTENSION_COMPILE_OPTION}>") + endforeach() + + set(comp_flags "$<JOIN:$<TARGET_PROPERTY:${target_value},INTERFACE_COMPILE_OPTIONS>;${stdFlag},;>") + + add_custom_command(OUTPUT ${sip_file} + COMMAND ${GPB_PYTHON2_COMMAND} ${GPB_MODULE_DIR}/sip_generator.py + --flags " ${inc_dirs};${sys_inc_dirs};${comp_defs};${comp_flags}" + --include_filename "${hdr_filename}" + ${libclang_LIBRARY} + ${GPB_RULES_FILE} + "${hdr_file}" + "${sip_file}" + DEPENDS ${hdr_file} ${generator_depends} + VERBATIM + ) + + file(APPEND "${CMAKE_CURRENT_BINARY_DIR}/sip/${pythonnamespace_value}/${modulename_value}/${modulename_value}mod.sip" + "%Include ${hdr}.sip\n") + endforeach() + + file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/pybuild/${pythonnamespace_value}/${modulename_value}/module.sbf" + " +target = ${modulename_value} +sources = sip${modulename_value}cmodule.cpp +headers = sipAPI${modulename_value} +" + ) + + get_filename_component(SIP_PYQT5_DIR ${SIP_Qt5Core_Mod_FILE} PATH) + get_filename_component(SIP_PYQT5_DIR ${SIP_PYQT5_DIR} PATH) + + set(sip_includes -I "${SIP_PYQT5_DIR}") + if (GPB_SIP_INCLUDES) + list(APPEND sip_includes -I "$<JOIN:${GPB_SIP_INCLUDES},-I>") + endif() + foreach(path ${CMAKE_PREFIX_PATH} ${CMAKE_INSTALL_PREFIX}) + if (EXISTS ${path}/share/sip) + list(APPEND sip_includes -I "${path}/share/sip") + endif() + endforeach() + + if (WIN32) + set(GPB_WS_Tag -t WS_WIN) + elif(APPLE) + set(GPB_WS_Tag -t WS_MACX) + else() + set(GPB_WS_Tag -t WS_X11) + endif() + + add_custom_target(generate_${modulename_value}_sip_files ALL DEPENDS ${sip_files}) + + add_custom_command(OUTPUT + "${CMAKE_CURRENT_BINARY_DIR}/pybuild/${pythonnamespace_value}/${modulename_value}/unified${modulename_value}.cpp" + COMMAND ${GPB_PYTHON2_COMMAND} "${GPB_MODULE_DIR}/run-sip.py" --sip ${GBP_SIP_COMMAND} + --unify "${CMAKE_CURRENT_BINARY_DIR}/pybuild/${pythonnamespace_value}/${modulename_value}/unified${modulename_value}.cpp" + --module-name "${modulename_value}" + -c "${CMAKE_CURRENT_BINARY_DIR}/pybuild/${pythonnamespace_value}/${modulename_value}" + -b "${CMAKE_CURRENT_BINARY_DIR}/pybuild/${pythonnamespace_value}/${modulename_value}/module.sbf" + -t ${GPB_Qt5_Tag} ${GPB_WS_Tag} + + -x VendorID -x Py_v3 + + -I "${CMAKE_CURRENT_BINARY_DIR}/sip/${pythonnamespace_value}/${modulename_value}" + ${sip_includes} + "${CMAKE_CURRENT_BINARY_DIR}/sip/${pythonnamespace_value}/${modulename_value}/${modulename_value}mod.sip" + DEPENDS generate_${modulename_value}_sip_files "${GPB_MODULE_DIR}/run-sip.py" ${generator_depends} + ) + + add_custom_target(sip_generated_${modulename_value}_files ALL + DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/pybuild/${pythonnamespace_value}/${modulename_value}/unified${modulename_value}.cpp") + + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/sip/${pythonnamespace_value}/${modulename_value}" + "${CMAKE_CURRENT_BINARY_DIR}/pybuild/${pythonnamespace_value}/${modulename_value}") + + foreach(pyversion ${_pyversions}) + file(MAKE_DIRECTORY + "${CMAKE_CURRENT_BINARY_DIR}/py${pyversion}/${pythonnamespace_value}") + execute_process(COMMAND "${CMAKE_COMMAND}" -E touch "${CMAKE_CURRENT_BINARY_DIR}/py${pyversion}/${pythonnamespace_value}/__init__.py") + + add_library(Py${pyversion}KF5${modulename_value} MODULE + "${CMAKE_CURRENT_BINARY_DIR}/pybuild/${pythonnamespace_value}/${modulename_value}/unified${modulename_value}.cpp" + ) + add_dependencies(Py${pyversion}KF5${modulename_value} sip_generated_${modulename_value}_files) + target_link_libraries(Py${pyversion}KF5${modulename_value} PRIVATE ${target_value} Python::Libs${pyversion}) + + target_compile_options(Py${pyversion}KF5${modulename_value} PRIVATE -fstack-protector-strong -Wno-deprecated-declarations -Wno-overloaded-virtual) + target_include_directories(Py${pyversion}KF5${modulename_value} PRIVATE ${GPB_SIP_INCLUDES}) + target_link_libraries(Py${pyversion}KF5${modulename_value} PRIVATE -Wl,-Bsymbolic-functions -Wl,-z,relro) + + set_property(TARGET Py${pyversion}KF5${modulename_value} PROPERTY AUTOMOC OFF) + set_property(TARGET Py${pyversion}KF5${modulename_value} PROPERTY PREFIX "") + set_property(TARGET Py${pyversion}KF5${modulename_value} PROPERTY OUTPUT_NAME py${pyversion}/${pythonnamespace_value}/${modulename_value}) + + add_test(NAME Py${pyversion}Test COMMAND ${GPB_PYTHON${pyversion}_COMMAND} "${CMAKE_SOURCE_DIR}/autotests/pythontest.py" ${CMAKE_CURRENT_BINARY_DIR}/py${pyversion}) + + install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/py${pyversion}/${pythonnamespace_value} + DESTINATION lib/python${pyversion${pyversion}_maj_min}/dist-packages) + install(FILES ${sip_files} "${CMAKE_CURRENT_BINARY_DIR}/sip/${pythonnamespace_value}/${modulename_value}/${modulename_value}mod.sip" + DESTINATION share/sip/${pythonnamespace_value}/${modulename_value} + ) + endforeach() +endfunction() |