diff --git a/CMakeLists.txt b/CMakeLists.txt index 8686914..2dd5e36 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,15 +7,9 @@ find_package(ament_cmake_python REQUIRED) # Copy python package into binary directory file(COPY src/${PROJECT_NAME} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) -# Avoid find_package(QT NAMES Qt6 Qt5 ...) due to CMake's default ascending path resolution -find_package(Qt6 QUIET COMPONENTS Widgets Core) -if(Qt6_FOUND) - set(QT_VERSION_MAJOR 6) -else() - find_package(Qt5 REQUIRED COMPONENTS Widgets Core) - set(QT_VERSION_MAJOR 5) -endif() -set(python_qt_binding_QT_MAJOR_VERSION "${QT_VERSION_MAJOR}" CACHE STRING "The major version of Qt to use (5 or 6)") +find_package(Qt6 REQUIRED COMPONENTS Widgets Core) +# Kept for downstream packages that still read this variable. +set(python_qt_binding_QT_MAJOR_VERSION "6" CACHE STRING "The major version of Qt to use") # Generate a version.py file inside the copied package directory file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}/version.py diff --git a/README.md b/README.md index 52795bb..d5ae426 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This stack provides Python bindings for Qt. There are two providers: pyside and pyqt. -PySide2 is available under the GPL, LGPL and a commercial license. +PySide6 is available under the GPL, LGPL and a commercial license. PyQt is released under the GPL. Both the bindings and tools to build bindings are included from each available provider. diff --git a/cmake/generate_pyproject_toml.cmake b/cmake/generate_pyproject_toml.cmake new file mode 100644 index 0000000..dd75578 --- /dev/null +++ b/cmake/generate_pyproject_toml.cmake @@ -0,0 +1,14 @@ +# Called via cmake -P from build_sip_binding() in sip_helper.cmake. +# Performs @VAR@ substitution on pyproject.toml.in and writes the result. +# +# Required variables (passed via -DVAR=value on the cmake command line): +# PROJECT_NAME +# python_qt_binding_QMAKE_EXECUTABLE +# SIP_BUILD_DIR +# sip_SOURCE_DIR +# SIP_FILE_NAME +# __PYQT_BINDINGS_DIR +# __QT_SIP_ABI_VERSION +# TEMPLATE_FILE +# OUTPUT_FILE +configure_file("${TEMPLATE_FILE}" "${OUTPUT_FILE}" @ONLY) diff --git a/cmake/shiboken_helper.cmake b/cmake/shiboken_helper.cmake index 7624157..0ca3871 100644 --- a/cmake/shiboken_helper.cmake +++ b/cmake/shiboken_helper.cmake @@ -21,37 +21,27 @@ if(__PYTHON_QT_BINDING_SHIBOKEN_HELPER_INCLUDED) endif() set(__PYTHON_QT_BINDING_SHIBOKEN_HELPER_INCLUDED TRUE) -# In CMake 3.27 and later, FindPythonInterp and FindPythonLibs are deprecated. -# However, Shiboken2 as packaged in Ubuntu 24.04 still use them, so set CMP0148 to -# "OLD" to silence this warning. -if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.27.0") - cmake_policy(SET CMP0148 OLD) -endif() -find_package(Shiboken2 QUIET) -if(Shiboken2_FOUND) - message(STATUS "Found Shiboken2 version ${Shiboken2_VERSION}") - if(NOT ${Shiboken2_VERSION} VERSION_LESS "5.13") - get_property(SHIBOKEN_INCLUDE_DIR TARGET Shiboken2::libshiboken PROPERTY INTERFACE_INCLUDE_DIRECTORIES) - get_property(SHIBOKEN_LIBRARY TARGET Shiboken2::libshiboken PROPERTY LOCATION) - set(SHIBOKEN_BINARY Shiboken2::shiboken2) - endif() +find_package(Shiboken6 QUIET) +if(Shiboken6_FOUND) + message(STATUS "Found Shiboken6 version ${Shiboken6_VERSION}") + get_property(SHIBOKEN_INCLUDE_DIR TARGET Shiboken6::libshiboken PROPERTY INTERFACE_INCLUDE_DIRECTORIES) + get_property(SHIBOKEN_LIBRARY TARGET Shiboken6::libshiboken PROPERTY LOCATION) + set(SHIBOKEN_BINARY Shiboken6::shiboken6) message(STATUS "Using SHIBOKEN_INCLUDE_DIR: ${SHIBOKEN_INCLUDE_DIR}") message(STATUS "Using SHIBOKEN_LIBRARY: ${SHIBOKEN_LIBRARY}") message(STATUS "Using SHIBOKEN_BINARY: ${SHIBOKEN_BINARY}") endif() -find_package(PySide2 QUIET) -if(PySide2_FOUND) - message(STATUS "Found PySide2 version ${PySide2_VERSION}") - if(NOT ${PySide2_VERSION} VERSION_LESS "5.13") - get_property(PYSIDE_INCLUDE_DIR TARGET PySide2::pyside2 PROPERTY INTERFACE_INCLUDE_DIRECTORIES) - get_property(PYSIDE_LIBRARY TARGET PySide2::pyside2 PROPERTY LOCATION) - endif() +find_package(PySide6 QUIET) +if(PySide6_FOUND) + message(STATUS "Found PySide6 version ${PySide6_VERSION}") + get_property(PYSIDE_INCLUDE_DIR TARGET PySide6::pyside6 PROPERTY INTERFACE_INCLUDE_DIRECTORIES) + get_property(PYSIDE_LIBRARY TARGET PySide6::pyside6 PROPERTY LOCATION) message(STATUS "Using PYSIDE_INCLUDE_DIR: ${PYSIDE_INCLUDE_DIR}") message(STATUS "Using PYSIDE_LIBRARY: ${PYSIDE_LIBRARY}") endif() -if(Shiboken2_FOUND AND PySide2_FOUND) +if(Shiboken6_FOUND AND PySide6_FOUND) message(STATUS "Shiboken binding generator available.") set(shiboken_helper_FOUND TRUE) else() @@ -64,7 +54,9 @@ macro(_shiboken_generator_command VAR GLOBAL TYPESYSTEM INCLUDE_PATH BUILD_DIR) # Add includes from current directory, Qt, PySide and compiler specific dirs get_directory_property(SHIBOKEN_HELPER_INCLUDE_DIRS INCLUDE_DIRECTORIES) list(APPEND SHIBOKEN_HELPER_INCLUDE_DIRS - ${QT_INCLUDE_DIR} + ${Qt6Core_INCLUDE_DIRS} + ${Qt6Gui_INCLUDE_DIRS} + ${Qt6Widgets_INCLUDE_DIRS} ${PYSIDE_INCLUDE_DIR} ${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES}) # See ticket https://code.ros.org/trac/ros-pkg/ticket/5219 @@ -157,8 +149,9 @@ function(shiboken_target_link_libraries PROJECT_NAME QT_COMPONENTS) ) foreach(component ${QT_COMPONENTS}) - string(TOUPPER ${component} component) - set(shiboken_LINK_LIBRARIES ${shiboken_LINK_LIBRARIES} ${QT_${component}_LIBRARY}) + # QT_COMPONENTS are given as QtCore/QtGui/QtWidgets; Qt6 targets drop the prefix. + string(REGEX REPLACE "^Qt" "" component "${component}") + set(shiboken_LINK_LIBRARIES ${shiboken_LINK_LIBRARIES} Qt6::${component}) endforeach() target_link_libraries(${PROJECT_NAME} ${shiboken_LINK_LIBRARIES}) diff --git a/cmake/sip_helper.cmake b/cmake/sip_helper.cmake index f5c2a4c..6b6247a 100644 --- a/cmake/sip_helper.cmake +++ b/cmake/sip_helper.cmake @@ -11,15 +11,13 @@ set(Python3_FIND_UNVERSIONED_NAMES FIRST) find_package(Python3 ${Python3_VERSION} REQUIRED COMPONENTS Interpreter Development) -# Find the directory containing the SIP bindings shipped by PyQt. -# -# :param python_qt_binding_QT_MAJOR_VERSION: The major version of Qt (e.g., 5 or 6). +# Find the directory containing the SIP bindings shipped by PyQt6. # # :out __PYQT_BINDINGS_DIR: Path to the directory containing QT*.sip files. # :out __PYQT_BINDINGS_FOUND: Boolean indicating if the bindings were located. # -function(__find_qt_sip_files python_qt_binding_QT_MAJOR_VERSION) - set(MODULE_NAME "PyQt${python_qt_binding_QT_MAJOR_VERSION}") +function(__find_qt_sip_files) + set(MODULE_NAME "PyQt6") execute_process( COMMAND ${Python3_EXECUTABLE} -c "import ${MODULE_NAME}.bindings as pb; print(pb.__path__[0])" @@ -76,9 +74,9 @@ else() endif() # Find Qt's installed SIP files -__find_qt_sip_files(${python_qt_binding_QT_MAJOR_VERSION}) +__find_qt_sip_files() if(NOT __PYQT_BINDINGS_FOUND) - message(FATAL_ERROR "PyQt${python_qt_binding_QT_MAJOR_VERSION} SIP bindings are required but were not found.") + message(FATAL_ERROR "PyQt6 SIP bindings are required but were not found.") endif() # Extract the sip-abi-version from PyQt @@ -87,8 +85,8 @@ __find_qt_sip_abi("${__PYQT_BINDINGS_DIR}") # Find qmake find_program(python_qt_binding_QMAKE_EXECUTABLE NAMES - qmake${python_qt_binding_QT_MAJOR_VERSION} - qmake-qt${python_qt_binding_QT_MAJOR_VERSION} + qmake6 + qmake-qt6 qmake REQUIRED) @@ -162,8 +160,17 @@ function(build_sip_binding PROJECT_NAME SIP_FILE) COMMENT "Generating C++ code for ${PROJECT_NAME} Python bindings using sip-build..." ) + # Drive the generator from a target of its own. Generating the code only + # needs the sip files and the headers, not the libraries the extension is + # later linked against. Keeping it out of lib${PROJECT_NAME} means it is + # not ordered after those libraries have been built and can run + # concurrently with them. + add_custom_target(${PROJECT_NAME}_codegen DEPENDS ${GENERATED_CPP}) + set_source_files_properties(${GENERATED_CPP} PROPERTIES GENERATED TRUE) + # Build the cPython extension natively using CMake python3_add_library(lib${PROJECT_NAME} MODULE ${GENERATED_CPP}) + add_dependencies(lib${PROJECT_NAME} ${PROJECT_NAME}_codegen) # Link project dependencies against this target target_include_directories(lib${PROJECT_NAME} PRIVATE ${${PROJECT_NAME}_INCLUDE_DIRS} ${SIP_BUILD_DIR}) diff --git a/package.xml b/package.xml index 1eb4466..fa36cf6 100644 --- a/package.xml +++ b/package.xml @@ -3,7 +3,7 @@ 2.6.1 This stack provides Python bindings for Qt. - There are two providers: pyside and pyqt. PySide2 is available under + There are two providers: pyside and pyqt. PySide6 is available under the GPL, LGPL and a commercial license. PyQt is released under the GPL. Both the bindings and tools to build bindings are included from each @@ -28,11 +28,18 @@ ament_cmake + python3-qt-bindings + qt6-base-dev + qt6-qmake python3-qt-bindings python3-dev - python3-qt-bindings + python3-pyqt6 ament_cmake_pytest ament_lint_auto diff --git a/python_qt_binding-extras.cmake.in b/python_qt_binding-extras.cmake.in index ea85bbc..717c91a 100644 --- a/python_qt_binding-extras.cmake.in +++ b/python_qt_binding-extras.cmake.in @@ -1 +1 @@ -set(python_qt_binding_QT_MAJOR_VERSION "@python_qt_binding_QT_MAJOR_VERSION@" CACHE STRING "The major version of Qt to use (5 or 6)") \ No newline at end of file +set(python_qt_binding_QT_MAJOR_VERSION "@python_qt_binding_QT_MAJOR_VERSION@" CACHE STRING "The major version of Qt to use") \ No newline at end of file diff --git a/src/python_qt_binding/__init__.py b/src/python_qt_binding/__init__.py index 1e209de..b19fef6 100644 --- a/src/python_qt_binding/__init__.py +++ b/src/python_qt_binding/__init__.py @@ -33,7 +33,7 @@ """ Abstraction for different Python Qt bindings. -Supported Python Qt 5 bindings are PyQt and PySide. +Supported Python Qt 6 bindings are PyQt6 and PySide6. The Qt modules can be imported like this: from python_qt_binding.QtCore import QObject diff --git a/src/python_qt_binding/binding_helper.py b/src/python_qt_binding/binding_helper.py index cb6b835..3df1772 100644 --- a/src/python_qt_binding/binding_helper.py +++ b/src/python_qt_binding/binding_helper.py @@ -36,8 +36,6 @@ import sys import traceback -from .version import QT_MAJOR_VERSION - QT_BINDING = None QT_BINDING_MODULES = {} @@ -75,7 +73,7 @@ def _select_qt_binding(binding_name=None, binding_order=None): 'QtMultimedia', 'QtMultimediaWidgets', 'QtNetwork', - 'QNetworkAuth', + 'QtNetworkAuth', 'QtNfc', 'QtOpenGL', 'QtPositioning', @@ -83,22 +81,17 @@ def _select_qt_binding(binding_name=None, binding_order=None): 'QtQml', 'QtQuick', 'QtQuickWidgets', - 'QtScript', - 'QtScriptTools', 'QtSensors', 'QtSerialPort', 'QtSql', 'QtSvg', 'QtTest', 'QtWebChannel', - 'QtWebEngine', # Qt 5.6 and higher + 'QtWebEngine', 'QtWebEngineCore', 'QtWebEngineWidgets', - 'QtWebKitWidgets', # Qt 5.0 - 5.5 'QtWebSockets', - 'QtX11Extras', 'QtXml', - 'QtXmlPatterns', ] # try to load preferred bindings @@ -152,29 +145,22 @@ def _load_pyqt(required_modules, optional_modules): # register required and optional PyQt modules for module_name in required_modules: - _named_import(f'PyQt{QT_MAJOR_VERSION}.{module_name}') + _named_import(f'PyQt6.{module_name}') for module_name in optional_modules: - _named_optional_import(f'PyQt{QT_MAJOR_VERSION}.{module_name}') + _named_optional_import(f'PyQt6.{module_name}') # set some names for compatibility with PySide sys.modules['QtCore'].Signal = sys.modules['QtCore'].pyqtSignal sys.modules['QtCore'].Slot = sys.modules['QtCore'].pyqtSlot sys.modules['QtCore'].Property = sys.modules['QtCore'].pyqtProperty - # try to register Qwt module - try: - Qwt = builtins.__import__(f'PyQt{QT_MAJOR_VERSION}.Qwt{QT_MAJOR_VERSION}', fromlist=['*']) - _register_binding_module('Qwt', Qwt) - except ImportError: - pass - global _loadUi def _loadUi(uifile, baseinstance=None, custom_widgets_=None): - uic = builtins.__import__(f'PyQt{QT_MAJOR_VERSION}.uic', fromlist=['*']) + uic = builtins.__import__('PyQt6.uic', fromlist=['*']) return uic.loadUi(uifile, baseinstance=baseinstance) - QtCore = builtins.__import__(f'PyQt{QT_MAJOR_VERSION}.QtCore', fromlist=['*']) + QtCore = builtins.__import__('PyQt6.QtCore', fromlist=['*']) return QtCore.PYQT_VERSION_STR @@ -182,13 +168,7 @@ def _load_pyside(required_modules, optional_modules): # set environment variable QT_API for matplotlib os.environ['QT_API'] = 'pyside' - # Determine the PySide module based on the Qt version - if str(QT_MAJOR_VERSION) == '6': - pyside_module = 'PySide6' - elif str(QT_MAJOR_VERSION) == '5': - pyside_module = 'PySide2' - else: - raise RuntimeError('Only Qt5 and Qt6 are supported') + pyside_module = 'PySide6' # register required and optional PySide modules for module_name in required_modules: @@ -201,13 +181,6 @@ def _load_pyside(required_modules, optional_modules): sys.modules['QtCore'].pyqtSlot = sys.modules['QtCore'].Slot sys.modules['QtCore'].pyqtProperty = sys.modules['QtCore'].Property - # try to register PySideQwt module - try: - PySideQwt = builtins.__import__(f'{pyside_module}Qwt', fromlist=['*']) - _register_binding_module('Qwt', PySideQwt) - except ImportError: - pass - global _loadUi def _loadUi(uifile, baseinstance=None, custom_widgets=None):