diff --git a/.github/workflows/build_cmake.yml b/.github/workflows/build_cmake.yml new file mode 100644 index 000000000..cf7122835 --- /dev/null +++ b/.github/workflows/build_cmake.yml @@ -0,0 +1,149 @@ +name: Build (CMake) + +on: + push: + branches: + - main + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: ['ubuntu', 'windows'] + # Qt 5.12.* is excluded: CMakeLists.txt requires Qt >= 5.15.0. + qt-version: [ '5.15.*', '6.10.*' ] + python-version: [ '3.12' ] + runs-on: ${{ matrix.os }}-latest + steps: + + - name: Install MSVC + if: ${{ matrix.os == 'windows' }} + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: amd64 + + - name: Install Qt ${{ matrix.qt-version }} + uses: jurplel/install-qt-action@v4 + with: + version: ${{ matrix.qt-version }} + modules: ${{ startsWith(matrix.qt-version, '6') && 'qt5compat qtscxml qtpositioning qtwebchannel qtmultimedia qtwebengine' || '' }} + arch: ${{ (matrix.os == 'ubuntu' && (startsWith(matrix.qt-version, '5') && 'gcc_64' || 'linux_gcc_64')) || startsWith(matrix.qt-version, '6') && 'win64_msvc2022_64' || 'win64_msvc2019_64' }} + + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: '${{ matrix.python-version }}' + + - name: Checkout PythonQt + uses: actions/checkout@v6 + + - name: Ccache + if: ${{ matrix.os == 'ubuntu' }} + uses: hendrikmuhs/ccache-action@v1.2.20 + with: + key: ${{ runner.os }}-cmake-${{ matrix.qt-version }} + evict-old-files: 'job' + + - name: Set environment + id: setenv + run: | + QT_VERSION_MAJOR=$(cut -f 1 -d . <<< "${{ matrix.qt-version }}") + echo "QT_VERSION_MAJOR=$QT_VERSION_MAJOR" >> $GITHUB_ENV + QT_VERSION_SHORT=$(cut -f 1,2 -d . <<< "${{ matrix.qt-version }}") + echo "QT_VERSION_SHORT=$QT_VERSION_SHORT" >> $GITHUB_OUTPUT + PYTHON_VERSION_FULL=$(python --version 2>&1 | cut -f 2 -d ' ') + PYTHON_VERSION_SHORT=$(cut -f 1,2 -d . <<< $PYTHON_VERSION_FULL) + echo "PYTHON_VERSION_SHORT=$PYTHON_VERSION_SHORT" >> $GITHUB_OUTPUT + echo "$pythonLocation/bin" >> $GITHUB_PATH + + - name: Build generator (Ubuntu) + if: ${{ matrix.os == 'ubuntu' }} + run: | + cmake -G Ninja -S generator -B generator/build \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_PREFIX_PATH="$QT_ROOT_DIR" \ + -DPythonQtGenerator_QT_VERSION=$QT_VERSION_MAJOR + cmake --build generator/build + + - name: Build generator (Windows) + if: ${{ matrix.os == 'windows' }} + shell: cmd + run: | + set QT_CMAKE_DIR=%QT_ROOT_DIR%\lib\cmake\Qt%QT_VERSION_MAJOR% + cmake -G "Visual Studio 17 2022" -S generator -B generator\build ^ + "-DCMAKE_PREFIX_PATH=%QT_ROOT_DIR%;%QT_ROOT_DIR%\lib\cmake" ^ + "-DQt%QT_VERSION_MAJOR%_DIR=%QT_CMAKE_DIR%" ^ + -DPythonQtGenerator_QT_VERSION=%QT_VERSION_MAJOR% || exit /b 1 + cmake --build generator\build --config Release || exit /b 1 + + - name: Generate Wrappers (Ubuntu) + if: ${{ matrix.os == 'ubuntu' }} + run: | + QTDIR="$QT_ROOT_DIR" \ + UBSAN_OPTIONS="halt_on_error=1" \ + ASAN_OPTIONS="detect_leaks=0:detect_stack_use_after_return=1:fast_unwind_on_malloc=0" \ + ./generator/build/PythonQtGenerator \ + --output-directory=. + + - name: Generate Wrappers (Windows) + if: ${{ matrix.os == 'windows' }} + shell: cmd + run: | + set QTDIR=%QT_ROOT_DIR% + generator\build\Release\PythonQtGenerator.exe --output-directory=. || exit /b 1 + + - name: Upload Wrappers + uses: actions/upload-artifact@v7 + with: + name: cmake_wrappers_${{ matrix.os }}_${{ steps.setenv.outputs.QT_VERSION_SHORT }} + path: generated_cpp + if-no-files-found: error + + - name: Build and test PythonQt (Ubuntu) + if: ${{ matrix.os == 'ubuntu' }} + run: | + cmake -G Ninja -S . -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_PREFIX_PATH="$QT_ROOT_DIR" \ + -DPythonQt_QT_VERSION=$QT_VERSION_MAJOR \ + "-DPythonQt_GENERATED_PATH=$(pwd)/generated_cpp" \ + -DPythonQt_Wrap_QtCore=ON \ + -DBUILD_TESTING=ON \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + "-DCMAKE_CXX_FLAGS=-fsanitize=address,undefined -fno-sanitize-recover=undefined" \ + "-DCMAKE_EXE_LINKER_FLAGS=-fsanitize=address,undefined" \ + "-DCMAKE_SHARED_LINKER_FLAGS=-fsanitize=address,undefined" + cmake --build build --parallel $(nproc) + PYTHONDEVMODE=1 PYTHONASYNCIODEBUG=1 PYTHONWARNINGS=error PYTHONMALLOC=malloc_debug \ + UBSAN_OPTIONS="halt_on_error=1" ASAN_OPTIONS="detect_leaks=0:detect_stack_use_after_return=1:fast_unwind_on_malloc=0" \ + QT_QPA_PLATFORM=offscreen \ + ctest --test-dir build --output-on-failure + + - name: Build and test PythonQt (Windows) + if: ${{ matrix.os == 'windows' }} + shell: cmd + run: | + set QT_CMAKE_DIR=%QT_ROOT_DIR%\lib\cmake\Qt%QT_VERSION_MAJOR% + cmake -G "Visual Studio 17 2022" -S . -B build ^ + "-DCMAKE_PREFIX_PATH=%QT_ROOT_DIR%;%QT_ROOT_DIR%\lib\cmake" ^ + "-DQt%QT_VERSION_MAJOR%_DIR=%QT_CMAKE_DIR%" ^ + -DPythonQt_QT_VERSION=%QT_VERSION_MAJOR% ^ + "-DPythonQt_GENERATED_PATH=%CD%\generated_cpp" ^ + -DPythonQt_Wrap_QtCore=ON ^ + -DBUILD_TESTING=ON || exit /b 1 + cmake --build build --config Release || exit /b 1 + set PYTHONDEVMODE=1 + set PYTHONASYNCIODEBUG=1 + set PYTHONWARNINGS=error + set QT_QPA_PLATFORM=offscreen + ctest --test-dir build --output-on-failure -C Release || exit /b 1 diff --git a/.github/workflows/build_latest.yml b/.github/workflows/build_latest.yml index 214c3e6b9..21d1874f3 100644 --- a/.github/workflows/build_latest.yml +++ b/.github/workflows/build_latest.yml @@ -122,7 +122,6 @@ jobs: python --version qmake CONFIG+=release CONFIG-=debug_and_release CONFIG-=debug_and_release_target ^ CONFIG+=exclude_generator ^ - "PYTHONQTALL_CONFIG=${{ matrix.pythonqtall-config }}" ^ "PYTHON_PATH=%pythonLocation%" ^ "PYTHON_VERSION=${{ steps.setenv.outputs.PYTHON_VERSION_SHORT }}" ^ PythonQt.pro diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 000000000..559aa495a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,420 @@ + +cmake_minimum_required(VERSION 3.20.6) + +project(PythonQt) + +#---------------------------------------------------------------------------- +# Qt version + +set(_default_qt_major_version 5) +if(DEFINED Qt6_DIR) + set(_default_qt_major_version 6) +endif() + +# Set PythonQt_QT_VERSION +set(PythonQt_QT_VERSION ${_default_qt_major_version} CACHE STRING "Qt major version (5 or 6)") +set_property(CACHE PythonQt_QT_VERSION PROPERTY STRINGS "5" "6") +if(NOT "${PythonQt_QT_VERSION}" MATCHES "^(5|6)$") + message(FATAL_ERROR "error: PythonQt_QT_VERSION must be 5 or 6.") +endif() + +# Requirements +set(minimum_required_qt5_version "5.15.0") +set(minimum_required_qt6_version "6.4.0") +set(minimum_required_qt_version ${minimum_required_qt${PythonQt_QT_VERSION}_version}) + +find_package(Qt${PythonQt_QT_VERSION} ${minimum_required_qt_version} QUIET) + +#---------------------------------------------------------------------------- +# Qt components +set(qtlibs + Core + Widgets + Network + OpenGL + Qml + Quick + Sql + Svg + Multimedia + UiTools + Xml + ) +# XmlPatterns is removed from Qt 6 +if(PythonQt_QT_VERSION VERSION_EQUAL "5") + list(APPEND qtlibs + XmlPatterns + ) +endif() + +#----------------------------------------------------------------------------- +# Python libraries + +find_package(Python3 COMPONENTS Development REQUIRED) + +#----------------------------------------------------------------------------- +# Build options + +option(PythonQt_SUPPORT_NAME_PROPERTY "Enable PythonQt name-property support" ON) +option(PythonQt_USE_RELEASE_PYTHON_FALLBACK "Fallback to Release python when Debug missing" ON) +option(PythonQt_DEBUG "Enable PythonQt debug output" OFF) + +if(NOT DEFINED PythonQt_INSTALL_RUNTIME_DIR) + set(PythonQt_INSTALL_RUNTIME_DIR bin) +endif() + +if(NOT DEFINED PythonQt_INSTALL_LIBRARY_DIR) + set(PythonQt_INSTALL_LIBRARY_DIR lib${LIB_SUFFIX}) +endif() + +if(NOT DEFINED PythonQt_INSTALL_ARCHIVE_DIR) + set(PythonQt_INSTALL_ARCHIVE_DIR lib${LIB_SUFFIX}) +endif() + +if(NOT DEFINED PythonQt_INSTALL_INCLUDE_DIR) + set(PythonQt_INSTALL_INCLUDE_DIR include/PythonQt) +endif() + +#----------------------------------------------------------------------------- +# Set qtlib_to_wraplib_* variables + +set(qtlib_to_wraplib_Widgets gui) +set(qtlib_to_wraplib_WebKitWidgets webkit) + +set(qt_wrapped_lib_depends_gui Multimedia PrintSupport) +if(PythonQt_QT_VERSION VERSION_EQUAL "6") + list(APPEND qt_wrapped_lib_depends_gui Widgets OpenGL) +endif() +set(qt_wrapped_lib_depends_multimedia MultimediaWidgets) +set(qt_wrapped_lib_depends_quick QuickWidgets) +set(qt_wrapped_lib_depends_svg ) +if(PythonQt_QT_VERSION VERSION_EQUAL "6") + list(APPEND qt_wrapped_lib_depends_svg SvgWidgets) +endif() +set(qt_wrapped_lib_depends_webkit ) +if(PythonQt_QT_VERSION VERSION_EQUAL "6") + list(APPEND qt_wrapped_lib_depends_webkit WebKitWidgets) +endif() + +foreach(qtlib ${qtlibs}) + string(TOLOWER ${qtlib} qtlib_lowercase) + if(DEFINED qtlib_to_wraplib_${qtlib}) + set(qtlib_lowercase ${qtlib_to_wraplib_${qtlib}}) + endif() + set(qtlib_to_wraplib_${qtlib} ${qtlib_lowercase}) +endforeach() + +#----------------------------------------------------------------------------- +# Define PythonQt_Wrap_Qt* options +option(PythonQt_Wrap_QtAll "Make all Qt components available in python" OFF) +foreach(qtlib ${qtlibs}) + OPTION(PythonQt_Wrap_Qt${qtlib_to_wraplib_${qtlib}} "Make all of Qt${qtlib} available in python" OFF) +endforeach() + +#----------------------------------------------------------------------------- +# Force option if it applies +if(PythonQt_Wrap_QtAll) + foreach(qtlib ${qtlibs}) + # XXX xmlpatterns wrapper does *NOT* build at all :( + if(${qtlib} STREQUAL "XmlPatterns") + continue() + endif() + set(qt_wrapped_lib ${qtlib_to_wraplib_${qtlib}}) + if(NOT ${PythonQt_Wrap_Qt${qt_wrapped_lib}}) + set(PythonQt_Wrap_Qt${qt_wrapped_lib} ON CACHE BOOL "Make all of Qt${qt_wrapped_lib} available in python" FORCE) + message(STATUS "Enabling [PythonQt_Wrap_Qt${qt_wrapped_lib}] because of [PythonQt_Wrap_QtAll] evaluates to True") + endif() + endforeach() +endif() + +#----------------------------------------------------------------------------- +# Setup Qt + +# Required components +set(qt_required_components Core Widgets) +foreach(qtlib ${qtlibs}) + set(qt_wrapped_lib ${qtlib_to_wraplib_${qtlib}}) + if(${PythonQt_Wrap_Qt${qt_wrapped_lib}}) + list(APPEND qt_required_components ${qtlib} ${qt_wrapped_lib_depends_${qt_wrapped_lib}}) + endif() +endforeach() +if(BUILD_TESTING) + list(APPEND qt_required_components Test) +endif() +if("${Qt${PythonQt_QT_VERSION}_VERSION}" VERSION_GREATER_EQUAL "6.10.0") + list(APPEND qt_required_components CorePrivate) +endif() +list(REMOVE_DUPLICATES qt_required_components) + +message(STATUS "${PROJECT_NAME}: Required Qt components [${qt_required_components}]") +find_package(Qt${PythonQt_QT_VERSION} ${minimum_required_qt_version} COMPONENTS ${qt_required_components} REQUIRED) + +set(QT_VERSION_MAJOR ${Qt${PythonQt_QT_VERSION}_VERSION_MAJOR}) +set(QT_VERSION_MINOR ${Qt${PythonQt_QT_VERSION}_VERSION_MINOR}) + +set(QT_LIBRARIES ) +foreach(qt_component ${qt_required_components}) + list(APPEND QT_LIBRARIES Qt${PythonQt_QT_VERSION}::${qt_component}) +endforeach() + +if(UNIX) + find_package(OpenGL) + if(OPENGL_FOUND) + list(APPEND QT_LIBRARIES ${OPENGL_LIBRARIES}) + endif() +endif() + +#----------------------------------------------------------------------------- +# Pre-generated wrappers default to Qt 5.15 set (generated_cpp_515). Users may override. +if(PythonQt_QT_VERSION VERSION_EQUAL "5") + set(generated_cpp_suffix "_515") + set(_default_generated_path "${CMAKE_CURRENT_SOURCE_DIR}/generated_cpp${generated_cpp_suffix}") +else() + set(_default_generated_path "PythonQt_GENERATED_PATH-NOTFOUND") +endif() + +set(PythonQt_GENERATED_PATH "${_default_generated_path}" + CACHE PATH "Directory containing pre-generated PythonQt Qt wrappers for Qt ${QT_VERSION_MAJOR}.${QT_VERSION_MINOR} (e.g., generated_cpp_515)") + +if(NOT IS_DIRECTORY "${PythonQt_GENERATED_PATH}") + message(FATAL_ERROR + "PythonQt: missing generated wrapper sources for Qt ${QT_VERSION_MAJOR}.${QT_VERSION_MINOR}.\n" + "Expected: ${PythonQt_GENERATED_PATH}\n" + "Hint: pass -DPythonQt_GENERATED_PATH:PATH=/path/to/generated_cpp") +endif() + +#----------------------------------------------------------------------------- +# Sources + +set(sources + src/PythonQtBoolResult.cpp + src/PythonQtClassInfo.cpp + src/PythonQtClassWrapper.cpp + src/PythonQtConversion.cpp + src/PythonQt.cpp + src/PythonQtImporter.cpp + src/PythonQtInstanceWrapper.cpp + src/PythonQtMethodInfo.cpp + src/PythonQtMisc.cpp + src/PythonQtObjectPtr.cpp + src/PythonQtProperty.cpp + src/PythonQtQFileImporter.cpp + src/PythonQtSignalReceiver.cpp + src/PythonQtSlot.cpp + src/PythonQtSlotDecorator.cpp + src/PythonQtSignal.cpp + src/PythonQtStdDecorators.cpp + src/PythonQtStdIn.cpp + src/PythonQtStdOut.cpp + src/PythonQtThreadSupport.cpp + src/gui/PythonQtScriptingConsole.cpp + + extensions/PythonQt_QtAll/PythonQt_QtAll.cpp + + ${PythonQt_GENERATED_PATH}/com_trolltech_qt_core_builtin/com_trolltech_qt_core_builtin0.cpp + ${PythonQt_GENERATED_PATH}/com_trolltech_qt_core_builtin/com_trolltech_qt_core_builtin_init.cpp + ${PythonQt_GENERATED_PATH}/com_trolltech_qt_gui_builtin/com_trolltech_qt_gui_builtin0.cpp + ${PythonQt_GENERATED_PATH}/com_trolltech_qt_gui_builtin/com_trolltech_qt_gui_builtin_init.cpp +) + +#----------------------------------------------------------------------------- +# List headers. This list is used for the install command. + +set(headers + src/PythonQtBoolResult.h + src/PythonQtClassInfo.h + src/PythonQtClassWrapper.h + src/PythonQtConversion.h + src/PythonQtCppWrapperFactory.h + src/PythonQtDoc.h + src/PythonQt.h + src/PythonQtImporter.h + src/PythonQtImportFileInterface.h + src/PythonQtInstanceWrapper.h + src/PythonQtMethodInfo.h + src/PythonQtMisc.h + src/PythonQtObjectPtr.h + src/PythonQtProperty.h + src/PythonQtQFileImporter.h + src/PythonQtSignalReceiver.h + src/PythonQtSlot.h + src/PythonQtSlotDecorator.h + src/PythonQtSignal.h + src/PythonQtStdDecorators.h + src/PythonQtStdIn.h + src/PythonQtStdOut.h + src/PythonQtSystem.h + src/PythonQtThreadSupport.h + src/PythonQtUtils.h + src/PythonQtVariants.h + src/PythonQtPythonInclude.h + extensions/PythonQt_QtAll/PythonQt_QtAll.h +) + +#----------------------------------------------------------------------------- +# Headers that should run through moc + +set(moc_sources +) + +#----------------------------------------------------------------------------- +# Add extra sources + +foreach(qtlib ${qtlibs}) + + set(qt_wrapped_lib ${qtlib_to_wraplib_${qtlib}}) + + if (${PythonQt_Wrap_Qt${qt_wrapped_lib}}) + + # Variable expected by PythonQt_QtAll.cpp + string(TOUPPER ${qt_wrapped_lib} qt_wrapped_lib_uppercase) + ADD_DEFINITIONS(-DPYTHONQT_WITH_${qt_wrapped_lib_uppercase}) + + set(file_prefix com_trolltech_qt_${qt_wrapped_lib}/com_trolltech_qt_${qt_wrapped_lib}) + + foreach(index RANGE 0 12) + + # Source files + if(EXISTS ${PythonQt_GENERATED_PATH}/${file_prefix}${index}.cpp) + list(APPEND sources ${PythonQt_GENERATED_PATH}/${file_prefix}${index}.cpp) + endif() + + # Headers that should run through moc + if(EXISTS ${PythonQt_GENERATED_PATH}/${file_prefix}${index}.h) + list(APPEND moc_sources ${PythonQt_GENERATED_PATH}/${file_prefix}${index}.h) + endif() + + endforeach() + + list(APPEND sources ${PythonQt_GENERATED_PATH}/${file_prefix}_init.cpp) + + endif() +endforeach() + +#----------------------------------------------------------------------------- +# Do wrapping +qt_wrap_cpp(sources ${moc_sources}) + +#----------------------------------------------------------------------------- +# Configure header + +# Map the CMake option `PythonQt_USE_RELEASE_PYTHON_FALLBACK` (camelCase) +# to a plain variable used by the configured header. The public-facing +# preprocessor symbol remains `PYTHONQT_USE_RELEASE_PYTHON_FALLBACK`. +set(PYTHONQT_USE_RELEASE_PYTHON_FALLBACK ${PythonQt_USE_RELEASE_PYTHON_FALLBACK}) +configure_file( + src/PythonQtConfigure.h.in + ${CMAKE_CURRENT_BINARY_DIR}/src/PythonQtConfigure.h + ) +unset(PYTHONQT_USE_RELEASE_PYTHON_FALLBACK) + +list(APPEND headers + ${CMAKE_CURRENT_BINARY_DIR}/src/PythonQtConfigure.h + ) + +#----------------------------------------------------------------------------- +# Build the library + +add_library(PythonQt SHARED ${sources}) +set_target_properties(PythonQt PROPERTIES DEFINE_SYMBOL PYTHONQT_EXPORTS) + +set_target_properties(PythonQt + PROPERTIES + AUTOMOC TRUE + ) + +# Disable AUTOMOC for specified moc sources to avoid QMetaTypeId specialization conflicts. +foreach(moc_source IN LISTS moc_sources) + set_property(SOURCE ${moc_source} PROPERTY SKIP_AUTOMOC ON) +endforeach() + +target_compile_definitions(PythonQt + PRIVATE + $<$:PYTHONQT_DEBUG> + $<$:PYTHONQT_SUPPORT_NAME_PROPERTY> + # No need to export PYTHONQT_USE_RELEASE_PYTHON_FALLBACK publicly. the + # configured header handles consumers. + ) + +target_compile_options(PythonQt PRIVATE + $<$:/bigobj> + ) + +target_include_directories(PythonQt + PUBLIC + $ + $ + $ + $ + ) + +target_link_libraries(PythonQt + PUBLIC + Python3::Python + ${QT_LIBRARIES} + PRIVATE + # Required for use of "QtCore/private/qmetaobjectbuilder_p.h" in "PythonQt.cpp" + Qt${PythonQt_QT_VERSION}::CorePrivate + ) + +#----------------------------------------------------------------------------- +# Install library (on windows, put the dll in 'bin' and the archive in 'lib') + +install(TARGETS PythonQt + RUNTIME DESTINATION ${PythonQt_INSTALL_RUNTIME_DIR} + LIBRARY DESTINATION ${PythonQt_INSTALL_LIBRARY_DIR} + ARCHIVE DESTINATION ${PythonQt_INSTALL_ARCHIVE_DIR}) +install(FILES ${headers} DESTINATION ${PythonQt_INSTALL_INCLUDE_DIR}) + +#----------------------------------------------------------------------------- +# Testing + +option(BUILD_TESTING "Build the testing tree." OFF) +include(CTest) + +if(BUILD_TESTING) + create_test_sourcelist(test_sources PythonQtCppTests.cpp + tests/PythonQtTestMain.cpp + ) + + set_property(SOURCE tests/PythonQtTestMain.cpp PROPERTY COMPILE_DEFINITIONS "main=tests_PythonQtTestMain") + + list(APPEND test_sources + tests/PythonQtTests.cpp + tests/PythonQtTests.h + ) + + if(PythonQt_Wrap_QtCore) + list(APPEND test_sources + tests/PythonQtTestCleanup.cpp + tests/PythonQtTestCleanup.h + ) + + set_property(SOURCE tests/PythonQtTestMain.cpp APPEND PROPERTY COMPILE_DEFINITIONS "PythonQt_Wrap_QtCore") + endif() + + add_executable(PythonQtCppTests ${test_sources}) + + target_include_directories(PythonQtCppTests + PRIVATE + $<$:${PythonQt_GENERATED_PATH}> + ) + + target_link_libraries(PythonQtCppTests PythonQt) + + set_target_properties(PythonQtCppTests + PROPERTIES + AUTOMOC TRUE + ) + + target_compile_definitions(PythonQtCppTests + PRIVATE + $<$:PYTHONQT_SUPPORT_NAME_PROPERTY> + ) + + add_test( + NAME tests_PythonQtTestMain + COMMAND ${Slicer_LAUNCH_COMMAND} $ tests/PythonQtTestMain + ) +endif() + diff --git a/extensions/PythonQt_QtAll/PythonQt_QtAll.h b/extensions/PythonQt_QtAll/PythonQt_QtAll.h index 381193960..6021e0aa9 100644 --- a/extensions/PythonQt_QtAll/PythonQt_QtAll.h +++ b/extensions/PythonQt_QtAll/PythonQt_QtAll.h @@ -33,18 +33,27 @@ * */ -#ifdef WIN32 - #ifdef PYTHONQT_QTALL_EXPORTS +// Export macro for the PythonQt_QtAll symbols. +// qmake builds define PYTHONQT_QTALL_EXPORTS for the dedicated QtAll DLL, +// while CMake builds compile this source into the PythonQt target which defines +// PYTHONQT_EXPORTS. Treat either define as "building" to avoid dllimport on +// in-target function definitions. +#if defined(WIN32) + #if defined(PYTHONQT_QTALL_EXPORTS) || defined(PYTHONQT_EXPORTS) #define PYTHONQT_QTALL_EXPORT __declspec(dllexport) #else #define PYTHONQT_QTALL_EXPORT __declspec(dllimport) #endif #else - #define PYTHONQT_QTALL_EXPORT + #if defined(PYTHONQT_QTALL_EXPORTS) || defined(PYTHONQT_EXPORTS) + #define PYTHONQT_QTALL_EXPORT __attribute__((__visibility__("default"))) + #else + #define PYTHONQT_QTALL_EXPORT + #endif #endif namespace PythonQt_QtAll { -//! initialize the Qt binding +//! Initialize the Qt bindings enabled at configuration time PYTHONQT_QTALL_EXPORT void init(); } diff --git a/generator/CMakeLists.txt b/generator/CMakeLists.txt index 0801c58c0..2aa573c6b 100644 --- a/generator/CMakeLists.txt +++ b/generator/CMakeLists.txt @@ -1,37 +1,39 @@ -cmake_minimum_required(VERSION 2.8) +cmake_minimum_required(VERSION 3.20.6) -#----------------------------------------------------------------------------- -project(PythonQtGenerator) -#----------------------------------------------------------------------------- - -include(CTestUseLaunchers OPTIONAL) +project(PythonQtGenerator LANGUAGES CXX) -#----------------------------------------------------------------------------- +#---------------------------------------------------------------------------- # Setup Qt -set(minimum_required_qt_version "4.6.2") - -find_package(Qt4) +set(_default_qt_major_version 5) +if(DEFINED Qt6_DIR) + set(_default_qt_major_version 6) +endif() -if(QT4_FOUND) +# Set PythonQtGenerator_QT_VERSION +set(PythonQtGenerator_QT_VERSION ${_default_qt_major_version} CACHE STRING "Qt major version (5 or 6)") +set_property(CACHE PythonQtGenerator_QT_VERSION PROPERTY STRINGS "5" "6") +if(NOT "${PythonQtGenerator_QT_VERSION}" MATCHES "^(5|6)$") + message(FATAL_ERROR "error: PythonQtGenerator_QT_VERSION must be 5 or 6.") +endif() - set(found_qt_version ${QT_VERSION_MAJOR}.${QT_VERSION_MINOR}.${QT_VERSION_PATCH}) +set(minimum_required_qt5_version "5.15.0") +set(minimum_required_qt6_version "6.4.0") +set(minimum_required_qt_version ${minimum_required_qt${PythonQtGenerator_QT_VERSION}_version}) - if(${found_qt_version} VERSION_LESS ${minimum_required_qt_version}) - message(FATAL_ERROR "error: PythonQt requires Qt >= ${minimum_required_qt_version} -- you cannot use Qt ${found_qt_version}.") - endif() +set(qt_required_components Core Xml) - set(QT_USE_QTXML ON) +message(STATUS "${PROJECT_NAME}: Required Qt${PythonQtGenerator_QT_VERSION} components [${qt_required_components}]") - include(${QT_USE_FILE}) -else() - message(FATAL_ERROR "error: Qt4 was not found on your system. You probably need to set the QT_QMAKE_EXECUTABLE variable") -endif() +# Requirements +find_package(Qt${PythonQtGenerator_QT_VERSION} ${minimum_required_qt_version} + COMPONENTS ${qt_required_components} REQUIRED) #----------------------------------------------------------------------------- # Sources set(sources + # Based of parser/rxx.pro parser/ast.cpp parser/binder.cpp parser/class_compiler.cpp @@ -51,108 +53,62 @@ set(sources parser/type_compiler.cpp parser/visitor.cpp + # Based of simplecpp/simplecpp.pri + simplecpp/simplecpp.cpp + + # Based of generator.pri abstractmetabuilder.cpp abstractmetalang.cpp asttoxml.cpp customtypes.cpp fileout.cpp generator.cpp + generator.qrc generatorset.cpp - generatorsetqtscript.cpp main.cpp metajava.cpp - metaqtscriptbuilder.cpp - metaqtscript.cpp prigenerator.cpp reporthandler.cpp + typeparser.cpp + typesystem.cpp + + # Based of generator.pro + generatorsetqtscript.cpp + metaqtscriptbuilder.cpp + metaqtscript.cpp setupgenerator.cpp shellgenerator.cpp shellheadergenerator.cpp shellimplgenerator.cpp - typeparser.cpp - typesystem.cpp ) -#----------------------------------------------------------------------------- -# List headers. This list is used for the install command. +add_executable(${PROJECT_NAME} ${sources}) -set(headers - ) - -#----------------------------------------------------------------------------- -# Headers that should run through moc - -set(moc_sources - fileout.h - generator.h - generatorset.h - generatorsetqtscript.h - prigenerator.h - setupgenerator.h - shellgenerator.h - shellheadergenerator.h - shellimplgenerator.h +set_target_properties(${PROJECT_NAME} + PROPERTIES + AUTOMOC TRUE + AUTORCC TRUE ) -#----------------------------------------------------------------------------- -# UI files - -set(ui_sources ) - -#----------------------------------------------------------------------------- -# Resources - -set(qrc_sources - generator.qrc +target_include_directories(${PROJECT_NAME} + PUBLIC + $ + $ + $ ) -#----------------------------------------------------------------------------- -# Do wrapping -qt4_wrap_cpp(gen_moc_sources ${moc_sources}) -qt4_wrap_ui(gen_ui_sources ${ui_sources}) -qt4_add_resources(gen_qrc_sources ${qrc_sources}) - -#----------------------------------------------------------------------------- -# Copy file expected by the generator and specify install rules - -file(GLOB files_to_copy RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "build_*.txt" "typesystem_*.xml") -list(APPEND files_to_copy qtscript_masterinclude.h parser/rpp/pp-qt-configuration) -foreach(file ${files_to_copy}) - configure_file( - ${file} - ${CMAKE_CURRENT_BINARY_DIR}/${file} - COPYONLY - ) - get_filename_component(destination_dir ${file} PATH) - install(FILES ${file} DESTINATION bin/${destination_dir}) -endforeach() - -#----------------------------------------------------------------------------- -# Build the library - -SOURCE_GROUP("Resources" FILES - ${qrc_sources} - ${ui_sources} - ${files_to_copy} +target_link_libraries(${PROJECT_NAME} + PUBLIC + Qt${PythonQtGenerator_QT_VERSION}::Core + Qt${PythonQtGenerator_QT_VERSION}::Xml ) -include_directories( - ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_CURRENT_SOURCE_DIR}/parser - ${CMAKE_CURRENT_SOURCE_DIR}/parser/rpp +target_compile_definitions(${PROJECT_NAME} + PRIVATE + RXX_ALLOCATOR_INIT_0 # Based of parser/rxx.pro + QT_NO_CAST_TO_ASCII # Based of generator.pro ) -add_definitions(-DRXX_ALLOCATOR_INIT_0) - -add_executable(${PROJECT_NAME} - ${sources} - ${gen_moc_sources} - ${gen_ui_sources} - ${gen_qrc_sources} -) - -target_link_libraries(${PROJECT_NAME} ${QT_LIBRARIES}) - #----------------------------------------------------------------------------- # Install library (on windows, put the dll in 'bin' and the archive in 'lib') diff --git a/src/PythonQtClassInfo.cpp b/src/PythonQtClassInfo.cpp index 68799e33f..6cd6120c0 100644 --- a/src/PythonQtClassInfo.cpp +++ b/src/PythonQtClassInfo.cpp @@ -289,10 +289,12 @@ bool PythonQtClassInfo::lookForEnumAndCache(const QMetaObject* meta, const char* int enumCount = meta->enumeratorCount(); for (int i = 0; i < enumCount; i++) { QMetaEnum e = meta->enumerator(i); - // we do not want flags, they will cause our values to appear two times - if (e.isFlag()) + if (_cachedMembers.contains(memberName)) { +#ifdef PYTHONQT_DEBUG + std::cout << "cached enum " << memberName << " on " << meta->className() << std::endl; +#endif continue; - + } for (int j = 0; j < e.keyCount(); j++) { if (escapeReservedNames(e.key(j)) == memberName) { PyObject* enumType = findEnumWrapper(e.name()); @@ -558,9 +560,6 @@ QStringList PythonQtClassInfo::memberList() for (int i = 0; i < meta->enumeratorCount(); i++) { QMetaEnum e = meta->enumerator(i); l << e.name(); - // we do not want flags, they will cause our values to appear two times - if (e.isFlag()) - continue; for (int j = 0; j < e.keyCount(); j++) { l << QString(e.key(j)); diff --git a/src/PythonQtConfigure.h.in b/src/PythonQtConfigure.h.in new file mode 100644 index 000000000..a8645abd7 --- /dev/null +++ b/src/PythonQtConfigure.h.in @@ -0,0 +1,45 @@ +/* + * + * Copyright (C) 2025 MeVis Medical Solutions AG All Rights Reserved. + * Copyright (C) 2025 Kitware, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * Further, this software is distributed without any warranty that it is + * free of the rightful claim of any third person regarding infringement + * or the like. Any license provided herein, whether implied or + * otherwise, applies only to this software file. Patent licenses, if + * any, provided herein do not apply to combinations of this program with + * other software, or any other product whatsoever. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Contact information: MeVis Medical Solutions AG, Universitaetsallee 29, + * 28359 Bremen, Germany or: + * + * http://www.mevis.de + * + */ + +#ifndef _PYTHONQTCONFIGURE_H +#define _PYTHONQTCONFIGURE_H + +// If the user has not pre-defined the macro, derive it from the CMake +// configure-time variable `PYTHONQT_USE_RELEASE_PYTHON_FALLBACK`, which +// mirrors the `PythonQt_USE_RELEASE_PYTHON_FALLBACK` (camelCase) CMake +// option. +#ifndef PYTHONQT_USE_RELEASE_PYTHON_FALLBACK +#cmakedefine PYTHONQT_USE_RELEASE_PYTHON_FALLBACK +#endif + +#endif diff --git a/src/PythonQtPythonInclude.h b/src/PythonQtPythonInclude.h index b88f51b26..663c28395 100644 --- a/src/PythonQtPythonInclude.h +++ b/src/PythonQtPythonInclude.h @@ -33,6 +33,8 @@ #ifndef __PythonQtPythonInclude_h #define __PythonQtPythonInclude_h +#include + // Undefine macros that features.h defines to avoid redefinition warning #ifdef _POSIX_C_SOURCE #undef _POSIX_C_SOURCE diff --git a/src/generate_configure_h.py b/src/generate_configure_h.py new file mode 100644 index 000000000..a0149a49e --- /dev/null +++ b/src/generate_configure_h.py @@ -0,0 +1,37 @@ +""" +Generate a C/C++ header from a CMake .in template for use with qmake builds. + +CMake processes .h.in files at configure time, replacing: + #cmakedefine VAR -> #define VAR (when VAR is ON/defined) + -> /* #undef VAR */ (when VAR is OFF/undefined) + +This script replaces all #cmakedefine directives with #define, which +matches the default PythonQt CMake option values (all defaults are ON). +""" + +import re +import sys + + +def main(): + if len(sys.argv) != 3: + print( + f"Usage: {sys.argv[0]} ", + file=sys.stderr, + ) + sys.exit(1) + + in_path, out_path = sys.argv[1], sys.argv[2] + + with open(in_path) as f: + content = f.read() + + # #cmakedefine VAR [rest] -> #define VAR [rest] + content = re.sub(r"^#cmakedefine\b", "#define", content, flags=re.MULTILINE) + + with open(out_path, "w") as f: + f.write(content) + + +if __name__ == "__main__": + main() diff --git a/src/src.pro b/src/src.pro index e7fba7578..bc4ab9f75 100644 --- a/src/src.pro +++ b/src/src.pro @@ -29,6 +29,17 @@ QT += widgets core-private INCLUDEPATH += $$PWD +# Generate PythonQtConfigure.h from template at qmake-configure time. +# The script replaces #cmakedefine directives (CMake syntax) with #define, +# enabling a pure qmake build without requiring a prior CMake run. +!exists($$PWD/PythonQtConfigure.h) { + win32: PYTHONQT_PYTHON = python + else: PYTHONQT_PYTHON = python3 + system($$PYTHONQT_PYTHON $$PWD/generate_configure_h.py \ + $$PWD/PythonQtConfigure.h.in \ + $$PWD/PythonQtConfigure.h) +} + macx { contains(QT_MAJOR_VERSION, 6) { QMAKE_APPLE_DEVICE_ARCHS = x86_64 arm64 diff --git a/tests/PythonQtTestMain.cpp b/tests/PythonQtTestMain.cpp index 1ae76e2a6..09fe6747f 100644 --- a/tests/PythonQtTestMain.cpp +++ b/tests/PythonQtTestMain.cpp @@ -54,7 +54,6 @@ int main(int argc, char* argv[]) QTest::qExec(&test, argc, argv); return 0; } - if (QProcessEnvironment::systemEnvironment().contains("PYTHONQT_RUN_ONLY_CLEANUP_TESTS")) { PythonQtTestCleanup cleanup; QTest::qExec(&cleanup, argc, argv); diff --git a/tests/PythonQtTests.cpp b/tests/PythonQtTests.cpp index b9286e48b..39ed288ee 100644 --- a/tests/PythonQtTests.cpp +++ b/tests/PythonQtTests.cpp @@ -393,6 +393,24 @@ void PythonQtTestSlotCalling::testCppFactory() QVERIFY(_helper->runScript( "obj.testNoArg()\nfrom PythonQt.private import PQCppObject2\na = PQCppObject2()\nif " "a.testEnumFlag3(PQCppObject2.TestEnumValue2)==PQCppObject2.TestEnumValue2: obj.setPassed();\n")); + + PythonQt::self()->registerCPPClass("PQCppObjectQFlagOnly", NULL, NULL, + PythonQtCreateObject); + + // local enum (decorated) + QVERIFY(_helper->runScript( + "obj.testNoArg()\nfrom PythonQt.private import PQCppObjectQFlagOnly\na = PQCppObjectQFlagOnly()\nprint " + "(a.testEnumFlag1)\nif a.testEnumFlag1(PQCppObjectQFlagOnly.TestEnumValue2)==PQCppObjectQFlagOnly.TestEnumValue2: " + "obj.setPassed();\n")); + + // enum with namespace (decorated) + QVERIFY(_helper->runScript( + "obj.testNoArg()\nfrom PythonQt.private import PQCppObjectQFlagOnly\na = PQCppObjectQFlagOnly()\nif " + "a.testEnumFlag2(PQCppObjectQFlagOnly.TestEnumValue2)==PQCppObjectQFlagOnly.TestEnumValue2: obj.setPassed();\n")); + // with int overload to check overloading + QVERIFY(_helper->runScript( + "obj.testNoArg()\nfrom PythonQt.private import PQCppObjectQFlagOnly\na = PQCppObjectQFlagOnly()\nif " + "a.testEnumFlag3(PQCppObjectQFlagOnly.TestEnumValue2)==PQCppObjectQFlagOnly.TestEnumValue2: obj.setPassed();\n")); } PQCppObject2Decorator::TestEnumFlag PQCppObject2Decorator::testEnumFlag1(PQCppObject2* /*obj*/, @@ -417,6 +435,34 @@ PQCppObject2Decorator::TestEnumFlag PQCppObject2Decorator::testEnumFlag3(PQCppOb return flag; } +// PQCppObjectQFlagOnlyDecorator + +PQCppObjectQFlagOnlyDecorator::TestEnumFlag PQCppObjectQFlagOnlyDecorator::testEnumFlag1(PQCppObjectQFlagOnly* obj, + PQCppObjectQFlagOnlyDecorator::TestEnumFlag flag) +{ + return flag; +} + +PQCppObjectQFlagOnly::TestEnumFlag PQCppObjectQFlagOnlyDecorator::testEnumFlag2(PQCppObjectQFlagOnly* obj, + PQCppObjectQFlagOnly::TestEnumFlag flag) +{ + return flag; +} + +// with int overload +PQCppObjectQFlagOnlyDecorator::TestEnumFlag PQCppObjectQFlagOnlyDecorator::testEnumFlag3(PQCppObjectQFlagOnly* obj, + int flag) +{ + return (TestEnumFlag)-1; +} +PQCppObjectQFlagOnlyDecorator::TestEnumFlag PQCppObjectQFlagOnlyDecorator::testEnumFlag3(PQCppObjectQFlagOnly* obj, + PQCppObjectQFlagOnlyDecorator::TestEnumFlag flag) +{ + return flag; +} + +// PythonQtTestSlotCalling + void PythonQtTestSlotCalling::testMultiArgsSlotCall() { QVERIFY(_helper->runScript("if obj.getMultiArgs(12,47.11,'test')==(12,47.11,'test'): obj.setPassed();\n")); diff --git a/tests/PythonQtTests.h b/tests/PythonQtTests.h index 3588107d1..14a4b6b4f 100644 --- a/tests/PythonQtTests.h +++ b/tests/PythonQtTests.h @@ -298,6 +298,31 @@ public Q_SLOTS: TestEnumFlag testEnumFlag3(PQCppObject2* obj, TestEnumFlag flag); }; +typedef PQCppObject2 PQCppObjectQFlagOnly; + +class PQCppObjectQFlagOnlyDecorator : public QObject +{ + Q_OBJECT + +public: + Q_FLAGS(TestEnumFlag) + + enum TestEnumFlag { TestEnumValue1 = 0, TestEnumValue2 = 1 }; + + Q_DECLARE_FLAGS(TestEnum, TestEnumFlag) + +public slots: + PQCppObjectQFlagOnly* new_PQCppObjectQFlagOnly() { return new PQCppObjectQFlagOnly(); } + + TestEnumFlag testEnumFlag1(PQCppObjectQFlagOnly* obj, TestEnumFlag flag); + + PQCppObjectQFlagOnly::TestEnumFlag testEnumFlag2(PQCppObjectQFlagOnly* obj, PQCppObjectQFlagOnly::TestEnumFlag flag); + + // with int overload + TestEnumFlag testEnumFlag3(PQCppObjectQFlagOnly* obj, int flag); + TestEnumFlag testEnumFlag3(PQCppObjectQFlagOnly* obj, TestEnumFlag flag); +}; + class PQUnknownValueObject { public: