diff --git a/.github/actions/setup-macos-toolchain/action.yml b/.github/actions/setup-macos-toolchain/action.yml index 51da2f8f3..04b61ef78 100644 --- a/.github/actions/setup-macos-toolchain/action.yml +++ b/.github/actions/setup-macos-toolchain/action.yml @@ -1,5 +1,5 @@ name: 'Setup macOS Toolchain' -description: 'Configure Xcode, Homebrew packages, MPI, LLVM, OpenMP, OpenSSL, and ninja' +description: 'Configure Xcode, Homebrew packages, LLVM, OpenMP, OpenSSL, and ninja' runs: using: 'composite' steps: @@ -11,5 +11,5 @@ runs: shell: bash run: | brew update - brew install ninja mpich llvm libomp openssl + brew install ninja llvm libomp openssl brew link libomp --overwrite --force diff --git a/.github/actions/setup-mpi-extensions/action.yml b/.github/actions/setup-mpi-extensions/action.yml new file mode 100644 index 000000000..d46d2e182 --- /dev/null +++ b/.github/actions/setup-mpi-extensions/action.yml @@ -0,0 +1,27 @@ +name: Setup MPI Extensions Open MPI +description: Install Open MPI from the mpi-extensions main prerelease +inputs: + prefix: + description: Installation prefix. Defaults to $RUNNER_TEMP/mpi-extensions-openmpi. + default: '' + platform: + description: mpi-extensions platform name + default: auto + purge-system-mpi: + description: Remove package-manager MPI installations before installing mpi-extensions + default: 'false' +runs: + using: composite + steps: + - name: Setup MPI extensions Open MPI + shell: bash + env: + GITHUB_TOKEN: ${{ github.token }} + run: >- + python3 "${GITHUB_WORKSPACE:-$PWD}/scripts/setup_mpi_extensions.py" + --repo-root "${GITHUB_WORKSPACE:-$PWD}" + --prefix "${{ inputs.prefix }}" + --platform "${{ inputs.platform }}" + --purge-system-mpi "${{ inputs.purge-system-mpi }}" + --github-env "$GITHUB_ENV" + --runner-temp "${RUNNER_TEMP:-${TMPDIR:-/tmp}}" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6f15a033b..55dd470fc 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -30,7 +30,7 @@ jobs: - name: Setup environment run: | sudo apt-get update - sudo apt-get install -y gcc-15 g++-15 ninja-build mpich libomp-dev valgrind + sudo apt-get install -y gcc-15 g++-15 ninja-build libomp-dev valgrind python3 -m pip install -r requirements.txt - name: ccache uses: hendrikmuhs/ccache-action@v1.2 @@ -38,6 +38,12 @@ jobs: key: ${{ runner.os }}-gcc create-symlink: true max-size: 1G + - name: Setup MPI extensions Open MPI + if: matrix.language == 'cpp' + uses: ./.github/actions/setup-mpi-extensions + with: + platform: linux-x86_64 + purge-system-mpi: 'true' - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: @@ -47,6 +53,7 @@ jobs: run: > cmake -S . -B build -G Ninja -D CMAKE_BUILD_TYPE=RELEASE + -D PPC_MPI_EXTENSIONS_HOME="$PPC_MPI_EXTENSIONS_HOME" env: CC: gcc-15 CXX: g++-15 diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index 8b330f92b..9b5873b1c 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -24,6 +24,11 @@ jobs: submodules: recursive - name: Setup macOS toolchain uses: ./.github/actions/setup-macos-toolchain + - name: Setup MPI extensions Open MPI + uses: ./.github/actions/setup-mpi-extensions + with: + platform: macos-arm64 + purge-system-mpi: 'true' - name: ccache uses: hendrikmuhs/ccache-action@v1.2 with: @@ -36,6 +41,7 @@ jobs: -DCMAKE_C_FLAGS="-I$(brew --prefix)/opt/libomp/include" -DCMAKE_CXX_FLAGS="-I$(brew --prefix)/opt/libomp/include" -D CMAKE_BUILD_TYPE=${{ matrix.build_type }} -DCMAKE_INSTALL_PREFIX=install + -D PPC_MPI_EXTENSIONS_HOME="$PPC_MPI_EXTENSIONS_HOME" -D PPC_TASKS="${{ inputs.ppc_tasks }}" - name: Build project run: | @@ -58,13 +64,18 @@ jobs: - uses: actions/checkout@v6 - name: Setup macOS toolchain uses: ./.github/actions/setup-macos-toolchain + - name: Setup MPI extensions Open MPI + uses: ./.github/actions/setup-mpi-extensions + with: + platform: macos-arm64 + purge-system-mpi: 'true' - name: Download installed package uses: ./.github/actions/download-install with: path: install name: macos-clang-install - name: Run func tests (MPI) - run: scripts/run_tests.py --running-type="processes" --counts 1 2 3 4 + run: scripts/run_tests.py --running-type="processes" --counts 1 2 3 4 --additional-mpi-args="--oversubscribe" env: PPC_NUM_THREADS: 1 PPC_TASK_MAX_TIME: 30 @@ -80,6 +91,11 @@ jobs: - uses: actions/checkout@v6 - name: Setup macOS toolchain uses: ./.github/actions/setup-macos-toolchain + - name: Setup MPI extensions Open MPI + uses: ./.github/actions/setup-mpi-extensions + with: + platform: macos-arm64 + purge-system-mpi: 'true' - name: Download installed package uses: ./.github/actions/download-install with: diff --git a/.github/workflows/perf.yml b/.github/workflows/perf.yml index 9452b3d84..812e7e5e8 100644 --- a/.github/workflows/perf.yml +++ b/.github/workflows/perf.yml @@ -20,6 +20,11 @@ jobs: - name: Setup environment run: | python3 -m pip install -r requirements.txt --break-system-packages --ignore-installed + - name: Setup MPI extensions Open MPI + uses: ./.github/actions/setup-mpi-extensions + with: + platform: linux-x86_64 + purge-system-mpi: 'true' - name: Download installed package uses: ./.github/actions/download-install with: @@ -47,6 +52,11 @@ jobs: - uses: actions/checkout@v6 - name: Setup macOS toolchain uses: ./.github/actions/setup-macos-toolchain + - name: Setup MPI extensions Open MPI + uses: ./.github/actions/setup-mpi-extensions + with: + platform: macos-arm64 + purge-system-mpi: 'true' - name: Setup environment run: | python3 -m pip install -r requirements.txt --break-system-packages diff --git a/.github/workflows/static-analysis-pr.yml b/.github/workflows/static-analysis-pr.yml index fa40d6623..aa8b470d1 100644 --- a/.github/workflows/static-analysis-pr.yml +++ b/.github/workflows/static-analysis-pr.yml @@ -10,8 +10,12 @@ on: - '**/CMakeLists.txt' - '**/*.cmake' - '**/.clang-tidy' + - '.github/actions/setup-mpi-extensions/**' - '.github/workflows/static-analysis-pr.yml' - 'scripts/check_task_backend_apis.py' + - 'scripts/install_mpi_extensions.py' + - 'scripts/setup_mpi_extensions.py' + - 'scripts/write_mpi_runtime_env.py' concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} @@ -66,10 +70,17 @@ jobs: create-symlink: true max-size: 1G + - name: Setup MPI extensions Open MPI + uses: ./.github/actions/setup-mpi-extensions + with: + platform: linux-x86_64 + purge-system-mpi: 'true' + - name: CMake configure run: > cmake -S . -B build -G Ninja -D CMAKE_BUILD_TYPE=RELEASE -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_COMPILE_WARNING_AS_ERROR=ON + -D PPC_MPI_EXTENSIONS_HOME="$PPC_MPI_EXTENSIONS_HOME" -D PPC_TASKS="${{ needs.ci-scope.outputs.ppc_tasks || 'all' }}" env: CC: clang-22 @@ -117,10 +128,17 @@ jobs: create-symlink: true max-size: 1G + - name: Setup MPI extensions Open MPI + uses: ./.github/actions/setup-mpi-extensions + with: + platform: linux-x86_64 + purge-system-mpi: 'true' + - name: CMake configure run: > cmake -S . -B build -G Ninja -D CMAKE_BUILD_TYPE=RELEASE -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_COMPILE_WARNING_AS_ERROR=ON + -D PPC_MPI_EXTENSIONS_HOME="$PPC_MPI_EXTENSIONS_HOME" -D PPC_TASKS="${{ needs.ci-scope.outputs.ppc_tasks || 'all' }}" env: CC: gcc-15 diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 34a25dee1..c638be985 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -35,10 +35,16 @@ jobs: key: ${{ runner.os }}-gcc create-symlink: true max-size: 1G + - name: Setup MPI extensions Open MPI + uses: ./.github/actions/setup-mpi-extensions + with: + platform: linux-x86_64 + purge-system-mpi: 'true' - name: CMake configure run: > cmake -S . -B build -G Ninja -D CMAKE_BUILD_TYPE=${{ matrix.build_type }} -DCMAKE_INSTALL_PREFIX=install + -D PPC_MPI_EXTENSIONS_HOME="$PPC_MPI_EXTENSIONS_HOME" -D PPC_TASKS="${{ inputs.ppc_tasks }}" env: CC: gcc-15 @@ -81,6 +87,11 @@ jobs: os: ["ubuntu-26.04"] steps: - uses: actions/checkout@v6 + - name: Setup MPI extensions Open MPI + uses: ./.github/actions/setup-mpi-extensions + with: + platform: linux-x86_64 + purge-system-mpi: 'true' - name: Download installed package uses: ./.github/actions/download-install with: @@ -110,6 +121,11 @@ jobs: os: ["ubuntu-26.04"] steps: - uses: actions/checkout@v6 + - name: Setup MPI extensions Open MPI + uses: ./.github/actions/setup-mpi-extensions + with: + platform: linux-x86_64 + purge-system-mpi: 'true' - name: Download installed package uses: ./.github/actions/download-install with: @@ -139,10 +155,16 @@ jobs: key: ${{ runner.os }}-clang create-symlink: true max-size: 1G + - name: Setup MPI extensions Open MPI + uses: ./.github/actions/setup-mpi-extensions + with: + platform: linux-x86_64 + purge-system-mpi: 'true' - name: CMake configure run: > cmake -S . -B build -G Ninja -D CMAKE_BUILD_TYPE=RELEASE -DCMAKE_INSTALL_PREFIX=install + -D PPC_MPI_EXTENSIONS_HOME="$PPC_MPI_EXTENSIONS_HOME" -D PPC_TASKS="${{ inputs.ppc_tasks }}" env: CC: clang-22 @@ -177,6 +199,11 @@ jobs: os: ["ubuntu-26.04"] steps: - uses: actions/checkout@v6 + - name: Setup MPI extensions Open MPI + uses: ./.github/actions/setup-mpi-extensions + with: + platform: linux-x86_64 + purge-system-mpi: 'true' - name: Download installed package uses: ./.github/actions/download-install with: @@ -206,6 +233,11 @@ jobs: os: ["ubuntu-26.04"] steps: - uses: actions/checkout@v6 + - name: Setup MPI extensions Open MPI + uses: ./.github/actions/setup-mpi-extensions + with: + platform: linux-x86_64 + purge-system-mpi: 'true' - name: Download installed package uses: ./.github/actions/download-install with: @@ -237,12 +269,18 @@ jobs: key: ${{ runner.os }}-clang create-symlink: true max-size: 1G + - name: Setup MPI extensions Open MPI + uses: ./.github/actions/setup-mpi-extensions + with: + platform: linux-x86_64 + purge-system-mpi: 'true' - name: CMake configure run: > cmake -S . -B build -G Ninja -D CMAKE_BUILD_TYPE=RelWithDebInfo -D ENABLE_ADDRESS_SANITIZER=ON -D ENABLE_UB_SANITIZER=ON -D ENABLE_LEAK_SANITIZER=ON -D CMAKE_INSTALL_PREFIX=install + -D PPC_MPI_EXTENSIONS_HOME="$PPC_MPI_EXTENSIONS_HOME" -D PPC_TASKS="${{ inputs.ppc_tasks }}" env: CC: clang-22 @@ -277,6 +315,11 @@ jobs: os: ["ubuntu-26.04"] steps: - uses: actions/checkout@v6 + - name: Setup MPI extensions Open MPI + uses: ./.github/actions/setup-mpi-extensions + with: + platform: linux-x86_64 + purge-system-mpi: 'true' - name: Download installed package uses: ./.github/actions/download-install with: @@ -312,6 +355,11 @@ jobs: os: ["ubuntu-26.04"] steps: - uses: actions/checkout@v6 + - name: Setup MPI extensions Open MPI + uses: ./.github/actions/setup-mpi-extensions + with: + platform: linux-x86_64 + purge-system-mpi: 'true' - name: Download installed package uses: ./.github/actions/download-install with: @@ -347,12 +395,18 @@ jobs: key: ${{ runner.os }}-gcc create-symlink: true max-size: 1G + - name: Setup MPI extensions Open MPI + uses: ./.github/actions/setup-mpi-extensions + with: + platform: linux-x86_64 + purge-system-mpi: 'true' - name: CMake configure run: > cmake -S . -B build -G Ninja -D CMAKE_BUILD_TYPE=RELEASE -D CMAKE_VERBOSE_MAKEFILE=ON -D USE_COVERAGE=ON -DCMAKE_INSTALL_PREFIX=install + -D PPC_MPI_EXTENSIONS_HOME="$PPC_MPI_EXTENSIONS_HOME" -D PPC_TASKS="${{ inputs.ppc_tasks }}" - name: Build project run: | @@ -375,7 +429,7 @@ jobs: run: | mkdir cov-report cd build - gcovr --gcov-executable `which gcov-15` \ + gcovr --gcov-executable "$(which gcov-15)" \ -r ../ \ --exclude '.*3rdparty/.*' \ --exclude '/usr/.*' \ diff --git a/.gitignore b/.gitignore index 5de0996c8..a5ec67e03 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /build* +/_deps/ docs/build* /xml docs/xml diff --git a/cmake/mpi.cmake b/cmake/mpi.cmake index 498b5450d..17ece25f8 100644 --- a/cmake/mpi.cmake +++ b/cmake/mpi.cmake @@ -1,5 +1,82 @@ include_guard() +set(PPC_MPI_EXTENSIONS_HOME + "" + CACHE PATH "Path to an unpacked mpi-extensions Open MPI package") + +if(NOT WIN32) + find_package(Python REQUIRED COMPONENTS Interpreter) + + if(NOT PPC_MPI_EXTENSIONS_HOME) + message( + FATAL_ERROR + "PPC_MPI_EXTENSIONS_HOME is required on Linux and macOS. " + "Install the mpi-extensions main Open MPI package and configure with " + "-DPPC_MPI_EXTENSIONS_HOME=/path/to/mpi-extensions-openmpi") + endif() + + get_filename_component(PPC_MPI_EXTENSIONS_HOME "${PPC_MPI_EXTENSIONS_HOME}" + REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") + set(PPC_MPI_EXTENSIONS_HOME + "${PPC_MPI_EXTENSIONS_HOME}" + CACHE PATH "Path to an unpacked mpi-extensions Open MPI package" FORCE) + + set(_PPC_MPI_EXTENSIONS_BIN "${PPC_MPI_EXTENSIONS_HOME}/bin") + set(_PPC_MPI_EXTENSIONS_LIB "${PPC_MPI_EXTENSIONS_HOME}/lib") + + foreach(tool mpicc mpicxx mpirun mpiexec) + if(NOT EXISTS "${_PPC_MPI_EXTENSIONS_BIN}/${tool}") + message( + FATAL_ERROR + "PPC_MPI_EXTENSIONS_HOME does not contain required tool '${tool}': " + "${_PPC_MPI_EXTENSIONS_BIN}/${tool}") + endif() + endforeach() + + if(NOT IS_DIRECTORY "${_PPC_MPI_EXTENSIONS_LIB}") + message( + FATAL_ERROR "PPC_MPI_EXTENSIONS_HOME does not contain a lib directory: " + "${_PPC_MPI_EXTENSIONS_LIB}") + endif() + + set(MPI_C_COMPILER + "${_PPC_MPI_EXTENSIONS_BIN}/mpicc" + CACHE FILEPATH "mpi-extensions C MPI wrapper" FORCE) + set(MPI_CXX_COMPILER + "${_PPC_MPI_EXTENSIONS_BIN}/mpicxx" + CACHE FILEPATH "mpi-extensions CXX MPI wrapper" FORCE) + set(MPIEXEC_EXECUTABLE + "${_PPC_MPI_EXTENSIONS_BIN}/mpirun" + CACHE FILEPATH "mpi-extensions MPI launcher" FORCE) + set(MPIEXEC_NUMPROC_FLAG + "-np" + CACHE STRING "MPI process count flag" FORCE) + + list(PREPEND CMAKE_PREFIX_PATH "${PPC_MPI_EXTENSIONS_HOME}") + list(PREPEND CMAKE_BUILD_RPATH "${_PPC_MPI_EXTENSIONS_LIB}") + list(PREPEND CMAKE_INSTALL_RPATH "${_PPC_MPI_EXTENSIONS_LIB}") + + set(_PPC_MPI_RUNTIME_CONFIG "${CMAKE_BINARY_DIR}/ppc_mpi_runtime_env.json") + execute_process( + COMMAND + "${Python_EXECUTABLE}" + "${CMAKE_SOURCE_DIR}/scripts/write_mpi_runtime_env.py" --output + "${_PPC_MPI_RUNTIME_CONFIG}" --mpi-extensions-home + "${PPC_MPI_EXTENSIONS_HOME}" --mpi-exec "${MPIEXEC_EXECUTABLE}" + --path-prepend "${_PPC_MPI_EXTENSIONS_BIN}" --library-path-prepend + "${_PPC_MPI_EXTENSIONS_LIB}" + RESULT_VARIABLE _PPC_MPI_RUNTIME_CONFIG_RESULT + OUTPUT_VARIABLE _PPC_MPI_RUNTIME_CONFIG_OUTPUT + ERROR_VARIABLE _PPC_MPI_RUNTIME_CONFIG_ERROR) + if(NOT _PPC_MPI_RUNTIME_CONFIG_RESULT EQUAL 0) + message( + FATAL_ERROR + "Failed to write MPI runtime config:\n" + "${_PPC_MPI_RUNTIME_CONFIG_OUTPUT}${_PPC_MPI_RUNTIME_CONFIG_ERROR}") + endif() + install(FILES "${_PPC_MPI_RUNTIME_CONFIG}" DESTINATION ".") +endif() + find_package(MPI REQUIRED COMPONENTS CXX) if(NOT MPI_FOUND) message(FATAL_ERROR "MPI NOT FOUND") diff --git a/docker/ubuntu.Dockerfile b/docker/ubuntu.Dockerfile index a4aaed63a..3c5900cce 100644 --- a/docker/ubuntu.Dockerfile +++ b/docker/ubuntu.Dockerfile @@ -12,8 +12,6 @@ RUN set -e \ ninja-build cmake make \ ccache \ valgrind \ - libmpich-dev mpich \ - openmpi-bin openmpi-common libopenmpi-dev \ libomp-dev \ gcc-15 g++-15 \ gcovr zip \ diff --git a/docs/locale/en/LC_MESSAGES/user_guide/environment.po b/docs/locale/en/LC_MESSAGES/user_guide/environment.po index a2b01b1c0..95a3fbba2 100644 --- a/docs/locale/en/LC_MESSAGES/user_guide/environment.po +++ b/docs/locale/en/LC_MESSAGES/user_guide/environment.po @@ -20,7 +20,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.17.0\n" +"Generated-By: Babel 2.18.0\n" #: ../../../../docs/user_guide/environment.rst:2 msgid "Set Up Your Environment" @@ -80,145 +80,149 @@ msgid "The container includes:" msgstr "" #: ../../../../docs/user_guide/environment.rst:21 -msgid "Ubuntu environment with gcc-15, CMake, MPI, OpenMP" +msgid "Ubuntu environment with gcc-15, CMake, OpenMP" msgstr "" #: ../../../../docs/user_guide/environment.rst:22 -msgid "Pre-configured C++ and Python development tools" +msgid "Open MPI from the ``learning-process/mpi-extensions`` main prerelease" msgstr "" #: ../../../../docs/user_guide/environment.rst:23 +msgid "Pre-configured C++ and Python development tools" +msgstr "" + +#: ../../../../docs/user_guide/environment.rst:24 msgid "All project dependencies ready to use" msgstr "" -#: ../../../../docs/user_guide/environment.rst:25 +#: ../../../../docs/user_guide/environment.rst:26 msgid "" "This provides a consistent development environment across all platforms " "without manual dependency installation." msgstr "" -#: ../../../../docs/user_guide/environment.rst:28 +#: ../../../../docs/user_guide/environment.rst:29 msgid "Manual Setup" msgstr "" -#: ../../../../docs/user_guide/environment.rst:30 +#: ../../../../docs/user_guide/environment.rst:31 msgid "" "If you prefer manual setup or cannot use containers, follow the " "instructions below." msgstr "" -#: ../../../../docs/user_guide/environment.rst:33 +#: ../../../../docs/user_guide/environment.rst:34 msgid "Build prerequisites" msgstr "" -#: ../../../../docs/user_guide/environment.rst:34 +#: ../../../../docs/user_guide/environment.rst:35 msgid "" "**Windows**: Download and install CMake from https://cmake.org/download " "(select the Windows installer) or install using Chocolatey:" msgstr "" -#: ../../../../docs/user_guide/environment.rst:40 +#: ../../../../docs/user_guide/environment.rst:41 msgid "**Linux (Ubuntu/Debian)**: Install using package manager:" msgstr "" -#: ../../../../docs/user_guide/environment.rst:47 +#: ../../../../docs/user_guide/environment.rst:48 msgid "**macOS**: Install using Homebrew:" msgstr "" -#: ../../../../docs/user_guide/environment.rst:55 +#: ../../../../docs/user_guide/environment.rst:56 msgid "Code Style Analysis" msgstr "" -#: ../../../../docs/user_guide/environment.rst:56 +#: ../../../../docs/user_guide/environment.rst:57 msgid "" "Please follow the `Google C++ Style Guide " "`_." msgstr "" -#: ../../../../docs/user_guide/environment.rst:58 +#: ../../../../docs/user_guide/environment.rst:59 msgid "" "Code style is checked using the `clang-format " "`_ tool." msgstr "" -#: ../../../../docs/user_guide/environment.rst:61 +#: ../../../../docs/user_guide/environment.rst:62 msgid "Optional tools (clang-tidy, gcovr)" msgstr "" -#: ../../../../docs/user_guide/environment.rst:62 +#: ../../../../docs/user_guide/environment.rst:63 msgid "" "Install these to match the CI toolchain for static analysis and coverage " "reports." msgstr "" -#: ../../../../docs/user_guide/environment.rst:64 +#: ../../../../docs/user_guide/environment.rst:65 msgid "Linux (Ubuntu/Debian):" msgstr "" -#: ../../../../docs/user_guide/environment.rst:75 +#: ../../../../docs/user_guide/environment.rst:74 msgid "macOS (Homebrew):" msgstr "" -#: ../../../../docs/user_guide/environment.rst:83 +#: ../../../../docs/user_guide/environment.rst:82 msgid "Windows:" msgstr "" -#: ../../../../docs/user_guide/environment.rst:92 +#: ../../../../docs/user_guide/environment.rst:91 msgid "Parallel Programming Technologies" msgstr "" -#: ../../../../docs/user_guide/environment.rst:95 +#: ../../../../docs/user_guide/environment.rst:94 msgid "``MPI``" msgstr "" -#: ../../../../docs/user_guide/environment.rst:96 +#: ../../../../docs/user_guide/environment.rst:95 msgid "**Windows (MSVC)**:" msgstr "" -#: ../../../../docs/user_guide/environment.rst:98 +#: ../../../../docs/user_guide/environment.rst:97 msgid "" "`Installers link `_. You have to install " "``msmpisdk.msi`` and ``msmpisetup.exe``." msgstr "" -#: ../../../../docs/user_guide/environment.rst:100 -#: ../../../../docs/user_guide/environment.rst:116 +#: ../../../../docs/user_guide/environment.rst:99 +#: ../../../../docs/user_guide/environment.rst:119 msgid "**Linux (gcc and clang)**:" msgstr "" -#: ../../../../docs/user_guide/environment.rst:106 +#: ../../../../docs/user_guide/environment.rst:107 msgid "**MacOS (apple clang)**:" msgstr "" -#: ../../../../docs/user_guide/environment.rst:113 +#: ../../../../docs/user_guide/environment.rst:116 msgid "``OpenMP``" msgstr "" -#: ../../../../docs/user_guide/environment.rst:114 +#: ../../../../docs/user_guide/environment.rst:117 msgid "" "``OpenMP`` is included in ``gcc`` and ``msvc``, but some components " "should be installed additionally:" msgstr "" -#: ../../../../docs/user_guide/environment.rst:122 +#: ../../../../docs/user_guide/environment.rst:125 msgid "**MacOS (llvm)**:" msgstr "" -#: ../../../../docs/user_guide/environment.rst:130 +#: ../../../../docs/user_guide/environment.rst:134 msgid "``TBB``" msgstr "" -#: ../../../../docs/user_guide/environment.rst:131 +#: ../../../../docs/user_guide/environment.rst:135 msgid "" "**Windows (MSVC)**, **Linux (gcc and clang)**, **MacOS (apple clang)**: " "Build as 3rdparty in the current project." msgstr "" -#: ../../../../docs/user_guide/environment.rst:135 +#: ../../../../docs/user_guide/environment.rst:139 msgid "``std::thread``" msgstr "" -#: ../../../../docs/user_guide/environment.rst:136 +#: ../../../../docs/user_guide/environment.rst:140 msgid "``std::thread`` is included in STL libraries." msgstr "" diff --git a/docs/locale/ru/LC_MESSAGES/user_guide/environment.po b/docs/locale/ru/LC_MESSAGES/user_guide/environment.po index 607dd1854..475ca67a6 100644 --- a/docs/locale/ru/LC_MESSAGES/user_guide/environment.po +++ b/docs/locale/ru/LC_MESSAGES/user_guide/environment.po @@ -20,7 +20,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.17.0\n" +"Generated-By: Babel 2.18.0\n" #: ../../../../docs/user_guide/environment.rst:2 msgid "Set Up Your Environment" @@ -89,18 +89,22 @@ msgid "The container includes:" msgstr "Контейнер включает:" #: ../../../../docs/user_guide/environment.rst:21 -msgid "Ubuntu environment with gcc-15, CMake, MPI, OpenMP" -msgstr "Окружение Ubuntu с gcc-15, CMake, MPI, OpenMP" +msgid "Ubuntu environment with gcc-15, CMake, OpenMP" +msgstr "Окружение Ubuntu с gcc-15, CMake и OpenMP" #: ../../../../docs/user_guide/environment.rst:22 +msgid "Open MPI from the ``learning-process/mpi-extensions`` main prerelease" +msgstr "Open MPI из основного предварительного релиза ``learning-process/mpi-extensions``" + +#: ../../../../docs/user_guide/environment.rst:23 msgid "Pre-configured C++ and Python development tools" msgstr "Предварительно настроенные инструменты разработки C++ и Python" -#: ../../../../docs/user_guide/environment.rst:23 +#: ../../../../docs/user_guide/environment.rst:24 msgid "All project dependencies ready to use" msgstr "Все зависимости проекта готовы к использованию" -#: ../../../../docs/user_guide/environment.rst:25 +#: ../../../../docs/user_guide/environment.rst:26 msgid "" "This provides a consistent development environment across all platforms " "without manual dependency installation." @@ -108,11 +112,11 @@ msgstr "" "Это обеспечивает единообразную среду разработки на всех платформах без " "ручной установки зависимостей." -#: ../../../../docs/user_guide/environment.rst:28 +#: ../../../../docs/user_guide/environment.rst:29 msgid "Manual Setup" msgstr "Ручная настройка" -#: ../../../../docs/user_guide/environment.rst:30 +#: ../../../../docs/user_guide/environment.rst:31 msgid "" "If you prefer manual setup or cannot use containers, follow the " "instructions below." @@ -120,11 +124,11 @@ msgstr "" "Если вы предпочитаете ручную настройку или не можете использовать " "контейнеры, следуйте инструкциям ниже." -#: ../../../../docs/user_guide/environment.rst:33 +#: ../../../../docs/user_guide/environment.rst:34 msgid "Build prerequisites" msgstr "Требования к сборке" -#: ../../../../docs/user_guide/environment.rst:34 +#: ../../../../docs/user_guide/environment.rst:35 msgid "" "**Windows**: Download and install CMake from https://cmake.org/download " "(select the Windows installer) or install using Chocolatey:" @@ -132,19 +136,19 @@ msgstr "" "**Windows**: Загрузите и установите CMake с https://cmake.org/download " "(выберите установщик для Windows) или установите с помощью Chocolatey:" -#: ../../../../docs/user_guide/environment.rst:40 +#: ../../../../docs/user_guide/environment.rst:41 msgid "**Linux (Ubuntu/Debian)**: Install using package manager:" msgstr "**Linux (Ubuntu/Debian)**: Установите с помощью менеджера пакетов:" -#: ../../../../docs/user_guide/environment.rst:47 +#: ../../../../docs/user_guide/environment.rst:48 msgid "**macOS**: Install using Homebrew:" msgstr "**macOS**: Установите с помощью Homebrew:" -#: ../../../../docs/user_guide/environment.rst:55 +#: ../../../../docs/user_guide/environment.rst:56 msgid "Code Style Analysis" msgstr "Анализ стиля кодирования" -#: ../../../../docs/user_guide/environment.rst:56 +#: ../../../../docs/user_guide/environment.rst:57 msgid "" "Please follow the `Google C++ Style Guide " "`_." @@ -152,7 +156,7 @@ msgstr "" "Пожалуйста пройдите по ссылке для изучения стиля кодирования - `Google " "C++ Style Guide `_." -#: ../../../../docs/user_guide/environment.rst:58 +#: ../../../../docs/user_guide/environment.rst:59 msgid "" "Code style is checked using the `clang-format " "`_ tool." @@ -160,41 +164,43 @@ msgstr "" "Проверка стиля кода выполняется с помощью инструмента `clang-format " "`_." -#: ../../../../docs/user_guide/environment.rst:61 +#: ../../../../docs/user_guide/environment.rst:62 msgid "Optional tools (clang-tidy, gcovr)" msgstr "Дополнительные инструменты (clang-tidy, gcovr)" -#: ../../../../docs/user_guide/environment.rst:62 +#: ../../../../docs/user_guide/environment.rst:63 msgid "" "Install these to match the CI toolchain for static analysis and coverage " "reports." -msgstr "Установите их, чтобы соответствовать инструментам CI для статического анализа и отчётов по покрытию." +msgstr "" +"Установите их, чтобы соответствовать инструментам CI для статического " +"анализа и отчётов по покрытию." -#: ../../../../docs/user_guide/environment.rst:64 +#: ../../../../docs/user_guide/environment.rst:65 msgid "Linux (Ubuntu/Debian):" msgstr "Linux (Ubuntu/Debian):" -#: ../../../../docs/user_guide/environment.rst:75 +#: ../../../../docs/user_guide/environment.rst:74 msgid "macOS (Homebrew):" msgstr "macOS (Homebrew):" -#: ../../../../docs/user_guide/environment.rst:83 +#: ../../../../docs/user_guide/environment.rst:82 msgid "Windows:" msgstr "Windows:" -#: ../../../../docs/user_guide/environment.rst:92 +#: ../../../../docs/user_guide/environment.rst:91 msgid "Parallel Programming Technologies" msgstr "Технологии параллельного программирования" -#: ../../../../docs/user_guide/environment.rst:95 +#: ../../../../docs/user_guide/environment.rst:94 msgid "``MPI``" msgstr "``MPI``" -#: ../../../../docs/user_guide/environment.rst:96 +#: ../../../../docs/user_guide/environment.rst:95 msgid "**Windows (MSVC)**:" msgstr "**Windows (MSVC)**:" -#: ../../../../docs/user_guide/environment.rst:98 +#: ../../../../docs/user_guide/environment.rst:97 msgid "" "`Installers link `_. You have to install " @@ -204,20 +210,20 @@ msgstr "" "us/download/details.aspx?id=105289>`_. Вы должны установить 2 файла - " "``msmpisdk.msi`` и ``msmpisetup.exe``." -#: ../../../../docs/user_guide/environment.rst:100 -#: ../../../../docs/user_guide/environment.rst:116 +#: ../../../../docs/user_guide/environment.rst:99 +#: ../../../../docs/user_guide/environment.rst:119 msgid "**Linux (gcc and clang)**:" msgstr "**Linux (gcc and clang)**:" -#: ../../../../docs/user_guide/environment.rst:106 +#: ../../../../docs/user_guide/environment.rst:107 msgid "**MacOS (apple clang)**:" msgstr "**MacOS (apple clang)**:" -#: ../../../../docs/user_guide/environment.rst:113 +#: ../../../../docs/user_guide/environment.rst:116 msgid "``OpenMP``" msgstr "``OpenMP``" -#: ../../../../docs/user_guide/environment.rst:114 +#: ../../../../docs/user_guide/environment.rst:117 msgid "" "``OpenMP`` is included in ``gcc`` and ``msvc``, but some components " "should be installed additionally:" @@ -226,15 +232,15 @@ msgstr "" "``msvc``, но ряд компонент все равно должны быть установлены " "дополнительно:" -#: ../../../../docs/user_guide/environment.rst:122 +#: ../../../../docs/user_guide/environment.rst:125 msgid "**MacOS (llvm)**:" msgstr "**MacOS (llvm)**:" -#: ../../../../docs/user_guide/environment.rst:130 +#: ../../../../docs/user_guide/environment.rst:134 msgid "``TBB``" msgstr "``TBB``" -#: ../../../../docs/user_guide/environment.rst:131 +#: ../../../../docs/user_guide/environment.rst:135 msgid "" "**Windows (MSVC)**, **Linux (gcc and clang)**, **MacOS (apple clang)**: " "Build as 3rdparty in the current project." @@ -243,10 +249,10 @@ msgstr "" "Данная библиотека строится как внешняя в составе текущего проекта и не " "требует дополнительных операций." -#: ../../../../docs/user_guide/environment.rst:135 +#: ../../../../docs/user_guide/environment.rst:139 msgid "``std::thread``" msgstr "``std::thread``" -#: ../../../../docs/user_guide/environment.rst:136 +#: ../../../../docs/user_guide/environment.rst:140 msgid "``std::thread`` is included in STL libraries." msgstr "``std::thread`` включена в состав STL библиотек." diff --git a/docs/user_guide/environment.rst b/docs/user_guide/environment.rst index a8a653a38..2ebcb89b4 100644 --- a/docs/user_guide/environment.rst +++ b/docs/user_guide/environment.rst @@ -18,7 +18,8 @@ The easiest way to set up your development environment is using the provided ``. 3. VS Code will automatically build the container with all dependencies pre-installed 4. The container includes: - - Ubuntu environment with gcc-15, CMake, MPI, OpenMP + - Ubuntu environment with gcc-15, CMake, OpenMP + - Open MPI from the ``learning-process/mpi-extensions`` main prerelease - Pre-configured C++ and Python development tools - All project dependencies ready to use @@ -96,29 +97,33 @@ Parallel Programming Technologies `Installers link `_. You have to install ``msmpisdk.msi`` and ``msmpisetup.exe``. - **Linux (gcc and clang)**: - + .. code-block:: bash - sudo apt install -y mpich openmpi-bin libopenmpi-dev + python3 -m pip install 'requests>=2.31,<3' + python3 scripts/install_mpi_extensions.py --prefix "$PWD/_deps/mpi-extensions-openmpi" --force + cmake -S . -B build -D PPC_MPI_EXTENSIONS_HOME="$PWD/_deps/mpi-extensions-openmpi" - **MacOS (apple clang)**: - + .. code-block:: bash - brew install open-mpi + python3 -m pip install 'requests>=2.31,<3' + python3 scripts/install_mpi_extensions.py --prefix "$PWD/_deps/mpi-extensions-openmpi" --force + cmake -S . -B build -D PPC_MPI_EXTENSIONS_HOME="$PWD/_deps/mpi-extensions-openmpi" ``OpenMP`` ~~~~~~~~~~ ``OpenMP`` is included in ``gcc`` and ``msvc``, but some components should be installed additionally: - **Linux (gcc and clang)**: - + .. code-block:: bash sudo apt install -y libomp-dev - **MacOS (llvm)**: - + .. code-block:: bash brew install llvm diff --git a/requirements.txt b/requirements.txt index eb505ddfb..8439d388e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ numpy==2.4.6 XlsxWriter==3.2.9 PyYAML==6.0.3 pre-commit==4.5.1 +requests>=2.31,<3 diff --git a/scripts/install_mpi_extensions.py b/scripts/install_mpi_extensions.py new file mode 100755 index 000000000..72354278d --- /dev/null +++ b/scripts/install_mpi_extensions.py @@ -0,0 +1,413 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import hashlib +import os +import platform +import re +import shutil +import subprocess +import sys +import tarfile +import tempfile +from pathlib import Path, PurePosixPath +from typing import Any + +try: + import requests +except ImportError as exc: + raise SystemExit( + "Missing Python dependency 'requests'. Install it with: " + "python3 -m pip install 'requests>=2.31,<3'" + ) from exc + + +DEFAULT_REPO = "learning-process/mpi-extensions" +RELEASE_TAG = "main" +SUPPORTED_PLATFORMS = { + "linux-x86_64", + "linux-arm64", + "macos-arm64", + "macos-x86_64", +} + + +def normalize_machine(machine: str) -> str: + normalized = machine.lower() + if normalized in {"x86_64", "amd64"}: + return "x86_64" + if normalized in {"arm64", "aarch64"}: + return "arm64" + return normalized + + +def detect_platform() -> str: + system = platform.system() + machine = normalize_machine(platform.machine()) + if system == "Linux": + return f"linux-{machine}" + if system == "Darwin": + return f"macos-{machine}" + raise SystemExit(f"Unsupported operating system for mpi-extensions: {system}") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Install Open MPI from the mpi-extensions main prerelease." + ) + parser.add_argument("--repo", default=DEFAULT_REPO, help=f"default: {DEFAULT_REPO}") + parser.add_argument( + "--platform", + default="auto", + help="auto, linux-x86_64, linux-arm64, macos-arm64, or macos-x86_64", + ) + parser.add_argument("--prefix", required=True, type=Path) + parser.add_argument( + "--force", + action="store_true", + help="remove an existing installation prefix before extracting", + ) + return parser.parse_args() + + +def github_headers() -> dict[str, str]: + headers = { + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": "parallel-programming-course-mpi-extensions-installer", + } + token = os.environ.get("GITHUB_TOKEN") + if token: + headers["Authorization"] = f"Bearer {token}" + return headers + + +def get_json(session: requests.Session, url: str) -> dict[str, Any]: + response = session.get(url, timeout=60) + if response.status_code == 404: + raise SystemExit(f"GitHub resource not found: {url}") + response.raise_for_status() + data = response.json() + if not isinstance(data, dict): + raise SystemExit(f"Unexpected GitHub API response from {url}") + return data + + +def select_asset(release: dict[str, Any], asset_name: str) -> dict[str, Any]: + for asset in release.get("assets", []): + if asset.get("name") == asset_name: + return asset + available = ", ".join( + asset.get("name", "") for asset in release.get("assets", []) + ) + raise SystemExit( + f"Release asset not found: {asset_name}. Available assets: {available}" + ) + + +def select_archive_asset( + release: dict[str, Any], target_platform: str +) -> dict[str, Any]: + pattern = re.compile( + rf"^mpi-extensions-openmpi-.+-{re.escape(target_platform)}\.tar\.gz$" + ) + matches = [ + asset + for asset in release.get("assets", []) + if pattern.match(str(asset.get("name", ""))) + ] + if len(matches) != 1: + available = ", ".join( + asset.get("name", "") for asset in release.get("assets", []) + ) + raise SystemExit( + f"Expected exactly one main release archive for {target_platform}, " + f"found {len(matches)}. Available assets: {available}" + ) + return matches[0] + + +def download(session: requests.Session, url: str, destination: Path) -> None: + with session.get(url, stream=True, timeout=300) as response: + response.raise_for_status() + with destination.open("wb") as output: + for chunk in response.iter_content(chunk_size=1024 * 1024): + if chunk: + output.write(chunk) + + +def sha256sum(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as input_file: + for chunk in iter(lambda: input_file.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def path_is_inside(path: Path, directory: Path) -> bool: + return path == directory or directory in path.parents + + +def archive_member_path(member: tarfile.TarInfo) -> PurePosixPath: + member_path = PurePosixPath(member.name) + if member_path.is_absolute() or not member_path.parts: + raise SystemExit(f"Archive contains unsafe path: {member.name}") + if any(part in {"", ".", ".."} for part in member_path.parts): + raise SystemExit(f"Archive contains unsafe path: {member.name}") + return member_path + + +def validate_archive_member_type(member: tarfile.TarInfo) -> None: + if member.isdir() or member.isfile() or member.issym() or member.islnk(): + return + raise SystemExit(f"Archive contains unsupported member type: {member.name}") + + +def validate_archive_link( + member: tarfile.TarInfo, + member_path: PurePosixPath, + destination: Path, + resolved_destination: Path, +) -> None: + if not (member.issym() or member.islnk()): + return + if not member.linkname: + raise SystemExit(f"Archive contains empty link target: {member.name}") + + link_path = PurePosixPath(member.linkname) + if link_path.is_absolute(): + raise SystemExit( + f"Archive link target is absolute: {member.name} -> {member.linkname}" + ) + + member_target = (destination / Path(*member_path.parts)).resolve(strict=False) + if member.issym(): + link_target = (member_target.parent / member.linkname).resolve(strict=False) + else: + link_target = (destination / member.linkname).resolve(strict=False) + + if not path_is_inside(link_target, resolved_destination): + raise SystemExit( + f"Archive link target escapes destination: " + f"{member.name} -> {member.linkname}" + ) + + +def safe_extract(archive: Path, destination: Path) -> Path: + destination.mkdir(parents=True, exist_ok=True) + resolved_destination = destination.resolve() + with tarfile.open(archive, "r:gz") as tar: + members = tar.getmembers() + roots = set() + member_paths = [] + for member in members: + member_path = archive_member_path(member) + validate_archive_member_type(member) + roots.add(member_path.parts[0]) + member_paths.append((member, member_path)) + target = (destination / Path(*member_path.parts)).resolve(strict=False) + if not path_is_inside(target, resolved_destination): + raise SystemExit(f"Archive contains unsafe path: {member.name}") + validate_archive_link( + member, member_path, destination, resolved_destination + ) + + symlink_paths = { + member_path for member, member_path in member_paths if member.issym() + } + for member, member_path in member_paths: + for symlink_path in symlink_paths: + if symlink_path in member_path.parents: + raise SystemExit( + f"Archive member is nested under a symlink: {member.name}" + ) + + if len(roots) != 1: + raise SystemExit( + f"Expected archive to contain one top-level directory, found: {roots}" + ) + tar.extractall(destination, members=members) + return destination / roots.pop() + + +def install_tree(source_root: Path, prefix: Path, force: bool) -> None: + required_tools = ["mpicc", "mpicxx", "mpirun", "mpiexec"] + missing_tools = [ + tool for tool in required_tools if not (source_root / "bin" / tool).exists() + ] + if missing_tools: + raise SystemExit( + f"Archive is missing required MPI tools: {', '.join(missing_tools)}" + ) + + if prefix.exists(): + if not force and any(prefix.iterdir()): + raise SystemExit( + f"Installation prefix is not empty: {prefix}. Use --force to replace it." + ) + shutil.rmtree(prefix) + prefix.parent.mkdir(parents=True, exist_ok=True) + shutil.copytree(source_root, prefix, symlinks=True) + + +def run_tool(command: list[str]) -> subprocess.CompletedProcess[str]: + return subprocess.run( + command, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ) + + +def otool_lines(path: Path) -> list[str]: + result = run_tool(["otool", "-L", str(path)]) + if result.returncode != 0: + return [] + return result.stdout.splitlines()[1:] + + +def patch_macos_install_names(prefix: Path) -> None: + if platform.system() != "Darwin": + return + if ( + not shutil.which("install_name_tool") + or not shutil.which("otool") + or not shutil.which("codesign") + ): + raise SystemExit( + "macOS install_name_tool, otool, and codesign are required to install mpi-extensions" + ) + + lib_dir = prefix / "lib" + if not lib_dir.exists(): + return + + local_libs = { + path.name: path.resolve() + for path in lib_dir.iterdir() + if path.is_file() and not path.is_symlink() and ".dylib" in path.name + } + mach_o_candidates = [ + path + for base_dir in (prefix / "bin", lib_dir) + if base_dir.exists() + for path in base_dir.iterdir() + if path.is_file() and not path.is_symlink() + ] + patched_paths: set[Path] = set() + + for path in local_libs.values(): + result = run_tool(["otool", "-D", str(path)]) + if result.returncode != 0: + continue + install_names = [ + line.strip() for line in result.stdout.splitlines()[1:] if line.strip() + ] + if install_names: + local_name = local_libs.get(Path(install_names[0]).name) + if local_name: + replacement = f"@rpath/{local_name.name}" + if install_names[0] != replacement: + subprocess.run( + [ + "install_name_tool", + "-id", + replacement, + str(path), + ], + check=True, + ) + patched_paths.add(path) + + for path in mach_o_candidates: + for line in otool_lines(path): + dependency = line.strip().split(" (", 1)[0] + if not dependency.startswith("/"): + continue + local_dependency = local_libs.get(Path(dependency).name) + if local_dependency and dependency != str(local_dependency): + if path.parent.resolve() == lib_dir.resolve(): + replacement = f"@loader_path/{local_dependency.name}" + elif path.parent.resolve() == (prefix / "bin").resolve(): + replacement = f"@loader_path/../lib/{local_dependency.name}" + else: + replacement = f"@rpath/{local_dependency.name}" + subprocess.run( + [ + "install_name_tool", + "-change", + dependency, + replacement, + str(path), + ], + check=True, + ) + patched_paths.add(path) + + for path in patched_paths: + subprocess.run( + ["codesign", "--force", "--sign", "-", "--timestamp=none", str(path)], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=True, + ) + + +def print_environment(prefix: Path) -> None: + prefix_text = str(prefix) + print(f"PPC_MPI_EXTENSIONS_HOME={prefix_text}") + print(f"CMake option: -DPPC_MPI_EXTENSIONS_HOME={prefix_text}") + + +def main() -> int: + args = parse_args() + target_platform = detect_platform() if args.platform == "auto" else args.platform + if target_platform not in SUPPORTED_PLATFORMS: + raise SystemExit( + f"Unsupported platform '{target_platform}'. Supported values: " + f"{', '.join(sorted(SUPPORTED_PLATFORMS))}" + ) + + session = requests.Session() + session.headers.update(github_headers()) + release_url = ( + f"https://api.github.com/repos/{args.repo}/releases/tags/{RELEASE_TAG}" + ) + release = get_json(session, release_url) + if release.get("tag_name") != RELEASE_TAG or not release.get("prerelease"): + raise SystemExit( + f"Release {args.repo}@{RELEASE_TAG} is not the main prerelease" + ) + + archive_asset = select_archive_asset(release, target_platform) + archive_name = archive_asset["name"] + checksum_asset = select_asset(release, f"{archive_name}.sha256") + + with tempfile.TemporaryDirectory(prefix="mpi-extensions-") as temp_dir_text: + temp_dir = Path(temp_dir_text) + archive_path = temp_dir / archive_name + checksum_path = temp_dir / f"{archive_name}.sha256" + print( + f"Downloading {archive_name} from {args.repo}@{RELEASE_TAG}", + file=sys.stderr, + ) + download(session, archive_asset["browser_download_url"], archive_path) + download(session, checksum_asset["browser_download_url"], checksum_path) + + expected_sha = checksum_path.read_text(encoding="utf-8").split()[0] + actual_sha = sha256sum(archive_path) + if actual_sha != expected_sha: + raise SystemExit( + f"SHA256 mismatch for {archive_name}: expected {expected_sha}, got {actual_sha}" + ) + + source_root = safe_extract(archive_path, temp_dir / "extract") + prefix = args.prefix.resolve() + install_tree(source_root, prefix, args.force) + patch_macos_install_names(prefix) + + print_environment(args.prefix.resolve()) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_tests.py b/scripts/run_tests.py index bdbfc2280..c0ec07773 100755 --- a/scripts/run_tests.py +++ b/scripts/run_tests.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 +import json import os import platform import shlex @@ -67,15 +68,12 @@ def __init__(self, build_dir="build", verbose=False): self.mpi_exec = "mpirun" self.platform = platform.system() - # Detect MPI implementation to choose compatible flags - self.mpi_env_mode = "unknown" # one of: openmpi, mpich, unknown - self.mpi_np_flag = "-np" if self.platform == "Windows": - # MSMPI uses -env and -n self.mpi_env_mode = "mpich" self.mpi_np_flag = "-n" else: - self.mpi_env_mode, self.mpi_np_flag = self.__detect_mpi_impl() + self.mpi_env_mode = "unknown" + self.mpi_np_flag = "-np" @staticmethod def __get_project_path(): @@ -105,11 +103,15 @@ def setup_env(self, ppc_env): build_dir = project_path / build_dir self.__build_dir_path = build_dir - install_bin_dir = project_path / "install" / "bin" + install_dir = project_path / "install" + install_bin_dir = install_dir / "bin" if install_bin_dir.exists(): + self.__apply_mpi_runtime_config(install_dir / "ppc_mpi_runtime_env.json") self.work_dir = install_bin_dir + self.__detect_configured_mpi() return + self.__apply_mpi_runtime_config(build_dir / "ppc_mpi_runtime_env.json") bin_dir = build_dir if build_dir.name == "bin" else build_dir / "bin" if not bin_dir.exists(): raise FileNotFoundError( @@ -117,6 +119,52 @@ def setup_env(self, ppc_env): "Build the project or pass a correct '--build-dir' (e.g. 'build', 'build_seq', or 'build/bin')." ) self.work_dir = bin_dir + self.__detect_configured_mpi() + + def __prepend_env_path(self, name, value): + current = self.__ppc_env.get(name) + self.__ppc_env[name] = ( + str(value) if not current else f"{value}{os.pathsep}{current}" + ) + + def __apply_mpi_runtime_config(self, config_path): + if self.platform == "Windows": + return + + if not config_path.exists(): + raise FileNotFoundError( + f"MPI runtime config not found: '{config_path}'. " + "Configure CMake with -DPPC_MPI_EXTENSIONS_HOME=/path/to/mpi-extensions-openmpi." + ) + + with config_path.open(encoding="utf-8") as input_file: + config = json.load(input_file) + + env_values = config.get("env", {}) + for name, value in env_values.items(): + self.__ppc_env[name] = str(value) + + path_prepend = config.get("path_prepend") + if path_prepend: + self.__prepend_env_path("PATH", path_prepend) + + library_path_prepend = config.get("library_path_prepend") + if library_path_prepend: + self.__prepend_env_path("LD_LIBRARY_PATH", library_path_prepend) + self.__prepend_env_path("DYLD_LIBRARY_PATH", library_path_prepend) + + mpi_exec = config.get("mpi_exec") + if mpi_exec: + mpi_exec_path = Path(mpi_exec) + if not mpi_exec_path.exists(): + raise FileNotFoundError( + f"Configured MPI launcher not found: '{mpi_exec_path}'" + ) + self.mpi_exec = str(mpi_exec_path) + + def __detect_configured_mpi(self): + if self.platform != "Windows": + self.mpi_env_mode, self.mpi_np_flag = self.__detect_mpi_impl() def __run_exec(self, command, extra_env=None): if self.verbose: @@ -142,6 +190,7 @@ def __detect_mpi_impl(self): stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, + env=self.__ppc_env, ) out = (proc.stdout or "").lower() if out: diff --git a/scripts/setup_mpi_extensions.py b/scripts/setup_mpi_extensions.py new file mode 100755 index 000000000..53c1a9934 --- /dev/null +++ b/scripts/setup_mpi_extensions.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import os +import platform +import shutil +import subprocess +import sys +from pathlib import Path + +MPI_PACKAGES_LINUX = [ + "mpich", + "libmpich-dev", + "openmpi-bin", + "openmpi-common", + "libopenmpi-dev", + "mpi-default-bin", + "mpi-default-dev", +] +MPI_PACKAGES_MACOS = ["mpich", "open-mpi"] + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Set up mpi-extensions Open MPI for GitHub Actions." + ) + parser.add_argument("--repo-root", required=True, type=Path) + parser.add_argument("--prefix", required=True) + parser.add_argument("--platform", required=True) + parser.add_argument("--purge-system-mpi", choices=["true", "false"], required=True) + parser.add_argument("--github-env", required=True, type=Path) + parser.add_argument("--runner-temp", required=True, type=Path) + return parser.parse_args() + + +def run( + command: list[str], *, env: dict[str, str] | None = None, check: bool = True +) -> subprocess.CompletedProcess[str]: + print("+ " + " ".join(command), flush=True) + return subprocess.run(command, check=check, env=env) + + +def sudo_prefix() -> list[str] | None: + if os.geteuid() == 0: + return [] + if shutil.which("sudo"): + return ["sudo"] + return None + + +def purge_linux_mpi() -> None: + if not shutil.which("apt-get"): + return + sudo = sudo_prefix() + if sudo is None: + print("::warning::sudo is unavailable; skipping Linux MPI package removal") + return + + apt_get = sudo + ["apt-get"] + run(apt_get + ["update"]) + run(apt_get + ["purge", "-y"] + MPI_PACKAGES_LINUX, check=False) + run(apt_get + ["autoremove", "-y"], check=False) + if os.geteuid() == 0: + shutil.rmtree("/var/lib/apt/lists", ignore_errors=True) + + +def purge_macos_mpi() -> None: + if not shutil.which("brew"): + return + for package in MPI_PACKAGES_MACOS: + if ( + subprocess.run( + ["brew", "list", "--versions", package], check=False + ).returncode + == 0 + ): + run(["brew", "uninstall", "--ignore-dependencies", "--force", package]) + + +def purge_system_mpi(enabled: bool) -> None: + if not enabled: + return + system = platform.system() + if system == "Linux": + purge_linux_mpi() + elif system == "Darwin": + purge_macos_mpi() + + +def prepare_python(runner_temp: Path) -> Path: + venv_dir = runner_temp / "mpi-extensions-python" + python_bin = venv_dir / "bin" / "python" + try: + run([sys.executable, "-m", "venv", str(venv_dir)]) + run([str(python_bin), "-m", "pip", "install", "--upgrade", "pip"]) + run([str(python_bin), "-m", "pip", "install", "requests>=2.31,<3"]) + return python_bin + except subprocess.CalledProcessError: + print( + "::warning::Python venv setup failed; trying the current interpreter with pip" + ) + if ( + subprocess.run( + [sys.executable, "-m", "pip", "install", "--user", "requests>=2.31,<3"], + check=False, + ).returncode + != 0 + ): + run( + [ + sys.executable, + "-m", + "pip", + "install", + "--break-system-packages", + "requests>=2.31,<3", + ] + ) + return Path(sys.executable) + + +def install_openmpi(args: argparse.Namespace, python_bin: Path) -> None: + installer = args.repo_root / "scripts" / "install_mpi_extensions.py" + if not installer.exists(): + raise SystemExit(f"mpi-extensions installer not found: {installer}") + run( + [ + str(python_bin), + str(installer), + "--platform", + args.platform, + "--prefix", + str(args.prefix), + "--force", + ] + ) + + +def append_github_environment(prefix: Path, github_env: Path) -> None: + with github_env.open("a", encoding="utf-8") as output: + output.write(f"PPC_MPI_EXTENSIONS_HOME={prefix}\n") + + +def validate_openmpi(prefix: Path) -> None: + expected_bin = prefix / "bin" + env = os.environ.copy() + env["MPI_EXTENSIONS_HOME"] = str(prefix) + env["MPI_HOME"] = str(prefix) + env["OPAL_PREFIX"] = str(prefix) + env["OMPI_MCA_shmem"] = "mmap" + env["PATH"] = f"{expected_bin}:{env.get('PATH', '')}" + env["LD_LIBRARY_PATH"] = f"{prefix / 'lib'}:{env.get('LD_LIBRARY_PATH', '')}" + env["DYLD_LIBRARY_PATH"] = f"{prefix / 'lib'}:{env.get('DYLD_LIBRARY_PATH', '')}" + + for tool in ("mpicc", "mpicxx", "mpirun", "mpiexec"): + actual = shutil.which(tool, path=env["PATH"]) + expected = expected_bin / tool + if actual != str(expected): + raise SystemExit(f"{tool} resolves to {actual} instead of {expected}") + + run([str(expected_bin / "mpirun"), "--version"], env=env) + run([str(expected_bin / "mpicc"), "--showme:version"], env=env) + + +def main() -> int: + args = parse_args() + if args.prefix: + prefix = Path(args.prefix).resolve() + else: + prefix = (args.runner_temp / "mpi-extensions-openmpi").resolve() + args.prefix = prefix + purge_system_mpi(args.purge_system_mpi == "true") + python_bin = prepare_python(args.runner_temp) + install_openmpi(args, python_bin) + append_github_environment(prefix, args.github_env) + validate_openmpi(prefix) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/write_mpi_runtime_env.py b/scripts/write_mpi_runtime_env.py new file mode 100755 index 000000000..8c3dfd175 --- /dev/null +++ b/scripts/write_mpi_runtime_env.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Write the PPC MPI runtime environment JSON file." + ) + parser.add_argument("--output", required=True, type=Path) + parser.add_argument("--mpi-extensions-home", required=True) + parser.add_argument("--mpi-exec", required=True) + parser.add_argument("--path-prepend", required=True) + parser.add_argument("--library-path-prepend", required=True) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + runtime_config = { + "mpi_extensions_home": args.mpi_extensions_home, + "mpi_exec": args.mpi_exec, + "path_prepend": args.path_prepend, + "library_path_prepend": args.library_path_prepend, + "env": { + "MPI_EXTENSIONS_HOME": args.mpi_extensions_home, + "MPI_HOME": args.mpi_extensions_home, + "OPAL_PREFIX": args.mpi_extensions_home, + "OMPI_MCA_shmem": "mmap", + }, + } + + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text( + json.dumps(runtime_config, indent=2) + "\n", encoding="utf-8" + ) + + +if __name__ == "__main__": + main()