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 100755 index 0000000..2282e43 --- /dev/null +++ b/cmake/pyside_config.py @@ -0,0 +1,341 @@ +#!/usr/bin/python3 +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from enum import Enum +from glob import glob +import os +import re +import sys +import sysconfig + + +PYSIDE = 'pyside6' +PYSIDE_MODULE = 'PySide6' +SHIBOKEN = 'shiboken6' + + +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 PySide6 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 shiboken6 shared library link flags for qmake')) +options.append(('--pyside-qmake-lflags', + lambda: get_package_qmake_lflags(Package.PYSIDE_MODULE), pyside_error, + 'Print PySide6 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 'pyside6' 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 PySide6 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 or 'dist-' 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 'x.y' (e.g. 3.9, 3.12, etc) +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(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(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 7624157..008ec9f 100644 --- a/cmake/shiboken_helper.cmake +++ b/cmake/shiboken_helper.cmake @@ -162,4 +162,4 @@ function(shiboken_target_link_libraries PROJECT_NAME QT_COMPONENTS) endforeach() target_link_libraries(${PROJECT_NAME} ${shiboken_LINK_LIBRARIES}) -endfunction() +endfunction() \ No newline at end of file diff --git a/cmake/sip_configure.py b/cmake/sip_configure.py index 5210ee5..a25c832 100644 --- a/cmake/sip_configure.py +++ b/cmake/sip_configure.py @@ -228,4 +228,4 @@ def split_paths(paths): makefile.LIBS.set(libs) # Generate the Makefile itself -makefile.generate() +makefile.generate() \ No newline at end of file diff --git a/cmake/sip_helper.cmake b/cmake/sip_helper.cmake index a5ac3c2..dfc9b7d 100644 --- a/cmake/sip_helper.cmake +++ b/cmake/sip_helper.cmake @@ -93,10 +93,13 @@ function(build_sip_binding PROJECT_NAME SIP_FILE) set(LIBRARY_DIRS ${${PROJECT_NAME}_LIBRARY_DIRS}) set(LDFLAGS_OTHER ${${PROJECT_NAME}_LDFLAGS_OTHER}) + # make_directory(${SIP_BUILD_DIR}) + add_custom_command( OUTPUT ${SIP_BUILD_DIR}/Makefile COMMAND ${Python3_EXECUTABLE} ${sip_SIP_CONFIGURE} ${SIP_BUILD_DIR} ${SIP_FILE} ${sip_LIBRARY_DIR} \"${INCLUDE_DIRS}\" \"${LIBRARIES}\" \"${LIBRARY_DIRS}\" \"${LDFLAGS_OTHER}\" + # COMMAND ${Python3_EXECUTABLE} -m sipbuild.tools.build --build-dir ${SIP_BUILD_DIR} --no-compile DEPENDS ${sip_SIP_CONFIGURE} ${SIP_FILE} ${sip_DEPENDS} WORKING_DIRECTORY ${sip_SOURCE_DIR} COMMENT "Running SIP generator for ${PROJECT_NAME} Python bindings..." diff --git a/package.xml b/package.xml index 34703d6..7a454d2 100644 --- a/package.xml +++ b/package.xml @@ -28,7 +28,7 @@ ament_cmake - qtbase5-dev + qt6-base-dev python3-qt5-bindings python3-qt5-bindings @@ -36,7 +36,7 @@ ament_cmake_pytest ament_lint_auto ament_lint_common - + ament_cmake diff --git a/src/python_qt_binding/__init__.py b/src/python_qt_binding/__init__.py index 1e209de..59a8769 100644 --- a/src/python_qt_binding/__init__.py +++ b/src/python_qt_binding/__init__.py @@ -58,6 +58,11 @@ from python_qt_binding.binding_helper import QT_BINDING_MODULES from python_qt_binding.binding_helper import QT_BINDING_VERSION # noqa: F401 +print('QT_BINDING', QT_BINDING) +for module, value in QT_BINDING_MODULES.items(): + print('QT_BINDING_MODULES', module, value) +print('QT_BINDING_VERSION', QT_BINDING_VERSION) + # register binding modules as sub modules of this package (python_qt_binding) for easy importing for module_name, module in QT_BINDING_MODULES.items(): sys.modules[__name__ + '.' + module_name] = module diff --git a/src/python_qt_binding/binding_helper.py b/src/python_qt_binding/binding_helper.py index 27c3237..d5ff4be 100644 --- a/src/python_qt_binding/binding_helper.py +++ b/src/python_qt_binding/binding_helper.py @@ -51,10 +51,11 @@ def _select_qt_binding(binding_name=None, binding_order=None): global QT_BINDING, QT_BINDING_VERSION # order of default bindings can be changed here - if platform.system() == 'Darwin': - DEFAULT_BINDING_ORDER = ['pyside'] - else: - DEFAULT_BINDING_ORDER = ['pyqt', 'pyside'] + DEFAULT_BINDING_ORDER = ['pyside'] + # if platform.system() == 'Darwin': + # DEFAULT_BINDING_ORDER = ['pyside'] + # else: + # DEFAULT_BINDING_ORDER = ['pyside', 'pyqt'] binding_order = binding_order or DEFAULT_BINDING_ORDER @@ -155,9 +156,9 @@ def _load_pyqt(required_modules, optional_modules): # register required and optional PyQt modules for module_name in required_modules: - _named_import('PyQt5.%s' % module_name) + _named_import('PyQt6.%s' % module_name) for module_name in optional_modules: - _named_optional_import('PyQt5.%s' % module_name) + _named_optional_import('PyQt6.%s' % module_name) # set some names for compatibility with PySide sys.modules['QtCore'].Signal = sys.modules['QtCore'].pyqtSignal @@ -166,19 +167,19 @@ def _load_pyqt(required_modules, optional_modules): # try to register Qwt module try: - import PyQt5.Qwt5 - _register_binding_module('Qwt', PyQt5.Qwt5) + import PyQt6.Qwt6 + _register_binding_module('Qwt', PyQt6.Qwt6) except ImportError: pass global _loadUi def _loadUi(uifile, baseinstance=None, custom_widgets_=None): - from PyQt5 import uic + from PyQt6 import uic return uic.loadUi(uifile, baseinstance=baseinstance) - import PyQt5.QtCore - return PyQt5.QtCore.PYQT_VERSION_STR + import PyQt6.QtCore + return PyQt6.QtCore.PYQT_VERSION_STR def _load_pyside(required_modules, optional_modules): @@ -187,9 +188,9 @@ def _load_pyside(required_modules, optional_modules): # register required and optional PySide modules for module_name in required_modules: - _named_import('PySide2.%s' % module_name) + _named_import('PySide6.%s' % module_name) for module_name in optional_modules: - _named_optional_import('PySide2.%s' % module_name) + _named_optional_import('PySide6.%s' % module_name) # set some names for compatibility with PyQt sys.modules['QtCore'].pyqtSignal = sys.modules['QtCore'].Signal @@ -206,8 +207,8 @@ def _load_pyside(required_modules, optional_modules): global _loadUi def _loadUi(uifile, baseinstance=None, custom_widgets=None): - from PySide2.QtUiTools import QUiLoader - from PySide2.QtCore import QMetaObject + from PySide6.QtUiTools import QUiLoader + from PySide6.QtCore import QMetaObject class CustomUiLoader(QUiLoader): class_aliases = { @@ -253,8 +254,8 @@ def createWidget(self, class_name, parent=None, name=''): QMetaObject.connectSlotsByName(ui) return ui - import PySide2 - return PySide2.__version__ + import PySide6 + return PySide6.__version__ def loadUi(uifile, baseinstance=None, custom_widgets=None):