diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9c00505..97f5cfe 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -9,7 +9,6 @@ ament_python_install_package(${PROJECT_NAME}
install(FILES
cmake/shiboken_helper.cmake
- cmake/sip_configure.py
cmake/sip_helper.cmake
DESTINATION share/${PROJECT_NAME}/cmake)
diff --git a/cmake/sip_configure.py b/cmake/sip_configure.py
deleted file mode 100644
index 5210ee5..0000000
--- a/cmake/sip_configure.py
+++ /dev/null
@@ -1,231 +0,0 @@
-import copy
-import os
-import re
-import shutil
-import subprocess
-import sys
-import tempfile
-
-import PyQt5
-from PyQt5 import QtCore
-import sipconfig
-
-libqt5_rename = False
-
-
-class Configuration(sipconfig.Configuration):
-
- def __init__(self):
- env = copy.copy(os.environ)
- env['QT_SELECT'] = '5'
- qmake_exe = 'qmake-qt5' if shutil.which('qmake-qt5') else 'qmake'
- qtconfig = subprocess.check_output(
- [qmake_exe, '-query'], env=env, universal_newlines=True)
- qtconfig = dict(line.split(':', 1) for line in qtconfig.splitlines())
- pyqtconfig = {
- 'qt_archdata_dir': qtconfig['QT_INSTALL_DATA'],
- 'qt_data_dir': qtconfig['QT_INSTALL_DATA'],
- 'qt_dir': qtconfig['QT_INSTALL_PREFIX'],
- 'qt_inc_dir': qtconfig['QT_INSTALL_HEADERS'],
- 'qt_lib_dir': qtconfig['QT_INSTALL_LIBS'],
- 'qt_threaded': 1,
- 'qt_version': QtCore.QT_VERSION,
- 'qt_winconfig': 'shared exceptions',
- }
- if sys.platform == 'darwin':
- if os.path.exists(os.path.join(qtconfig['QT_INSTALL_LIBS'], 'QtCore.framework')):
- pyqtconfig['qt_framework'] = 1
- else:
- global libqt5_rename
- libqt5_rename = True
-
- sipconfig.Configuration.__init__(self, [pyqtconfig])
-
- macros = sipconfig._default_macros.copy()
- macros['INCDIR_QT'] = qtconfig['QT_INSTALL_HEADERS']
- macros['LIBDIR_QT'] = qtconfig['QT_INSTALL_LIBS']
- macros['MOC'] = 'moc-qt5' if shutil.which('moc-qt5') else 'moc'
- self.set_build_macros(macros)
-
-
-def get_sip_dir_flags(config):
- """
- Get the extra SIP flags needed by the imported qt module, and locate PyQt5 sip install files.
-
- Note that this normally only includes those flags (-x and -t) that relate to SIP's versioning
- system.
- """
- try:
- sip_dir = config.pyqt_sip_dir
- sip_flags = config.pyqt_sip_flags
- return sip_dir, sip_flags
- except AttributeError:
- pass
-
- # We didn't find the sip_dir and sip_flags from the config, continue looking
-
- # sipconfig.Configuration does not have a pyqt_sip_dir or pyqt_sip_flags AttributeError
- sip_flags = QtCore.PYQT_CONFIGURATION['sip_flags']
-
- candidate_sip_dirs = []
-
- # Archlinux installs sip files here by default
- candidate_sip_dirs.append(os.path.join(PyQt5.__path__[0], 'bindings'))
-
- # sip4 installs here by default
- candidate_sip_dirs.append(os.path.join(sipconfig._pkg_config['default_sip_dir'], 'PyQt5'))
-
- # Homebrew installs sip files here by default
- candidate_sip_dirs.append(os.path.join(sipconfig._pkg_config['default_sip_dir'], 'Qt5'))
-
- for sip_dir in candidate_sip_dirs:
- if os.path.exists(sip_dir):
- return sip_dir, sip_flags
-
- raise FileNotFoundError('The sip directory for PyQt5 could not be located. Please ensure' +
- ' that PyQt5 is installed')
-
-
-if len(sys.argv) != 8:
- print('usage: %s build-dir sip-file output_dir include_dirs libs lib_dirs ldflags' %
- sys.argv[0])
- sys.exit(1)
-
-# The SIP build folder, the SIP file, the output directory, the include
-# directories, the libraries, the library directories and the linker
-# flags.
-build_dir, sip_file, output_dir, include_dirs, libs, lib_dirs, ldflags = sys.argv[1:]
-
-# The name of the SIP build file generated by SIP and used by the build system.
-build_file = 'pyqtscripting.sbf'
-
-# Get the PyQt configuration information.
-config = Configuration()
-
-sip_dir, sip_flags = get_sip_dir_flags(config)
-
-try:
- os.makedirs(build_dir)
-except OSError:
- pass
-
-# Run SIP to generate the code. Note that we tell SIP where to find the qt
-# module's specification files using the -I flag.
-
-sip_bin = config.sip_bin
-# Without the .exe, this might actually be a directory in Windows
-if sys.platform == 'win32' and os.path.isdir(sip_bin):
- sip_bin += '.exe'
-
-# SIP4 has an incompatibility with Qt 5.15.6. In particular, Qt 5.15.6 uses a new SIP directive
-# called py_ssize_t_clean in QtCoremod.sip that SIP4 does not understand.
-#
-# Unfortunately, the combination of SIP4 and Qt 5.15.6 is common. Archlinux, Ubuntu 22.04
-# and RHEL-9 all have this combination. On Ubuntu 22.04, there is a custom patch to SIP4
-# to make it understand the py_ssize_t_clean tag, so the combination works. But on most
-# other platforms, it fails.
-#
-# To workaround this, copy all of the SIP files into a temporary directory, remove the offending
-# line, and then use that temporary directory as the include path. This is unnecessary on
-# Ubuntu 22.04, but shouldn't hurt anything there.
-with tempfile.TemporaryDirectory() as tmpdirname:
- shutil.copytree(sip_dir, tmpdirname, dirs_exist_ok=True)
-
- output = ''
- with open(os.path.join(tmpdirname, 'QtCore', 'QtCoremod.sip'), 'r') as infp:
- for line in infp:
- if line.startswith('%Module(name='):
- result = re.sub(r', py_ssize_t_clean=True', '', line)
- output += result
- else:
- output += line
-
- with open(os.path.join(tmpdirname, 'QtCore', 'QtCoremod.sip'), 'w') as outfp:
- outfp.write(output)
-
- cmd = [
- sip_bin,
- '-c', build_dir,
- '-b', os.path.join(build_dir, build_file),
- '-I', tmpdirname,
- '-w'
- ]
- cmd += sip_flags.split(' ')
- cmd.append(sip_file)
-
- subprocess.check_call(cmd)
-
-# Create the Makefile. The QtModuleMakefile class provided by the
-# pyqtconfig module takes care of all the extra preprocessor, compiler and
-# linker flags needed by the Qt library.
-makefile = sipconfig.SIPModuleMakefile(
- dir=build_dir,
- configuration=config,
- build_file=build_file,
- qt=['QtCore', 'QtGui']
-)
-
-# hack to override makefile behavior which always prepend -l to libraries
-# which is wrong for absolute paths
-default_platform_lib_function = sipconfig.SIPModuleMakefile.platform_lib
-
-
-def custom_platform_lib_function(self, clib, framework=0):
- if not clib or clib.isspace():
- return None
- # Only add '-l' if a library doesn't already start with '-l' and is not an absolute path
- if os.path.isabs(clib) or clib.startswith('-l'):
- return clib
-
- global libqt5_rename
- # sip renames libs to Qt5 automatically on Linux, but not on macOS
- if libqt5_rename and not framework and clib.startswith('Qt') and not clib.startswith('Qt5'):
- return '-lQt5' + clib[2:]
-
- return default_platform_lib_function(self, clib, framework)
-
-
-sipconfig.SIPModuleMakefile.platform_lib = custom_platform_lib_function
-
-
-# split paths on whitespace
-# while dealing with whitespaces within the paths if they are escaped with backslashes
-def split_paths(paths):
- paths = re.split('(?<=[^\\\\]) ', paths)
- return paths
-
-
-for include_dir in split_paths(include_dirs):
- include_dir = include_dir.replace('\\', '')
- makefile.extra_include_dirs.append(include_dir)
-for lib in split_paths(libs):
- makefile.extra_libs.append(lib)
-for lib_dir in split_paths(lib_dirs):
- lib_dir = lib_dir.replace('\\', '')
- makefile.extra_lib_dirs.append(lib_dir)
-for ldflag in ldflags.split('\\ '):
- makefile.LFLAGS.append(ldflag)
-
-# redirect location of generated library
-makefile._target = '"%s"' % os.path.join(output_dir, makefile._target)
-
-# Force c++17
-if sys.platform == 'win32':
- makefile.extra_cxxflags.append('/std:c++17')
- # The __cplusplus flag is not properly set on Windows for backwards
- # compatibilty. This flag sets it correctly
- makefile.CXXFLAGS.append('/Zc:__cplusplus')
-else:
- makefile.extra_cxxflags.append('-std=c++17')
-
-# Finalise the Makefile, preparing it to be saved to disk
-makefile.finalise()
-
-# Replace Qt variables from libraries
-libs = makefile.LIBS.as_list()
-for i in range(len(libs)):
- libs[i] = libs[i].replace('$$[QT_INSTALL_LIBS]', config.build_macros()['LIBDIR_QT'])
-makefile.LIBS.set(libs)
-
-# Generate the Makefile itself
-makefile.generate()
diff --git a/cmake/sip_helper.cmake b/cmake/sip_helper.cmake
index a5ac3c2..8f4fd24 100644
--- a/cmake/sip_helper.cmake
+++ b/cmake/sip_helper.cmake
@@ -5,47 +5,30 @@ set(__PYTHON_QT_BINDING_SIP_HELPER_INCLUDED TRUE)
set(__PYTHON_QT_BINDING_SIP_HELPER_DIR ${CMAKE_CURRENT_LIST_DIR})
-# By default, without the settings below, find_package(Python3) will attempt
-# to find the newest python version it can, and additionally will find the
-# most specific version. For instance, on a system that has
-# /usr/bin/python3.10, /usr/bin/python3.11, and /usr/bin/python3, it will find
-# /usr/bin/python3.11, even if /usr/bin/python3 points to /usr/bin/python3.10.
-# The behavior we want is to prefer the "system" installed version unless the
-# user specifically tells us othewise through the Python3_EXECUTABLE hint.
-# Setting CMP0094 to NEW means that the search will stop after the first
-# python version is found. Setting Python3_FIND_UNVERSIONED_NAMES means that
-# the search will prefer /usr/bin/python3 over /usr/bin/python3.11. And that
-# latter functionality is only available in CMake 3.20 or later, so we need
-# at least that version.
cmake_minimum_required(VERSION 3.20)
cmake_policy(SET CMP0094 NEW)
set(Python3_FIND_UNVERSIONED_NAMES FIRST)
find_package(Python3 ${Python3_VERSION} REQUIRED COMPONENTS Interpreter Development)
+# Check if modern sipbuild is available via python module
execute_process(
- COMMAND ${Python3_EXECUTABLE} -c "import sipconfig; print(sipconfig.Configuration().sip_bin)"
- OUTPUT_VARIABLE PYTHON_SIP_EXECUTABLE
+ COMMAND ${Python3_EXECUTABLE} -c "import sipbuild"
+ RESULT_VARIABLE _sipbuild_res
ERROR_QUIET)
-if(PYTHON_SIP_EXECUTABLE)
- string(STRIP ${PYTHON_SIP_EXECUTABLE} SIP_EXECUTABLE)
-else()
- find_program(SIP_EXECUTABLE sip)
-endif()
-
-if(SIP_EXECUTABLE)
- message(STATUS "SIP binding generator available at: ${SIP_EXECUTABLE}")
+if(_sipbuild_res EQUAL 0)
+ message(STATUS "Modern SIP binding generator (sip-build) is available.")
set(sip_helper_FOUND TRUE)
else()
- message(STATUS "SIP binding generator NOT available.")
+ message(STATUS "Modern SIP binding generator NOT available.")
set(sip_helper_NOTFOUND TRUE)
endif()
#
# Run the SIP generator and compile the generated code into a library.
#
-# .. note:: The target lib${PROJECT_NAME} is created.
+# .. note:: Creates a target named lib${PROJECT_NAME}
#
# :param PROJECT_NAME: The name of the sip project
# :type PROJECT_NAME: string
@@ -53,17 +36,14 @@ endif()
# :type SIP_FILE: string
#
# The following options can be used to override the default behavior:
-# SIP_CONFIGURE: the used configure script for SIP
-# (default: sip_configure.py in the same folder as this file)
+# SIP_CONFIGURE: (IGNORED) Retained for CMake API compatibility only.
# SOURCE_DIR: the source dir (default: ${PROJECT_SOURCE_DIR}/src)
# LIBRARY_DIR: the library dir (default: ${PROJECT_SOURCE_DIR}/src)
# BINARY_DIR: the binary dir (default: ${PROJECT_BINARY_DIR})
#
# The following keywords arguments can be used to specify:
-# DEPENDS: depends for the custom command
-# (should list all sip and header files)
+# DEPENDS: depends for the custom command (should list all sip and header files)
# DEPENDENCIES: target dependencies
-# (should list the library for which SIP generates the bindings)
#
function(build_sip_binding PROJECT_NAME SIP_FILE)
cmake_parse_arguments(sip "" "SIP_CONFIGURE;SOURCE_DIR;LIBRARY_DIR;BINARY_DIR" "DEPENDS;DEPENDENCIES" ${ARGN})
@@ -71,11 +51,11 @@ function(build_sip_binding PROJECT_NAME SIP_FILE)
message(WARNING "build_sip_binding(${PROJECT_NAME}) called with unused arguments: ${sip_UNPARSED_ARGUMENTS}")
endif()
- # set default values for optional arguments
- if(NOT sip_SIP_CONFIGURE)
- # default to sip_configure.py in this directory
- set(sip_SIP_CONFIGURE ${__PYTHON_QT_BINDING_SIP_HELPER_DIR}/sip_configure.py)
+ if(sip_SIP_CONFIGURE)
+ message(WARNING "SIP_CONFIGURE argument is deprecated and ignored. CMake now handles configuration natively.")
endif()
+
+ # set default values for optional arguments
if(NOT sip_SOURCE_DIR)
set(sip_SOURCE_DIR ${PROJECT_SOURCE_DIR}/src)
endif()
@@ -88,43 +68,52 @@ function(build_sip_binding PROJECT_NAME SIP_FILE)
set(SIP_BUILD_DIR ${sip_BINARY_DIR}/sip/${PROJECT_NAME})
- set(INCLUDE_DIRS ${${PROJECT_NAME}_INCLUDE_DIRS} ${Python3_INCLUDE_DIRS})
- set(LIBRARIES ${${PROJECT_NAME}_LIBRARIES})
- set(LIBRARY_DIRS ${${PROJECT_NAME}_LIBRARY_DIRS})
- set(LDFLAGS_OTHER ${${PROJECT_NAME}_LDFLAGS_OTHER})
+ # Extract the filename from the SIP_FILE path
+ get_filename_component(SIP_FILE_NAME ${SIP_FILE} NAME)
- 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}\"
- DEPENDS ${sip_SIP_CONFIGURE} ${SIP_FILE} ${sip_DEPENDS}
- WORKING_DIRECTORY ${sip_SOURCE_DIR}
- COMMENT "Running SIP generator for ${PROJECT_NAME} Python bindings..."
- )
+ # Generate a pyproject.toml to be given to sip-build
+ file(MAKE_DIRECTORY ${SIP_BUILD_DIR})
+ set(TOML_CONTENT
+"[build-system]
+requires = [\"sip >= 5.3\"]
+build-backend = \"sipbuild.api\"
- if(NOT EXISTS "${sip_LIBRARY_DIR}")
- file(MAKE_DIRECTORY ${sip_LIBRARY_DIR})
- endif()
+[project]
+name = \"${PROJECT_NAME}\"
+version = \"1.0.0\"
- if(WIN32)
- set(MAKE_EXECUTABLE NMake.exe)
- else()
- find_program(MAKE_PROGRAM NAMES make)
- message(STATUS "Found required make: ${MAKE_PROGRAM}")
- set(MAKE_EXECUTABLE ${MAKE_PROGRAM})
- endif()
+[tool.sip.metadata]
+name = \"${PROJECT_NAME}\"
+
+[tool.sip.project]
+sip-files-dir = \"${sip_SOURCE_DIR}\"
+
+[tool.sip.bindings.${PROJECT_NAME}]
+sip-file = \"${SIP_FILE_NAME}\"
+")
+ file(WRITE ${SIP_BUILD_DIR}/pyproject.toml "${TOML_CONTENT}")
+
+ # Expect sip-build to produce a single output file because of the --concatenate 1 agrument below
+ set(GENERATED_CPP ${SIP_BUILD_DIR}/build/${PROJECT_NAME}/sip${PROJECT_NAME}part0.cpp)
+ # Generate code for a cPython extension using sip-build
add_custom_command(
- OUTPUT ${sip_LIBRARY_DIR}/lib${PROJECT_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}
- COMMAND ${MAKE_EXECUTABLE}
- DEPENDS ${SIP_BUILD_DIR}/Makefile
+ OUTPUT ${GENERATED_CPP}
+ COMMAND ${Python3_EXECUTABLE} -m sipbuild.tools.build --no-compile --concatenate 1 --build-dir build
+ DEPENDS ${SIP_FILE} ${sip_DEPENDS}
WORKING_DIRECTORY ${SIP_BUILD_DIR}
- COMMENT "Compiling generated code for ${PROJECT_NAME} Python bindings..."
+ COMMENT "Generating C++ code for ${PROJECT_NAME} Python bindings using sip-build..."
)
- add_custom_target(lib${PROJECT_NAME} ALL
- DEPENDS ${sip_LIBRARY_DIR}/lib${PROJECT_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}
- COMMENT "Meta target for ${PROJECT_NAME} Python bindings..."
- )
- add_dependencies(lib${PROJECT_NAME} ${sip_DEPENDENCIES})
-endfunction()
+ # Build the cPython extension natively using CMake
+ Python3_add_library(lib${PROJECT_NAME} MODULE ${GENERATED_CPP})
+
+ # Link project dependencies against this target
+ target_include_directories(lib${PROJECT_NAME} PRIVATE ${${PROJECT_NAME}_INCLUDE_DIRS})
+ target_link_libraries(lib${PROJECT_NAME} PRIVATE ${${PROJECT_NAME}_LIBRARIES})
+ target_link_directories(lib${PROJECT_NAME} PRIVATE ${${PROJECT_NAME}_LIBRARY_DIRS})
+
+ if(${PROJECT_NAME}_LDFLAGS_OTHER)
+ target_link_options(lib${PROJECT_NAME} PRIVATE ${${PROJECT_NAME}_LDFLAGS_OTHER})
+ endif()
+endfunction()
\ No newline at end of file
diff --git a/package.xml b/package.xml
index df2b169..29dc41c 100644
--- a/package.xml
+++ b/package.xml
@@ -32,6 +32,7 @@
python3-qt5-bindings
python3-qt5-bindings
+
ament_cmake_pytest
ament_lint_auto