diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9ccf58816..b8995b955 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -118,12 +118,25 @@ jobs: CXX: ${{ matrix.plat.cxx_compiler }}-${{ matrix.plat.c_version }} run: | cmake -S $GITHUB_WORKSPACE -B build \ - -DBoost_ROOT_DIR=`pwd`/boost_1_${{ matrix.plat.boost_version }}_0 \ + -DBoost_SRC_DIR="$GITHUB_WORKSPACE/boost_1_${{ matrix.plat.boost_version }}_0" \ + -DCMAKE_INSTALL_PREFIX="$GITHUB_WORKSPACE/build/install" \ -DCMAKE_CXX_STANDARD=${{ matrix.plat.cxx_standard || '17' }} \ -DCMAKE_CXX_FLAGS="-fpermissive -DBOOST_NO_AUTO_PTR -DBOOST_FILESYSTEM_NO_CXX20_ATOMIC_REF -DBOOST_TIMER_ENABLE_DEPRECATED" cmake --build build -j 2 cmake --build build -j 2 -t test + # Compile the public exporter as a downstream project, rather than only + # checking that its compatibility header is present in the install. + - name: Smoke test downstream nanobind simulation extension + if: matrix.plat.os == 'ubuntu-24.04' && matrix.plat.c_compiler == 'gcc' && matrix.plat.c_version == 14 && matrix.plat.py_version == '3.14' && matrix.plat.boost_version == 91 && matrix.plat.cxx_standard == null + run: | + python -m pip install "nanobind>=2.10,<3" + cmake --install build + cmake -S tutorials/ngs/5_export_python -B downstream-export-build \ + -DALPS_DIR="$PWD/build/install/share/alps" \ + -DPython_EXECUTABLE="$(command -v python)" + cmake --build downstream-export-build -j 2 + macos-build: name: >- ALPS / ${{ matrix.plat.os }} / ${{ matrix.plat.c_compiler }} / @@ -170,7 +183,7 @@ jobs: cmake -S $GITHUB_WORKSPACE -B build \ -DCMAKE_C_COMPILER=${{ matrix.plat.c_compiler }} \ -DCMAKE_CXX_COMPILER=${{ matrix.plat.cxx_compiler }} \ - -DBoost_ROOT_DIR=`pwd`/boost_1_${{ matrix.plat.boost_version }}_0 \ + -DBoost_SRC_DIR="$GITHUB_WORKSPACE/boost_1_${{ matrix.plat.boost_version }}_0" \ -DCMAKE_CXX_STANDARD=17 \ -DCMAKE_CXX_FLAGS="${{ matrix.plat.cxx_stdlib }} -fpermissive -DBOOST_NO_AUTO_PTR -DBOOST_FILESYSTEM_NO_CXX20_ATOMIC_REF -DBOOST_TIMER_ENABLE_DEPRECATED" \ -DPython_ROOT_DIR=`$(brew --prefix)/bin/python${{ matrix.plat.py_version }} -c "import sys, os; print(os.path.dirname(os.path.dirname(str(sys.executable))));"` \ diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 06c3ca26d..86bf920ef 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/setup-python@v6 with: python-version: '3.13' - - run: python -m pip install packaging pytest + - run: python -m pip install packaging pytest "scikit-build-core>=1.0" - name: Test release validation run: python -m pytest test/packaging -q - name: Check package, SDK, and tag versions @@ -36,42 +36,29 @@ jobs: strategy: matrix: plat: - - { os: ubuntu-latest, target: "", arch: x86_64, homebrew: ''} - - { os: macos-15, target: "15.0" , arch: arm64, homebrew: '/opt/homebrew'} - - { os: macos-26, target: "26.0" , arch: arm64, homebrew: '/opt/homebrew'} + - { os: ubuntu-latest, target: "", arch: x86_64 } + - { os: macos-15, target: "15.0" , arch: arm64 } steps: - uses: actions/checkout@v7 - # Install Fortran compiler based on OS - - name: Install dependencies - timeout-minutes: 20 - run: | - if [ "${{ matrix.plat.os }}" = "ubuntu-latest" ]; then - printf 'Acquire::Retries "3";\nAcquire::http::Timeout "30";\nAcquire::https::Timeout "30";\n' \ - | sudo tee /etc/apt/apt.conf.d/99ci-timeouts > /dev/null - sudo apt-get update - sudo apt-get install -y gfortran - else - brew update - brew install gfortran - fi + - name: Restore compiler cache + uses: actions/cache@v6 + with: + path: _build/ccache + key: pyalps-ccache-${{ runner.os }}-${{ matrix.plat.arch }}-${{ hashFiles('src/**', 'bindings/python/**', 'applications/**', 'tool/maxent*') }} + restore-keys: pyalps-ccache-${{ runner.os }}-${{ matrix.plat.arch }}- - name: Build wheels uses: pypa/cibuildwheel@v4.2.0 + with: + package-dir: bindings/python/pyalps env: - CIBW_BUILD: cp39-* cp310-* cp311-* cp312-* cp313-* cp314-* + # The wheel build is configured in [tool.cibuildwheel] in + # bindings/python/pyalps/pyproject.toml and the wheel-deps preset in + # CMakePresets.json; only matrix-derived values live here. CIBW_ARCHS: ${{ matrix.plat.arch }} - CIBW_ARCHS_MACOS: ${{ matrix.plat.arch }} - - # Set Fortran compiler for all platforms - CIBW_ENVIRONMENT: "FC=gfortran" - - # macOS-specific settings - CIBW_ENVIRONMENT_MACOS: > - Boost_ROOT_DIR=/Users/runner/work/ALPS/ALPS/boost_1_87_0 - MACOSX_DEPLOYMENT_TARGET=${{ matrix.plat.target }} - CXXFLAGS="-stdlib=libc++" + MACOSX_DEPLOYMENT_TARGET: ${{ matrix.plat.target }} - uses: actions/upload-artifact@v7 with: @@ -87,7 +74,20 @@ jobs: - uses: actions/checkout@v7 - name: Build sdist - run: pipx run build --sdist + run: pipx run build --sdist --outdir dist bindings/python/pyalps + + # The sdist must remain buildable outside the repository checkout: + # verify its metadata and that the vendored ALPS sources it needs + # (applications, tools, XML library) actually made it in. + - name: Smoke check sdist + run: | + pipx run twine check dist/*.tar.gz + tar -tzf dist/*.tar.gz > sdist-manifest.txt + for path in _vendor/lib/xml/ALPS.xsl _vendor/tool/maxent.cpp \ + _vendor/applications/dmft/qmc \ + LICENSE.txt src/pyalps/__init__.py; do + grep -q "$path" sdist-manifest.txt || { echo "missing $path in sdist"; exit 1; } + done - uses: actions/upload-artifact@v7 with: @@ -95,8 +95,89 @@ jobs: path: dist/*.tar.gz + # cibuildwheel already runs test/pyalps against every wheel inside the build + # environment; this job checks the artifacts as they would reach PyPI: the + # repaired, uploaded-and-downloaded wheel installed with pip on a clean + # runner, at the oldest and newest supported Python. + smoke_test: + name: Smoke test wheels on ${{ matrix.plat.os }} / py${{ matrix.python }} + needs: [build_wheels] + runs-on: ${{ matrix.plat.os }} + strategy: + fail-fast: false + matrix: + plat: + - { os: ubuntu-latest, artifact_os: ubuntu-latest } + - { os: macos-15, artifact_os: macos-15 } + # The macOS 15 deployment target is forward-compatible; test that + # wheel on the newest runner rather than publishing a second copy. + - { os: macos-26, artifact_os: macos-15 } + python: ["3.10", "3.14"] + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-python@v7 + with: + python-version: ${{ matrix.python }} + + - uses: actions/download-artifact@v8 + with: + pattern: cibw-wheels-${{ matrix.plat.artifact_os }}-* + path: wheelhouse + merge-multiple: true + + - name: Install wheel from artifacts + run: | + pipx run twine check wheelhouse/*.whl + python -m pip install numpy scipy pytest + python -m pip install --no-index --find-links wheelhouse pyalps + + - name: Import and run binding surface tests + run: | + python script/validate_pyalps.py --output _build/validation --wheelhouse wheelhouse + + - name: Retain installed-wheel validation evidence + if: always() + uses: actions/upload-artifact@v7 + with: + name: pyalps-validation-${{ matrix.plat.os }}-py${{ matrix.python }} + path: _build/validation + + # The ordinary wheel tests deliberately keep mpi4py optional. This job + # installs one consistent Open MPI stack and verifies real inter-rank + # collectives, requests and point-to-point traffic through pyalps.mpi. + mpi_smoke: + name: Smoke test MPI adapter with two ranks + needs: [build_wheels] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-python@v7 + with: + python-version: "3.14" + + - uses: actions/download-artifact@v8 + with: + pattern: cibw-wheels-ubuntu-latest-* + path: wheelhouse + merge-multiple: true + + - name: Install wheel and MPI runtime + timeout-minutes: 20 + run: | + printf 'Acquire::Retries "3";\nAcquire::http::Timeout "30";\nAcquire::https::Timeout "30";\n' \ + | sudo tee /etc/apt/apt.conf.d/99ci-timeouts > /dev/null + sudo apt-get update + sudo apt-get install -y libopenmpi-dev openmpi-bin + python -m pip install numpy scipy pytest mpi4py + python -m pip install --no-index --find-links wheelhouse pyalps + + - name: Run two-rank compatibility surface + run: mpiexec -n 2 python -m pytest -q test/pyalps/test_binding_surface.py::test_mpi4py_compatibility_surface test/pyalps/test_mpi_requests.py + upload_pypi: - needs: [build_wheels, build_sdist] + needs: [build_wheels, build_sdist, smoke_test, mpi_smoke] runs-on: ubuntu-latest environment: pypi permissions: diff --git a/CMakeLists.txt b/CMakeLists.txt index 0cb22678b..4a4b295ce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,12 +28,6 @@ if (NOT_BUILD_SHARED_LIBS) else (NOT_BUILD_SHARED_LIBS) option(BUILD_SHARED_LIBS "Build shared libraries" ON) endif (NOT_BUILD_SHARED_LIBS) -if (NOT_ALPS_BUILD_PYTHON) - option(ALPS_BUILD_PYTHON "Build ALPS python extentions" OFF) -else (NOT_ALPS_BUILD_PYTHON) - option(ALPS_BUILD_PYTHON "Build ALPS python extentions" ON) -endif (NOT_ALPS_BUILD_PYTHON) - option(ALPS_BUILD_DEVELOPER_TOOLS "Build tools used by developers to maintain ALPS" OFF) option(ALPS_ENABLE_OPENMP "Enable OpenMP parallelization" OFF) option(ALPS_ENABLE_OPENMP_WORKER "Enable OpenMP worker support" OFF) @@ -46,7 +40,6 @@ option(ALPS_LINK_BOOST_TEST "Link Boost.test pre-built library" OFF) option(ALPS_INSTALL_BOOST_TEST "Install Boost Test framework" OFF) option(ALPS_NGS_USE_NEW_ALEA "Use the ALPS ngs-alea instead of just alea" OFF) option(ALPS_NGS_OPENMPI_ULFM "Userlevel failure mitigation is available" OFF) -option(ALPS_PYTHON_WHEEL "Build python wheel" OFF) option(ALPS_BUILD_LIBS_ONLY "Build only libraries" OFF) @@ -56,7 +49,6 @@ mark_as_advanced(ALPS_LINK_BOOST_TEST) mark_as_advanced(ALPS_INSTALL_BOOST_TEST) mark_as_advanced(ALPS_NGS_USE_NEW_ALEA) mark_as_advanced(ALPS_NGS_OPENMPI_ULFM) -mark_as_advanced(ALPS_PYTHON_WHEEL) mark_as_advanced(ALPS_BUILD_LIBS_ONLY) option(ALPS_USE_MKL_PARALLEL "Use parallel version of MKL" OFF) @@ -65,7 +57,6 @@ mark_as_advanced(ALPS_USE_MKL_PARALLEL) SET (APPLICATIONS_CAN_BE_BUILT ON) set(ALPS_BOOST_LIBRARY_NAME "boost" CACHE STRING "name of the boost library") -set(ALPS_BOOST_PYTHON_LIBRARY_NAME "boost_python" CACHE STRING "name of the boost library") set(ALPS_ENABLE_MPI ON CACHE BOOL "Enable MPI Parallelization") set(ALPS_INSTALL_HEADERS ON CACHE BOOL "Install headers for ALPS and all dependent libraries") set(ALPS_BUILD_EXAMPLES ON CACHE BOOL "Build ALPS examples") @@ -74,11 +65,6 @@ set(ALPS_BUILD_APPLICATIONS ${APPLICATIONS_CAN_BE_BUILT} CACHE BOOL "Build ALPS mark_as_advanced(ALPS_BOOST_LIBRARY_NAME) -if(ALPS_PYTHON_WHEEL) - set(ALPS_BUILD_APPLICATIONS ON) - set(ALPS_ENABLE_MPI OFF) -endif() - if(ALPS_BUILD_LIBS_ONLY) set(ALPS_BUILD_APPLICATIONS OFF) endif() @@ -136,10 +122,6 @@ list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) # set default CMAKE_INSTALL_PREFIX ###################################################################### -if (ALPS_BUILD_PYTHON) - find_package(PythonMod REQUIRED) # COMPONENTS Interpreter Development.Module ) -endif (ALPS_BUILD_PYTHON) - ###################################################################### # Version information ###################################################################### @@ -293,24 +275,8 @@ IF(HDF5_IS_PARALLEL) INCLUDE_DIRECTORIES(${MPI_INCLUDE_PATH}) ENDIF(HDF5_IS_PARALLEL) -# python - - -if (ALPS_BUILD_PYTHON) - set(PYTHON_SCRIPTDIR "${CMAKE_INSTALL_PREFIX}/lib/python") - find_package(PythonMod) -endif (ALPS_BUILD_PYTHON) - -IF (PYTHONLIBS_FOUND AND ALPS_BUILD_PYTHON) - include_directories(${PYTHON_NUMPY_INCLUDE_DIR}) - MESSAGE (STATUS "Numpy include in ${PYTHON_NUMPY_INCLUDE_DIR}") - SET(ALPS_HAVE_PYTHON ON) - INCLUDE_DIRECTORIES(${PYTHON_INCLUDE_DIRS}) - set(BUILD_BOOST_PYTHON TRUE) -ELSE (PYTHONLIBS_FOUND AND ALPS_BUILD_PYTHON) - set(BUILD_BOOST_PYTHON OFF) - SET(ALPS_HAVE_PYTHON OFF) -ENDIF (PYTHONLIBS_FOUND AND ALPS_BUILD_PYTHON) +# Python bindings are built by the standalone scikit-build-core project in +# bindings/python/pyalps. The C++ SDK deliberately has no Python dependency. # Boost Libraries find_package(BoostForALPS REQUIRED) @@ -385,6 +351,7 @@ set(CMAKE_MACOSX_RPATH 1) ###################################################################### set(ALPS_USE_FILE ${CMAKE_INSTALL_PREFIX}/share/alps/UseALPS.cmake) +set(ALPS_PYTHON_USE_FILE ${CMAKE_INSTALL_PREFIX}/share/alps/UsePyALPS.cmake) set(Boost_INCLUDE_DIR_CONFIG ${Boost_INCLUDE_DIR}) @@ -413,12 +380,6 @@ endif(MPI_INCLUDE_DIR) if(HDF5_INCLUDE_DIR) list(APPEND ALPS_EXTRA_INCLUDE_DIRS ${HDF5_INCLUDE_DIR}) endif(HDF5_INCLUDE_DIR) -if(PYTHON_INCLUDE_DIRS) - list(APPEND ALPS_EXTRA_INCLUDE_DIRS ${PYTHON_INCLUDE_DIRS}) -endif(PYTHON_INCLUDE_DIRS) -if(PYTHON_NUMPY_INCLUDE_DIR) - list(APPEND ALPS_EXTRA_INCLUDE_DIRS ${PYTHON_NUMPY_INCLUDE_DIR}) -endif(PYTHON_NUMPY_INCLUDE_DIR) if(Boost_INCLUDE_DIR_CONFIG) list(APPEND ALPS_EXTRA_INCLUDE_DIRS ${Boost_INCLUDE_DIR_CONFIG}) endif(Boost_INCLUDE_DIR_CONFIG) @@ -450,10 +411,6 @@ endif(LAPACK_LIBRARIES) if(HDF5_LIBRARIES) list(APPEND ALPS_EXTRA_LIBRARIES ${HDF5_LIBRARIES}) endif(HDF5_LIBRARIES) -if(PYTHON_LIBRARY) - list(APPEND ALPS_EXTRA_LIBRARIES ${PYTHON_LIBRARY}) -endif(PYTHON_LIBRARY) - configure_file(cmake/ALPSConfig.cmake.in ${PROJECT_BINARY_DIR}/cmake/ALPSConfig.cmake @ONLY) configure_file(cmake/include.mk.in ${PROJECT_BINARY_DIR}/cmake/include.mk) @@ -469,22 +426,17 @@ write_basic_package_version_file( # installation ###################################################################### include(InstallRequiredSystemLibraries) -if(ALPS_PYTHON_WHEEL) - install(DIRECTORY lib/pyalps DESTINATION . COMPONENT python - FILES_MATCHING PATTERN "*.py" - ) - install(DIRECTORY lib/xml DESTINATION pyalps COMPONENT xml - FILES_MATCHING PATTERN "*.xsl" - ) - install(DIRECTORY ${PROJECT_BINARY_DIR}/lib/xml DESTINATION pyalps COMPONENT xml) -elseif(ALPS_BUILD_LIBS_ONLY) -else() - if (ALPS_INSTALL_HEADERS) set(ALPS_HEADER_DIR "include") install(DIRECTORY src/alps src/boost src/ietl src/mocasito COMPONENT headers DESTINATION ${ALPS_HEADER_DIR} FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp" PATTERN "*.ipp" ) + # ALPS' public headers include the vendored Boost.Numeric bindings. Install + # them as part of the C++ SDK so downstream consumers do not need the ALPS + # source tree on their include path. + install(DIRECTORY bindings/boost COMPONENT headers DESTINATION ${ALPS_HEADER_DIR} + FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp" PATTERN "*.ipp" + ) install(FILES ${PROJECT_BINARY_DIR}/src/alps/config.h ${PROJECT_BINARY_DIR}/src/alps/version.h DESTINATION ${ALPS_HEADER_DIR}/alps COMPONENT headers) endif(ALPS_INSTALL_HEADERS) @@ -492,17 +444,12 @@ install(DIRECTORY lib/xml DESTINATION ${ALPS_XML_PATH} COMPONENT xml FILES_MATCHING PATTERN "*.xsl" ) -if(ALPS_BUILD_PYTHON AND ALPS_PYTHON_LIB_DEST_ROOT) - install(DIRECTORY lib/pyalps DESTINATION ${ALPS_PYTHON_LIB_DEST_ROOT} COMPONENT python - FILES_MATCHING PATTERN "*.py" PATTERN "*.pyc" - ) -endif() - install(DIRECTORY ${PROJECT_BINARY_DIR}/lib/xml DESTINATION ${ALPS_XML_PATH} COMPONENT xml) add_subdirectory(cmake) install(FILES cmake/UseALPS.cmake + cmake/UsePyALPS.cmake ${PROJECT_BINARY_DIR}/cmake/ALPSConfig.cmake ${PROJECT_BINARY_DIR}/cmake/ALPSConfigVersion.cmake ${PROJECT_BINARY_DIR}/cmake/include.mk @@ -513,43 +460,24 @@ install(FILES cmake/UseALPS.cmake install(FILES CITATION.md LICENSE.txt README.md DESTINATION share/alps COMPONENT libraries) -if(ALPS_BUILD_PYTHON AND ALPS_PYTHON_LIB_DEST_ROOT) - string(CONFIGURE [[ - set(PROJECT_SOURCE_DIR "@PROJECT_SOURCE_DIR@") - set(ALPS_PYTHON_LIB_DEST_ROOT "@ALPS_PYTHON_LIB_DEST_ROOT@") - message(STATUS "PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}") - message(STATUS "CMAKE_BINARY_DIR: ${CMAKE_BINARY_DIR}") - message(STATUS "CMAKE_SOURCE_DIR: ${CMAKE_SOURCE_DIR}") - message(STATUS "CMAKE_INSTALL_PREFIX: ${CMAKE_INSTALL_PREFIX}") - message(STATUS "ALPS_PYTHON_LIB_DEST_ROOT: ${ALPS_PYTHON_LIB_DEST_ROOT}") - - configure_file(${PROJECT_SOURCE_DIR}/lib/pyalps/pyalps_config.py.in ${CMAKE_INSTALL_PREFIX}/${ALPS_PYTHON_LIB_DEST_ROOT}/pyalps/pyalps_config.py) - ]] install_script @ONLY) - install(CODE ${install_script}) -endif() - -set(PYTHONEXEC python) - -endif() ###################################################################### # libraries ###################################################################### add_subdirectory(src/boost) add_subdirectory(src/alps) -add_subdirectory(lib/pyalps) ###################################################################### # programs ###################################################################### -if (NOT ALPS_PYTHON_WHEEL AND NOT ALPS_BUILD_LIBS_ONLY) +if (NOT ALPS_BUILD_LIBS_ONLY) add_subdirectory(tool) if (ALPS_BUILD_EXAMPLES) add_subdirectory(example) endif (ALPS_BUILD_EXAMPLES) -endif (NOT ALPS_PYTHON_WHEEL AND NOT ALPS_BUILD_LIBS_ONLY) +endif (NOT ALPS_BUILD_LIBS_ONLY) ###################################################################### @@ -560,9 +488,9 @@ if (ALPS_BUILD_APPLICATIONS AND NOT ALPS_BUILD_LIBS_ONLY) add_subdirectory(applications) endif (ALPS_BUILD_APPLICATIONS AND NOT ALPS_BUILD_LIBS_ONLY) -if (ALPS_INCLUDE_TUTORIALS AND NOT ALPS_PYTHON_WHEEL AND NOT ALPS_BUILD_LIBS_ONLY) +if (ALPS_INCLUDE_TUTORIALS AND NOT ALPS_BUILD_LIBS_ONLY) add_subdirectory(tutorials) -endif (ALPS_INCLUDE_TUTORIALS AND NOT ALPS_PYTHON_WHEEL AND NOT ALPS_BUILD_LIBS_ONLY) +endif (ALPS_INCLUDE_TUTORIALS AND NOT ALPS_BUILD_LIBS_ONLY) ###################################################################### # developer tools diff --git a/CMakePresets.json b/CMakePresets.json index 512f514ea..98b158bf6 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -13,12 +13,36 @@ "cacheVariables": { "CMAKE_BUILD_TYPE": "Release" } + }, + { + "name": "wheel-deps", + "displayName": "ALPS C++ SDK for pyalps wheels", + "description": "SDK install that the standalone pyalps wheel build links against (ALPS_DIR=_build/wheel-deps/install/share/alps). Applications are built so the wheel can bundle them into pyalps/bin, as the legacy ALPS_PYTHON_WHEEL build did; MPI is off, matching that build.", + "generator": "Ninja", + "binaryDir": "${sourceDir}/_build/wheel-deps", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_INSTALL_PREFIX": "${sourceDir}/_build/wheel-deps/install", + "ALPS_BUILD_LIBS_ONLY": "OFF", + "ALPS_BUILD_TESTS": "OFF", + "ALPS_BUILD_EXAMPLES": "OFF", + "ALPS_BUILD_APPLICATIONS": "ON", + "ALPS_ENABLE_MPI": "OFF", + "ALPS_INCLUDE_TUTORIALS": "OFF" + } } ], "buildPresets": [ { "name": "default", "configurePreset": "default" + }, + { + "name": "wheel-deps", + "configurePreset": "wheel-deps", + "targets": [ + "install" + ] } ], "testPresets": [ diff --git a/README-py.md b/README-py.md deleted file mode 100644 index 66a571a86..000000000 --- a/README-py.md +++ /dev/null @@ -1,50 +0,0 @@ -[![ALPS CI/CD](https://github.com/ALPSim/legacy/actions/workflows/build.yml/badge.svg)](https://github.com/ALPSim/legacy/actions/workflows/build.yml) - -## Python Algorithms and Libraries for Physics Simulations - -This is python packages for `Algorithms and Libraries for Physics Simulations` project. For more information check [README.txt](https://pypi.org/project/pyalps/2.3.3/README.txt). - -### Installation instruction from binaries - -1. pyALPS can be installed on most Linux and MacOS mcachines from prebuilt biniaries available on [PyPi](https://pypi.org/project/pyalps). -pyALPS can be installed using `pip` Python package manager: - -``` -pip install pyalps -``` - -### Installation instruction from sources - -1. Prerequisites - - CMake >= 3.22 - - Boost sources >= 1.76 - - BLAS/LAPACK - - HDF5 - - MPI - - Python >= 3.9 - - Python 3.13 requires Boost version 1.87 or later - - Earlier versions maybe also work but unsupported - - C++ compiler with C++17 support (CI covers GCC 11 through 15, Clang 14 through 22, and AppleClang) - - GNU Make or Ninja build system - -You need to download and unpack boost library: -``` -wget https://archives.boost.io/release/1.86.0/source/boost_1_86_0.tar.gz -tar -xzf boost_1_86_0.tar.gz -``` -Here we download `boost v1.86.0`, we have tested ALPS with versions `1.76.0` and `1.86.0`. - -2. Downloading and building sources -``` -git clone https://github.com/alpsim/ALPS ALPS -cd ALPS -Boost_SRC_DIR=`pwd`/../boost_1_86_0 python3 -m build --wheel -``` -This will download the most recent version of ALPS from the github repository, and build pyALPS python package. - -3. Installation - -Based on the version of the Python used to build pyALPS, the corresponding Python wheel will be created and stored in `dist` subdirectory. It can be installed using `pip`: -``` -pip install dist/pyalps-.whl -``` diff --git a/applications/diag/fulldiag/CMakeLists.txt b/applications/diag/fulldiag/CMakeLists.txt index 4d0a4eecb..b5f5bd00e 100644 --- a/applications/diag/fulldiag/CMakeLists.txt +++ b/applications/diag/fulldiag/CMakeLists.txt @@ -39,18 +39,7 @@ if(LAPACK_FOUND) set_tests_properties(fulldiag_evaluate_no_arguments PROPERTIES WILL_FAIL TRUE) endif() - if(ALPS_PYTHON_WHEEL) - if(APPLE) - set_target_properties(fulldiag PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - set_target_properties(fulldiag_evaluate PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - else(APPLE) - set_target_properties(fulldiag PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - set_target_properties(fulldiag_evaluate PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - endif(APPLE) - install(TARGETS fulldiag fulldiag_evaluate RUNTIME DESTINATION pyalps/bin COMPONENT applications) - else() install(TARGETS fulldiag fulldiag_evaluate RUNTIME DESTINATION bin COMPONENT applications) - endif() else(LAPACK_FOUND) message(STATUS "fulldiag will not be built since lapack library has not been found") endif(LAPACK_FOUND) diff --git a/applications/diag/sparsediag/CMakeLists.txt b/applications/diag/sparsediag/CMakeLists.txt index 71f251adc..d3fd8f28a 100644 --- a/applications/diag/sparsediag/CMakeLists.txt +++ b/applications/diag/sparsediag/CMakeLists.txt @@ -22,16 +22,7 @@ if(LAPACK_FOUND) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${LAPACK_LINKER_FLAGS}") add_executable(sparsediag sparsediag.C factory.C) target_link_libraries(sparsediag alps ${LAPACK_LIBRARY} ${BLAS_LIBRARY}) - if(ALPS_PYTHON_WHEEL) - if(APPLE) - set_target_properties(sparsediag PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - else(APPLE) - set_target_properties(sparsediag PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - endif(APPLE) - install(TARGETS sparsediag RUNTIME DESTINATION pyalps/bin COMPONENT applications) - else() install(TARGETS sparsediag RUNTIME DESTINATION bin COMPONENT applications) - endif() else(LAPACK_FOUND) message(STATUS "sparsediag will not be built since lapack library is not found") endif(LAPACK_FOUND) diff --git a/applications/dmft/qmc/CMakeLists.txt b/applications/dmft/qmc/CMakeLists.txt index aa95cfe07..394bec0a4 100644 --- a/applications/dmft/qmc/CMakeLists.txt +++ b/applications/dmft/qmc/CMakeLists.txt @@ -115,29 +115,11 @@ if(LAPACK_FOUND) add_executable(dmft_interaction_expansion_choice dmft_interaction_expansion_choice.C) set_property(TARGET dmft_interaction_expansion_choice PROPERTY LABELS dmft) add_alps_test(dmft_interaction_expansion_choice) -if(ALPS_PYTHON_WHEEL) - if(APPLE) - set_target_properties(dmft PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - set_target_properties(hirschfye PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - set_target_properties(hybridization PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - set_target_properties(interaction PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - else(APPLE) - set_target_properties(dmft PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - set_target_properties(hirschfye PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - set_target_properties(hybridization PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - set_target_properties(interaction PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - endif(APPLE) - install(TARGETS dmft RUNTIME DESTINATION pyalps/bin COMPONENT applications) - install(TARGETS hirschfye RUNTIME DESTINATION pyalps/bin COMPONENT applications) - install(TARGETS hybridization RUNTIME DESTINATION pyalps/bin COMPONENT applications) - install(TARGETS interaction RUNTIME DESTINATION pyalps/bin COMPONENT applications) -else() install(TARGETS dmft RUNTIME DESTINATION bin COMPONENT applications) install(TARGETS hirschfye RUNTIME DESTINATION bin COMPONENT applications) install(TARGETS hybridization RUNTIME DESTINATION bin COMPONENT applications) install(TARGETS interaction RUNTIME DESTINATION bin COMPONENT applications) install(FILES hybridization/Documentation/hybdoc.pdf DESTINATION doc) -endif() else(LAPACK_FOUND) message(STATUS "dmft will not be built since the lapack library has not been found") endif(LAPACK_FOUND) diff --git a/applications/dmft/qmc/hybridization/Documentation/hybdoc.tex b/applications/dmft/qmc/hybridization/Documentation/hybdoc.tex index 3ac25a4af..701bc2012 100644 --- a/applications/dmft/qmc/hybridization/Documentation/hybdoc.tex +++ b/applications/dmft/qmc/hybridization/Documentation/hybdoc.tex @@ -141,7 +141,7 @@ \subsubsection{Running the solver} \subsection{Python interface} \label{pythoninterface} -If ALPS is built with Python support (parameter \verb#ALPS_BUILD_PYTHON=ON#), the solver is also built as a Python module. It can directly be called from within a Python script. This provides a flexible framework which allows one to easily set up tasks ranging from calculations for multiple parameters to complex selfconsistency schemes. +The standalone pyalps wheel includes the solver module when built with application bindings enabled (the default). It can directly be called from within a Python script. This provides a flexible framework which allows one to easily set up tasks ranging from calculations for multiple parameters to complex selfconsistency schemes. Basic usage of the Python interface is illustrated by the following script, which repeats the previous example for the standalone executable: \begin{verbatim} diff --git a/applications/dmft/qmc/hybridization/hybmain.cpp b/applications/dmft/qmc/hybridization/hybmain.cpp index d02b7d5f2..45b307faa 100644 --- a/applications/dmft/qmc/hybridization/hybmain.cpp +++ b/applications/dmft/qmc/hybridization/hybmain.cpp @@ -33,11 +33,13 @@ void master_final_tasks(const alps::results_type::type &results, int global_mpi_rank; #ifdef BUILD_PYTHON_MODULE -//compile it as a python module (requires boost::python library) -using namespace boost::python; +#include "dict_to_params.hpp" +#include "scoped_signal_handlers.hpp" +namespace nb = nanobind; -void solve(boost::python::dict parms_){ - alps::parameters_type::type parms(parms_); +void solve(nb::dict const & parms_){ + pyalps::scoped_signal_handlers signal_handlers; + alps::parameters_type::type parms = pyalps::params_from_dict(parms_); std::string output_file = boost::lexical_cast(parms["BASENAME"]|"results")+std::string(".out.h5"); #else int main(int argc, char** argv){ @@ -134,12 +136,9 @@ void master_final_tasks(const alps::results_type::type &results, } #ifdef BUILD_PYTHON_MODULE -BOOST_PYTHON_MODULE(cthyb) -{ - def("solve",solve);//define python-callable run method -}; +NB_MODULE(cthyb, m) { + m.def("solve", solve); +} #endif - - diff --git a/applications/dmft/qmc/interaction_expansion2/main.cpp b/applications/dmft/qmc/interaction_expansion2/main.cpp index da462c34b..cf1d3f9b0 100644 --- a/applications/dmft/qmc/interaction_expansion2/main.cpp +++ b/applications/dmft/qmc/interaction_expansion2/main.cpp @@ -30,11 +30,13 @@ void compute_greens_functions(const alps::results_type::type parms(parms_); +void solve(nb::dict const & parms_){ + pyalps::scoped_signal_handlers signal_handlers; + alps::parameters_type::type parms = pyalps::params_from_dict(parms_); std::string output_file = boost::lexical_cast(parms["BASENAME"]|"results")+std::string(".out.h5"); #else int main(int argc, char** argv) @@ -110,9 +112,8 @@ int main(int argc, char** argv) } #ifdef BUILD_PYTHON_MODULE - BOOST_PYTHON_MODULE(ctint) - { - def("solve",solve);//define python-callable run method - }; + NB_MODULE(ctint, m) { + m.def("solve", solve); + } #endif diff --git a/applications/dmft/qmc/interaction_expansion2/observables.cpp b/applications/dmft/qmc/interaction_expansion2/observables.cpp index 774c239f1..ebe47dcdb 100644 --- a/applications/dmft/qmc/interaction_expansion2/observables.cpp +++ b/applications/dmft/qmc/interaction_expansion2/observables.cpp @@ -101,13 +101,13 @@ void InteractionExpansion::initialize_observables(void) sz_name<<"Sz_"</bin to PATH, so +# without these files runApplication('spinmc', ...) fails on a wheel install +# with "There is no spinmc on the path!" -- the fallback it tries instead, +# pyalps_config.ALPS_BIN_INSTALL_DIR, is the build machine's SDK path. +# +# The programs are copied from the SDK rather than rebuilt here. The root +# CMakeLists gives every installed target an RPATH of @loader_path/../lib +# (APPLE) or $ORIGIN/../lib, so they resolve libalps out of pyalps/lib below +# without any post-install patching -- the same directory the extension +# modules point at, so one copy of each library serves both. +if(PYALPS_BUNDLE_APPLICATIONS) + set(_alps_bin_dir "${ALPS_ROOT_DIR}/bin") + set(_alps_application_names + checksign + dirloop_sse + dmft + dmrg + fulldiag + fulldiag_evaluate + hirschfye + hybridization + interaction + loop + qwl + qwl_evaluate + simplemc + sparsediag + spinmc + spinmc_evaluate + worm + worm_evaluate) + set(_alps_programs "") + set(_alps_missing_programs "") + foreach(_program IN LISTS _alps_application_names) + if(EXISTS "${_alps_bin_dir}/${_program}") + list(APPEND _alps_programs "${_alps_bin_dir}/${_program}") + else() + list(APPEND _alps_missing_programs "${_program}") + endif() + endforeach() + if(_alps_missing_programs) + message(FATAL_ERROR + "PYALPS_BUNDLE_APPLICATIONS is ON but the wheel SDK is missing these " + "legacy wheel applications: ${_alps_missing_programs}. Build the ALPS " + "SDK with the wheel-deps preset, or configure with " + "-DPYALPS_BUNDLE_APPLICATIONS=OFF for a bindings-only wheel -- in which " + "case pyalps.runApplication() will not work.") + endif() + list(LENGTH _alps_programs _alps_program_count) + message(STATUS "pyalps: bundling ${_alps_program_count} ALPS programs into pyalps/bin") + install(PROGRAMS ${_alps_programs} DESTINATION pyalps/bin) + + # The bundled programs resolve libalps and libboost through their ../lib + # RPATH, so the SDK's shared libraries have to travel with them -- each one + # exactly once. install(DIRECTORY) is the wrong tool here: it would copy the + # whole SONAME chain (libalps.so, libalps.so.2, libalps.so.2.3.4), and since + # a wheel cannot carry symlinks those arrive as three byte-identical 10 MB + # files. Install only the SONAME-named file, which is the name every + # DT_NEEDED / LC_LOAD_DYLIB entry actually asks for. + # + # src/alps/CMakeLists.txt gives libalps SOVERSION ${ALPS_VERSION_MAJOR} on + # ELF platforms and no version at all on APPLE, so "." is the + # SONAME spelling wherever a versioned form exists. test/pyalps guards both + # halves of this: that no library is bundled twice, and that every bundled + # program can still be loaded. + file(GLOB _alps_runtime_libs LIST_DIRECTORIES FALSE + "${ALPS_ROOT_DIR}/lib/*.so" "${ALPS_ROOT_DIR}/lib/*.dylib") + if(NOT _alps_runtime_libs) + message(FATAL_ERROR + "PYALPS_BUNDLE_APPLICATIONS is ON but no shared libraries were found in " + "${ALPS_ROOT_DIR}/lib. The bundled programs would ship without the " + "libalps they link against.") + endif() + foreach(_lib IN LISTS _alps_runtime_libs) + get_filename_component(_lib_name "${_lib}" NAME) + set(_lib_soname "${_lib_name}") + if(EXISTS "${_lib}.${ALPS_VERSION_MAJOR}") + set(_lib_soname "${_lib_name}.${ALPS_VERSION_MAJOR}") + endif() + get_filename_component(_lib_real "${_lib}" REALPATH) + install(PROGRAMS "${_lib_real}" DESTINATION pyalps/lib + RENAME "${_lib_soname}") + endforeach() +endif() + +# The ALPS XML/XSL library ships inside the package (pyalps/xml), as the +# legacy wheel build did: the stylesheets plus the lattice and model +# libraries that parameter files reference by default. +set(_alps_xml_source "${_alps_source_root}/lib/xml") +if(NOT EXISTS "${_alps_xml_source}/ALPS.xsl") + message(FATAL_ERROR + "ALPS XML stylesheets not found at ${_alps_xml_source}. " + "They are required so the pyalps package can bundle pyalps/xml.") +endif() +install(DIRECTORY "${_alps_xml_source}/" DESTINATION pyalps/xml + FILES_MATCHING PATTERN "*.xsl") +configure_file("${_alps_xml_source}/lattices.xml.in" + "${CMAKE_CURRENT_BINARY_DIR}/xml/lattices.xml" COPYONLY) +configure_file("${_alps_xml_source}/models.xml.in" + "${CMAKE_CURRENT_BINARY_DIR}/xml/models.xml" COPYONLY) +install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/xml/lattices.xml" + "${CMAKE_CURRENT_BINARY_DIR}/xml/models.xml" + DESTINATION pyalps/xml) diff --git a/bindings/python/pyalps/README.md b/bindings/python/pyalps/README.md new file mode 100644 index 000000000..a7853e1ac --- /dev/null +++ b/bindings/python/pyalps/README.md @@ -0,0 +1,163 @@ +# pyalps + +Python applications and libraries for the Algorithms and Libraries for +Physics Simulations (ALPS) project. Binary wheels are available from PyPI: + +```sh +python -m pip install pyalps +``` + +Install `pyalps[plot]` to use the Matplotlib plotting helpers. +Install `pyalps[mpi]` for the mpi4py-backed `pyalps.mpi` compatibility layer. + +The bindings are built as a standalone `scikit-build-core` project using +nanobind. A source build requires Python 3.10 or newer, CMake 3.22 or newer, +Ninja, a C++17 compiler, BLAS/LAPACK, HDF5, and an installed ALPS C++ SDK. +Point `ALPS_DIR` at the SDK's `share/alps` package directory. + +The `wheel-deps` CMake preset builds the SDK exactly as the wheel CI does. +From the repository root: + +```sh +cmake --preset wheel-deps +cmake --build --preset wheel-deps + +ALPS_DIR="$PWD/_build/wheel-deps/install/share/alps" \ + python -m build --wheel bindings/python/pyalps +``` + +The wheel is written to `bindings/python/pyalps/dist` and can be installed +with `python -m pip install`. With ccache installed, configure with +`cmake --preset wheel-deps -DCMAKE_CXX_COMPILER_LAUNCHER=ccache` and set +`CMAKE_ARGS="-DCMAKE_CXX_COMPILER_LAUNCHER=ccache"` for the wheel build to +speed up rebuilds. + +`PYALPS_BUILD_APPLICATIONS=ON` is the default and preserves the MaxEnt, +CT-HYB, and CT-INT extension modules. Set it to `OFF` through CMake +configuration for a smaller core-only developer build. + +`PYALPS_BUNDLE_APPLICATIONS=ON` is the default and copies the ALPS +application executables (`spinmc`, `dmrg`, `sparsediag`, `loop`, `qwl`, ...) +from the SDK into `pyalps/bin`, together with the SDK's shared libraries in +`pyalps/lib` that their `../lib` RPATH resolves against. `pyalps.tools` +prepends `pyalps/bin` to `PATH`, so this is what makes +`pyalps.runApplication('spinmc', ...)` work from a wheel install — the +`wheel-deps` preset therefore builds the applications. Configure with +`-DPYALPS_BUNDLE_APPLICATIONS=OFF` for a bindings-only wheel; the +`runApplication` helpers then require the executables on `PATH` by other +means. + +## Free-threading and stable-ABI policy + +pyalps ships per-version wheels (CPython 3.10–3.14) and deliberately opts +into neither of nanobind's special ABI modes: + +- **Free-threading (3.13t/3.14t):** the extension modules do not declare + free-threading support, so importing pyalps on a free-threaded + interpreter re-enables the GIL for the process. That is intentional: + the ALPS C++ library relies on the GIL as its lock around shared state + (`mcobservable`'s reference-count table, the `alps::ngs::signal` + singleton, `mcdata`'s lazily-computed statistics). Do not add + `FREE_THREADED` to `nanobind_add_module` without first making that + state thread-safe. +- **Stable ABI (abi3):** not enabled. Nanobind isolates stable-ABI and + ordinary extensions from each other. ALPS supports downstream nanobind + modules that derive from pyalps types, so an abi3 pyalps wheel would force + every such consumer to use the limited API too. Per-version wheels preserve + ordinary downstream extension interoperability. + +## Compatibility and checkpoints + +Parameters created from Python retain their Python values. NumPy arrays keep +array arithmetic, and changes through a list, array, or shared reference are +visible to subsequent Python and C++ reads. A C++ consumer converts the current +value to its requested scalar or one-dimensional vector type; incompatible +metadata and out-of-range conversions raise an exception. Python metadata may +use other shapes and containers supported by the HDF5 writer. Objects such as +`None` can be held in memory but have no ALPS HDF5 representation. + +Native C++ numeric and Boolean vectors become NumPy arrays when accessed from +Python. Native string vectors become lists so names can be replaced with longer +strings or appended without NumPy's fixed-width string truncation. These +materialized objects retain mutations for subsequent Python and C++ reads. +Explicitly supplied Python lists and NumPy arrays keep their original types. + +Integer conversion from text is range checked in the C++ SDK, including when +parameters originate outside Python. Negative text converted to an unsigned +integer now raises an exception instead of wrapping; replace negative textual +sentinels with an explicit value in the target type's range. + +The C++ SDK remains independent of Python and nanobind. Python-owned values and +their checkpoint decoder are supplied by the bindings. Rebuild downstream C++ +extensions against the SDK from the same source revision as the wheel; the +parameter layout changed during this migration. + +Downstream nanobind modules must also use the same nanobind internals ABI as +the installed wheel; otherwise nanobind cannot see pyalps types and aborts the +interpreter at import (`base type "alps::mcbase" not known to nanobind`). The +wheel is built with the nanobind version pinned in `pyproject.toml` and records +its ABI in `pyalps.pyalps_config`; `alps_target_link_pyalps` rejects a +mismatched nanobind at CMake configure time. Install the matching release, for +example `python -m pip install "nanobind==$(python -c 'import +pyalps.pyalps_config as c; print(c.NANOBIND_VERSION)')"`. The compiler's C++ +standard library must also match (libc++ on macOS, libstdc++ on Linux). + +New HDF5 writes distinguish Boolean and signed-byte values with an +`__alps_type__` attribute while retaining the existing numeric storage format. +Unmarked signed-byte data from old ALPS files retains the legacy Boolean +interpretation. The old format cannot distinguish an unmarked `int8` array +from a Boolean mask; use a typed reader such as h5py when an old dataset is +known to contain signed bytes. + +Rectangular mixtures of numeric rows are stored as a single array when every +integer remains exact in the common dtype. If mixing integer widths or mixing +integers with floating-point or complex rows would round a value, the archive +stores the rows separately and reads them back as a list. For example, a +`uint64` row containing `2**63 + 1` alongside an `int64` row retains its exact +integer values instead of silently converting them to `float64`. + +`pyalps.mpi` receives Python objects using matched probes, so asynchronous +receives and the wait/test helpers can handle messages larger than mpi4py's +default object receive buffer. This adapter exchanges mpi4py messages; +Boost.MPI's C++ serialization protocol and skeleton/content API are not wire +compatible. Communicating processes must use the same protocol. + +Communicator wrappers compare equal when their underlying mpi4py communicators +compare equal. They are intentionally unhashable, matching mpi4py. Unlike the +old Boost.MPI wrappers, they cannot be used as dictionary keys or set members; +applications needing such associations should use explicit application keys. + +## Versioning + +pyalps does not carry a version of its own. The numeric version is read from +`ALPS_VERSION.txt` at the repository root — the same file +`cmake/ALPSVersion.cmake` reads for `ALPS_VERSION_CORE` — so a release bump is +one edit rather than two that can drift. `test/pyalps/test_wheel_payload.py` +fails if the installed version and that file disagree. + +A prerelease label cannot live in that file: `project(VERSION ...)` rejects a +non-numeric version, and neither `find_package()` matching nor the library +SOVERSION has a notion of prerelease ordering. CMake takes it from the +`ALPS_VERSION_PRERELEASE` cache variable; the Python build takes it from the +environment variable of the same name, using the same vocabulary: + +| `ALPS_VERSION_PRERELEASE` | version with `ALPS_VERSION.txt` = 2.3.4 | +|---|---| +| unset | `2.3.4` | +| `beta.1` | `2.3.4b1` | +| `alpha.2` | `2.3.4a2` | +| `rc.1` | `2.3.4rc1` | +| `dev.3` | `2.3.4.dev3` | + +In GitHub release builds, the provider takes the prerelease label from +`GITHUB_REF` (for example, `refs/tags/v3.0.0-beta.1`). It rejects a tag whose +numeric version differs from `ALPS_VERSION.txt`, or whose label conflicts with +an explicit `ALPS_VERSION_PRERELEASE`. Wheels and source distributions use the +same provider. `python bindings/python/pyalps/_build_support/alps_version.py` +prints the version a build would produce, from any working directory. +An sdist preserves its recorded version when rebuilt without the original +build environment. + +Note the consequence: because the number is inherited, a Python-only API change +cannot be signalled in the pyalps version alone — it takes a bump of +`ALPS_VERSION.txt`, which moves the whole project. diff --git a/bindings/python/pyalps/_build_support/alps_version.py b/bindings/python/pyalps/_build_support/alps_version.py new file mode 100644 index 000000000..a34ed24a1 --- /dev/null +++ b/bindings/python/pyalps/_build_support/alps_version.py @@ -0,0 +1,158 @@ +# Copyright (C) 2026 by the ALPS collaboration +# SPDX-License-Identifier: MIT + +"""Derive the pyalps version from the repository, not from pyproject.toml. + +ALPS_VERSION.txt at the repository root is the single source of truth for the +release version; cmake/ALPSVersion.cmake reads the same file to set +ALPS_VERSION_CORE before ``project()``. This provider reads it for the Python +package metadata, so a release bump is one edit rather than two that can drift. + +A prerelease label is deliberately *not* in that file: ``project(VERSION ...)`` +rejects a non-numeric version, and neither ``find_package()`` matching nor the +library SOVERSION has a notion of prerelease ordering. CMake therefore takes it +from the ALPS_VERSION_PRERELEASE cache variable, set by the release process. The +environment variable of the same name is the equivalent here, using the same +vocabulary, translated to the PEP 440 spelling Python requires: + + (unset) -> 2.3.4 a final release + beta.1 -> 2.3.4b1 + alpha.2 -> 2.3.4a2 + rc.1 -> 2.3.4rc1 + dev.3 -> 2.3.4.dev3 + +Wired up in pyproject.toml as:: + + [project] + dynamic = ["version"] + + [[tool.dynamic-metadata]] + provider = { path = "_build_support", module = "alps_version" } +""" + +from __future__ import annotations + +import os +import re +from email.parser import BytesParser +from pathlib import Path + +# A checkout carries the version file three levels above this project; an +# sdist carries it at the project root through sdist.force-include. +_CORE_PATTERN = r"(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)" +_CORE = re.compile(_CORE_PATTERN) +_TAG = re.compile(rf"v(?P{_CORE_PATTERN})(?:-(?P