aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStephen Kelly <steveire@gmail.com>2016-04-23 17:24:11 +0200
committerStephen Kelly <steveire@gmail.com>2016-10-31 15:38:08 +0000
commit64eb5f8e1320feb78c56ec0acb7399ee6085770d (patch)
tree6ced89f400b6f4208fb5ad2fbd8501a20f1751bd
parente052fc95db845a5e0f0b450c8fbffb35f0bbc638 (diff)
downloadextra-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.
-rw-r--r--find-modules/FindPythonModuleGeneration.cmake436
-rw-r--r--tests/CMakeLists.txt18
-rw-r--r--tests/GenerateSipBindings/CMakeLists.txt30
-rw-r--r--tests/GenerateSipBindings/cpplib.cpp143
-rw-r--r--tests/GenerateSipBindings/cpplib.h113
-rw-r--r--tests/GenerateSipBindings/rules_SipTest.py17
-rw-r--r--tests/GenerateSipBindings/testscript.py87
7 files changed, 844 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()
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 4880f5a5..d83db8a4 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -79,6 +79,24 @@ macro(add_test_macro NAME)
add_test_variant("${NAME}" "${NAME}" ${ARGN})
endmacro()
+list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/find-modules)
+
+find_package(PythonModuleGeneration REQUIRED)
+
+foreach(pyversion 2 3)
+ if (GPB_PYTHON${pyversion}_COMMAND)
+ if (pythonCommands)
+ list(APPEND pythonCommands " && ")
+ endif()
+ set(pythonCommands
+ ${GPB_PYTHON${pyversion}_COMMAND}
+ "${CMAKE_CURRENT_SOURCE_DIR}/GenerateSipBindings/testscript.py"
+ "${CMAKE_CURRENT_BINARY_DIR}/GenerateSipBindings/py${pyversion}"
+ )
+ endif()
+endforeach()
+add_test_macro(GenerateSipBindings ${pythonCommands})
+
add_test_macro(ExecuteCoreModules dummy)
add_test_macro(ExecuteKDEModules dummy)
diff --git a/tests/GenerateSipBindings/CMakeLists.txt b/tests/GenerateSipBindings/CMakeLists.txt
new file mode 100644
index 00000000..c48b1bce
--- /dev/null
+++ b/tests/GenerateSipBindings/CMakeLists.txt
@@ -0,0 +1,30 @@
+cmake_minimum_required(VERSION 3.3)
+
+project(GenerateSipBindings)
+
+find_package(Qt5Core REQUIRED)
+
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_INCLUDE_CURRENT_DIR ON)
+set(CMAKE_INCLUDE_CURRENT_DIR_IN_INTERFACE ON)
+
+set(CMAKE_CXX_STANDARD 14)
+
+add_library(CppLib SHARED cpplib.cpp)
+target_link_libraries(CppLib PUBLIC Qt5::Core)
+target_compile_features(CppLib PUBLIC cxx_nullptr)
+
+list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../find-modules)
+
+find_package(PythonModuleGeneration REQUIRED)
+
+ecm_generate_python_binding(
+ TARGET CppLib
+ PYTHONNAMESPACE PyTest
+ MODULENAME CppLib
+ RULES_FILE "${CMAKE_CURRENT_SOURCE_DIR}/rules_SipTest.py"
+ SIP_DEPENDS
+ QtCore/QtCoremod.sip
+ HEADERS
+ cpplib.h
+)
diff --git a/tests/GenerateSipBindings/cpplib.cpp b/tests/GenerateSipBindings/cpplib.cpp
new file mode 100644
index 00000000..c5b7a5fb
--- /dev/null
+++ b/tests/GenerateSipBindings/cpplib.cpp
@@ -0,0 +1,143 @@
+
+#include "cpplib.h"
+
+MyObject::MyObject(QObject* parent)
+ : QObject(parent)
+{
+
+}
+
+double MyObject::unnamedParameters(int i, double d)
+{
+ return i * d;
+}
+
+int MyObject::addThree(int input) const
+{
+ return input + 3;
+}
+
+QList<int> MyObject::addThree(QList<int> input) const
+{
+ auto output = input;
+ std::transform(output.begin(), output.end(),
+ output.begin(),
+ [](int in) { return in + 3; });
+ return output;
+}
+
+const QString MyObject::addThree(const QString& input, const QString& prefix) const
+{
+ return prefix + input + QStringLiteral("Three");
+}
+
+int MyObject::findNeedle(QStringList list, QString needle, Qt::MatchFlags flags) const
+{
+ if (flags & Qt::MatchStartsWith) {
+ auto it = std::find_if(list.begin(), list.end(), [needle](QString cand) {
+ return cand.startsWith(needle);
+ });
+ if (it != list.end()) {
+ return std::distance(list.begin(), it);
+ }
+ return -1;
+ }
+ return list.indexOf(needle);
+}
+
+int MyObject::qtEnumTest(QFlags<Qt::MatchFlag> flags)
+{
+ return flags;
+}
+
+int MyObject::localEnumTest(QFlags<LocalEnum> flags)
+{
+ return flags;
+}
+
+int MyObject::functionParam(std::function<int()> fn)
+{
+ return fn();
+}
+
+int MyObject::groups(unsigned int maxCount) const
+{
+ return maxCount;
+}
+
+class FwdDecl
+{
+
+};
+
+int MyObject::fwdDecl(const FwdDecl&)
+{
+ return 42;
+}
+
+int MyObject::const_parameters(const int input, QObject* const obj) const
+{
+ if (obj) return input / 3;
+ return input / 2;
+}
+
+NonCopyable::NonCopyable()
+ : mNum(new int(42))
+{
+
+}
+
+NonCopyable::~NonCopyable()
+{
+ delete mNum;
+}
+
+NonCopyableByMacro::NonCopyableByMacro()
+{
+
+}
+
+namespace SomeNS {
+
+NonCopyableInNS::NonCopyableInNS()
+ : mNum(new int(42))
+{
+
+}
+
+NonCopyableInNS::~NonCopyableInNS()
+{
+ delete mNum;
+}
+
+}
+
+void MyObject::publicSlot1()
+{
+ Q_EMIT publicSlotCalled();
+}
+
+void MyObject::publicSlot2()
+{
+ Q_EMIT publicSlotCalled();
+}
+
+void MyObject::protectedSlot1()
+{
+ Q_EMIT protectedSlotCalled();
+}
+
+void MyObject::protectedSlot2()
+{
+ Q_EMIT protectedSlotCalled();
+}
+
+void MyObject::privateSlot1()
+{
+ Q_EMIT privateSlotCalled();
+}
+
+void MyObject::privateSlot2()
+{
+ Q_EMIT privateSlotCalled();
+}
diff --git a/tests/GenerateSipBindings/cpplib.h b/tests/GenerateSipBindings/cpplib.h
new file mode 100644
index 00000000..f8db75b5
--- /dev/null
+++ b/tests/GenerateSipBindings/cpplib.h
@@ -0,0 +1,113 @@
+
+#pragma once
+
+#include <QtCore/QObject>
+#include <QtCore/QMap>
+
+#include <functional>
+
+class FwdDecl;
+
+class MyObject : public QObject
+{
+ Q_OBJECT
+public:
+ MyObject(QObject* parent = nullptr);
+
+ enum LocalEnum {
+ Val1 = 1,
+ Val2
+ };
+ Q_DECLARE_FLAGS(LocalEnums, LocalEnum)
+
+ enum {
+ AnonVal1,
+ AnonVal2
+ };
+
+ double unnamedParameters(int, double);
+
+ int addThree(int input) const;
+ QList<int> addThree(QList<int> input) const;
+
+ const QString addThree(const QString& input, const QString& prefix = QStringLiteral("Default")) const;
+
+ int findNeedle(QStringList list, QString needle, Qt::MatchFlags flags = Qt::MatchFlags(Qt::MatchStartsWith | Qt::MatchWrap)) const;
+
+ int qtEnumTest(QFlags<Qt::MatchFlag> flags);
+ int localEnumTest(QFlags<MyObject::LocalEnum> flags);
+
+ int functionParam(std::function<int()> fn);
+ int groups(unsigned int maxCount = std::numeric_limits<uint>::max()) const;
+
+ int const_parameters(const int input, QObject* const obj = 0) const;
+
+ int fwdDecl(const FwdDecl& f);
+ int fwdDeclRef(FwdDecl& f);
+
+signals:
+ void publicSlotCalled();
+
+Q_SIGNALS:
+ void privateSlotCalled();
+ void protectedSlotCalled();
+
+public slots:
+ void publicSlot1();
+
+public Q_SLOTS:
+ void publicSlot2();
+
+protected slots:
+ void protectedSlot1();
+
+protected Q_SLOTS:
+ void protectedSlot2();
+
+private slots:
+ void privateSlot1();
+
+private Q_SLOTS:
+ void privateSlot2();
+};
+
+class NonCopyable
+{
+public:
+ NonCopyable();
+ ~NonCopyable();
+
+private:
+ int* const mNum;
+};
+
+
+class NonCopyableByMacro
+{
+public:
+ NonCopyableByMacro();
+
+private:
+ Q_DISABLE_COPY(NonCopyableByMacro)
+};
+
+class HasPrivateDefaultCtor
+{
+public:
+private:
+ HasPrivateDefaultCtor(int param = 0);
+};
+
+namespace SomeNS {
+
+class NonCopyableInNS
+{
+public:
+ NonCopyableInNS();
+ ~NonCopyableInNS();
+
+private:
+ int* const mNum;
+};
+
+}
diff --git a/tests/GenerateSipBindings/rules_SipTest.py b/tests/GenerateSipBindings/rules_SipTest.py
new file mode 100644
index 00000000..e0cd2b9e
--- /dev/null
+++ b/tests/GenerateSipBindings/rules_SipTest.py
@@ -0,0 +1,17 @@
+
+import os, sys
+
+import rules_engine
+sys.path.append(os.path.dirname(os.path.dirname(rules_engine.__file__)))
+import Qt5Ruleset
+
+def local_function_rules():
+ return [
+ ["MyObject", "fwdDecl", ".*", ".*", ".*", rules_engine.function_discard],
+ ["MyObject", "fwdDeclRef", ".*", ".*", ".*", rules_engine.function_discard],
+ ]
+
+class RuleSet(Qt5Ruleset.RuleSet):
+ def __init__(self):
+ Qt5Ruleset.RuleSet.__init__(self)
+ self._fn_db = rules_engine.FunctionRuleDb(lambda: local_function_rules() + Qt5Ruleset.function_rules())
diff --git a/tests/GenerateSipBindings/testscript.py b/tests/GenerateSipBindings/testscript.py
new file mode 100644
index 00000000..6f2132a2
--- /dev/null
+++ b/tests/GenerateSipBindings/testscript.py
@@ -0,0 +1,87 @@
+
+import sys
+
+from PyQt5 import QtCore
+
+sys.path.append(sys.argv[1])
+
+import PyTest.CppLib
+
+mo = PyTest.CppLib.MyObject()
+
+assert(mo.addThree(39) == 42)
+
+assert(mo.addThree([38, 39, 40]) == [41, 42, 43])
+
+assert(mo.addThree("SomeString") == "DefaultSomeStringThree")
+
+assert(mo.findNeedle(["One", "Two", "Three"], "Two") == 1)
+assert(mo.findNeedle(["One", "Two", "Three"], "Four") == -1)
+assert(mo.findNeedle(["One", "Two", "Three"], "Th") == 2)
+assert(mo.findNeedle(["One", "Two", "Three"], "Th", QtCore.Qt.MatchExactly) == -1)
+
+assert(mo.const_parameters(30) == 15)
+assert(mo.const_parameters(30, mo) == 10)
+
+assert(mo.qtEnumTest(QtCore.Qt.MatchContains | QtCore.Qt.MatchStartsWith) == 3)
+assert(mo.localEnumTest(PyTest.CppLib.MyObject.Val2) == 2)
+
+class Reactor(QtCore.QObject):
+ def __init__(self, obj):
+ QtCore.QObject.__init__(self)
+ self.gotPrivateSlotCalledSignal = False
+ self.gotProtectedSlotCalledSignal = False
+ self.gotPublicSlotCalledSignal = False
+
+ obj.privateSlotCalled.connect(self.react_to_privateSlotCalled)
+ obj.protectedSlotCalled.connect(self.react_to_protectedSlotCalled)
+ obj.publicSlotCalled.connect(self.react_to_publicSlotCalled)
+
+ def react_to_privateSlotCalled(self):
+ self.gotPrivateSlotCalledSignal = True
+
+ def react_to_protectedSlotCalled(self):
+ self.gotProtectedSlotCalledSignal = True
+
+ def react_to_publicSlotCalled(self):
+ self.gotPublicSlotCalledSignal = True
+
+class Emitter(QtCore.QObject):
+ privateTrigger = QtCore.pyqtSignal()
+ protectedTrigger = QtCore.pyqtSignal()
+ publicTrigger = QtCore.pyqtSignal()
+
+ def __init__(self, obj):
+ QtCore.QObject.__init__(self)
+ self.privateTrigger.connect(obj.privateSlot1)
+ self.protectedTrigger.connect(obj.protectedSlot1)
+ self.publicTrigger.connect(obj.publicSlot1)
+
+ def emitSignalForPublic(self):
+ self.publicTrigger.emit()
+
+ def emitSignalForPrivate(self):
+ self.privateTrigger.emit()
+
+ def emitSignalForProtected(self):
+ self.protectedTrigger.emit()
+
+e = Emitter(mo)
+
+r = Reactor(mo)
+
+assert(not r.gotPrivateSlotCalledSignal)
+assert(not r.gotProtectedSlotCalledSignal)
+assert(not r.gotPublicSlotCalledSignal)
+
+e.emitSignalForPrivate()
+
+assert(r.gotPrivateSlotCalledSignal)
+
+e.emitSignalForProtected()
+
+assert(r.gotProtectedSlotCalledSignal)
+
+e.emitSignalForPublic()
+
+assert(r.gotPublicSlotCalledSignal)