diff --git a/CMakeLists.txt b/CMakeLists.txt index 919969e..e3ddec5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,7 @@ install(FILES cmake/shiboken_helper.cmake cmake/sip_configure.py cmake/sip_helper.cmake + cmake/pyside_config.py DESTINATION share/${PROJECT_NAME}/cmake) if(BUILD_TESTING) diff --git a/cmake/pyside_config.py b/cmake/pyside_config.py new file mode 100644 index 0000000..f287eca --- /dev/null +++ b/cmake/pyside_config.py @@ -0,0 +1,415 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# 2022 Christoph Hellmann Santos +# SPDX-License-Identifier: BSD-3-Clause + +import sysconfig +from enum import Enum +import glob +import os +import re +import sys + + +PYSIDE = "pyside2" +PYSIDE_MODULE = "PySide2" +SHIBOKEN = "shiboken2" + + +class Package(Enum): + SHIBOKEN_MODULE = 1 + SHIBOKEN_GENERATOR = 2 + PYSIDE_MODULE = 3 + + +generic_error = ( + "Did you forget to activate your virtualenv? Or perhaps" + f" you forgot to build / install {PYSIDE_MODULE} into your currently active Python" + " environment?" +) +pyside_error = f"Unable to locate {PYSIDE_MODULE}. {generic_error}" +shiboken_module_error = f"Unable to locate {SHIBOKEN}-module. {generic_error}" +shiboken_generator_error = f"Unable to locate shiboken-generator. {generic_error}" +pyside_libs_error = f"Unable to locate the PySide shared libraries. {generic_error}" +python_link_error = "Unable to locate the Python library for linking." +python_include_error = "Unable to locate the Python include headers directory." + +options = [] + +# option, function, error, description +options.append( + ( + "--shiboken-module-path", + lambda: find_shiboken_module(), + shiboken_module_error, + "Print shiboken module location", + ) +) +options.append( + ( + "--shiboken-generator-path", + lambda: find_shiboken_generator(), + shiboken_generator_error, + "Print shiboken generator location", + ) +) +options.append( + ( + "--pyside-path", + lambda: find_pyside(), + pyside_error, + f"Print {PYSIDE_MODULE} location", + ) +) + +options.append( + ( + "--python-include-path", + lambda: get_python_include_path(), + python_include_error, + "Print Python include path", + ) +) +options.append( + ( + "--shiboken-generator-include-path", + lambda: get_package_include_path(Package.SHIBOKEN_GENERATOR), + pyside_error, + "Print shiboken generator include paths", + ) +) +options.append( + ( + "--pyside-include-path", + lambda: get_package_include_path(Package.PYSIDE_MODULE), + pyside_error, + "Print PySide include paths", + ) +) + +options.append( + ( + "--python-link-flags-qmake", + lambda: python_link_flags_qmake(), + python_link_error, + "Print python link flags for qmake", + ) +) +options.append( + ( + "--python-link-flags-cmake", + lambda: python_link_flags_cmake(), + python_link_error, + "Print python link flags for cmake", + ) +) + +options.append( + ( + "--shiboken-module-qmake-lflags", + lambda: get_package_qmake_lflags(Package.SHIBOKEN_MODULE), + pyside_error, + "Print shiboken shared library link flags for qmake", + ) +) +options.append( + ( + "--pyside-qmake-lflags", + lambda: get_package_qmake_lflags(Package.PYSIDE_MODULE), + pyside_error, + "Print PySide shared library link flags for qmake", + ) +) + +options.append( + ( + "--shiboken-module-shared-libraries-qmake", + lambda: get_shared_libraries_qmake(Package.SHIBOKEN_MODULE), + pyside_libs_error, + "Print paths of shiboken shared libraries (.so's, .dylib's, .dll's) for qmake", + ) +) +options.append( + ( + "--shiboken-module-shared-libraries-cmake", + lambda: get_shared_libraries_cmake(Package.SHIBOKEN_MODULE), + pyside_libs_error, + "Print paths of shiboken shared libraries (.so's, .dylib's, .dll's) for cmake", + ) +) + +options.append( + ( + "--pyside-shared-libraries-qmake", + lambda: get_shared_libraries_qmake(Package.PYSIDE_MODULE), + pyside_libs_error, + "Print paths of f{PYSIDE_MODULE} shared libraries (.so's, .dylib's, .dll's) for qmake", + ) +) +options.append( + ( + "--pyside-shared-libraries-cmake", + lambda: get_shared_libraries_cmake(Package.PYSIDE_MODULE), + pyside_libs_error, + f"Print paths of {PYSIDE_MODULE} shared libraries (.so's, .dylib's, .dll's) for cmake", + ) +) + +options_usage = "" +for i, (flag, _, _, description) in enumerate(options): + options_usage += f" {flag:<45} {description}" + if i < len(options) - 1: + options_usage += "\n" + +usage = f""" +Utility to determine include/link options of shiboken/PySide and Python for qmake/CMake projects +that would like to embed or build custom shiboken/PySide bindings. + +Usage: pyside_config.py [option] +Options: +{options_usage} + -a Print all options and their values + --help/-h Print this help +""" + +option = sys.argv[1] if len(sys.argv) == 2 else "-a" +if option == "-h" or option == "--help": + print(usage) + sys.exit(0) + + +def clean_path(path): + return path if sys.platform != "win32" else path.replace("\\", "/") + + +def shared_library_suffix(): + if sys.platform == "win32": + return "lib" + elif sys.platform == "darwin": + return "dylib" + # Linux + else: + return "so.*" + + +def import_suffixes(): + import importlib.machinery + + return importlib.machinery.EXTENSION_SUFFIXES + + +def is_debug(): + debug_suffix = "_d.pyd" if sys.platform == "win32" else "_d.so" + return any([s.endswith(debug_suffix) for s in import_suffixes()]) + + +def shared_library_glob_pattern(): + glob = "*." + shared_library_suffix() + return glob if sys.platform == "win32" else "lib" + glob + + +def filter_shared_libraries(libs_list): + def predicate(lib_name): + basename = os.path.basename(lib_name) + if "shiboken" in basename or "pyside" in basename: + return True + return False + + result = [lib for lib in libs_list if predicate(lib)] + return result + + +# Return qmake link option for a library file name +def link_option(lib): + # On Linux: + # Since we cannot include symlinks with wheel packages + # we are using an absolute path for the libpyside and libshiboken + # libraries when compiling the project + baseName = os.path.basename(lib) + link = " -l" + if sys.platform in [ + "linux", + "linux2", + ]: # Linux: 'libfoo.so' -> '/absolute/path/libfoo.so' + link = lib + elif sys.platform in ["darwin"]: # Darwin: 'libfoo.so' -> '-lfoo' + link += os.path.splitext(baseName[3:])[0] + else: # Windows: 'libfoo.dll' -> 'libfoo.dll' + link += os.path.splitext(baseName)[0] + return link + + +# Locate PySide via sys.path package path. +def find_pyside(): + return find_package_path(PYSIDE_MODULE) + + +def find_shiboken_module(): + return find_package_path(SHIBOKEN) + + +def find_shiboken_generator(): + return find_package_path(f"{SHIBOKEN}_generator") + + +def find_package(which_package): + if which_package == Package.SHIBOKEN_MODULE: + return find_shiboken_module() + if which_package == Package.SHIBOKEN_GENERATOR: + return find_shiboken_generator() + if which_package == Package.PYSIDE_MODULE: + return find_pyside() + return None + + +def find_package_path(dir_name): + for p in sys.path: + if "site-" in p: + package = os.path.join(p, dir_name) + if os.path.exists(package): + return clean_path(os.path.realpath(package)) + return None + + +# Return version as "3.6" +def python_version(): + return str(sys.version_info[0]) + "." + str(sys.version_info[1]) + + +def get_python_include_path(): + return sysconfig.get_path("include") + + +def python_link_flags_qmake(): + flags = python_link_data() + if sys.platform == "win32": + libdir = flags["libdir"] + # This will add the "~1" shortcut for directories that + # contain white spaces + # e.g.: "Program Files" to "Progra~1" + for d in libdir.split("\\"): + if " " in d: + libdir = libdir.replace(d, d.split(" ")[0][:-1] + "~1") + lib_flags = flags["lib"] + return f"-L{libdir} -l{lib_flags}" + elif sys.platform == "darwin": + libdir = flags["libdir"] + lib_flags = flags["lib"] + return f"-L{libdir} -l{lib_flags}" + else: + # Linux and anything else + libdir = flags["libdir"] + lib_flags = flags["lib"] + return f"-L{libdir} -l{lib_flags}" + + +def python_link_flags_cmake(): + flags = python_link_data() + libdir = flags["libdir"] + lib = re.sub(r".dll$", ".lib", flags["lib"]) + return f"{libdir};{lib}" + + +def python_link_data(): + # @TODO Fix to work with static builds of Python + libdir = sysconfig.get_config_var("LIBDIR") + if libdir is None: + libdir = os.path.abspath( + os.path.join(sysconfig.get_config_var("LIBDEST"), "..", "libs") + ) + version = python_version() + version_no_dots = version.replace(".", "") + + flags = {} + flags["libdir"] = libdir + if sys.platform == "win32": + suffix = "_d" if is_debug() else "" + flags["lib"] = f"python{version_no_dots}{suffix}" + + elif sys.platform == "darwin": + flags["lib"] = f"python{version}" + + # Linux and anything else + else: + flags["lib"] = f"python{version}{sys.abiflags}" + + return flags + + +def get_package_include_path(which_package): + package_path = find_package(which_package) + if package_path is None: + return None + + includes = f"{package_path}/include" + + return includes + + +def get_package_qmake_lflags(which_package): + package_path = find_package(which_package) + if package_path is None: + return None + + link = f"-L{package_path}" + glob_result = glob.glob(os.path.join(package_path, shared_library_glob_pattern())) + for lib in filter_shared_libraries(glob_result): + link += " " + link += link_option(lib) + return link + + +def get_shared_libraries_data(which_package): + package_path = find_package(which_package) + if package_path is None: + return None + + glob_result = glob.glob(os.path.join(package_path, shared_library_glob_pattern())) + filtered_libs = filter_shared_libraries(glob_result) + libs = [] + if sys.platform == "win32": + for lib in filtered_libs: + libs.append(os.path.realpath(lib)) + else: + for lib in filtered_libs: + libs.append(lib) + return libs + + +def get_shared_libraries_qmake(which_package): + libs = get_shared_libraries_data(which_package) + if libs is None: + return None + + if sys.platform == "win32": + if not libs: + return "" + dlls = "" + for lib in libs: + dll = os.path.splitext(lib)[0] + ".dll" + dlls += dll + " " + + return dlls + else: + libs_string = "" + for lib in libs: + libs_string += lib + " " + return libs_string + + +def get_shared_libraries_cmake(which_package): + libs = get_shared_libraries_data(which_package) + result = ";".join(libs) + return result + + +print_all = option == "-a" +for argument, handler, error, _ in options: + if option == argument or print_all: + handler_result = handler() + if handler_result is None: + sys.exit(error) + + line = handler_result + if print_all: + line = f"{argument:<40}: {line}" + print(line) diff --git a/cmake/shiboken_helper.cmake b/cmake/shiboken_helper.cmake index 5b96d9f..a75f390 100644 --- a/cmake/shiboken_helper.cmake +++ b/cmake/shiboken_helper.cmake @@ -8,6 +8,31 @@ set(__PYTHON_QT_BINDING_SHIBOKEN_HELPER_INCLUDED TRUE) set(PYTHON_SUFFIX ".cpython-${PYTHON_VERSION_MAJOR}${PYTHON_VERSION_MINOR}m") set(PYTHON_EXTENSION_SUFFIX "${PYTHON_SUFFIX}-${CMAKE_CXX_LIBRARY_ARCHITECTURE}") +find_package(python_qt_binding REQUIRED) + +macro(pyside_config option output_var) + if(${ARGC} GREATER 2) + set(is_list ${ARGV2}) + else() + set(is_list "") + endif() + + execute_process( + COMMAND ${Python3_EXECUTABLE} "${python_qt_binding_DIR}/pyside_config.py" + ${option} + OUTPUT_VARIABLE ${output_var} + OUTPUT_STRIP_TRAILING_WHITESPACE) + + if("${${output_var}}" STREQUAL "") + message(FATAL_ERROR "Error: Calling pyside_config.py ${option} returned no output.") + endif() + if(is_list) + string(REPLACE " " ";" ${output_var} "${${output_var}}") + endif() +endmacro() + + + find_package(Shiboken2 QUIET) if(Shiboken2_FOUND) message(STATUS "Found Shiboken2 version ${Shiboken2_VERSION}") @@ -16,10 +41,22 @@ if(Shiboken2_FOUND) get_property(SHIBOKEN_LIBRARY TARGET Shiboken2::libshiboken PROPERTY LOCATION) set(SHIBOKEN_BINARY Shiboken2::shiboken2) endif() - message(STATUS "Using SHIBOKEN_INCLUDE_DIR: ${SHIBOKEN_INCLUDE_DIR}") - message(STATUS "Using SHIBOKEN_LIBRARY: ${SHIBOKEN_LIBRARY}") - message(STATUS "Using SHIBOKEN_BINARY: ${SHIBOKEN_BINARY}") +else() + if(WIN32) + pyside_config(--shiboken-generator-path shiboken_generator_path) + pyside_config(--shiboken-generator-include-path shiboken_include_dir 1) + pyside_config(--shiboken-module-shared-libraries-cmake shiboken_shared_libraries 0) + + set(SHIBOKEN_BINARY "${shiboken_generator_path}/shiboken2${CMAKE_EXECUTABLE_SUFFIX}") + set(SHIBOKEN_LIBRARY ${shiboken_shared_libraries}) + set(SHIBOKEN_INCLUDE_DIR ${shiboken_include_dir}) + endif() endif() + +message(STATUS "Using SHIBOKEN_INCLUDE_DIR: ${SHIBOKEN_INCLUDE_DIR}") +message(STATUS "Using SHIBOKEN_LIBRARY: ${SHIBOKEN_LIBRARY}") +message(STATUS "Using SHIBOKEN_BINARY: ${SHIBOKEN_BINARY}") + set(PYTHON_BASENAME "${PYTHON_SUFFIX}-${CMAKE_CXX_LIBRARY_ARCHITECTURE}") find_package(PySide2 QUIET) @@ -31,7 +68,18 @@ if(PySide2_FOUND) endif() message(STATUS "Using PYSIDE_INCLUDE_DIR: ${PYSIDE_INCLUDE_DIR}") message(STATUS "Using PYSIDE_LIBRARY: ${PYSIDE_LIBRARY}") +else() + if(WIN32) + pyside_config(--pyside-include-path pyside_include_dir 1) + pyside_config(--pyside-shared-libraries-cmake pyside_shared_libraries 0) + pyside_config(--pyside-path pyside_dir 1) + set(PYSIDE_INCLUDE_DIR ${pyside_include_dir}) + set(PYSIDE_LIBRARY ${pyside_shared_libraries}) + set(PYSIDE_TYPESYSTEMS "${pyside_dir}/typesystems") + endif() endif() +message(STATUS "Using PYSIDE_INCLUDE_DIR: ${PYSIDE_INCLUDE_DIR}") +message(STATUS "Using PYSIDE_LIBRARY: ${PYSIDE_LIBRARY}") set(Python_ADDITIONAL_VERSIONS "${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}") find_package(PythonLibs "${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}") @@ -52,6 +100,12 @@ else() set(shiboken_helper_NOTFOUND TRUE) endif() +if(WIN32) + set(PATH_SPLITTER "\\\;") +else() + set(PATH_SPLITTER ":") +endif() + macro(_shiboken_generator_command VAR GLOBAL TYPESYSTEM INCLUDE_PATH BUILD_DIR) # Add includes from current directory, Qt, PySide and compiler specific dirs @@ -63,15 +117,16 @@ macro(_shiboken_generator_command VAR GLOBAL TYPESYSTEM INCLUDE_PATH BUILD_DIR) # See ticket https://code.ros.org/trac/ros-pkg/ticket/5219 set(SHIBOKEN_HELPER_INCLUDE_DIRS_WITH_COLONS "") foreach(dir ${SHIBOKEN_HELPER_INCLUDE_DIRS}) - set(SHIBOKEN_HELPER_INCLUDE_DIRS_WITH_COLONS "${SHIBOKEN_HELPER_INCLUDE_DIRS_WITH_COLONS}:${dir}") + set(SHIBOKEN_HELPER_INCLUDE_DIRS_WITH_COLONS "${SHIBOKEN_HELPER_INCLUDE_DIRS_WITH_COLONS}${PATH_SPLITTER}${dir}") endforeach() - string(REPLACE ";" ":" INCLUDE_PATH_WITH_COLONS "${INCLUDE_PATH}") + string(REPLACE ";" "${PATH_SPLITTER}" INCLUDE_PATH_WITH_COLONS "${INCLUDE_PATH}") set(${VAR} ${SHIBOKEN_BINARY} --generatorSet=shiboken --enable-pyside-extensions --include-paths=${INCLUDE_PATH_WITH_COLONS}${SHIBOKEN_HELPER_INCLUDE_DIRS_WITH_COLONS} --typesystem-paths=${PYSIDE_TYPESYSTEMS} - --output-directory=${BUILD_DIR} ${GLOBAL} ${TYPESYSTEM}) + --output-directory=${BUILD_DIR} ${GLOBAL} ${TYPESYSTEM} + --language-level=c++17) endmacro() @@ -107,7 +162,16 @@ function(shiboken_generator PROJECT_NAME GLOBAL TYPESYSTEM WORKING_DIR GENERATED ) endfunction() - +function(shiboken_generator_ext PROJECT_NAME GLOBAL TYPESYSTEM WORKING_DIR GENERATED_SRCS INCLUDE_PATH BUILD_DIR) + _shiboken_generator_command(COMMAND "${GLOBAL}" "${TYPESYSTEM}" "${INCLUDE_PATH}" "${BUILD_DIR}") + add_custom_command( + OUTPUT ${GENERATED_SRCS} + COMMAND ${COMMAND} + DEPENDS ${GLOBAL} ${TYPESYSTEM} + WORKING_DIRECTORY ${WORKING_DIR} + COMMENT "Running Shiboken generator for ${PROJECT_NAME} Python bindings..." + ) +endfunction() # # Add the Shiboken/PySide specific include directories. #