diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c176f13e..c1a92db5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,13 @@ jobs: fail-fast: false matrix: include: + - BUILD_TYPE: Release + PYTHON_VERSION: '3.13' + BUILD_SHARED_LIBS: yes + SYMENGINE_PY_LIMITED_API: '3.11' + OS: ubuntu-22.04 + CC: gcc + - BUILD_TYPE: Debug WITH_BFD: yes PYTHON_VERSION: '3.12' @@ -197,6 +204,7 @@ jobs: MAKEFLAGS: ${{ matrix.MAKEFLAGS }} BUILD_SHARED_LIBS: ${{ matrix.BUILD_SHARED_LIBS }} PYTHON_VERSION: ${{ matrix.PYTHON_VERSION }} + SYMENGINE_PY_LIMITED_API: ${{ matrix.SYMENGINE_PY_LIMITED_API }} - name: Deploy Documentation if: ${{ (github.ref == 'refs/heads/main' && github.repository == 'Symengine/symengine.py') || (github.ref == 'refs/heads/master' && github.repository == 'Symengine/symengine.py')}} diff --git a/CMakeLists.txt b/CMakeLists.txt index e83c95b1..cc990c4e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,7 @@ set(CMAKE_CXX_FLAGS_DEBUG ${SYMENGINE_CXX_FLAGS_DEBUG}) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${SYMENGINE_CXX_FLAGS}") include_directories(${SYMENGINE_INCLUDE_DIRS}) +set(WITH_PY_LIMITED_API OFF CACHE STRING "Use CPython's limited API") set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake/") find_package(Python REQUIRED) find_package(Cython REQUIRED) diff --git a/appveyor.yml b/appveyor.yml index ce1c487c..52e44eb5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,7 +4,7 @@ image: "Visual Studio 2019" environment: global: - PLATFORMTOOLSET: "v140" + PLATFORMTOOLSET: "v142" matrix: - BUILD_TYPE: "Release" @@ -107,8 +107,8 @@ install: - mkdir build - cd build -- if [%COMPILER%]==[MSVC15] if [%PLATFORM%]==[Win32] set "CMAKE_GENERATOR=Visual Studio 14 2015" -- if [%COMPILER%]==[MSVC15] if [%PLATFORM%]==[x64] set "CMAKE_GENERATOR=Visual Studio 14 2015 Win64" +- if [%COMPILER%]==[MSVC15] set "CMAKE_GENERATOR=Visual Studio 16 2019" +- if [%COMPILER%]==[MSVC15] set "CMAKE_GENERATOR_PLATFORM=%PLATFORM%" - if [%COMPILER%]==[MinGW] set "CMAKE_GENERATOR=MinGW Makefiles" - if [%COMPILER%]==[MinGW-w64] set "CMAKE_GENERATOR=MinGW Makefiles" diff --git a/bin/install_travis.sh b/bin/install_travis.sh index f63a4747..c9199843 100644 --- a/bin/install_travis.sh +++ b/bin/install_travis.sh @@ -20,6 +20,10 @@ if [[ "${WITH_FLINT_PY}" == "yes" ]]; then export conda_pkgs="${conda_pkgs} python-flint"; # python-flint affects sympy, see e.g. sympy/sympy#26645 fi +if [[ "${SYMENGINE_PY_LIMITED_API}" != "" ]]; then + export conda_pkgs="${conda_pkgs} abi3audit" +fi + if [[ "${WITH_SAGE}" == "yes" ]]; then # This is split to avoid the 10 minute limit conda install -q sagelib=8.1 @@ -33,4 +37,4 @@ if [[ "${WITH_SYMPY}" != "no" ]]; then pip install sympy; fi -conda clean --all \ No newline at end of file +conda clean --all diff --git a/bin/test_travis.sh b/bin/test_travis.sh index d83b27db..fe579cdc 100755 --- a/bin/test_travis.sh +++ b/bin/test_travis.sh @@ -14,6 +14,10 @@ cd symengine-* # Build inplace so that nosetests can be run inside source directory python3 setup.py install build_ext --inplace --symengine-dir=$our_install_dir +if [[ "${SYMENGINE_PY_LIMITED_API:-}" != "" ]]; then + python3 -m abi3audit --assume-minimum-abi3 ${SYMENGINE_PY_LIMITED_API} symengine/lib/symengine_wrapper.abi3.so -v +fi + # Test python wrappers python3 -m pip install pytest python3 -m pytest -s -v $PWD/symengine/tests/test_*.py diff --git a/cmake/FindCython.cmake b/cmake/FindCython.cmake index beac6c56..e35d44ba 100644 --- a/cmake/FindCython.cmake +++ b/cmake/FindCython.cmake @@ -24,6 +24,19 @@ IF (CYTHON_BIN) else (CYTHON_RESULT EQUAL 0) SET(Cython_Compilation_Failed TRUE) endif (CYTHON_RESULT EQUAL 0) + execute_process( + COMMAND ${CYTHON_BIN} --version + RESULT_VARIABLE CYTHON_VERSION_RESULT + OUTPUT_VARIABLE CYTHON_VERSION_OUTPUT + ERROR_VARIABLE CYTHON_VERSION_ERROR + ) + if (CYTHON_VERSION_RESULT EQUAL 0) + string(STRIP ${CYTHON_VERSION_OUTPUT} CYTHON_VERSION_OUTPUT) + if ("${CYTHON_VERSION_OUTPUT}" MATCHES "Cython version") + string(SUBSTRING "${CYTHON_VERSION_OUTPUT}" 15 -1 CYTHON_VERSION) + endif () + endif () + message(STATUS "Cython version: ${CYTHON_VERSION}") ENDIF (CYTHON_BIN) @@ -31,6 +44,11 @@ IF (Cython_FOUND) IF (NOT Cython_FIND_QUIETLY) MESSAGE(STATUS "Found CYTHON: ${CYTHON_BIN}") ENDIF (NOT Cython_FIND_QUIETLY) + IF (WITH_PY_LIMITED_API AND "${CYTHON_VERSION}" VERSION_LESS "3.1") + MESSAGE(FATAL_ERROR + "Your Cython version (${CYTHON_VERSION}) is too old. Please upgrade Cython to 3.1 or newer." + ) + ENDIF () ELSE (Cython_FOUND) IF (Cython_FIND_REQUIRED) if(Cython_Compilation_Failed) diff --git a/cmake/FindPython.cmake b/cmake/FindPython.cmake index c1f6c439..7ed8287f 100644 --- a/cmake/FindPython.cmake +++ b/cmake/FindPython.cmake @@ -48,15 +48,17 @@ if ("${PY_GIL_DISABLED}" STREQUAL "True") endif() if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") - FIND_LIBRARY(PYTHON_LIBRARY NAMES - python${PYTHON_VERSION}${PY_THREAD} - python${PYTHON_VERSION}m - python${PYTHON_VERSION_WITHOUT_DOTS}${PY_THREAD} - PATHS ${PYTHON_LIB_PATH} ${PYTHON_PREFIX_PATH}/lib ${PYTHON_PREFIX_PATH}/libs - PATH_SUFFIXES ${CMAKE_LIBRARY_ARCHITECTURE} - NO_DEFAULT_PATH - NO_SYSTEM_ENVIRONMENT_PATH - ) + if (WITH_PY_LIMITED_API) + set(PYTHON_LIBRARY_NAMES python3) + else() + set(PYTHON_LIBRARY_NAMES python${PYTHON_VERSION}${PY_THREAD} python${PYTHON_VERSION}m python${PYTHON_VERSION_WITHOUT_DOTS}${PY_THREAD}) + endif() + FIND_LIBRARY(PYTHON_LIBRARY NAMES ${PYTHON_LIBRARY_NAMES} + PATHS ${PYTHON_LIB_PATH} ${PYTHON_PREFIX_PATH}/lib ${PYTHON_PREFIX_PATH}/libs + PATH_SUFFIXES ${CMAKE_LIBRARY_ARCHITECTURE} + NO_DEFAULT_PATH + NO_SYSTEM_ENVIRONMENT_PATH + ) endif() execute_process( @@ -74,6 +76,14 @@ execute_process( ) string(STRIP ${PYTHON_EXTENSION_SOABI_tmp} PYTHON_EXTENSION_SOABI_tmp) +if (WITH_PY_LIMITED_API) + if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + set(PYTHON_EXTENSION_SOABI_tmp "") + else() + set(PYTHON_EXTENSION_SOABI_tmp ".abi3") + endif() +endif() + set(PYTHON_EXTENSION_SOABI ${PYTHON_EXTENSION_SOABI_tmp} CACHE STRING "Suffix for python extensions") @@ -143,5 +153,12 @@ macro(ADD_PYTHON_LIBRARY name) target_compile_definitions(${name} PRIVATE Py_GIL_DISABLED=1) ENDIF() ENDIF() - + IF(WITH_PY_LIMITED_API) + target_compile_definitions( + ${name} + PRIVATE + Py_LIMITED_API=${WITH_PY_LIMITED_API} + CYTHON_LIMITED_API=1 + ) + ENDIF() endmacro(ADD_PYTHON_LIBRARY) diff --git a/setup.py b/setup.py index 23d948ef..b0eb0206 100644 --- a/setup.py +++ b/setup.py @@ -10,6 +10,18 @@ "Python %d.%d detected" % sys.version_info[:2]) sys.exit(-1) +def _get_limited_api(): + value = os.environ.get("SYMENGINE_PY_LIMITED_API") + if not value: + return None + else: + version = tuple(map(int, value.split("."))) + if version < (3, 11): + raise ValueError(f"symengine needs at least python 3.11 limited API support. Got {value}") + return version + +limited_api = _get_limited_api() + # use setuptools by default as per the official advice at: # packaging.python.org/en/latest/current.html#packaging-tool-recommendations use_setuptools = True @@ -65,6 +77,7 @@ def get_build_dir(dist): ('build-type=', None, 'build type: Release or Debug'), ('define=', 'D', 'options to cmake :='), + ('py-limited-api=', None, 'Use Py_LIMITED_API with given version.'), ] def _process_define(arg): @@ -122,6 +135,11 @@ def cmake_build(self): cmake_cmd.extend(process_opts(cmake_opts)) if not path.exists(path.join(build_dir, "CMakeCache.txt")): cmake_cmd.extend(self.get_generator()) + + if limited_api: + h = limited_api[0] * 16**6 + limited_api[1] * 16**4 + cmake_cmd.append(f"-DWITH_PY_LIMITED_API={h}") + if subprocess.call(cmake_cmd, cwd=build_dir) != 0: raise OSError("error calling cmake") @@ -202,11 +220,17 @@ def run(self): } try: - from wheel.bdist_wheel import bdist_wheel + try: + from setuptools.command.bdist_wheel import bdist_wheel + except ImportError: + from wheel.bdist_wheel import bdist_wheel + class BdistWheelWithCmake(bdist_wheel): def finalize_options(self): bdist_wheel.finalize_options(self) self.root_is_pure = False + if limited_api: + self.py_limited_api = "cp" + "".join(str(c) for c in limited_api) cmdclass["bdist_wheel"] = BdistWheelWithCmake except ImportError: pass