From 59a8463a5889fc243b7898c82abb9037c59b52aa Mon Sep 17 00:00:00 2001 From: Keith Kraus Date: Tue, 18 Aug 2026 03:33:33 -0400 Subject: [PATCH 1/6] ci: spike Moon-native task orchestration Signed-off-by: Keith Kraus --- .github/workflows/build-docs.yml | 59 +- .github/workflows/build-pure-wheel.yml | 143 +++ .github/workflows/build-wheel.yml | 354 +++---- .github/workflows/ci.yml | 960 +++++++++++------- .github/workflows/release-cuda-pathfinder.yml | 1 + .github/workflows/release.yml | 1 + .github/workflows/test-sdist-linux.yml | 167 ++- .github/workflows/test-sdist-windows.yml | 155 ++- .github/workflows/test-wheel-linux.yml | 84 +- .github/workflows/test-wheel-windows.yml | 78 +- .gitignore | 2 + .moon/workspace.yml | 41 + CONTRIBUTING.md | 35 + benchmarks/cuda_bindings/moon.yml | 80 ++ ci/moon.yml | 473 +++++++++ ci/tools/moon_ci.py | 559 ++++++++++ ci/tools/moon_fingerprint.py | 137 +++ ci/tools/tests/test_moon_ci.py | 117 +++ ci/tools/tests/test_moon_workspace.py | 370 +++++++ cuda_bindings/moon.yml | 201 ++++ cuda_core/moon.yml | 309 ++++++ cuda_pathfinder/moon.yml | 177 ++++ cuda_python/moon.yml | 122 +++ cuda_python_test_helpers/moon.yml | 10 + moon.yml | 70 ++ toolshed/check_spdx.py | 1 + 26 files changed, 3836 insertions(+), 870 deletions(-) create mode 100644 .github/workflows/build-pure-wheel.yml create mode 100644 .moon/workspace.yml create mode 100644 benchmarks/cuda_bindings/moon.yml create mode 100644 ci/moon.yml create mode 100644 ci/tools/moon_ci.py create mode 100644 ci/tools/moon_fingerprint.py create mode 100644 ci/tools/tests/test_moon_ci.py create mode 100644 ci/tools/tests/test_moon_workspace.py create mode 100644 cuda_bindings/moon.yml create mode 100644 cuda_core/moon.yml create mode 100644 cuda_pathfinder/moon.yml create mode 100644 cuda_python/moon.yml create mode 100644 cuda_python_test_helpers/moon.yml create mode 100644 moon.yml diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 7bb70809556..12c2098ec1e 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -28,6 +28,28 @@ on: required: false default: ${{ github.run_id }} type: string + portable-run-id: + description: > + Workflow run ID containing cuda.pathfinder and metapackage wheels. + Falls back to run-id when empty. + required: false + default: "" + type: string + sha: + description: "Commit SHA used in native wheel artifact names" + required: false + default: ${{ github.sha }} + type: string + moon-base: + description: "Base revision used by Moon affected checks" + required: false + default: "" + type: string + force-all: + description: "Force selected Moon tasks" + required: false + default: false + type: boolean is-release: description: "Are we building release docs?" required: false @@ -57,9 +79,18 @@ jobs: - name: Checkout ${{ github.event.repository.name }} uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: - fetch-depth: 1 + fetch-depth: 0 + filter: blob:none ref: ${{ inputs.git-tag }} + - name: Set up Moon + if: ${{ !inputs.is-release && inputs.component == 'all' }} + uses: moonrepo/setup-toolchain@261c62cb5b0f580c7be7c8cd0f023a2e96756095 # v0.6.4 + with: + moon-version: "2.5.1" + auto-install: false + auto-setup: false + - name: Read build CTK version run: | if [[ -f ci/versions.yml ]]; then @@ -113,8 +144,8 @@ jobs: DOCS_GITHUB_REF="${GITHUB_REF_NAME}" fi else - FILE_HASH="${{ github.sha }}" - DOCS_GITHUB_REF="${{ github.sha }}" + FILE_HASH="${{ inputs.sha }}" + DOCS_GITHUB_REF="${{ inputs.sha }}" fi # make outputs from the previous job as env vars @@ -133,7 +164,7 @@ jobs: with: name: cuda-python-wheel path: . - run-id: ${{ inputs.run-id }} + run-id: ${{ inputs.portable-run-id || inputs.run-id }} github-token: ${{ github.token }} - name: Display structure of downloaded cuda-python artifacts @@ -146,7 +177,7 @@ jobs: with: name: cuda-pathfinder-wheel path: ./cuda_pathfinder - run-id: ${{ inputs.run-id }} + run-id: ${{ inputs.portable-run-id || inputs.run-id }} github-token: ${{ github.token }} - name: Display structure of downloaded cuda-pathfinder artifacts @@ -160,6 +191,8 @@ jobs: with: name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }} path: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} + run-id: ${{ inputs.run-id }} + github-token: ${{ github.token }} - name: Download cuda.bindings build artifacts if: ${{ inputs.is-release }} @@ -182,6 +215,8 @@ jobs: with: name: ${{ env.CUDA_CORE_ARTIFACT_NAME }} path: ${{ env.CUDA_CORE_ARTIFACTS_DIR }} + run-id: ${{ inputs.run-id }} + github-token: ${{ github.token }} - name: Download cuda.core build artifacts if: ${{ inputs.is-release }} @@ -229,18 +264,22 @@ jobs: - name: Build all docs if: ${{ inputs.component == 'all' }} + env: + MOON_BASE: ${{ inputs.moon-base }} + MOON_HEAD: ${{ github.sha }} run: | - pushd cuda_python/docs/ if [[ "${{ inputs.is-release }}" == "false" ]]; then - ./build_all_docs.sh latest-only + moon ci root:docs-ci --upstream none --downstream none ${{ inputs.force-all && '--force' || '' }} + mv .moon-out/docs/* artifacts/docs/ else + pushd cuda_python/docs/ ./build_all_docs.sh # At release time, we don't want to update the latest docs rm -rf build/html/latest + ls -l build + popd + mv cuda_python/docs/build/html/* artifacts/docs/ fi - ls -l build - popd - mv cuda_python/docs/build/html/* artifacts/docs/ - name: Build component docs if: ${{ inputs.component != 'all' }} diff --git a/.github/workflows/build-pure-wheel.yml b/.github/workflows/build-pure-wheel.yml new file mode 100644 index 00000000000..302b9f860ad --- /dev/null +++ b/.github/workflows/build-pure-wheel.yml @@ -0,0 +1,143 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +name: "CI: Build portable wheels with Moon" + +on: + workflow_call: + inputs: + build-pathfinder: + required: true + type: boolean + build-metapackage: + required: true + type: boolean + moon-base: + required: true + type: string + baseline-run-id: + required: false + default: "" + type: string + force-all: + required: false + default: false + type: boolean + +permissions: + actions: read + contents: read + +jobs: + build: + runs-on: ubuntu-latest + defaults: + run: + shell: bash --noprofile --norc -xeuo pipefail {0} + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + filter: blob:none + + - name: Set up Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: "3.12" + + - name: Set up Moon + uses: moonrepo/setup-toolchain@261c62cb5b0f580c7be7c8cd0f023a2e96756095 # v0.6.4 + with: + moon-version: "2.5.1" + auto-install: false + auto-setup: false + + - name: Install externally managed build tools + run: >- + python -m pip install + "setuptools>=80" + "setuptools-scm[simple]>=8,!=10.1" + "twine" + "wheel" + + - name: Restore trusted exact-base Moon cache + if: ${{ inputs.baseline-run-id != '' && !inputs.force-all }} + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: moon-cache-build-portable + path: .moon/cache + github-token: ${{ github.token }} + run-id: ${{ inputs.baseline-run-id }} + + - name: Restore unchanged cuda.pathfinder wheel + if: ${{ !inputs.build-pathfinder && inputs.baseline-run-id != '' }} + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: cuda-pathfinder-wheel + path: cuda_pathfinder/.moon-out/wheel-pure + github-token: ${{ github.token }} + run-id: ${{ inputs.baseline-run-id }} + + - name: Restore unchanged cuda-python metapackage wheel + if: ${{ !inputs.build-metapackage && inputs.baseline-run-id != '' }} + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: cuda-python-wheel + path: cuda_python/.moon-out/wheel-pure + github-token: ${{ github.token }} + run-id: ${{ inputs.baseline-run-id }} + + - name: Build affected portable wheels with Moon + env: + CUDA_PYTHON_LANE: portable-py312 + MOON_BASE: ${{ inputs.moon-base }} + MOON_HEAD: ${{ github.sha }} + MOON_FORCE_ALL: ${{ inputs.force-all && 'true' || 'false' }} + run: | + args=() + if [[ "${MOON_FORCE_ALL}" == "true" ]]; then + args+=(--force) + fi + moon ci ':#ci-wheel-pure' --downstream none "${args[@]}" + + - name: Validate portable wheels + run: | + test "$(find cuda_pathfinder/.moon-out/wheel-pure -maxdepth 1 -name '*.whl' -type f | wc -l)" -eq 1 + test "$(find cuda_python/.moon-out/wheel-pure -maxdepth 1 -name '*.whl' -type f | wc -l)" -eq 1 + python -m twine check --strict \ + cuda_pathfinder/.moon-out/wheel-pure/*.whl \ + cuda_python/.moon-out/wheel-pure/*.whl + + - name: Upload cuda.pathfinder wheel + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: cuda-pathfinder-wheel + path: cuda_pathfinder/.moon-out/wheel-pure/*.whl + if-no-files-found: error + overwrite: true + + - name: Upload cuda-python metapackage wheel + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: cuda-python-wheel + path: cuda_python/.moon-out/wheel-pure/*.whl + if-no-files-found: error + overwrite: true + + # Moon documents hashes/ and outputs/ as the portable subset of its + # local cache. GitHub artifacts provide trusted exact-run transport; + # Moon remains responsible for hashes, hits, and output hydration. + - name: Upload portable Moon cache + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: moon-cache-build-portable + path: | + .moon/cache/hashes + .moon/cache/outputs + if-no-files-found: error + include-hidden-files: true + overwrite: true + retention-days: 30 diff --git a/.github/workflows/build-wheel.yml b/.github/workflows/build-wheel.yml index 390d6f88ae2..8ae90a3c941 100644 --- a/.github/workflows/build-wheel.yml +++ b/.github/workflows/build-wheel.yml @@ -14,10 +14,6 @@ on: prev-cuda-version: required: true type: string - build-pathfinder: - required: false - type: boolean - default: true build-bindings: required: false type: boolean @@ -26,10 +22,6 @@ on: required: false type: boolean default: true - build-python: - required: false - type: boolean - default: true test-bindings: required: false type: boolean @@ -46,6 +38,19 @@ on: required: false type: string default: "" + portable-run-id: + description: "Workflow run containing the selected portable wheels" + required: true + type: string + moon-base: + description: "Base revision used by Moon affected checks" + required: true + type: string + force-all: + description: "Force selected Moon tasks when no reusable baseline exists" + required: false + type: boolean + default: false defaults: run: @@ -117,6 +122,22 @@ jobs: # see https://github.com/actions/setup-python/issues/871 python-version: "3.12" + - name: Set up Moon + uses: moonrepo/setup-toolchain@261c62cb5b0f580c7be7c8cd0f023a2e96756095 # v0.6.4 + with: + moon-version: "2.5.1" + auto-install: false + auto-setup: false + + - name: Restore trusted exact-base Moon cache + if: ${{ inputs.baseline-run-id != '' && !inputs.force-all }} + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: moon-cache-build-${{ inputs.host-platform }}-py${{ matrix.python-version }} + path: .moon/cache + github-token: ${{ github.token }} + run-id: ${{ inputs.baseline-run-id }} + - name: Set up MSVC if: ${{ startsWith(inputs.host-platform, 'win') && (inputs.build-bindings || inputs.build-core || inputs.test-bindings || inputs.test-core) }} uses: step-security/msvc-dev-cmd@22c98154b708dbd743e6f27a933cf6ceba3305c4 # v1.13.1 @@ -156,26 +177,16 @@ jobs: run: | env - - name: Install twine - run: | - pip install twine - - # To keep the build workflow simple, all matrix jobs will build a wheel for later use within this workflow. - - name: Build and check cuda.pathfinder wheel - if: ${{ inputs.build-pathfinder }} - run: | - pushd cuda_pathfinder - pip wheel -v --no-deps . - popd + - name: Install externally managed build tools + run: python -m pip install "cibuildwheel==4.1.1" twine wheel - - name: Download reusable cuda.pathfinder wheel - if: ${{ !inputs.build-pathfinder }} + - name: Download cuda.pathfinder wheel from the portable producer uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: cuda-pathfinder-wheel - path: cuda_pathfinder + path: cuda_pathfinder/.moon-out/wheel-pure github-token: ${{ github.token }} - run-id: ${{ inputs.baseline-run-id }} + run-id: ${{ inputs.portable-run-id }} - name: List the cuda.pathfinder artifacts directory run: | @@ -184,37 +195,8 @@ jobs: else export CHOWN="sudo chown" fi - $CHOWN -R $(whoami) cuda_pathfinder/*.whl - ls -lahR cuda_pathfinder - - # We only need/want a single pure python wheel, pick linux-64 index 0. - # This is what we will use for testing & releasing. - - name: Check cuda.pathfinder wheel - if: ${{ inputs.build-pathfinder && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} - run: | - twine check --strict cuda_pathfinder/*.whl - - - name: Constrain builds to the local cuda.pathfinder wheel - if: ${{ inputs.build-bindings }} - run: | - pathfinder_wheels=(cuda_pathfinder/cuda_pathfinder-*.whl) - test "${#pathfinder_wheels[@]}" -eq 1 - test -f "${pathfinder_wheels[0]}" - mkdir -p wheel-constraints - if [[ "${{ inputs.host-platform }}" == win* ]]; then - pathfinder_uri="file:///$(cygpath -am "${pathfinder_wheels[0]}")" - else - pathfinder_uri="file:///host$(realpath "${pathfinder_wheels[0]}")" - fi - printf 'cuda-pathfinder @ %s\n' "${pathfinder_uri}" | tee wheel-constraints/cuda-bindings.txt - - - name: Upload cuda.pathfinder build artifacts - if: ${{ strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: cuda-pathfinder-wheel - path: cuda_pathfinder/*.whl - if-no-files-found: error + $CHOWN -R $(whoami) cuda_pathfinder/.moon-out/wheel-pure/*.whl + ls -lahR cuda_pathfinder/.moon-out/wheel-pure - name: Set up mini CTK if: ${{ inputs.build-bindings || inputs.build-core || inputs.test-bindings || inputs.test-core }} @@ -226,11 +208,11 @@ jobs: - name: Build cuda.bindings wheel if: ${{ inputs.build-bindings }} - uses: pypa/cibuildwheel@4726cd35bb13f7bde50cf2761f2499ac7b3aa32c # v4.1.1 - with: - package-dir: ./cuda_bindings/ - output-dir: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} + run: moon ci bindings:wheel-current --upstream none --downstream none ${{ inputs.force-all && '--force' || '' }} env: + CUDA_PYTHON_LANE: ${{ inputs.host-platform }}-py${{ matrix.python-version }}-cuda${{ inputs.cuda-version }} + MOON_BASE: ${{ inputs.moon-base }} + MOON_HEAD: ${{ github.sha }} CIBW_BUILD: ${{ env.CIBW_BUILD }} CIBW_BEFORE_BUILD_LINUX: 'python -m pip install --upgrade "pip>=25.3"' CIBW_BEFORE_BUILD_WINDOWS: 'python -m pip install --upgrade "pip>=25.3" delvewheel' @@ -243,8 +225,6 @@ jobs: CIBW_ENVIRONMENT_LINUX: > CUDA_PATH=/host/${{ env.CUDA_PATH }} CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} - PIP_BUILD_CONSTRAINT=/host/${{ github.workspace }}/wheel-constraints/cuda-bindings.txt - PIP_CONSTRAINT=/host/${{ github.workspace }}/wheel-constraints/cuda-bindings.txt CC="/host/${{ env.SCCACHE_PATH }} cc" CXX="/host/${{ env.SCCACHE_PATH }} c++" SCCACHE_GHA_ENABLED=true @@ -258,8 +238,6 @@ jobs: CIBW_ENVIRONMENT_WINDOWS: > CUDA_PATH="$(cygpath -w ${{ env.CUDA_PATH }})" CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} - PIP_BUILD_CONSTRAINT="$(cygpath -w ./wheel-constraints/cuda-bindings.txt)" - PIP_CONSTRAINT="$(cygpath -w ./wheel-constraints/cuda-bindings.txt)" # check cache stats before leaving cibuildwheel CIBW_BEFORE_TEST_LINUX: > "/host/${{ env.SCCACHE_PATH }}" --show-adv-stats && @@ -283,7 +261,7 @@ jobs: uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ env.CUDA_BINDINGS_ARTIFACT_BASENAME }}-${{ inputs.baseline-sha }} - path: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} + path: cuda_bindings/.moon-out/wheel-current github-token: ${{ github.token }} run-id: ${{ inputs.baseline-run-id }} @@ -294,50 +272,29 @@ jobs: else export CHOWN="sudo chown" fi - $CHOWN -R $(whoami) ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} - ls -lahR ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} + $CHOWN -R $(whoami) cuda_bindings/.moon-out/wheel-current + ls -lahR cuda_bindings/.moon-out/wheel-current - name: Check cuda.bindings wheel if: ${{ inputs.build-bindings }} run: | - twine check --strict ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}/*.whl - - - name: Constrain cuda.core to the local cuda.bindings wheel - if: ${{ inputs.build-core }} - run: | - pathfinder_wheels=(cuda_pathfinder/cuda_pathfinder-*.whl) - bindings_wheels=("${CUDA_BINDINGS_ARTIFACTS_DIR}"/cuda_bindings-"${BUILD_CUDA_MAJOR}".*.whl) - test "${#pathfinder_wheels[@]}" -eq 1 - test "${#bindings_wheels[@]}" -eq 1 - test -f "${pathfinder_wheels[0]}" - test -f "${bindings_wheels[0]}" - mkdir -p wheel-constraints - if [[ "${{ inputs.host-platform }}" == win* ]]; then - pathfinder_uri="file:///$(cygpath -am "${pathfinder_wheels[0]}")" - bindings_uri="file:///$(cygpath -am "${bindings_wheels[0]}")" - else - pathfinder_uri="file:///host$(realpath "${pathfinder_wheels[0]}")" - bindings_uri="file:///host$(realpath "${bindings_wheels[0]}")" - fi - { - printf 'cuda-pathfinder @ %s\n' "${pathfinder_uri}" - printf 'cuda-bindings @ %s\n' "${bindings_uri}" - } | tee wheel-constraints/cuda-core.txt + twine check --strict cuda_bindings/.moon-out/wheel-current/*.whl - name: Upload cuda.bindings build artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }} - path: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}/*.whl + path: cuda_bindings/.moon-out/wheel-current/*.whl if-no-files-found: error + overwrite: true - name: Build cuda.core wheel if: ${{ inputs.build-core }} - uses: pypa/cibuildwheel@4726cd35bb13f7bde50cf2761f2499ac7b3aa32c # v4.1.1 - with: - package-dir: ./cuda_core/ - output-dir: ${{ env.CUDA_CORE_ARTIFACTS_DIR }} + run: moon ci core:wheel-current --upstream none --downstream none ${{ inputs.force-all && '--force' || '' }} env: + CUDA_PYTHON_LANE: ${{ inputs.host-platform }}-py${{ matrix.python-version }}-cuda${{ inputs.cuda-version }} + MOON_BASE: ${{ inputs.moon-base }} + MOON_HEAD: ${{ github.sha }} CIBW_BUILD: ${{ env.CIBW_BUILD }} CIBW_BEFORE_BUILD_LINUX: 'python -m pip install --upgrade "pip>=25.3"' CIBW_BEFORE_BUILD_WINDOWS: 'python -m pip install --upgrade "pip>=25.3" delvewheel' @@ -351,8 +308,6 @@ jobs: CUDA_PATH=/host/${{ env.CUDA_PATH }} CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} CUDA_CORE_BUILD_MAJOR=${{ env.BUILD_CUDA_MAJOR }} - PIP_BUILD_CONSTRAINT=/host/${{ github.workspace }}/wheel-constraints/cuda-core.txt - PIP_CONSTRAINT=/host/${{ github.workspace }}/wheel-constraints/cuda-core.txt CC="/host/${{ env.SCCACHE_PATH }} cc" CXX="/host/${{ env.SCCACHE_PATH }} c++" SCCACHE_GHA_ENABLED=true @@ -367,8 +322,6 @@ jobs: CUDA_PATH="$(cygpath -w ${{ env.CUDA_PATH }})" CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} CUDA_CORE_BUILD_MAJOR=${{ env.BUILD_CUDA_MAJOR }} - PIP_BUILD_CONSTRAINT="$(cygpath -w ./wheel-constraints/cuda-core.txt)" - PIP_CONSTRAINT="$(cygpath -w ./wheel-constraints/cuda-core.txt)" # check cache stats before leaving cibuildwheel CIBW_BEFORE_TEST_LINUX: > "/host${{ env.SCCACHE_PATH }}" --show-adv-stats && @@ -387,7 +340,7 @@ jobs: label: "cuda.core" build-step: "Build cuda.core wheel" - - name: List the cuda.core artifacts directory and rename + - name: List the current cuda.core artifacts if: ${{ inputs.build-core }} run: | if [[ "${{ inputs.host-platform }}" == win* ]]; then @@ -395,66 +348,50 @@ jobs: else export CHOWN="sudo chown" fi - $CHOWN -R $(whoami) ${{ env.CUDA_CORE_ARTIFACTS_DIR }} - - # Rename wheel to include CUDA version suffix - mkdir -p "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${BUILD_CUDA_MAJOR}" - for wheel in ${{ env.CUDA_CORE_ARTIFACTS_DIR }}/*.whl; do - if [[ -f "${wheel}" ]]; then - base_name=$(basename "${wheel}" .whl) - new_name="${base_name}.cu${BUILD_CUDA_MAJOR}.whl" - mv "${wheel}" "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${BUILD_CUDA_MAJOR}/${new_name}" - echo "Renamed wheel to: ${new_name}" - fi - done - - ls -lahR ${{ env.CUDA_CORE_ARTIFACTS_DIR }} + $CHOWN -R $(whoami) cuda_core/.moon-out/wheel-current + ls -lahR cuda_core/.moon-out/wheel-current - name: Download reusable cuda.core wheel if: ${{ !inputs.build-core }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ env.CUDA_CORE_ARTIFACT_BASENAME }}-${{ inputs.baseline-sha }} - path: ${{ env.CUDA_CORE_ARTIFACTS_DIR }} + path: cuda_core/.moon-out/wheel-merged github-token: ${{ github.token }} run-id: ${{ inputs.baseline-run-id }} - # We only need/want a single pure python wheel, pick linux-64 index 0. - - name: Build and check cuda-python wheel - if: ${{ inputs.build-python && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} + - name: Stage reusable cuda.core wheel for Cython test assets + if: ${{ !inputs.build-core && inputs.test-core }} run: | - pushd cuda_python - pip wheel -v --no-deps . - twine check --strict *.whl - popd + mkdir -p cuda_core/.moon-out/wheel-current + cp cuda_core/.moon-out/wheel-merged/*.whl cuda_core/.moon-out/wheel-current/ - - name: Download reusable cuda-python wheel - if: ${{ !inputs.build-python && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} + - name: Restore reusable cuda.bindings Cython test assets + if: ${{ inputs.test-bindings && inputs.baseline-run-id != '' && !inputs.force-all }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: cuda-python-wheel - path: cuda_python + name: ${{ env.CUDA_BINDINGS_ARTIFACT_BASENAME }}-${{ inputs.baseline-sha }}-tests + path: cuda_bindings/.moon-out/cython-tests github-token: ${{ github.token }} run-id: ${{ inputs.baseline-run-id }} - - name: List the cuda-python artifacts directory - if: ${{ strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} - run: | - if [[ "${{ inputs.host-platform }}" == win* ]]; then - export CHOWN=chown - else - export CHOWN="sudo chown" - fi - $CHOWN -R $(whoami) cuda_python/*.whl - ls -lahR cuda_python + - name: Restore reusable cuda.core Cython test assets + if: ${{ inputs.test-core && inputs.baseline-run-id != '' && !inputs.force-all }} + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ env.CUDA_CORE_ARTIFACT_BASENAME }}-${{ inputs.baseline-sha }}-tests + path: cuda_core/.moon-out/cython-tests + github-token: ${{ github.token }} + run-id: ${{ inputs.baseline-run-id }} - - name: Upload cuda-python build artifacts - if: ${{ strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + - name: Restore reusable cuda.core test binaries + if: ${{ inputs.test-core && inputs.baseline-run-id != '' && !inputs.force-all }} + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: cuda-python-wheel - path: cuda_python/*.whl - if-no-files-found: error + name: ${{ env.CUDA_CORE_ARTIFACT_BASENAME }}-${{ inputs.baseline-sha }}-test-binaries + path: cuda_core/.moon-out/test-binaries + github-token: ${{ github.token }} + run-id: ${{ inputs.baseline-run-id }} - name: Set up Python id: setup-python2 @@ -489,7 +426,7 @@ jobs: - name: Install cuda.pathfinder (required for next step) if: ${{ inputs.test-bindings || inputs.test-core }} run: | - pip install cuda_pathfinder/*.whl + pip install cuda_pathfinder/.moon-out/wheel-pure/*.whl - name: Hide GNU link.exe so Meson finds MSVC link.exe if: ${{ startsWith(inputs.host-platform, 'win') && (inputs.test-bindings || inputs.test-core) }} @@ -500,45 +437,37 @@ jobs: - name: Build cuda.bindings Cython tests if: ${{ inputs.test-bindings }} - run: | - pip install ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}/*.whl --group ./cuda_bindings/pyproject.toml:test - pushd ${{ env.CUDA_BINDINGS_CYTHON_TESTS_DIR }} - bash build_tests.sh - popd + env: + CUDA_PYTHON_LANE: ${{ inputs.host-platform }}-py${{ matrix.python-version }}-cuda${{ inputs.cuda-version }} + MOON_BASE: ${{ inputs.moon-base }} + MOON_HEAD: ${{ github.sha }} + run: moon ci bindings:cython-test-assets --upstream none --downstream none ${{ inputs.force-all && '--force' || '' }} - name: Upload cuda.bindings Cython tests if: ${{ inputs.test-bindings }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }}-tests - path: ${{ env.CUDA_BINDINGS_CYTHON_TESTS_DIR }}/test_*${{ env.PY_EXT_SUFFIX }} + path: cuda_bindings/.moon-out/cython-tests/test_*${{ env.PY_EXT_SUFFIX }} if-no-files-found: error + overwrite: true - name: Build cuda.core Cython tests if: ${{ inputs.test-core }} - run: | - pip install ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}/*.whl - if ${{ inputs.build-core }}; then - core_wheel=$(find "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${BUILD_CUDA_MAJOR}" -maxdepth 1 -type f -name '*.whl' -print -quit) - else - core_wheel=$(find "${{ env.CUDA_CORE_ARTIFACTS_DIR }}" -maxdepth 1 -type f -name '*.whl' -print -quit) - fi - if [[ -z "${core_wheel}" ]]; then - echo "No cuda.core wheel found" >&2 - exit 1 - fi - pip install "${core_wheel}" --group ./cuda_core/pyproject.toml:test - pushd ${{ env.CUDA_CORE_CYTHON_TESTS_DIR }} - bash build_tests.sh - popd + env: + CUDA_PYTHON_LANE: ${{ inputs.host-platform }}-py${{ matrix.python-version }}-cuda${{ inputs.cuda-version }} + MOON_BASE: ${{ inputs.moon-base }} + MOON_HEAD: ${{ github.sha }} + run: moon ci core:cython-test-assets --upstream none --downstream none ${{ inputs.force-all && '--force' || '' }} - name: Upload cuda.core Cython tests if: ${{ inputs.test-core }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-tests - path: ${{ env.CUDA_CORE_CYTHON_TESTS_DIR }}/test_*${{ env.PY_EXT_SUFFIX }} + path: cuda_core/.moon-out/cython-tests/test_*${{ env.PY_EXT_SUFFIX }} if-no-files-found: error + overwrite: true # Note: This overwrites CUDA_PATH etc - name: Set up mini CTK @@ -552,9 +481,11 @@ jobs: - name: Build cuda.core test binaries if: ${{ inputs.test-core }} - run: | - nvcc --version - python "${{ env.CUDA_CORE_TEST_BINARIES_DIR }}/build_test_binaries.py" + env: + CUDA_PYTHON_LANE: ${{ inputs.host-platform }}-py${{ matrix.python-version }}-cuda${{ inputs.prev-cuda-version }} + MOON_BASE: ${{ inputs.moon-base }} + MOON_HEAD: ${{ github.sha }} + run: moon ci core:test-binaries --upstream none --downstream none ${{ inputs.force-all && '--force' || '' }} - name: Upload cuda.core test binaries if: ${{ inputs.test-core }} @@ -562,10 +493,11 @@ jobs: with: name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-test-binaries path: | - ${{ env.CUDA_CORE_TEST_BINARIES_DIR }}/*.o - ${{ env.CUDA_CORE_TEST_BINARIES_DIR }}/*.a - ${{ env.CUDA_CORE_TEST_BINARIES_DIR }}/*.lib + cuda_core/.moon-out/test-binaries/*.o + cuda_core/.moon-out/test-binaries/*.a + cuda_core/.moon-out/test-binaries/*.lib if-no-files-found: error + overwrite: true - name: Download cuda.bindings build artifacts from the prior branch if: ${{ inputs.build-core }} @@ -587,7 +519,7 @@ jobs: OLD_BRANCH=$(yq '.backport_branch' ci/versions.yml) OLD_BASENAME="cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda*-${{ inputs.host-platform }}*" LATEST_PRIOR_RUN_ID=$(./ci/tools/lookup-run-id --branch "${OLD_BRANCH}" NVIDIA/cuda-python "CI") - PREV_BINDINGS_DIR="cuda_bindings/dist-prev" + PREV_BINDINGS_DIR="cuda_bindings/.moon-out/wheel-previous" gh run download $LATEST_PRIOR_RUN_ID -p ${OLD_BASENAME} -R NVIDIA/cuda-python rm -rf ${OLD_BASENAME}-tests # exclude cython test artifacts @@ -596,35 +528,13 @@ jobs: mv $OLD_BASENAME/*.whl "${PREV_BINDINGS_DIR}" rmdir $OLD_BASENAME - - name: Constrain previous cuda.core to the downloaded cuda.bindings wheel - if: ${{ inputs.build-core }} - run: | - pathfinder_wheels=(cuda_pathfinder/cuda_pathfinder-*.whl) - bindings_wheels=(cuda_bindings/dist-prev/cuda_bindings-"${BUILD_PREV_CUDA_MAJOR}".*.whl) - test "${#pathfinder_wheels[@]}" -eq 1 - test "${#bindings_wheels[@]}" -eq 1 - test -f "${pathfinder_wheels[0]}" - test -f "${bindings_wheels[0]}" - mkdir -p wheel-constraints - if [[ "${{ inputs.host-platform }}" == win* ]]; then - pathfinder_uri="file:///$(cygpath -am "${pathfinder_wheels[0]}")" - bindings_uri="file:///$(cygpath -am "${bindings_wheels[0]}")" - else - pathfinder_uri="file:///host$(realpath "${pathfinder_wheels[0]}")" - bindings_uri="file:///host$(realpath "${bindings_wheels[0]}")" - fi - { - printf 'cuda-pathfinder @ %s\n' "${pathfinder_uri}" - printf 'cuda-bindings @ %s\n' "${bindings_uri}" - } | tee wheel-constraints/cuda-core-prev.txt - - name: Build cuda.core wheel if: ${{ inputs.build-core }} - uses: pypa/cibuildwheel@4726cd35bb13f7bde50cf2761f2499ac7b3aa32c # v4.1.1 - with: - package-dir: ./cuda_core/ - output-dir: ${{ env.CUDA_CORE_ARTIFACTS_DIR }} + run: moon ci core:wheel-previous --upstream none --downstream none ${{ inputs.force-all && '--force' || '' }} env: + CUDA_PYTHON_LANE: ${{ inputs.host-platform }}-py${{ matrix.python-version }}-cuda${{ inputs.prev-cuda-version }} + MOON_BASE: ${{ inputs.moon-base }} + MOON_HEAD: ${{ github.sha }} CIBW_BUILD: ${{ env.CIBW_BUILD }} CIBW_BEFORE_BUILD_LINUX: 'python -m pip install --upgrade "pip>=25.3"' CIBW_BEFORE_BUILD_WINDOWS: 'python -m pip install --upgrade "pip>=25.3" delvewheel' @@ -638,8 +548,6 @@ jobs: CUDA_PATH=/host/${{ env.CUDA_PATH }} CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} CUDA_CORE_BUILD_MAJOR=${{ env.BUILD_PREV_CUDA_MAJOR }} - PIP_BUILD_CONSTRAINT=/host/${{ github.workspace }}/wheel-constraints/cuda-core-prev.txt - PIP_CONSTRAINT=/host/${{ github.workspace }}/wheel-constraints/cuda-core-prev.txt CC="/host/${{ env.SCCACHE_PATH }} cc" CXX="/host/${{ env.SCCACHE_PATH }} c++" SCCACHE_GHA_ENABLED=true @@ -654,8 +562,6 @@ jobs: CUDA_PATH="$(cygpath -w ${{ env.CUDA_PATH }})" CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} CUDA_CORE_BUILD_MAJOR=${{ env.BUILD_PREV_CUDA_MAJOR }} - PIP_BUILD_CONSTRAINT="$(cygpath -w ./wheel-constraints/cuda-core-prev.txt)" - PIP_CONSTRAINT="$(cygpath -w ./wheel-constraints/cuda-core-prev.txt)" # check cache stats before leaving cibuildwheel CIBW_BEFORE_TEST_LINUX: > "/host${{ env.SCCACHE_PATH }}" --show-adv-stats && @@ -674,7 +580,7 @@ jobs: label: "cuda.core (prev CTK)" build-step: "Build cuda.core wheel" - - name: List the cuda.core artifacts directory and rename + - name: List the previous cuda.core artifacts if: ${{ inputs.build-core }} run: | if [[ "${{ inputs.host-platform }}" == win* ]]; then @@ -682,39 +588,39 @@ jobs: else export CHOWN="sudo chown" fi - $CHOWN -R $(whoami) ${{ env.CUDA_CORE_ARTIFACTS_DIR }} - ls -lahR ${{ env.CUDA_CORE_ARTIFACTS_DIR }} - - # Rename wheel to include CUDA version suffix - mkdir -p "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${BUILD_PREV_CUDA_MAJOR}" - for wheel in ${{ env.CUDA_CORE_ARTIFACTS_DIR }}/*.whl; do - if [[ -f "${wheel}" ]]; then - base_name=$(basename "${wheel}" .whl) - new_name="${base_name}.cu${BUILD_PREV_CUDA_MAJOR}.whl" - mv "${wheel}" "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${BUILD_PREV_CUDA_MAJOR}/${new_name}" - echo "Renamed wheel to: ${new_name}" - fi - done - - ls -lahR ${{ env.CUDA_CORE_ARTIFACTS_DIR }} + $CHOWN -R $(whoami) cuda_core/.moon-out/wheel-previous + ls -lahR cuda_core/.moon-out/wheel-previous - name: Merge cuda.core wheels if: ${{ inputs.build-core }} - run: | - pip install wheel - python ci/tools/merge_cuda_core_wheels.py \ - "${{ env.CUDA_CORE_ARTIFACTS_DIR }}"/cu"${BUILD_CUDA_MAJOR}"/cuda_core*.whl \ - "${{ env.CUDA_CORE_ARTIFACTS_DIR }}"/cu"${BUILD_PREV_CUDA_MAJOR}"/cuda_core*.whl \ - --output-dir "${{ env.CUDA_CORE_ARTIFACTS_DIR }}" + env: + CUDA_PYTHON_LANE: ${{ inputs.host-platform }}-py${{ matrix.python-version }}-cuda${{ inputs.cuda-version }}-${{ inputs.prev-cuda-version }} + MOON_BASE: ${{ inputs.moon-base }} + MOON_HEAD: ${{ github.sha }} + run: moon ci core:wheel-merge --upstream none --downstream none ${{ inputs.force-all && '--force' || '' }} - name: Check cuda.core wheel if: ${{ inputs.build-core }} run: | - twine check --strict ${{ env.CUDA_CORE_ARTIFACTS_DIR }}/*.whl + twine check --strict cuda_core/.moon-out/wheel-merged/*.whl - name: Upload cuda.core build artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ env.CUDA_CORE_ARTIFACT_NAME }} - path: ${{ env.CUDA_CORE_ARTIFACTS_DIR }}/*.whl + path: cuda_core/.moon-out/wheel-merged/*.whl + if-no-files-found: error + overwrite: true + + - name: Upload Moon cache for this build lane + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: moon-cache-build-${{ inputs.host-platform }}-py${{ matrix.python-version }} + path: | + .moon/cache/hashes + .moon/cache/outputs if-no-files-found: error + include-hidden-files: true + overwrite: true + retention-days: 30 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2aadf222306..a212524e44f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,8 +2,7 @@ # # SPDX-License-Identifier: Apache-2.0 -# Note: This name is referred to in the test job, so make sure any changes are sync'd up! -# Further this is referencing a run in the backport branch to fetch old bindings. +# This name is used when resolving an exact trusted baseline run. name: "CI" concurrency: @@ -22,223 +21,284 @@ on: - "cuda-core-v*" - "cuda-pathfinder-v*" schedule: - # every 24 hours at midnight UTC + # Every 24 hours at midnight UTC. - cron: "0 0 * * *" workflow_dispatch: {} jobs: - ci-vars: + # This is the only unconditional planning runner. Moon owns affected + # detection and writes marker outputs before any expensive runner is started. + gate: + name: Plan affected CI lanes runs-on: ubuntu-latest + permissions: + actions: read + contents: read + pull-requests: read outputs: - CUDA_BUILD_VER: ${{ steps.get-vars.outputs.cuda_build_ver }} - CUDA_PREV_BUILD_VER: ${{ steps.get-vars.outputs.cuda_prev_build_ver }} + cuda-build-ver: ${{ steps.vars.outputs.cuda-build-ver }} + cuda-prev-build-ver: ${{ steps.vars.outputs.cuda-prev-build-ver }} + skip: ${{ steps.directives.outputs.skip }} + doc-only: ${{ steps.directives.outputs.doc-only }} + moon-base: ${{ steps.baseline.outputs.moon-base }} + moon-head: ${{ steps.baseline.outputs.moon-head }} + moon-base-run-id: ${{ steps.baseline.outputs.moon-base-run-id }} + moon-base-sha: ${{ steps.baseline.outputs.moon-base-sha }} + moon-force-all: ${{ steps.markers.outputs.force-all }} + build-portable: ${{ steps.markers.outputs.build-portable }} + build-linux-64: ${{ steps.markers.outputs.build-linux-64 }} + build-linux-aarch64: ${{ steps.markers.outputs.build-linux-aarch64 }} + build-windows: ${{ steps.markers.outputs.build-windows }} + test-sdist-linux: ${{ steps.markers.outputs.test-sdist-linux }} + test-sdist-windows: ${{ steps.markers.outputs.test-sdist-windows }} + test-linux: ${{ steps.markers.outputs.test-linux }} + test-windows: ${{ steps.markers.outputs.test-windows }} + docs: ${{ steps.markers.outputs.docs }} + core-api: ${{ steps.markers.outputs.core-api }} + build-pathfinder: ${{ steps.markers.outputs.build-pathfinder }} + build-bindings: ${{ steps.markers.outputs.build-bindings }} + build-core: ${{ steps.markers.outputs.build-core }} + build-metapackage: ${{ steps.markers.outputs.build-metapackage }} + test-pathfinder: ${{ steps.markers.outputs.test-pathfinder }} + test-bindings: ${{ steps.markers.outputs.test-bindings }} + test-core: ${{ steps.markers.outputs.test-core }} + test-metapackage: ${{ steps.markers.outputs.test-metapackage }} + defaults: + run: + shell: bash --noprofile --norc -euo pipefail {0} steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: - fetch-depth: 1 - - name: Get CUDA build versions - id: get-vars - run: | - cuda_build_ver=$(yq '.cuda.build.version' ci/versions.yml) - echo "cuda_build_ver=$cuda_build_ver" >> $GITHUB_OUTPUT + # Moon needs the full commit graph for affected detection, but it + # does not need historical blobs. + fetch-depth: 0 + filter: blob:none + persist-credentials: false - cuda_prev_build_ver=$(yq '.cuda.prev_build.version' ci/versions.yml) - echo "cuda_prev_build_ver=$cuda_prev_build_ver" >> $GITHUB_OUTPUT + - name: Read CI variables + id: vars + run: | + echo "cuda-build-ver=$(yq '.cuda.build.version' ci/versions.yml)" >> "$GITHUB_OUTPUT" + echo "cuda-prev-build-ver=$(yq '.cuda.prev_build.version' ci/versions.yml)" >> "$GITHUB_OUTPUT" - should-skip: - runs-on: ubuntu-latest - outputs: - skip: ${{ steps.get-should-skip.outputs.skip }} - doc-only: ${{ steps.get-should-skip.outputs.doc_only }} - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - name: Compute whether to skip builds and tests - id: get-should-skip + - name: Read PR title directives + id: directives env: GH_TOKEN: ${{ github.token }} run: | - set -euxo pipefail - if ${{ startsWith(github.ref_name, 'pull-request/') }}; then - pr_number="$(grep -Po '(\d+)$' <<< '${{ github.ref_name }}')" - pr_title="$(gh pr view "${pr_number}" --json title --jq '.title')" - skip="$(echo "${pr_title}" | grep -q '\[no-ci\]' && echo true || echo false)" - doc_only="$(echo "${pr_title}" | grep -q '\[doc-only\]' && echo true || echo false)" - else - skip=false - doc_only=false + skip=false + doc_only=false + base_ref="" + if [[ "${GITHUB_REF_NAME}" =~ ^pull-request/([0-9]+)$ ]]; then + pr="$(gh pr view "${BASH_REMATCH[1]}" --json baseRefName,title)" + pr_title="$(jq -r '.title' <<< "${pr}")" + base_ref="$(jq -r '.baseRefName' <<< "${pr}")" + if [[ "${pr_title}" == *"[no-ci]"* ]]; then + skip=true + fi + if [[ "${pr_title}" == *"[doc-only]"* ]]; then + doc_only=true + fi fi echo "skip=${skip}" >> "$GITHUB_OUTPUT" - echo "doc_only=${doc_only}" >> "$GITHUB_OUTPUT" - - # Detect which top-level modules were touched by the PR so downstream build - # and test jobs can avoid rebuilding/retesting modules unaffected by the - # change. See issue #299. - # - # Dependency graph (verified in pyproject.toml files): - # cuda_pathfinder -> (no internal deps) - # cuda_bindings -> cuda_pathfinder - # cuda_core -> cuda_pathfinder, cuda_bindings - # cuda_python -> cuda_bindings (meta package) - # - # A change to cuda_pathfinder (or shared infra) forces a rebuild of every - # downstream module. A change to cuda_bindings forces rebuild of cuda_core. - # A change to cuda_core alone skips rebuilding/retesting cuda_bindings. - # On push to main, tag refs, schedule, or workflow_dispatch events we - # unconditionally run everything because there is no meaningful "changed - # paths" baseline for those events. - detect-changes: - runs-on: ubuntu-latest - outputs: - bindings: ${{ steps.compose.outputs.bindings }} - core: ${{ steps.compose.outputs.core }} - pathfinder: ${{ steps.compose.outputs.pathfinder }} - python_meta: ${{ steps.compose.outputs.python_meta }} - test_helpers: ${{ steps.compose.outputs.test_helpers }} - shared: ${{ steps.compose.outputs.shared }} - build_bindings: ${{ steps.compose.outputs.build_bindings }} - build_core: ${{ steps.compose.outputs.build_core }} - build_pathfinder: ${{ steps.compose.outputs.build_pathfinder }} - test_bindings: ${{ steps.compose.outputs.test_bindings }} - test_core: ${{ steps.compose.outputs.test_core }} - test_pathfinder: ${{ steps.compose.outputs.test_pathfinder }} - pr_merge_base: ${{ steps.filter.outputs.merge_base }} - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - # Treeless clone: commit graph is needed for `git merge-base` and - # `git diff --name-only` below, but historical blobs aren't. - fetch-depth: 0 - filter: blob:none + echo "doc-only=${doc_only}" >> "$GITHUB_OUTPUT" + echo "base-ref=${base_ref}" >> "$GITHUB_OUTPUT" - # copy-pr-bot pushes every PR (whether it targets main or a backport - # branch such as 12.9.x) to pull-request/, so the base branch - # cannot be inferred from github.ref_name. Look it up via the - # upstream PR metadata so the diff below is rooted at the right place. - - name: Resolve PR base branch - id: pr-info - if: ${{ startsWith(github.ref_name, 'pull-request/') }} - uses: nv-gha-runners/get-pr-info@main - - - name: Detect changed paths - id: filter - if: ${{ startsWith(github.ref_name, 'pull-request/') }} + - name: Resolve exact trusted Moon baseline + id: baseline env: - # GitHub Actions evaluates step-level `env:` expressions eagerly — - # the step's `if:` gate does NOT short-circuit them. On non-PR - # events (push/tag/schedule), `pr-info` is skipped and its outputs - # are empty strings, so `fromJSON('')` would raise a template error - # and fail the step despite `if:` being false. Guard the - # `fromJSON` call with a short-circuit so the expression resolves - # to an empty string on non-PR events; the step is still gated - # off by `if:`, so `BASE_REF` is never consumed there. - BASE_REF: ${{ steps.pr-info.outputs.pr-info && fromJSON(steps.pr-info.outputs.pr-info).base.ref || '' }} + GH_TOKEN: ${{ github.token }} + IS_PR: ${{ startsWith(github.ref_name, 'pull-request/') }} + SKIP: ${{ steps.directives.outputs.skip }} + BASE_REF: ${{ steps.directives.outputs.base-ref }} + CUDA_BUILD_VER: ${{ steps.vars.outputs.cuda-build-ver }} run: | - # Diff against the merge base with the PR's actual target branch. - # Uses merge-base so diverged branches only show files changed on - # the PR side, not upstream commits. - if [[ -z "${BASE_REF}" ]]; then - echo "Could not resolve PR base branch from get-pr-info output" >&2 - exit 1 + head="$(git rev-parse HEAD)" + base="${head}" + base_run_id="" + force_all=true + + if [[ "${SKIP}" != "true" && "${IS_PR}" == "true" ]]; then + if [[ -z "${BASE_REF}" ]]; then + echo "::error::Could not resolve the PR base branch." >&2 + exit 1 + fi + base="$(git merge-base HEAD "origin/${BASE_REF}")" + + # Only an exact successful push run on the PR's target branch is + # trusted. PR-produced artifacts are never eligible as a baseline. + runs="$(gh run list \ + --repo "${GITHUB_REPOSITORY}" \ + --workflow ci.yml \ + --branch "${BASE_REF}" \ + --commit "${base}" \ + --event push \ + --status success \ + --limit 100 \ + --json databaseId,headSha,headBranch,event,conclusion,createdAt)" + candidate="$( + jq -r \ + --arg base "${base}" \ + --arg branch "${BASE_REF}" \ + '[.[] | + select( + .headSha == $base and + .headBranch == $branch and + .event == "push" and + .conclusion == "success" + ) + ] | sort_by(.createdAt) | reverse | .[0].databaseId // empty' \ + <<< "${runs}" + )" + + if [[ -n "${candidate}" ]]; then + artifacts="$( + gh api --paginate --slurp \ + "repos/${GITHUB_REPOSITORY}/actions/runs/${candidate}/artifacts?per_page=100" \ + | jq -c '[.[].artifacts[]]' + )" + + expected=( + cuda-pathfinder-wheel + cuda-python-wheel + moon-cache-build-portable + moon-cache-sdist-linux-64 + moon-cache-sdist-win-64 + ) + python_versions=(3.10 3.11 3.12 3.13 3.14 3.14t 3.15 3.15t) + host_platforms=(linux-64 linux-aarch64 win-64) + for host_platform in "${host_platforms[@]}"; do + for python_version in "${python_versions[@]}"; do + python_version_formatted="${python_version//./}" + bindings_artifact="cuda-bindings-python${python_version_formatted}-cuda${CUDA_BUILD_VER}-${host_platform}-${base}" + core_artifact="cuda-core-python${python_version_formatted}-${host_platform}-${base}" + expected+=( + "${bindings_artifact}" + "${bindings_artifact}-tests" + "${core_artifact}" + "${core_artifact}-tests" + "${core_artifact}-test-binaries" + ) + expected+=("moon-cache-build-${host_platform}-py${python_version}") + done + done + + complete=true + for artifact_name in "${expected[@]}"; do + artifact_valid="$( + jq \ + --arg name "${artifact_name}" \ + '[.[] | select(.name == $name)] | + length == 1 and + (.[0].expired | not) and + .[0].size_in_bytes > 0 and + ((.[0].digest // "") | + test("^sha256:[0-9a-fA-F]{64}$"))' \ + <<< "${artifacts}" + )" + if [[ "${artifact_valid}" != "true" ]]; then + echo "No unique valid ${artifact_name} in run ${candidate}; forcing all lanes." + complete=false + fi + done + + if [[ "${complete}" == "true" ]]; then + base_run_id="${candidate}" + force_all=false + fi + fi + elif [[ "${SKIP}" != "true" ]]; then + # Push, tag, schedule, and manual runs deliberately exercise the + # full pipeline and publish a complete baseline for future PRs. + base="$(git rev-parse HEAD^ 2>/dev/null || git rev-parse HEAD)" fi - base=$(git merge-base HEAD "origin/${BASE_REF}") - changed=$(git diff --name-only "$base"...HEAD) - - has_match() { - grep -qE "$1" <<< "$changed" && echo true || echo false - } { - echo "bindings=$(has_match '^cuda_bindings/')" - echo "core=$(has_match '^cuda_core/')" - echo "pathfinder=$(has_match '^cuda_pathfinder/')" - echo "python_meta=$(has_match '^cuda_python/')" - echo "test_helpers=$(has_match '^cuda_python_test_helpers/')" - echo "shared=$(has_match '^(\.github/|ci/|scripts/|toolshed/|conftest\.py$|pyproject\.toml$|pixi\.(toml|lock)$|pytest\.ini$|ruff\.toml$)')" - echo "merge_base=${base}" + echo "moon-base=${base}" + echo "moon-head=${head}" + echo "moon-base-run-id=${base_run_id}" + echo "moon-base-sha=${base}" + echo "moon-force-all=${force_all}" } >> "$GITHUB_OUTPUT" - - name: Compose gating outputs - id: compose + - name: Set up Moon + if: ${{ steps.directives.outputs.skip != 'true' }} + uses: moonrepo/setup-toolchain@261c62cb5b0f580c7be7c8cd0f023a2e96756095 # v0.6.4 + with: + moon-version: "2.5.1" + auto-install: false + auto-setup: false + + - name: Validate Moon CI contracts + if: ${{ steps.directives.outputs.skip != 'true' }} + run: python -m unittest ci.tools.tests.test_moon_ci ci.tools.tests.test_moon_workspace + + - name: Materialize affected CI markers with Moon + if: ${{ steps.directives.outputs.skip != 'true' }} env: - IS_PR: ${{ startsWith(github.ref_name, 'pull-request/') }} - BINDINGS: ${{ steps.filter.outputs.bindings || 'false' }} - CORE: ${{ steps.filter.outputs.core || 'false' }} - PATHFINDER: ${{ steps.filter.outputs.pathfinder || 'false' }} - PYTHON_META: ${{ steps.filter.outputs.python_meta || 'false' }} - TEST_HELPERS: ${{ steps.filter.outputs.test_helpers || 'false' }} - SHARED: ${{ steps.filter.outputs.shared || 'false' }} + MOON_BASE: ${{ steps.baseline.outputs.moon-base }} + MOON_HEAD: ${{ steps.baseline.outputs.moon-head }} + MOON_FORCE_ALL: ${{ steps.baseline.outputs.moon-force-all }} run: | - set -euxo pipefail - # Non-PR events (push to main, tag push, schedule, workflow_dispatch) - # always exercise the full pipeline because there is no baseline for - # a meaningful diff. - if [[ "${IS_PR}" != "true" ]]; then - bindings=true - core=true - pathfinder=true - python_meta=true - test_helpers=true - shared=true - else - bindings="${BINDINGS}" - core="${CORE}" - pathfinder="${PATHFINDER}" - python_meta="${PYTHON_META}" - test_helpers="${TEST_HELPERS}" - shared="${SHARED}" + args=() + if [[ "${MOON_FORCE_ALL}" == "true" ]]; then + args+=(--force) fi + moon ci ':#ci-gate' --downstream none "${args[@]}" - or_flag() { - for v in "$@"; do - if [[ "${v}" == "true" ]]; then - echo "true" - return - fi - done - echo "false" - } - - # Build gating: pathfinder change forces rebuild of bindings and - # core; bindings change forces rebuild of core. shared changes force - # a full rebuild. - build_pathfinder="$(or_flag "${shared}" "${pathfinder}")" - build_bindings="$(or_flag "${shared}" "${pathfinder}" "${bindings}")" - build_core="$(or_flag "${shared}" "${pathfinder}" "${bindings}" "${core}")" - - # Test gating: tests for a module must run whenever that module, any - # of its runtime dependencies, the shared test helper package, or - # shared infra changes. pathfinder tests are cheap and always run. - test_pathfinder=true - test_bindings="$(or_flag "${shared}" "${pathfinder}" "${bindings}" "${test_helpers}")" - test_core="$(or_flag "${shared}" "${pathfinder}" "${bindings}" "${core}" "${test_helpers}")" - - { - echo "bindings=${bindings}" - echo "core=${core}" - echo "pathfinder=${pathfinder}" - echo "python_meta=${python_meta}" - echo "test_helpers=${test_helpers}" - echo "shared=${shared}" - echo "build_bindings=${build_bindings}" - echo "build_core=${build_core}" - echo "build_pathfinder=${build_pathfinder}" - echo "test_bindings=${test_bindings}" - echo "test_core=${test_core}" - echo "test_pathfinder=${test_pathfinder}" - } >> "$GITHUB_OUTPUT" + - name: Publish lane and module markers + id: markers + if: ${{ always() }} + env: + SKIP: ${{ steps.directives.outputs.skip }} + FORCE_ALL: ${{ steps.baseline.outputs.moon-force-all }} + run: | + force_all="${FORCE_ALL}" + if [[ -f "ci/.moon-out/ci-gates/force-all" || + -f "ci/.moon-out/ci-gates/force-all-unowned" ]]; then + force_all=true + fi + echo "force-all=${force_all}" >> "$GITHUB_OUTPUT" + + markers=( + build-portable + build-linux-64 + build-linux-aarch64 + build-windows + test-sdist-linux + test-sdist-windows + test-linux + test-windows + docs + core-api + build-pathfinder + build-bindings + build-core + build-metapackage + test-pathfinder + test-bindings + test-core + test-metapackage + ) + for marker in "${markers[@]}"; do + selected=false + if [[ "${SKIP}" != "true" ]] && + { [[ "${force_all}" == "true" ]] || + [[ -f "ci/.moon-out/ci-gates/${marker}" ]]; }; then + selected=true + fi + echo "${marker}=${selected}" >> "$GITHUB_OUTPUT" + done api-check-core-vs-release: name: API check (cuda_core vs. latest release) if: >- - ${{ !fromJSON(needs.should-skip.outputs.skip) && - fromJSON(needs.detect-changes.outputs.core) }} + ${{ !fromJSON(needs.gate.outputs.skip) && + fromJSON(needs.gate.outputs.core-api) }} runs-on: ubuntu-latest needs: - - should-skip - - detect-changes + - gate permissions: contents: read steps: @@ -254,9 +314,6 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - # --paginate fetches all pages; jq outputs one name per line per page; - # sed prints the first (newest) match while consuming all pages, so - # gh can complete without SIGPIPE. Fails if no cuda-core-v* tag is found. tag="$(gh api "repos/$GITHUB_REPOSITORY/tags" --paginate \ --jq '.[] | select(.name | startswith("cuda-core-v")) | .name' \ | sed -n '1p')" @@ -273,7 +330,6 @@ jobs: "refs/tags/${{ steps.latest-tag.outputs.tag }}:refs/tags/${{ steps.latest-tag.outputs.tag }}" - name: Check cuda_core public API - id: griffe uses: ./.github/actions/griffe-api-check with: package-name: cuda.core @@ -284,12 +340,11 @@ jobs: name: API check (cuda_core vs. merge base) if: >- ${{ startsWith(github.ref_name, 'pull-request/') && - !fromJSON(needs.should-skip.outputs.skip) && - fromJSON(needs.detect-changes.outputs.core) }} + !fromJSON(needs.gate.outputs.skip) && + fromJSON(needs.gate.outputs.core-api) }} runs-on: ubuntu-latest needs: - - should-skip - - detect-changes + - gate permissions: contents: read steps: @@ -301,212 +356,322 @@ jobs: - name: Fetch merge base commit shell: bash --noprofile --norc -euo pipefail {0} - run: | - git fetch --depth=1 --filter=blob:none origin \ - "${{ needs.detect-changes.outputs.pr_merge_base }}" + run: git fetch --depth=1 --filter=blob:none origin "${{ needs.gate.outputs.moon-base }}" - name: Check cuda_core public API - id: griffe uses: ./.github/actions/griffe-api-check with: package-name: cuda.core package-dir: cuda_core - merge-base: ${{ needs.detect-changes.outputs.pr_merge_base }} - - # NOTE: Build jobs are intentionally split by platform rather than using a single - # matrix. This allows each test job to depend only on its corresponding build, - # so faster platforms can proceed through build & test without waiting for slower - # ones. Keep these job definitions textually identical except for: - # - host-platform value - # - if: condition (build-linux-64 omits doc-only check since it's needed for docs) + merge-base: ${{ needs.gate.outputs.moon-base }} + + build-portable: + name: Build portable wheels + needs: + - gate + if: >- + ${{ github.repository_owner == 'nvidia' && + !fromJSON(needs.gate.outputs.skip) && + fromJSON(needs.gate.outputs.build-portable) && + (!fromJSON(needs.gate.outputs.doc-only) || + fromJSON(needs.gate.outputs.moon-force-all)) }} + permissions: + actions: read + contents: read + uses: ./.github/workflows/build-pure-wheel.yml + with: + build-pathfinder: ${{ fromJSON(needs.gate.outputs.build-pathfinder) }} + build-metapackage: ${{ fromJSON(needs.gate.outputs.build-metapackage) }} + moon-base: ${{ needs.gate.outputs.moon-base }} + baseline-run-id: ${{ needs.gate.outputs.moon-base-run-id }} + force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} + + # Native builds remain split by platform so their tests can start as soon as + # the corresponding platform finishes. Each reusable workflow owns its + # eight-version Python matrix and per-row Moon cache artifact. build-linux-64: + name: Build linux-64, CUDA ${{ needs.gate.outputs.cuda-build-ver }} needs: - - ci-vars - - should-skip - strategy: - fail-fast: false - matrix: - host-platform: - - linux-64 - name: Build ${{ matrix.host-platform }}, CUDA ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} - if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.skip) }} - secrets: inherit + - gate + - build-portable + if: >- + ${{ always() && + github.repository_owner == 'nvidia' && + needs.gate.result == 'success' && + (needs.build-portable.result == 'success' || + needs.build-portable.result == 'skipped') && + !fromJSON(needs.gate.outputs.skip) && + fromJSON(needs.gate.outputs.build-linux-64) && + (!fromJSON(needs.gate.outputs.doc-only) || + fromJSON(needs.gate.outputs.moon-force-all)) }} + permissions: + actions: read + contents: read uses: ./.github/workflows/build-wheel.yml with: - host-platform: ${{ matrix.host-platform }} - cuda-version: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} - prev-cuda-version: ${{ needs.ci-vars.outputs.CUDA_PREV_BUILD_VER }} + host-platform: linux-64 + cuda-version: ${{ needs.gate.outputs.cuda-build-ver }} + prev-cuda-version: ${{ needs.gate.outputs.cuda-prev-build-ver }} + build-bindings: ${{ fromJSON(needs.gate.outputs.build-bindings) }} + build-core: ${{ fromJSON(needs.gate.outputs.build-core) }} + test-bindings: ${{ fromJSON(needs.gate.outputs.test-bindings) }} + test-core: ${{ fromJSON(needs.gate.outputs.test-core) }} + baseline-run-id: ${{ needs.gate.outputs.moon-base-run-id }} + baseline-sha: ${{ needs.gate.outputs.moon-base-sha }} + portable-run-id: ${{ needs.build-portable.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} + moon-base: ${{ needs.gate.outputs.moon-base }} + force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} - # See build-linux-64 for why build jobs are split by platform. build-linux-aarch64: + name: Build linux-aarch64, CUDA ${{ needs.gate.outputs.cuda-build-ver }} needs: - - ci-vars - - should-skip - strategy: - fail-fast: false - matrix: - host-platform: - - linux-aarch64 - name: Build ${{ matrix.host-platform }}, CUDA ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} - if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.skip) && !fromJSON(needs.should-skip.outputs.doc-only) }} - secrets: inherit + - gate + - build-portable + if: >- + ${{ always() && + github.repository_owner == 'nvidia' && + needs.gate.result == 'success' && + (needs.build-portable.result == 'success' || + needs.build-portable.result == 'skipped') && + !fromJSON(needs.gate.outputs.skip) && + !fromJSON(needs.gate.outputs.doc-only) && + fromJSON(needs.gate.outputs.build-linux-aarch64) }} + permissions: + actions: read + contents: read uses: ./.github/workflows/build-wheel.yml with: - host-platform: ${{ matrix.host-platform }} - cuda-version: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} - prev-cuda-version: ${{ needs.ci-vars.outputs.CUDA_PREV_BUILD_VER }} + host-platform: linux-aarch64 + cuda-version: ${{ needs.gate.outputs.cuda-build-ver }} + prev-cuda-version: ${{ needs.gate.outputs.cuda-prev-build-ver }} + build-bindings: ${{ fromJSON(needs.gate.outputs.build-bindings) }} + build-core: ${{ fromJSON(needs.gate.outputs.build-core) }} + test-bindings: ${{ fromJSON(needs.gate.outputs.test-bindings) }} + test-core: ${{ fromJSON(needs.gate.outputs.test-core) }} + baseline-run-id: ${{ needs.gate.outputs.moon-base-run-id }} + baseline-sha: ${{ needs.gate.outputs.moon-base-sha }} + portable-run-id: ${{ needs.build-portable.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} + moon-base: ${{ needs.gate.outputs.moon-base }} + force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} - # See build-linux-64 for why build jobs are split by platform. build-windows: + name: Build win-64, CUDA ${{ needs.gate.outputs.cuda-build-ver }} needs: - - ci-vars - - should-skip - strategy: - fail-fast: false - matrix: - host-platform: - - win-64 - name: Build ${{ matrix.host-platform }}, CUDA ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} - if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.skip) && !fromJSON(needs.should-skip.outputs.doc-only) }} - secrets: inherit + - gate + - build-portable + if: >- + ${{ always() && + github.repository_owner == 'nvidia' && + needs.gate.result == 'success' && + (needs.build-portable.result == 'success' || + needs.build-portable.result == 'skipped') && + !fromJSON(needs.gate.outputs.skip) && + !fromJSON(needs.gate.outputs.doc-only) && + fromJSON(needs.gate.outputs.build-windows) }} + permissions: + actions: read + contents: read uses: ./.github/workflows/build-wheel.yml with: - host-platform: ${{ matrix.host-platform }} - cuda-version: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} - prev-cuda-version: ${{ needs.ci-vars.outputs.CUDA_PREV_BUILD_VER }} - - # NOTE: test-sdist jobs are split by platform (mirroring build-* and test-wheel-*) - # so platform-specific sources (e.g. cuda_bindings/*_windows.pyx selected by - # build_hooks.py) are exercised on their target OS. Keep these job definitions - # textually identical except for: - # - host-platform value - # - uses: (test-sdist-linux.yml vs test-sdist-windows.yml) + host-platform: win-64 + cuda-version: ${{ needs.gate.outputs.cuda-build-ver }} + prev-cuda-version: ${{ needs.gate.outputs.cuda-prev-build-ver }} + build-bindings: ${{ fromJSON(needs.gate.outputs.build-bindings) }} + build-core: ${{ fromJSON(needs.gate.outputs.build-core) }} + test-bindings: ${{ fromJSON(needs.gate.outputs.test-bindings) }} + test-core: ${{ fromJSON(needs.gate.outputs.test-core) }} + baseline-run-id: ${{ needs.gate.outputs.moon-base-run-id }} + baseline-sha: ${{ needs.gate.outputs.moon-base-sha }} + portable-run-id: ${{ needs.build-portable.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} + moon-base: ${{ needs.gate.outputs.moon-base }} + force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} + + # GitHub allocates each platform lane; Moon selects the affected sdist tasks + # and their declared package dependencies inside the reusable workflow. test-sdist-linux: - needs: - - ci-vars - - should-skip name: Test sdist linux-64 - if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.skip) && !fromJSON(needs.should-skip.outputs.doc-only) }} - secrets: inherit + needs: + - gate + if: >- + ${{ github.repository_owner == 'nvidia' && + !fromJSON(needs.gate.outputs.skip) && + !fromJSON(needs.gate.outputs.doc-only) && + fromJSON(needs.gate.outputs.test-sdist-linux) }} + permissions: + actions: read + contents: read uses: ./.github/workflows/test-sdist-linux.yml with: host-platform: linux-64 - cuda-version: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} + cuda-version: ${{ needs.gate.outputs.cuda-build-ver }} + baseline-run-id: ${{ needs.gate.outputs.moon-base-run-id }} + moon-base: ${{ needs.gate.outputs.moon-base }} + force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} - # See test-sdist-linux for why sdist test jobs are split by platform. test-sdist-windows: - needs: - - ci-vars - - should-skip name: Test sdist win-64 - if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.skip) && !fromJSON(needs.should-skip.outputs.doc-only) }} - secrets: inherit + needs: + - gate + if: >- + ${{ github.repository_owner == 'nvidia' && + !fromJSON(needs.gate.outputs.skip) && + !fromJSON(needs.gate.outputs.doc-only) && + fromJSON(needs.gate.outputs.test-sdist-windows) }} + permissions: + actions: read + contents: read uses: ./.github/workflows/test-sdist-windows.yml with: host-platform: win-64 - cuda-version: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} + cuda-version: ${{ needs.gate.outputs.cuda-build-ver }} + baseline-run-id: ${{ needs.gate.outputs.moon-base-run-id }} + moon-base: ${{ needs.gate.outputs.moon-base }} + force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} - # NOTE: Test jobs are split by platform for the same reason as build jobs (see - # build-linux-64). Keep these job definitions textually identical except for: - # - host-platform value - # - build job under needs: - # - uses: (test-wheel-linux.yml vs test-wheel-windows.yml) test-linux-64: - strategy: - fail-fast: false - matrix: - host-platform: - - linux-64 - name: Test ${{ matrix.host-platform }} - if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.doc-only) }} - permissions: - contents: read # This is required for actions/checkout + name: Test linux-64 needs: - - ci-vars - - should-skip - - detect-changes + - gate + - build-portable - build-linux-64 - secrets: inherit + if: >- + ${{ always() && + github.repository_owner == 'nvidia' && + needs.gate.result == 'success' && + (needs.build-portable.result == 'success' || + needs.build-portable.result == 'skipped') && + (needs.build-linux-64.result == 'success' || + needs.build-linux-64.result == 'skipped') && + !fromJSON(needs.gate.outputs.skip) && + !fromJSON(needs.gate.outputs.doc-only) && + fromJSON(needs.gate.outputs.test-linux) }} + permissions: + actions: read + contents: read uses: ./.github/workflows/test-wheel-linux.yml with: build-type: pull-request - host-platform: ${{ matrix.host-platform }} - build-ctk-ver: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} - nruns: ${{ (github.event_name == 'schedule' && 5) || 1}} - test-bindings: ${{ fromJSON(needs.detect-changes.outputs.test_bindings) }} + host-platform: linux-64 + build-ctk-ver: ${{ needs.gate.outputs.cuda-build-ver }} + nruns: ${{ (github.event_name == 'schedule' && 5) || 1 }} + test-pathfinder: ${{ fromJSON(needs.gate.outputs.test-pathfinder) }} + test-bindings: ${{ fromJSON(needs.gate.outputs.test-bindings) }} + test-core: ${{ fromJSON(needs.gate.outputs.test-core) }} + test-python: ${{ fromJSON(needs.gate.outputs.test-metapackage) }} + run-id: ${{ needs.build-linux-64.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} + sha: ${{ needs.build-linux-64.result == 'success' && github.sha || needs.gate.outputs.moon-base-sha }} + portable-run-id: ${{ needs.build-portable.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} + moon-base: ${{ needs.gate.outputs.moon-base }} + force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} - # See test-linux-64 for why test jobs are split by platform. test-linux-aarch64: - strategy: - fail-fast: false - matrix: - host-platform: - - linux-aarch64 - name: Test ${{ matrix.host-platform }} - if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.doc-only) }} - permissions: - contents: read # This is required for actions/checkout + name: Test linux-aarch64 needs: - - ci-vars - - should-skip - - detect-changes + - gate + - build-portable - build-linux-aarch64 - secrets: inherit + if: >- + ${{ always() && + github.repository_owner == 'nvidia' && + needs.gate.result == 'success' && + (needs.build-portable.result == 'success' || + needs.build-portable.result == 'skipped') && + (needs.build-linux-aarch64.result == 'success' || + needs.build-linux-aarch64.result == 'skipped') && + !fromJSON(needs.gate.outputs.skip) && + !fromJSON(needs.gate.outputs.doc-only) && + fromJSON(needs.gate.outputs.test-linux) }} + permissions: + actions: read + contents: read uses: ./.github/workflows/test-wheel-linux.yml with: build-type: pull-request - host-platform: ${{ matrix.host-platform }} - build-ctk-ver: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} - nruns: ${{ (github.event_name == 'schedule' && 5) || 1}} - test-bindings: ${{ fromJSON(needs.detect-changes.outputs.test_bindings) }} + host-platform: linux-aarch64 + build-ctk-ver: ${{ needs.gate.outputs.cuda-build-ver }} + nruns: ${{ (github.event_name == 'schedule' && 5) || 1 }} + test-pathfinder: ${{ fromJSON(needs.gate.outputs.test-pathfinder) }} + test-bindings: ${{ fromJSON(needs.gate.outputs.test-bindings) }} + test-core: ${{ fromJSON(needs.gate.outputs.test-core) }} + test-python: ${{ fromJSON(needs.gate.outputs.test-metapackage) }} + run-id: ${{ needs.build-linux-aarch64.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} + sha: ${{ needs.build-linux-aarch64.result == 'success' && github.sha || needs.gate.outputs.moon-base-sha }} + portable-run-id: ${{ needs.build-portable.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} + moon-base: ${{ needs.gate.outputs.moon-base }} + force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} - # See test-linux-64 for why test jobs are split by platform. test-windows: - strategy: - fail-fast: false - matrix: - host-platform: - - win-64 - name: Test ${{ matrix.host-platform }} - if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.doc-only) }} - permissions: - contents: read # This is required for actions/checkout + name: Test win-64 needs: - - ci-vars - - should-skip - - detect-changes + - gate + - build-portable - build-windows - secrets: inherit + if: >- + ${{ always() && + github.repository_owner == 'nvidia' && + needs.gate.result == 'success' && + (needs.build-portable.result == 'success' || + needs.build-portable.result == 'skipped') && + (needs.build-windows.result == 'success' || + needs.build-windows.result == 'skipped') && + !fromJSON(needs.gate.outputs.skip) && + !fromJSON(needs.gate.outputs.doc-only) && + fromJSON(needs.gate.outputs.test-windows) }} + permissions: + actions: read + contents: read uses: ./.github/workflows/test-wheel-windows.yml with: build-type: pull-request - host-platform: ${{ matrix.host-platform }} - build-ctk-ver: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} - nruns: ${{ (github.event_name == 'schedule' && 5) || 1}} - test-bindings: ${{ fromJSON(needs.detect-changes.outputs.test_bindings) }} + host-platform: win-64 + build-ctk-ver: ${{ needs.gate.outputs.cuda-build-ver }} + nruns: ${{ (github.event_name == 'schedule' && 5) || 1 }} + test-pathfinder: ${{ fromJSON(needs.gate.outputs.test-pathfinder) }} + test-bindings: ${{ fromJSON(needs.gate.outputs.test-bindings) }} + test-core: ${{ fromJSON(needs.gate.outputs.test-core) }} + test-python: ${{ fromJSON(needs.gate.outputs.test-metapackage) }} + run-id: ${{ needs.build-windows.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} + sha: ${{ needs.build-windows.result == 'success' && github.sha || needs.gate.outputs.moon-base-sha }} + portable-run-id: ${{ needs.build-portable.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} + moon-base: ${{ needs.gate.outputs.moon-base }} + force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} doc: name: Docs - if: ${{ github.repository_owner == 'nvidia' }} - # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages + needs: + - gate + - build-portable + - build-linux-64 + if: >- + ${{ always() && + github.repository_owner == 'nvidia' && + needs.gate.result == 'success' && + (needs.build-portable.result == 'success' || + needs.build-portable.result == 'skipped') && + (needs.build-linux-64.result == 'success' || + needs.build-linux-64.result == 'skipped') && + !fromJSON(needs.gate.outputs.skip) && + fromJSON(needs.gate.outputs.docs) }} permissions: + actions: read id-token: write contents: write pull-requests: write - needs: - - ci-vars - - build-linux-64 - secrets: inherit uses: ./.github/workflows/build-docs.yml with: is-release: ${{ github.ref_type == 'tag' }} + run-id: ${{ needs.build-linux-64.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} + sha: ${{ needs.build-linux-64.result == 'success' && github.sha || needs.gate.outputs.moon-base-sha }} + portable-run-id: ${{ needs.build-portable.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} + moon-base: ${{ needs.gate.outputs.moon-base }} + force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} precommit-windows: name: Pre-commit on Windows runs-on: windows-latest - if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.skip) }} + if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.gate.outputs.skip) }} needs: - - should-skip + - gate permissions: contents: read steps: @@ -519,27 +684,29 @@ jobs: - name: Set up Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: - python-version: '3.13' + python-version: "3.13" - name: Install pre-commit shell: bash - run: | - set -euxo pipefail - python -m pip install --upgrade pip pre-commit + run: python -m pip install --upgrade pip pre-commit - name: Run pre-commit shell: bash - run: | - set -euxo pipefail - SKIP=lychee pre-commit run --all-files + run: SKIP=lychee pre-commit run --all-files checks: name: Check job status - if: always() + if: ${{ always() }} runs-on: ubuntu-latest + permissions: {} needs: - - should-skip - - detect-changes + - gate + - api-check-core-vs-release + - api-check-core-vs-base + - build-portable + - build-linux-64 + - build-linux-aarch64 + - build-windows - test-sdist-linux - test-sdist-windows - test-linux-64 @@ -547,46 +714,83 @@ jobs: - test-windows - doc - precommit-windows + defaults: + run: + shell: bash --noprofile --norc -euo pipefail {0} steps: - - name: Exit + - name: Verify selected jobs run: | - # GitHub treats `result == 'skipped'` as success for required - # status checks (see CCCL gate comment + cccl#605). The previous - # `cancelled || failure` predicate let upstream build failures - # propagate as `skipped` on downstream test jobs and silently - # pass this aggregator. Adopt CCCL's `check_result` pattern: - # require an explicit `expected` status per dependency, where - # anything else (including `skipped` from a failed upstream) - # fails the gate. `if: always()` on the job still ensures this - # step runs even when needs are skipped. - if [[ "${{ needs.should-skip.outputs.skip }}" == "true" ]]; then - echo "[no-ci] - skipping aggregator checks" - exit 0 - fi - - doc_only="${{ needs.should-skip.outputs.doc-only }}" - status="success" + status=success check_result() { - name=$1; expected=$2; result=$3 - echo "Checking $name: result='$result' (expected '$expected')" - if [[ "$result" != "$expected" ]]; then - echo "::error::$name did not match expected result" - status="failed" + local name=$1 + local expected=$2 + local result=$3 + echo "Checking ${name}: result='${result}' (expected '${expected}')" + if [[ "${result}" != "${expected}" ]]; then + echo "::error::${name} did not match its expected result" + status=failed fi } + expected_for() { + if [[ "$1" == "true" ]]; then + echo success + else + echo skipped + fi + } + + check_result gate success "${{ needs.gate.result }}" + if [[ "${{ needs.gate.outputs.skip }}" == "true" ]]; then + echo "[no-ci] - no downstream jobs were selected" + [[ "${status}" == "success" ]] + exit + fi + + doc_only="${{ needs.gate.outputs.doc-only }}" + force_all="${{ needs.gate.outputs.moon-force-all }}" + + portable=false + linux_64=false + linux_aarch64=false + windows=false + if [[ "${doc_only}" != "true" || "${force_all}" == "true" ]]; then + portable="${{ needs.gate.outputs.build-portable }}" + linux_64="${{ needs.gate.outputs.build-linux-64 }}" + fi + if [[ "${doc_only}" != "true" ]]; then + linux_aarch64="${{ needs.gate.outputs.build-linux-aarch64 }}" + windows="${{ needs.gate.outputs.build-windows }}" + fi + + check_result build-portable "$(expected_for "${portable}")" "${{ needs.build-portable.result }}" + check_result build-linux-64 "$(expected_for "${linux_64}")" "${{ needs.build-linux-64.result }}" + check_result build-linux-aarch64 "$(expected_for "${linux_aarch64}")" "${{ needs.build-linux-aarch64.result }}" + check_result build-windows "$(expected_for "${windows}")" "${{ needs.build-windows.result }}" + + sdist_linux=false + sdist_windows=false + test_linux=false + test_windows=false + if [[ "${doc_only}" != "true" ]]; then + sdist_linux="${{ needs.gate.outputs.test-sdist-linux }}" + sdist_windows="${{ needs.gate.outputs.test-sdist-windows }}" + test_linux="${{ needs.gate.outputs.test-linux }}" + test_windows="${{ needs.gate.outputs.test-windows }}" + fi + check_result test-sdist-linux "$(expected_for "${sdist_linux}")" "${{ needs.test-sdist-linux.result }}" + check_result test-sdist-windows "$(expected_for "${sdist_windows}")" "${{ needs.test-sdist-windows.result }}" + check_result test-linux-64 "$(expected_for "${test_linux}")" "${{ needs.test-linux-64.result }}" + check_result test-linux-aarch64 "$(expected_for "${test_linux}")" "${{ needs.test-linux-aarch64.result }}" + check_result test-windows "$(expected_for "${test_windows}")" "${{ needs.test-windows.result }}" + + core_api="${{ needs.gate.outputs.core-api }}" + core_api_base=false + if [[ "${GITHUB_REF_NAME}" == pull-request/* ]]; then + core_api_base="${core_api}" + fi + check_result api-check-core-vs-release "$(expected_for "${core_api}")" "${{ needs.api-check-core-vs-release.result }}" + check_result api-check-core-vs-base "$(expected_for "${core_api_base}")" "${{ needs.api-check-core-vs-base.result }}" + check_result doc "$(expected_for "${{ needs.gate.outputs.docs }}")" "${{ needs.doc.result }}" + check_result precommit-windows success "${{ needs.precommit-windows.result }}" - # always expected to succeed (even in [doc-only] mode) - check_result "should-skip" "success" "${{ needs.should-skip.result }}" - check_result "detect-changes" "success" "${{ needs.detect-changes.result }}" - check_result "doc" "success" "${{ needs.doc.result }}" - check_result "precommit-windows" "success" "${{ needs.precommit-windows.result }}" - - # [doc-only] flips these from 'success' to 'skipped' - if [[ "$doc_only" == "true" ]]; then expected="skipped"; else expected="success"; fi - check_result "test-sdist-linux" "$expected" "${{ needs.test-sdist-linux.result }}" - check_result "test-sdist-windows" "$expected" "${{ needs.test-sdist-windows.result }}" - check_result "test-linux-64" "$expected" "${{ needs.test-linux-64.result }}" - check_result "test-linux-aarch64" "$expected" "${{ needs.test-linux-aarch64.result }}" - check_result "test-windows" "$expected" "${{ needs.test-windows.result }}" - - [[ "$status" == "success" ]] + [[ "${status}" == "success" ]] diff --git a/.github/workflows/release-cuda-pathfinder.yml b/.github/workflows/release-cuda-pathfinder.yml index f3d1952e9ec..83161a5305d 100644 --- a/.github/workflows/release-cuda-pathfinder.yml +++ b/.github/workflows/release-cuda-pathfinder.yml @@ -135,6 +135,7 @@ jobs: needs: prepare if: ${{ github.repository_owner == 'nvidia' }} permissions: + actions: read id-token: write contents: write pull-requests: write diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4f2c54f4509..932f6f83de3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -178,6 +178,7 @@ jobs: if: ${{ github.repository_owner == 'nvidia' }} # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: + actions: read id-token: write contents: write pull-requests: write diff --git a/.github/workflows/test-sdist-linux.yml b/.github/workflows/test-sdist-linux.yml index f0f64492f2d..16519b8d55d 100644 --- a/.github/workflows/test-sdist-linux.yml +++ b/.github/workflows/test-sdist-linux.yml @@ -11,21 +11,16 @@ on: cuda-version: required: true type: string - build-pathfinder: + baseline-run-id: required: false - default: true - type: boolean - build-bindings: - required: false - default: true - type: boolean - build-core: - required: false - default: true - type: boolean - build-python: + default: "" + type: string + moon-base: + required: true + type: string + force-all: required: false - default: true + default: false type: boolean defaults: @@ -39,9 +34,16 @@ permissions: jobs: test-sdist: name: Test sdist builds - if: ${{ inputs.build-pathfinder || inputs.build-bindings || inputs.build-core || inputs.build-python }} timeout-minutes: 60 runs-on: linux-amd64-cpu8 + env: + BUILD_CUDA_VER: ${{ inputs.cuda-version }} + CUDA_VER: ${{ inputs.cuda-version }} + HOST_PLATFORM: ${{ inputs.host-platform }} + MOON_BASE: ${{ inputs.moon-base }} + MOON_FORCE_ALL: ${{ inputs.force-all && 'true' || 'false' }} + MOON_HEAD: ${{ github.sha }} + PY_VER: "3.12" steps: - name: Checkout ${{ github.event.repository.name }} uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -56,51 +58,26 @@ jobs: with: python-version: "3.12" - - name: Install build tools - run: python -m pip install "pip>=25.3" build - - # Pure Python packages -- no CTK needed. - - name: Build cuda.pathfinder sdist and wheel-from-sdist - if: ${{ inputs.build-pathfinder }} - run: | - python -m build --sdist cuda_pathfinder/ - pip wheel --no-deps --wheel-dir cuda_pathfinder/dist cuda_pathfinder/dist/*.tar.gz - - - name: Build cuda-python sdist and wheel-from-sdist - if: ${{ inputs.build-python }} - run: | - python -m build --sdist cuda_python/ - pip wheel --no-deps --wheel-dir cuda_python/dist cuda_python/dist/*.tar.gz - - - name: Download cuda.pathfinder wheel - if: ${{ !inputs.build-pathfinder && (inputs.build-bindings || inputs.build-core) }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - name: Set up Moon + uses: moonrepo/setup-toolchain@261c62cb5b0f580c7be7c8cd0f023a2e96756095 # v0.6.4 with: - name: cuda-pathfinder-wheel - path: cuda_pathfinder/dist + moon-version: "2.5.1" + auto-install: false + auto-setup: false - - name: Constrain builds to the local cuda.pathfinder wheel - if: ${{ inputs.build-bindings }} - run: | - pathfinder_wheels=(cuda_pathfinder/dist/cuda_pathfinder-*.whl) - test "${#pathfinder_wheels[@]}" -eq 1 - test -f "${pathfinder_wheels[0]}" - mkdir -p wheel-constraints - pathfinder_uri="file://$(realpath "${pathfinder_wheels[0]}")" - printf 'cuda-pathfinder @ %s\n' "${pathfinder_uri}" | tee wheel-constraints/cuda-bindings.txt + - name: Install build tools + run: python -m pip install "pip>=25.3" build # Cython packages need CTK + sccache. # The env vars ACTIONS_CACHE_SERVICE_V2, ACTIONS_RESULTS_URL, and ACTIONS_RUNTIME_TOKEN # are exposed by this action. - name: Enable sccache - if: ${{ inputs.build-bindings || inputs.build-core }} uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # 0.0.10 with: disable_annotations: 'true' # xref: https://github.com/orgs/community/discussions/42856#discussioncomment-7678867 - name: Adding additional GHA cache-related env vars - if: ${{ inputs.build-bindings || inputs.build-core }} uses: actions/github-script@v9 with: script: | @@ -108,73 +85,71 @@ jobs: core.exportVariable('ACTIONS_RUNTIME_URL', process.env['ACTIONS_RUNTIME_URL']) - name: Setup proxy cache - if: ${{ inputs.build-bindings || inputs.build-core }} uses: nv-gha-runners/setup-proxy-cache@main continue-on-error: true with: enable-apt: true + - name: Set environment variables + env: + SHA: ${{ github.sha }} + run: ./ci/tools/env-vars build + + - name: Restore trusted exact-base Moon cache + if: ${{ inputs.baseline-run-id != '' && !inputs.force-all }} + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: moon-cache-sdist-${{ inputs.host-platform }} + path: .moon/cache + github-token: ${{ github.token }} + run-id: ${{ inputs.baseline-run-id }} + - name: Set up mini CTK - if: ${{ inputs.build-bindings || inputs.build-core }} uses: ./.github/actions/fetch_ctk continue-on-error: false with: host-platform: ${{ inputs.host-platform }} cuda-version: ${{ inputs.cuda-version }} - # cuda_bindings/setup.py parses CUDA headers at import time, so CUDA_PATH - # (set by fetch_ctk) must be available for both sdist and wheel builds. - - name: Build cuda.bindings sdist and wheel-from-sdist - if: ${{ inputs.build-bindings }} + - name: Build affected sdists and verify wheels with Moon run: | - export CUDA_PYTHON_PARALLEL_LEVEL=$(nproc) export CC="sccache cc" export CXX="sccache c++" - export PIP_BUILD_CONSTRAINT="$(pwd)/wheel-constraints/cuda-bindings.txt" - export PIP_CONSTRAINT="${PIP_BUILD_CONSTRAINT}" - python -m build --sdist cuda_bindings/ - pip wheel --no-deps --wheel-dir cuda_bindings/dist cuda_bindings/dist/*.tar.gz + args=() + if [[ "${MOON_FORCE_ALL}" == "true" ]]; then + args+=(--force) + fi + moon ci ':#ci-sdist' --downstream none "${args[@]}" - - name: Download cuda.bindings wheel - if: ${{ !inputs.build-bindings && inputs.build-core }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: cuda-bindings-python312-cuda${{ inputs.cuda-version }}-${{ inputs.host-platform }}-${{ github.sha }} - path: cuda_bindings/dist - - - name: Constrain cuda.core to the local cuda.bindings wheel - if: ${{ inputs.build-core }} + - name: Validate sdist outputs run: | - CUDA_MAJOR="$(echo '${{ inputs.cuda-version }}' | cut -d. -f1)" - pathfinder_wheels=(cuda_pathfinder/dist/cuda_pathfinder-*.whl) - bindings_wheels=(cuda_bindings/dist/cuda_bindings-"${CUDA_MAJOR}".*.whl) - test "${#pathfinder_wheels[@]}" -eq 1 - test "${#bindings_wheels[@]}" -eq 1 - test -f "${pathfinder_wheels[0]}" - test -f "${bindings_wheels[0]}" - mkdir -p wheel-constraints - pathfinder_uri="file://$(realpath "${pathfinder_wheels[0]}")" - bindings_uri="file://$(realpath "${bindings_wheels[0]}")" - { - printf 'cuda-pathfinder @ %s\n' "${pathfinder_uri}" - printf 'cuda-bindings @ %s\n' "${bindings_uri}" - } | tee wheel-constraints/cuda-core.txt - - # cuda_core sdist delegates to setuptools (no CTK needed), but - # wheel-from-sdist needs CTK and cuda-bindings (dynamic build dep via - # get_requires_for_build_wheel in build_hooks.py). - - name: Build cuda.core sdist and wheel-from-sdist - if: ${{ inputs.build-core }} - run: | - export CUDA_PYTHON_PARALLEL_LEVEL=$(nproc) - export CUDA_CORE_BUILD_MAJOR="$(echo '${{ inputs.cuda-version }}' | cut -d. -f1)" - export CC="sccache cc" - export CXX="sccache c++" - export PIP_BUILD_CONSTRAINT="$(pwd)/wheel-constraints/cuda-core.txt" - export PIP_CONSTRAINT="${PIP_BUILD_CONSTRAINT}" - python -m build --sdist cuda_core/ - pip wheel --no-deps --wheel-dir cuda_core/dist cuda_core/dist/*.tar.gz + validated=0 + for project in cuda_pathfinder cuda_bindings cuda_core cuda_python; do + output="${project}/.moon-out/sdist" + if [[ ! -d "${output}" ]]; then + continue + fi + test "$(find "${output}" -maxdepth 1 -name '*.tar.gz' -type f | wc -l)" -eq 1 + test "$(find "${output}" -maxdepth 1 -name '*.whl' -type f | wc -l)" -eq 1 + validated=$((validated + 1)) + done + test "${validated}" -gt 0 - name: Show sccache stats - if: ${{ always() && (inputs.build-bindings || inputs.build-core) }} + if: ${{ always() }} run: sccache --show-stats + + # GitHub transports Moon's portable cache between trusted exact runs; + # Moon remains responsible for hashes, hits, and output hydration. + - name: Upload portable Moon cache + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: moon-cache-sdist-${{ inputs.host-platform }} + path: | + .moon/cache/hashes + .moon/cache/outputs + if-no-files-found: error + include-hidden-files: true + overwrite: true + retention-days: 30 diff --git a/.github/workflows/test-sdist-windows.yml b/.github/workflows/test-sdist-windows.yml index 5451d20429e..96d7dc6a070 100644 --- a/.github/workflows/test-sdist-windows.yml +++ b/.github/workflows/test-sdist-windows.yml @@ -17,21 +17,16 @@ on: cuda-version: required: true type: string - build-pathfinder: + baseline-run-id: required: false - default: true - type: boolean - build-bindings: - required: false - default: true - type: boolean - build-core: - required: false - default: true - type: boolean - build-python: + default: "" + type: string + moon-base: + required: true + type: string + force-all: required: false - default: true + default: false type: boolean defaults: @@ -45,9 +40,16 @@ permissions: jobs: test-sdist: name: Test sdist builds - if: ${{ inputs.build-pathfinder || inputs.build-bindings || inputs.build-core || inputs.build-python }} timeout-minutes: 60 runs-on: windows-2022 + env: + BUILD_CUDA_VER: ${{ inputs.cuda-version }} + CUDA_VER: ${{ inputs.cuda-version }} + HOST_PLATFORM: ${{ inputs.host-platform }} + MOON_BASE: ${{ inputs.moon-base }} + MOON_FORCE_ALL: ${{ inputs.force-all && 'true' || 'false' }} + MOON_HEAD: ${{ github.sha }} + PY_VER: "3.12" steps: - name: Checkout ${{ github.event.repository.name }} uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -62,101 +64,76 @@ jobs: with: python-version: "3.12" + - name: Set up Moon + uses: moonrepo/setup-toolchain@261c62cb5b0f580c7be7c8cd0f023a2e96756095 # v0.6.4 + with: + moon-version: "2.5.1" + auto-install: false + auto-setup: false + - name: Set up MSVC - if: ${{ inputs.build-bindings || inputs.build-core }} uses: step-security/msvc-dev-cmd@22c98154b708dbd743e6f27a933cf6ceba3305c4 # v1.13.1 - name: Install build tools run: python -m pip install "pip>=25.3" build - # Pure Python packages -- no CTK needed. - - name: Build cuda.pathfinder sdist and wheel-from-sdist - if: ${{ inputs.build-pathfinder }} - run: | - python -m build --sdist cuda_pathfinder/ - pip wheel --no-deps --wheel-dir cuda_pathfinder/dist cuda_pathfinder/dist/*.tar.gz - - - name: Build cuda-python sdist and wheel-from-sdist - if: ${{ inputs.build-python }} - run: | - python -m build --sdist cuda_python/ - pip wheel --no-deps --wheel-dir cuda_python/dist cuda_python/dist/*.tar.gz - - - name: Download cuda.pathfinder wheel - if: ${{ !inputs.build-pathfinder && (inputs.build-bindings || inputs.build-core) }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: cuda-pathfinder-wheel - path: cuda_pathfinder/dist - - - name: Constrain builds to the local cuda.pathfinder wheel - if: ${{ inputs.build-bindings }} - run: | - pathfinder_wheels=(cuda_pathfinder/dist/cuda_pathfinder-*.whl) - test "${#pathfinder_wheels[@]}" -eq 1 - test -f "${pathfinder_wheels[0]}" - mkdir -p wheel-constraints - pathfinder_uri="file:///$(cygpath -am "${pathfinder_wheels[0]}")" - printf 'cuda-pathfinder @ %s\n' "${pathfinder_uri}" | tee wheel-constraints/cuda-bindings.txt - # Cython packages need CTK. No sccache on Windows (this is a correctness # smoke test, not a production build; see build-wheel.yml which also # limits sccache to Linux). - name: Set up mini CTK - if: ${{ inputs.build-bindings || inputs.build-core }} uses: ./.github/actions/fetch_ctk continue-on-error: false with: host-platform: ${{ inputs.host-platform }} cuda-version: ${{ inputs.cuda-version }} - # cuda_bindings/setup.py parses CUDA headers at import time, so CUDA_PATH - # (set by fetch_ctk) must be available for both sdist and wheel builds. - # Constraint paths are passed as native Windows paths because the pip - # subprocesses run outside Git Bash. - - name: Build cuda.bindings sdist and wheel-from-sdist - if: ${{ inputs.build-bindings }} - run: | - export CUDA_PYTHON_PARALLEL_LEVEL=$(nproc) - export PIP_BUILD_CONSTRAINT="$(cygpath -w "$(pwd)/wheel-constraints/cuda-bindings.txt")" - export PIP_CONSTRAINT="${PIP_BUILD_CONSTRAINT}" - python -m build --sdist cuda_bindings/ - pip wheel --no-deps --wheel-dir cuda_bindings/dist cuda_bindings/dist/*.tar.gz + - name: Set environment variables + env: + SHA: ${{ github.sha }} + run: ./ci/tools/env-vars build - - name: Download cuda.bindings wheel - if: ${{ !inputs.build-bindings && inputs.build-core }} + - name: Restore trusted exact-base Moon cache + if: ${{ inputs.baseline-run-id != '' && !inputs.force-all }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: cuda-bindings-python312-cuda${{ inputs.cuda-version }}-${{ inputs.host-platform }}-${{ github.sha }} - path: cuda_bindings/dist + name: moon-cache-sdist-${{ inputs.host-platform }} + path: .moon/cache + github-token: ${{ github.token }} + run-id: ${{ inputs.baseline-run-id }} - - name: Constrain cuda.core to the local cuda.bindings wheel - if: ${{ inputs.build-core }} + - name: Build affected sdists and verify wheels with Moon run: | - CUDA_MAJOR="$(echo '${{ inputs.cuda-version }}' | cut -d. -f1)" - pathfinder_wheels=(cuda_pathfinder/dist/cuda_pathfinder-*.whl) - bindings_wheels=(cuda_bindings/dist/cuda_bindings-"${CUDA_MAJOR}".*.whl) - test "${#pathfinder_wheels[@]}" -eq 1 - test "${#bindings_wheels[@]}" -eq 1 - test -f "${pathfinder_wheels[0]}" - test -f "${bindings_wheels[0]}" - mkdir -p wheel-constraints - pathfinder_uri="file:///$(cygpath -am "${pathfinder_wheels[0]}")" - bindings_uri="file:///$(cygpath -am "${bindings_wheels[0]}")" - { - printf 'cuda-pathfinder @ %s\n' "${pathfinder_uri}" - printf 'cuda-bindings @ %s\n' "${bindings_uri}" - } | tee wheel-constraints/cuda-core.txt + args=() + if [[ "${MOON_FORCE_ALL}" == "true" ]]; then + args+=(--force) + fi + moon ci ':#ci-sdist' --downstream none "${args[@]}" - # cuda_core sdist delegates to setuptools (no CTK needed), but - # wheel-from-sdist needs CTK and cuda-bindings (dynamic build dep via - # get_requires_for_build_wheel in build_hooks.py). - - name: Build cuda.core sdist and wheel-from-sdist - if: ${{ inputs.build-core }} + - name: Validate sdist outputs run: | - export CUDA_PYTHON_PARALLEL_LEVEL=$(nproc) - export CUDA_CORE_BUILD_MAJOR="$(echo '${{ inputs.cuda-version }}' | cut -d. -f1)" - export PIP_BUILD_CONSTRAINT="$(cygpath -w "$(pwd)/wheel-constraints/cuda-core.txt")" - export PIP_CONSTRAINT="${PIP_BUILD_CONSTRAINT}" - python -m build --sdist cuda_core/ - pip wheel --no-deps --wheel-dir cuda_core/dist cuda_core/dist/*.tar.gz + validated=0 + for project in cuda_pathfinder cuda_bindings cuda_core cuda_python; do + output="${project}/.moon-out/sdist" + if [[ ! -d "${output}" ]]; then + continue + fi + test "$(find "${output}" -maxdepth 1 -name '*.tar.gz' -type f | wc -l)" -eq 1 + test "$(find "${output}" -maxdepth 1 -name '*.whl' -type f | wc -l)" -eq 1 + validated=$((validated + 1)) + done + test "${validated}" -gt 0 + + # GitHub transports Moon's portable cache between trusted exact runs; + # Moon remains responsible for hashes, hits, and output hydration. + - name: Upload portable Moon cache + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: moon-cache-sdist-${{ inputs.host-platform }} + path: | + .moon/cache/hashes + .moon/cache/outputs + if-no-files-found: error + include-hidden-files: true + overwrite: true + retention-days: 30 diff --git a/.github/workflows/test-wheel-linux.yml b/.github/workflows/test-wheel-linux.yml index 8134a6844fd..6b898dfe494 100644 --- a/.github/workflows/test-wheel-linux.yml +++ b/.github/workflows/test-wheel-linux.yml @@ -40,6 +40,10 @@ on: Defaults to the current run when empty. type: string default: '' + portable-run-id: + description: "Workflow run ID containing cuda.pathfinder and metapackage wheels" + type: string + default: '' test-mode: description: > Test mode: 'standard' (default), 'nightly-pytorch', @@ -53,6 +57,14 @@ on: Defaults to github.sha (current run) when empty. type: string default: '' + moon-base: + description: "Base revision used by Moon affected checks" + type: string + default: '' + force-all: + description: "Force selected Moon tasks" + type: boolean + default: false defaults: run: @@ -109,6 +121,10 @@ jobs: name: Python ${{ matrix.PY_VER }}, CUDA ${{ matrix.CUDA_VER }} (${{ (matrix.LOCAL_CTK == '1' && 'local') || 'wheels' }}), GPU ${{ matrix.GPU }}${{ matrix.GPU_COUNT != '1' && format(' (x{0})', matrix.GPU_COUNT) || '' }}${{ matrix.FLAVOR && format(', {0}', matrix.FLAVOR) || '' }}${{ matrix.ENV.TORCH_VER && format(', {0}+{1}', matrix.ENV.TORCH_VER, matrix.ENV.TORCH_CUDA) || '' }}${{ matrix.ENV.MODE == 'nightly-numba-cuda' && ', latest' || '' }} timeout-minutes: 60 needs: compute-matrix + env: + CUDA_PYTHON_LANE: test-${{ inputs.host-platform }}-py${{ matrix.PY_VER }}-cuda${{ matrix.CUDA_VER }} + MOON_BASE: ${{ inputs.moon-base }} + MOON_HEAD: ${{ github.sha }} strategy: fail-fast: false matrix: ${{ fromJSON(needs.compute-matrix.outputs.MATRIX) }} @@ -131,6 +147,17 @@ jobs: steps: - name: Checkout ${{ github.event.repository.name }} uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + filter: blob:none + + - name: Set up Moon + if: ${{ inputs.test-mode == 'standard' }} + uses: moonrepo/setup-toolchain@261c62cb5b0f580c7be7c8cd0f023a2e96756095 # v0.6.4 + with: + moon-version: "2.5.1" + auto-install: false + auto-setup: false - name: Setup proxy cache uses: nv-gha-runners/setup-proxy-cache@main @@ -179,7 +206,7 @@ jobs: with: name: cuda-pathfinder-wheel path: ./cuda_pathfinder - run-id: ${{ inputs.run-id || github.run_id }} + run-id: ${{ inputs.portable-run-id || inputs.run-id || github.run_id }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: Download cuda-python build artifacts @@ -188,7 +215,7 @@ jobs: with: name: cuda-python-wheel path: . - run-id: ${{ inputs.run-id || github.run_id }} + run-id: ${{ inputs.portable-run-id || inputs.run-id || github.run_id }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: Download cuda.bindings build artifacts @@ -253,7 +280,7 @@ jobs: uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }}-tests - path: ${{ env.CUDA_BINDINGS_CYTHON_TESTS_DIR }} + path: cuda_bindings/.moon-out/cython-tests run-id: ${{ inputs.run-id || github.run_id }} github-token: ${{ secrets.GITHUB_TOKEN }} @@ -261,10 +288,10 @@ jobs: if: ${{ inputs.test-bindings && env.SKIP_CYTHON_TEST == '0' }} run: | pwd - ls -lahR $CUDA_BINDINGS_CYTHON_TESTS_DIR + ls -lahR cuda_bindings/.moon-out/cython-tests - name: Download cuda.core build artifacts - if: ${{ inputs.test-core }} + if: ${{ inputs.test-core || (inputs.test-python && env.BINDINGS_SOURCE == 'main') }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ env.CUDA_CORE_ARTIFACT_NAME }} @@ -273,32 +300,32 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} - name: Display structure of downloaded cuda.core build artifacts - if: ${{ inputs.test-core }} + if: ${{ inputs.test-core || (inputs.test-python && env.BINDINGS_SOURCE == 'main') }} run: | pwd ls -lahR $CUDA_CORE_ARTIFACTS_DIR - name: Download cuda.core Cython tests - if: ${{ inputs.test-core && env.SKIP_CYTHON_TEST == '0' }} + if: ${{ inputs.test-core }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-tests - path: ${{ env.CUDA_CORE_CYTHON_TESTS_DIR }} + path: cuda_core/.moon-out/cython-tests run-id: ${{ inputs.run-id || github.run_id }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: Display structure of downloaded cuda.core Cython tests - if: ${{ inputs.test-core && env.SKIP_CYTHON_TEST == '0' }} + if: ${{ inputs.test-core }} run: | pwd - ls -lahR $CUDA_CORE_CYTHON_TESTS_DIR + ls -lahR cuda_core/.moon-out/cython-tests - name: Download cuda.core test binaries if: ${{ inputs.test-core }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-test-binaries - path: ${{ env.CUDA_CORE_TEST_BINARIES_DIR }} + path: cuda_core/.moon-out/test-binaries run-id: ${{ inputs.run-id || github.run_id }} github-token: ${{ secrets.GITHUB_TOKEN }} @@ -306,7 +333,7 @@ jobs: if: ${{ inputs.test-core }} run: | pwd - ls -lahR $CUDA_CORE_TEST_BINARIES_DIR + ls -lahR cuda_core/.moon-out/test-binaries - name: Set up Python ${{ matrix.PY_VER }} uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 @@ -356,50 +383,29 @@ jobs: CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS: see_what_works CUDA_PATHFINDER_TEST_FIND_NVIDIA_HEADERS_STRICTNESS: see_what_works CUDA_PATHFINDER_TEST_FIND_NVIDIA_BITCODE_LIB_STRICTNESS: see_what_works - run: run-tests pathfinder + run: moon ci pathfinder:test-installed-linux --upstream none --downstream none ${{ (inputs.force-all || inputs.build-type != 'pull-request') && '--force' || '' }} - name: Run cuda.bindings tests if: ${{ inputs.test-mode == 'standard' && inputs.test-bindings && env.SKIP_CUDA_BINDINGS_TEST == '0' }} env: CUDA_VER: ${{ matrix.CUDA_VER }} LOCAL_CTK: ${{ matrix.LOCAL_CTK }} - run: run-tests bindings + run: moon ci bindings:test-installed-linux --upstream none --downstream none ${{ (inputs.force-all || inputs.build-type != 'pull-request') && '--force' || '' }} - name: Run cuda.bindings benchmarks (smoke test) if: ${{ inputs.test-mode == 'standard' && inputs.test-bindings && env.SKIP_CUDA_BINDINGS_TEST == '0' }} - run: | - pip install pyperf - pushd benchmarks/cuda_bindings - python run_pyperf.py --debug-single-value - popd + run: moon ci bindings-benchmarks:smoke-linux --upstream none --downstream none ${{ (inputs.force-all || inputs.build-type != 'pull-request') && '--force' || '' }} - name: Run cuda.core tests if: ${{ inputs.test-mode == 'standard' && inputs.test-core }} env: CUDA_VER: ${{ matrix.CUDA_VER }} LOCAL_CTK: ${{ matrix.LOCAL_CTK }} - run: run-tests core + run: moon ci core:test-installed-linux --upstream none --downstream none ${{ (inputs.force-all || inputs.build-type != 'pull-request') && '--force' || '' }} - name: Ensure cuda-python installable if: ${{ inputs.test-mode == 'standard' && inputs.test-python && env.BINDINGS_SOURCE == 'main' }} - run: | - # Package suites install their own dependencies. A metapackage-only - # run has no preceding suite, so install the exact local internal - # wheels in one transaction while resolving released dependencies - # such as cuda-core from the package index. - if ${{ inputs.test-bindings || inputs.test-core }}; then - dependency_args=(--no-deps) - else - dependency_args=( - ./cuda_pathfinder/cuda_pathfinder-*.whl - "${CUDA_BINDINGS_ARTIFACTS_DIR}"/cuda_bindings-*.whl - ) - fi - python_requirements=(cuda_python*.whl) - if [[ "${{ matrix.LOCAL_CTK }}" != 1 ]]; then - python_requirements=("${python_requirements[@]/%/[all]}") - fi - pip install --only-binary=:all: "${dependency_args[@]}" "${python_requirements[@]}" + run: moon ci metapackage:test-installed-linux --upstream none --downstream none ${{ (inputs.force-all || inputs.build-type != 'pull-request') && '--force' || '' }} - name: Install cuda.pathfinder extra wheels for testing if: ${{ inputs.test-mode == 'standard' && inputs.test-pathfinder }} @@ -416,7 +422,7 @@ jobs: CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS: all_must_work CUDA_PATHFINDER_TEST_FIND_NVIDIA_HEADERS_STRICTNESS: all_must_work CUDA_PATHFINDER_TEST_FIND_NVIDIA_BITCODE_LIB_STRICTNESS: all_must_work - run: run-tests pathfinder + run: moon ci pathfinder:test-installed-linux-strict --upstream none --downstream none ${{ (inputs.force-all || inputs.build-type != 'pull-request') && '--force' || '' }} # ── Nightly: install wheels + optional dep together ── - name: Install cuda-python wheels + PyTorch diff --git a/.github/workflows/test-wheel-windows.yml b/.github/workflows/test-wheel-windows.yml index 04b290b1cd0..af377de8074 100644 --- a/.github/workflows/test-wheel-windows.yml +++ b/.github/workflows/test-wheel-windows.yml @@ -40,6 +40,10 @@ on: Defaults to the current run when empty. type: string default: '' + portable-run-id: + description: "Workflow run ID containing cuda.pathfinder and metapackage wheels" + type: string + default: '' test-mode: description: > Test mode: 'standard' (default), 'nightly-pytorch', @@ -53,6 +57,14 @@ on: Defaults to github.sha (current run) when empty. type: string default: '' + moon-base: + description: "Base revision used by Moon affected checks" + type: string + default: '' + force-all: + description: "Force selected Moon tasks" + type: boolean + default: false jobs: compute-matrix: @@ -100,6 +112,10 @@ jobs: timeout-minutes: 60 # The build stage could fail but we want the CI to keep moving. needs: compute-matrix + env: + CUDA_PYTHON_LANE: test-${{ inputs.host-platform }}-py${{ matrix.PY_VER }}-cuda${{ matrix.CUDA_VER }} + MOON_BASE: ${{ inputs.moon-base }} + MOON_HEAD: ${{ github.sha }} strategy: fail-fast: false matrix: ${{ fromJSON(needs.compute-matrix.outputs.MATRIX) }} @@ -110,6 +126,17 @@ jobs: steps: - name: Checkout ${{ github.event.repository.name }} uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + filter: blob:none + + - name: Set up Moon + if: ${{ inputs.test-mode == 'standard' }} + uses: moonrepo/setup-toolchain@261c62cb5b0f580c7be7c8cd0f023a2e96756095 # v0.6.4 + with: + moon-version: "2.5.1" + auto-install: false + auto-setup: false - name: Setup proxy cache uses: nv-gha-runners/setup-proxy-cache@main @@ -168,7 +195,7 @@ jobs: with: name: cuda-pathfinder-wheel path: ./cuda_pathfinder - run-id: ${{ inputs.run-id || github.run_id }} + run-id: ${{ inputs.portable-run-id || inputs.run-id || github.run_id }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: Download cuda-python build artifacts @@ -177,7 +204,7 @@ jobs: with: name: cuda-python-wheel path: . - run-id: ${{ inputs.run-id || github.run_id }} + run-id: ${{ inputs.portable-run-id || inputs.run-id || github.run_id }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: Download cuda.bindings build artifacts @@ -233,7 +260,7 @@ jobs: uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }}-tests - path: ${{ env.CUDA_BINDINGS_CYTHON_TESTS_DIR }} + path: cuda_bindings/.moon-out/cython-tests run-id: ${{ inputs.run-id || github.run_id }} github-token: ${{ secrets.GITHUB_TOKEN }} @@ -241,10 +268,10 @@ jobs: if: ${{ inputs.test-bindings && env.SKIP_CYTHON_TEST == '0' }} run: | Get-Location - Get-ChildItem -Recurse -Force $env:CUDA_BINDINGS_CYTHON_TESTS_DIR | Select-Object Mode, LastWriteTime, Length, FullName + Get-ChildItem -Recurse -Force cuda_bindings/.moon-out/cython-tests | Select-Object Mode, LastWriteTime, Length, FullName - name: Download cuda.core build artifacts - if: ${{ inputs.test-core }} + if: ${{ inputs.test-core || (inputs.test-python && env.BINDINGS_SOURCE == 'main') }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ env.CUDA_CORE_ARTIFACT_NAME }} @@ -253,32 +280,32 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} - name: Display structure of downloaded cuda.core build artifacts - if: ${{ inputs.test-core }} + if: ${{ inputs.test-core || (inputs.test-python && env.BINDINGS_SOURCE == 'main') }} run: | Get-Location Get-ChildItem -Recurse -Force $env:CUDA_CORE_ARTIFACTS_DIR | Select-Object Mode, LastWriteTime, Length, FullName - name: Download cuda.core Cython tests - if: ${{ inputs.test-core && env.SKIP_CYTHON_TEST == '0' }} + if: ${{ inputs.test-core }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-tests - path: ${{ env.CUDA_CORE_CYTHON_TESTS_DIR }} + path: cuda_core/.moon-out/cython-tests run-id: ${{ inputs.run-id || github.run_id }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: Display structure of downloaded cuda.core Cython tests - if: ${{ inputs.test-core && env.SKIP_CYTHON_TEST == '0' }} + if: ${{ inputs.test-core }} run: | Get-Location - Get-ChildItem -Recurse -Force $env:CUDA_CORE_CYTHON_TESTS_DIR | Select-Object Mode, LastWriteTime, Length, FullName + Get-ChildItem -Recurse -Force cuda_core/.moon-out/cython-tests | Select-Object Mode, LastWriteTime, Length, FullName - name: Download cuda.core test binaries if: ${{ inputs.test-core }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-test-binaries - path: ${{ env.CUDA_CORE_TEST_BINARIES_DIR }} + path: cuda_core/.moon-out/test-binaries run-id: ${{ inputs.run-id || github.run_id }} github-token: ${{ secrets.GITHUB_TOKEN }} @@ -286,7 +313,7 @@ jobs: if: ${{ inputs.test-core }} run: | Get-Location - Get-ChildItem -Recurse -Force $env:CUDA_CORE_TEST_BINARIES_DIR | Select-Object Mode, LastWriteTime, Length, FullName + Get-ChildItem -Recurse -Force cuda_core/.moon-out/test-binaries | Select-Object Mode, LastWriteTime, Length, FullName - name: Set up Python ${{ matrix.PY_VER }} uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 @@ -332,7 +359,7 @@ jobs: CUDA_PATHFINDER_TEST_FIND_NVIDIA_HEADERS_STRICTNESS: see_what_works CUDA_PATHFINDER_TEST_FIND_NVIDIA_BITCODE_LIB_STRICTNESS: see_what_works shell: bash --noprofile --norc -xeuo pipefail {0} - run: run-tests pathfinder + run: moon ci pathfinder:test-installed-windows --upstream none --downstream none ${{ (inputs.force-all || inputs.build-type != 'pull-request') && '--force' || '' }} - name: Run cuda.bindings tests if: ${{ inputs.test-mode == 'standard' && inputs.test-bindings && env.SKIP_CUDA_BINDINGS_TEST == '0' }} @@ -340,7 +367,7 @@ jobs: CUDA_VER: ${{ matrix.CUDA_VER }} LOCAL_CTK: ${{ matrix.LOCAL_CTK }} shell: bash --noprofile --norc -xeuo pipefail {0} - run: run-tests bindings + run: moon ci bindings:test-installed-windows --upstream none --downstream none ${{ (inputs.force-all || inputs.build-type != 'pull-request') && '--force' || '' }} - name: Run cuda.core tests if: ${{ inputs.test-mode == 'standard' && inputs.test-core }} @@ -348,28 +375,11 @@ jobs: CUDA_VER: ${{ matrix.CUDA_VER }} LOCAL_CTK: ${{ matrix.LOCAL_CTK }} shell: bash --noprofile --norc -xeuo pipefail {0} - run: run-tests core + run: moon ci core:test-installed-windows --upstream none --downstream none ${{ (inputs.force-all || inputs.build-type != 'pull-request') && '--force' || '' }} - name: Ensure cuda-python installable if: ${{ inputs.test-mode == 'standard' && inputs.test-python && env.BINDINGS_SOURCE == 'main' }} - run: | - # Package suites install their own dependencies. A metapackage-only - # run has no preceding suite, so install the exact local internal - # wheels in one transaction while resolving released dependencies - # such as cuda-core from the package index. - if ('${{ inputs.test-bindings || inputs.test-core }}' -eq 'true') { - $dependencyArgs = @('--no-deps') - } else { - $dependencyArgs = @( - (Get-Item ./cuda_pathfinder/cuda_pathfinder-*.whl).FullName - (Get-Item "$env:CUDA_BINDINGS_ARTIFACTS_DIR/cuda_bindings-*.whl").FullName - ) - } - $pythonRequirements = @((Get-Item ./cuda_python*.whl).FullName) - if ('${{ matrix.LOCAL_CTK }}' -ne '1') { - $pythonRequirements = @($pythonRequirements | ForEach-Object { "$($_)[all]" }) - } - pip install --only-binary=:all: @dependencyArgs @pythonRequirements + run: moon ci metapackage:test-installed-windows --upstream none --downstream none ${{ (inputs.force-all || inputs.build-type != 'pull-request') && '--force' || '' }} - name: Install cuda.pathfinder extra wheels for testing if: ${{ inputs.test-mode == 'standard' && inputs.test-pathfinder }} @@ -387,7 +397,7 @@ jobs: CUDA_PATHFINDER_TEST_FIND_NVIDIA_HEADERS_STRICTNESS: all_must_work CUDA_PATHFINDER_TEST_FIND_NVIDIA_BITCODE_LIB_STRICTNESS: all_must_work shell: bash --noprofile --norc -xeuo pipefail {0} - run: run-tests pathfinder + run: moon ci pathfinder:test-installed-windows-strict --upstream none --downstream none ${{ (inputs.force-all || inputs.build-type != 'pull-request') && '--force' || '' }} # ── Nightly: install wheels + optional dep together ── - name: Install Visual C++ Redistributable (required by PyTorch on Windows) diff --git a/.gitignore b/.gitignore index 6b6a7dfc0b5..aa55a675b56 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ __pycache__/ .cache/ .lycheecache .pytest_cache/ +.moon/cache/ +.moon-out/ .benchmarks/ *.cpp !*_impl.cpp diff --git a/.moon/workspace.yml b/.moon/workspace.yml new file mode 100644 index 00000000000..eb5fce4662e --- /dev/null +++ b/.moon/workspace.yml @@ -0,0 +1,41 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +$schema: https://moonrepo.dev/schemas/v2/workspace.json + +versionConstraint: '=2.5.1' + +projects: + root: '.' + ci: 'ci' + pathfinder: 'cuda_pathfinder' + bindings: 'cuda_bindings' + core: 'cuda_core' + metapackage: 'cuda_python' + test-helpers: 'cuda_python_test_helpers' + bindings-benchmarks: 'benchmarks/cuda_bindings' + +vcs: + client: git + provider: github + defaultBranch: main + remoteCandidates: + - origin + - upstream + +pipeline: + installDependencies: false + syncProjects: false + syncWorkspace: false + +cache: + cas: + verifyIntegrity: true + +experiments: + asyncAffectedTracking: false + asyncGraphBuilding: false + nativeFileHashing: false + +telemetry: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7474ac4d840..868758e9eaf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,6 +30,7 @@ Thank you for your interest in contributing to CUDA Python! Based on the type of - [Code signing](#code-signing) - [Developer Certificate of Origin (DCO)](#developer-certificate-of-origin-dco) - [CI infrastructure overview](#ci-infrastructure-overview) + - [Local and CI task orchestration with Moon](#local-and-ci-task-orchestration-with-moon) - [CI Pipeline Flow](#ci-pipeline-flow) - [Pipeline Execution Details](#pipeline-execution-details) - [Branch-specific Artifact Flow](#branch-specific-artifact-flow) @@ -253,6 +254,40 @@ By making a contribution to this project, I certify that: The CUDA Python project uses a comprehensive CI pipeline that builds, tests, and releases multiple components across different platforms. This section provides a visual overview of our CI infrastructure to help contributors understand the build and release process. +### Local and CI task orchestration with Moon + +The draft CI orchestration spike uses [Moon 2.5.1](https://moonrepo.dev/moon) as the task graph and execution +engine for both local development and CI. Moon uses only the system toolchain in this repository: it does not +install, select, or configure Python, and it does not create Python environments. Existing Pixi and uv commands +remain supported and can still be invoked directly; Moon delegates to the environment that the contributor or CI +runner has already prepared. + +Use Moon to inspect the graph, run one task locally, or execute the affected portion of the graph as CI does: + +```console +$ moon projects +$ moon tasks +$ moon run : +$ moon run metapackage:wheel-pure +$ moon run root:test +$ moon run root:pure-wheel +$ MOON_BASE=origin/main MOON_HEAD=HEAD moon ci ':#ci-test-linux' --downstream none +``` + +CI workers follow Moon's CI model: after GitHub Actions provisions the required Python, CUDA toolkit, compiler, or +GPU, the worker invokes a tagged `moon ci` target group. Moon owns affected selection, task dependencies, command +execution, cache hits, and output hydration. GitHub Actions retains heterogeneous runner allocation, credentials, +and release or Pages publishing. The explicit `--downstream none` keeps work from crossing those runner-class +boundaries; upstream dependencies remain part of the Moon task graph where they share a cache and environment. + +For ephemeral runners, CI uploads the portable `.moon/cache/hashes` and `.moon/cache/outputs` directories as +ordinary immutable GitHub workflow artifacts. A later producer restores the lane-qualified artifact from the +successful trusted `main` run at the exact merge-base commit, then runs `moon ci`; GitHub transports the local cache +while Moon alone interprets its hashes and hydrates task outputs. Conventional wheel artifacts remain the +cross-runner input to GPU tests and release tooling. Missing or incomplete cache artifacts conservatively allocate +the producer runners and start with an empty cache. Generated `.moon/cache` and `.moon-out` directories are ignored +by Git. + ### CI Pipeline Flow ![CUDA Python CI Pipeline Flow](ci/ci-pipeline.svg) diff --git a/benchmarks/cuda_bindings/moon.yml b/benchmarks/cuda_bindings/moon.yml new file mode 100644 index 00000000000..37c39fe0e25 --- /dev/null +++ b/benchmarks/cuda_bindings/moon.yml @@ -0,0 +1,80 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +$schema: https://moonrepo.dev/schemas/v2/project.json + +language: unknown +layer: tool +dependsOn: + - id: bindings + scope: development + - id: pathfinder + scope: development +toolchains: + default: system + +taskOptions: + cache: false + runFromWorkspaceRoot: true + runInCI: false + +fileGroups: + benchmarks: + - 'benchmarks/**/*' + - 'runner/**/*' + - 'compare.py' + - 'run_cpp.py' + - 'run_pyperf.py' + - 'pixi.toml' + - 'pixi.lock' + +tasks: + bench: + command: python + args: [ci/tools/moon_ci.py, pixi-test, bindings-benchmarks, --task, bench, --environment, source] + inputs: + - '@group(benchmarks)' + - '/cuda_bindings/**/*' + - '/ci/tools/moon_ci.py' + + smoke: + command: python + args: + - ci/tools/moon_ci.py + - pixi-test + - bindings-benchmarks + - --task + - bench-smoke-test + - --environment + - source + inputs: + - '@group(benchmarks)' + - '/cuda_bindings/**/*' + - '/ci/tools/moon_ci.py' + + smoke-linux: + command: python + args: + - ci/tools/moon_ci.py + - pixi-test + - bindings-benchmarks + - --task + - bench-smoke-test + - --environment + - source + inputs: + - '@group(benchmarks)' + - {project: bindings, group: package} + - {project: pathfinder, group: package} + - '/ci/tools/moon_ci.py' + - '/ci/test-matrix.yml' + - '/ci/tools/env-vars' + - '/ci/tools/guess_latest.sh' + - '/ci/tools/install_gpu_driver.sh' + - '/tests/**/*' + - '/.github/workflows/test-wheel-linux.yml' + tags: [ci-test-linux] + type: test + options: + runInCI: true diff --git a/ci/moon.yml b/ci/moon.yml new file mode 100644 index 00000000000..968db00ef08 --- /dev/null +++ b/ci/moon.yml @@ -0,0 +1,473 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +$schema: https://moonrepo.dev/schemas/v2/project.json + +language: unknown +layer: automation +dependsOn: + - id: pathfinder + scope: development + - id: bindings + scope: development + - id: core + scope: development + - id: metapackage + scope: development + - id: bindings-benchmarks + scope: development +toolchains: + default: system + +taskOptions: + cache: false + internal: false + runFromWorkspaceRoot: true + runInCI: true + +# Runner allocation is modeled as ordinary affected Moon tasks. Package-owned +# file groups remain the source of truth; these tasks only map them onto the +# heterogeneous runner classes that GitHub Actions must allocate. +fileGroups: + orchestration: + - '/.moon/**/*' + - '/moon.yml' + - '/**/moon.yml' + - '/ci/moon.yml' + - '/ci/tools/moon_ci.py' + - '/ci/tools/moon_fingerprint.py' + - '/ci/tools/env-vars' + - '/ci/versions.yml' + - '/.github/actions/**/*' + - '/.github/workflows/ci.yml' + - '/.git_archival.txt' + - '/.gitattributes' + - '/pixi.toml' + - '/pixi.lock' + - '/pytest.ini' + - '/ruff.toml' + + test-common: + - '/tests/**/*' + - '/ci/test-matrix.yml' + - '/ci/tools/env-vars' + test-library-runner: + - '/ci/tools/run-tests' + test-helpers: + - '/cuda_python_test_helpers/**/*' + test-assets-bindings: + - '/cuda_bindings/tests/cython/**/*' + test-assets-core: + - '/cuda_core/tests/cython/**/*' + - '/cuda_core/tests/test_binaries/**/*' + + build-portable: + - '/.github/workflows/build-pure-wheel.yml' + build-native-common: + - '/.github/workflows/build-wheel.yml' + - '/.github/actions/fetch_ctk/**/*' + - '/ci/versions.yml' + - '/ci/tools/env-vars' + build-native-core: + - '/ci/tools/merge_cuda_core_wheels.py' + sdist-linux: + - '/.github/workflows/test-sdist-linux.yml' + sdist-windows: + - '/.github/workflows/test-sdist-windows.yml' + test-linux-common: + - '/.github/workflows/test-wheel-linux.yml' + - '/ci/tools/guess_latest.sh' + - '/ci/tools/install_gpu_driver.sh' + test-linux-native: + - '/ci/tools/setup-sanitizer' + test-windows-common: + - '/.github/workflows/test-wheel-windows.yml' + - '/ci/tools/configure_driver_mode.ps1' + - '/ci/tools/install_gpu_driver.ps1' + docs: + - '/.github/workflows/build-docs.yml' + + # A new or otherwise unowned path must never silently suppress CI. Known + # source, test, docs, and platform inputs are excluded because their precise + # gates handle them; anything left forces the complete pipeline. + unowned: + - '/**/*' + - '!/.moon/cache/**/*' + - '!/.pixi/**/*' + - '!/**/.moon-out/**/*' + - '!/**/AGENTS.md' + - '!/**/CLAUDE.md' + - '!/README.md' + - '!/CONTRIBUTING.md' + - '!/SECURITY.md' + - '!/benchmarks/cuda_bindings/README.md' + - '!/benchmarks/cuda_core/README.md' + - '!/ci/ci-pipeline.svg' + - '!/cuda_bindings/README.md' + - '!/cuda_core/README.md' + - '!/cuda_python/README.md' + - '!/toolshed/README.md' + - '!/.agents/**/*' + - '!/.github/actions/**/*' + - '!/.github/workflows/ci.yml' + - '!/.github/workflows/build-docs.yml' + - '!/.github/workflows/build-pure-wheel.yml' + - '!/.github/workflows/build-wheel.yml' + - '!/.github/workflows/test-sdist-linux.yml' + - '!/.github/workflows/test-sdist-windows.yml' + - '!/.github/workflows/test-wheel-linux.yml' + - '!/.github/workflows/test-wheel-windows.yml' + - '!/.moon/**/*' + - '!/benchmarks/cuda_bindings/**/*' + - '!/ci/moon.yml' + - '!/ci/test-matrix.yml' + - '!/ci/versions.yml' + - '!/ci/tools/configure_driver_mode.ps1' + - '!/ci/tools/env-vars' + - '!/ci/tools/guess_latest.sh' + - '!/ci/tools/install_gpu_driver.ps1' + - '!/ci/tools/install_gpu_driver.sh' + - '!/ci/tools/merge_cuda_core_wheels.py' + - '!/ci/tools/moon_ci.py' + - '!/ci/tools/moon_fingerprint.py' + - '!/ci/tools/run-tests' + - '!/ci/tools/setup-sanitizer' + - '!/cuda_bindings/cuda/**/*' + - '!/cuda_bindings/docs/**/*' + - '!/cuda_bindings/examples/**/*' + - '!/cuda_bindings/tests/**/*' + - '!/cuda_bindings/.git_archival.txt' + - '!/cuda_bindings/DESCRIPTION.rst' + - '!/cuda_bindings/LICENSE' + - '!/cuda_bindings/MANIFEST.in' + - '!/cuda_bindings/build_hooks.py' + - '!/cuda_bindings/moon.yml' + - '!/cuda_bindings/pixi.lock' + - '!/cuda_bindings/pixi.toml' + - '!/cuda_bindings/pyproject.toml' + - '!/cuda_bindings/setup.py' + - '!/cuda_core/cuda/**/*' + - '!/cuda_core/docs/**/*' + - '!/cuda_core/examples/**/*' + - '!/cuda_core/tests/**/*' + - '!/cuda_core/.git_archival.txt' + - '!/cuda_core/DESCRIPTION.rst' + - '!/cuda_core/LICENSE' + - '!/cuda_core/MANIFEST.in' + - '!/cuda_core/NOTICE' + - '!/cuda_core/build_hooks.py' + - '!/cuda_core/moon.yml' + - '!/cuda_core/pixi.lock' + - '!/cuda_core/pixi.toml' + - '!/cuda_core/pyproject.toml' + - '!/cuda_core/pytest.ini' + - '!/cuda_core/setup.py' + - '!/cuda_pathfinder/cuda/**/*' + - '!/cuda_pathfinder/docs/**/*' + - '!/cuda_pathfinder/examples/**/*' + - '!/cuda_pathfinder/tests/**/*' + - '!/cuda_pathfinder/.git_archival.txt' + - '!/cuda_pathfinder/DESCRIPTION.rst' + - '!/cuda_pathfinder/LICENSE' + - '!/cuda_pathfinder/moon.yml' + - '!/cuda_pathfinder/pixi.lock' + - '!/cuda_pathfinder/pixi.toml' + - '!/cuda_pathfinder/pyproject.toml' + - '!/cuda_python/docs/**/*' + - '!/cuda_python/DESCRIPTION.rst' + - '!/cuda_python/LICENSE' + - '!/cuda_python/moon.yml' + - '!/cuda_python/pyproject.toml' + - '!/cuda_python/setup.py' + - '!/cuda_python_test_helpers/**/*' + - '!/moon.yml' + - '!/pixi.lock' + - '!/pixi.toml' + - '!/pytest.ini' + - '!/ruff.toml' + - '!/tests/**/*' + - '!/.git_archival.txt' + - '!/.gitattributes' + - '!/.gitignore' + - '!/.pre-commit-config.yaml' + - '!/.spdx-ignore' + - '!/LICENSE' + +tasks: + gate-force-all: + command: python + args: [ci/tools/moon_ci.py, gate, force-all] + inputs: + - '@group(orchestration)' + outputs: ['.moon-out/ci-gates/force-all'] + tags: [ci-gate] + + gate-force-all-unowned: + command: python + args: [ci/tools/moon_ci.py, gate, force-all-unowned] + inputs: + - '@group(unowned)' + outputs: ['.moon-out/ci-gates/force-all-unowned'] + tags: [ci-gate] + + gate-build-portable: + command: python + args: [ci/tools/moon_ci.py, gate, build-portable] + inputs: + - {project: pathfinder, group: package} + - {project: bindings, group: package} + - {project: metapackage, group: package} + - '@group(build-portable)' + outputs: ['.moon-out/ci-gates/build-portable'] + tags: [ci-gate] + + gate-build-linux-64: + command: python + args: [ci/tools/moon_ci.py, gate, build-linux-64] + inputs: + - {project: pathfinder, group: package} + - {project: bindings, group: package} + - {project: core, group: package} + - '@group(build-portable)' + - '@group(build-native-common)' + - '@group(build-native-core)' + - '@group(test-assets-bindings)' + - '@group(test-assets-core)' + outputs: ['.moon-out/ci-gates/build-linux-64'] + tags: [ci-gate] + + gate-build-linux-aarch64: + command: python + args: [ci/tools/moon_ci.py, gate, build-linux-aarch64] + inputs: + - {project: pathfinder, group: package} + - {project: bindings, group: package} + - {project: core, group: package} + - '@group(build-portable)' + - '@group(build-native-common)' + - '@group(build-native-core)' + - '@group(test-assets-bindings)' + - '@group(test-assets-core)' + outputs: ['.moon-out/ci-gates/build-linux-aarch64'] + tags: [ci-gate] + + gate-build-windows: + command: python + args: [ci/tools/moon_ci.py, gate, build-windows] + inputs: + - {project: pathfinder, group: package} + - {project: bindings, group: package} + - {project: core, group: package} + - '@group(build-portable)' + - '@group(build-native-common)' + - '@group(build-native-core)' + - '@group(test-assets-bindings)' + - '@group(test-assets-core)' + outputs: ['.moon-out/ci-gates/build-windows'] + tags: [ci-gate] + + gate-test-sdist-linux: + command: python + args: [ci/tools/moon_ci.py, gate, test-sdist-linux] + inputs: + - {project: pathfinder, group: package} + - {project: bindings, group: package} + - {project: core, group: package} + - {project: metapackage, group: package} + - '@group(sdist-linux)' + outputs: ['.moon-out/ci-gates/test-sdist-linux'] + tags: [ci-gate] + + gate-test-sdist-windows: + command: python + args: [ci/tools/moon_ci.py, gate, test-sdist-windows] + inputs: + - {project: pathfinder, group: package} + - {project: bindings, group: package} + - {project: core, group: package} + - {project: metapackage, group: package} + - '@group(sdist-windows)' + outputs: ['.moon-out/ci-gates/test-sdist-windows'] + tags: [ci-gate] + + gate-test-linux: + command: python + args: [ci/tools/moon_ci.py, gate, test-linux] + inputs: + - {project: pathfinder, group: package} + - {project: pathfinder, group: tests} + - {project: bindings, group: package} + - {project: bindings, group: tests} + - {project: core, group: package} + - {project: core, group: tests} + - {project: metapackage, group: package} + - '@group(build-portable)' + - '@group(build-native-common)' + - '@group(build-native-core)' + - '@group(test-common)' + - '@group(test-library-runner)' + - '@group(test-helpers)' + - {project: bindings-benchmarks, group: benchmarks} + - '@group(test-linux-common)' + - '@group(test-linux-native)' + outputs: ['.moon-out/ci-gates/test-linux'] + tags: [ci-gate] + + gate-test-windows: + command: python + args: [ci/tools/moon_ci.py, gate, test-windows] + inputs: + - {project: pathfinder, group: package} + - {project: pathfinder, group: tests} + - {project: bindings, group: package} + - {project: bindings, group: tests} + - {project: core, group: package} + - {project: core, group: tests} + - {project: metapackage, group: package} + - '@group(build-portable)' + - '@group(build-native-common)' + - '@group(build-native-core)' + - '@group(test-common)' + - '@group(test-library-runner)' + - '@group(test-helpers)' + - '@group(test-windows-common)' + outputs: ['.moon-out/ci-gates/test-windows'] + tags: [ci-gate] + + gate-docs: + command: python + args: [ci/tools/moon_ci.py, gate, docs] + inputs: + - {project: pathfinder, group: package} + - {project: pathfinder, group: docs} + - {project: bindings, group: package} + - {project: bindings, group: docs} + - {project: core, group: package} + - {project: core, group: docs} + - {project: metapackage, group: package} + - {project: metapackage, group: docs} + - '@group(docs)' + outputs: ['.moon-out/ci-gates/docs'] + tags: [ci-gate] + + gate-core-api: + command: python + args: [ci/tools/moon_ci.py, gate, core-api] + inputs: + - '/cuda_core/cuda/core/**/*' + outputs: ['.moon-out/ci-gates/core-api'] + tags: [ci-gate] + + gate-build-pathfinder: + command: python + args: [ci/tools/moon_ci.py, gate, build-pathfinder] + inputs: + - {project: pathfinder, group: package} + - '@group(build-portable)' + outputs: ['.moon-out/ci-gates/build-pathfinder'] + tags: [ci-gate] + + gate-build-bindings: + command: python + args: [ci/tools/moon_ci.py, gate, build-bindings] + inputs: + - {project: pathfinder, group: package} + - {project: bindings, group: package} + - '@group(build-portable)' + - '@group(build-native-common)' + outputs: ['.moon-out/ci-gates/build-bindings'] + tags: [ci-gate] + + gate-build-core: + command: python + args: [ci/tools/moon_ci.py, gate, build-core] + inputs: + - {project: pathfinder, group: package} + - {project: bindings, group: package} + - {project: core, group: package} + - '@group(build-portable)' + - '@group(build-native-common)' + - '@group(build-native-core)' + outputs: ['.moon-out/ci-gates/build-core'] + tags: [ci-gate] + + gate-build-metapackage: + command: python + args: [ci/tools/moon_ci.py, gate, build-metapackage] + inputs: + - {project: bindings, group: package} + - {project: metapackage, group: package} + - '@group(build-portable)' + outputs: ['.moon-out/ci-gates/build-metapackage'] + tags: [ci-gate] + + gate-test-pathfinder: + command: python + args: [ci/tools/moon_ci.py, gate, test-pathfinder] + inputs: + - {project: pathfinder, group: package} + - {project: pathfinder, group: tests} + - '@group(build-portable)' + - '@group(test-common)' + - '@group(test-library-runner)' + - '@group(test-linux-common)' + - '@group(test-windows-common)' + outputs: ['.moon-out/ci-gates/test-pathfinder'] + tags: [ci-gate] + + gate-test-bindings: + command: python + args: [ci/tools/moon_ci.py, gate, test-bindings] + inputs: + - {project: pathfinder, group: package} + - {project: bindings, group: package} + - {project: bindings, group: tests} + - '@group(build-portable)' + - '@group(build-native-common)' + - '@group(test-common)' + - '@group(test-library-runner)' + - '@group(test-helpers)' + - {project: bindings-benchmarks, group: benchmarks} + - '@group(test-linux-common)' + - '@group(test-linux-native)' + - '@group(test-windows-common)' + outputs: ['.moon-out/ci-gates/test-bindings'] + tags: [ci-gate] + + gate-test-core: + command: python + args: [ci/tools/moon_ci.py, gate, test-core] + inputs: + - {project: pathfinder, group: package} + - {project: bindings, group: package} + - {project: core, group: package} + - {project: core, group: tests} + - '@group(build-portable)' + - '@group(build-native-common)' + - '@group(build-native-core)' + - '@group(test-common)' + - '@group(test-library-runner)' + - '@group(test-helpers)' + - '@group(test-linux-common)' + - '@group(test-linux-native)' + - '@group(test-windows-common)' + outputs: ['.moon-out/ci-gates/test-core'] + tags: [ci-gate] + + gate-test-metapackage: + command: python + args: [ci/tools/moon_ci.py, gate, test-metapackage] + inputs: + - {project: pathfinder, group: package} + - {project: bindings, group: package} + - {project: core, group: package} + - {project: metapackage, group: package} + - '@group(build-portable)' + - '@group(build-native-common)' + - '@group(build-native-core)' + - '@group(test-common)' + - '@group(test-linux-common)' + - '@group(test-windows-common)' + outputs: ['.moon-out/ci-gates/test-metapackage'] + tags: [ci-gate] diff --git a/ci/tools/moon_ci.py b/ci/tools/moon_ci.py new file mode 100644 index 00000000000..1b6dd6097a1 --- /dev/null +++ b/ci/tools/moon_ci.py @@ -0,0 +1,559 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Cross-platform commands used by Moon's local and CI task graph. + +Moon intentionally uses the system toolchain for this repository. These +commands consume the Python environment prepared by a contributor or CI and +continue to delegate local development tasks to Pixi. +""" + +from __future__ import annotations + +import argparse +import os +import shutil +import subprocess +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] +PROJECT_PATHS = { + "root": Path("."), + "ci": Path("ci"), + "pathfinder": Path("cuda_pathfinder"), + "bindings": Path("cuda_bindings"), + "core": Path("cuda_core"), + "metapackage": Path("cuda_python"), + "bindings-benchmarks": Path("benchmarks/cuda_bindings"), +} +DOC_TASKS = { + "pathfinder": "build-docs", + "bindings": "build-docs", + "core": "docs-build", +} +PACKAGE_PROJECTS = ("pathfinder", "bindings", "core", "metapackage") +CYTHON_PROJECTS = ("bindings", "core") +GATE_MARKERS = { + "force-all", + "force-all-unowned", + "build-portable", + "build-linux-64", + "build-linux-aarch64", + "build-windows", + "test-sdist-linux", + "test-sdist-windows", + "test-linux", + "test-windows", + "docs", + "core-api", + "build-pathfinder", + "build-bindings", + "build-core", + "build-metapackage", + "test-pathfinder", + "test-bindings", + "test-core", + "test-metapackage", +} + + +def _run( + command: list[str], + *, + cwd: Path = REPO_ROOT, + env: dict[str, str] | None = None, +) -> None: + print(f"+ {subprocess.list2cmdline(command)}", flush=True) + subprocess.run(command, cwd=cwd, env=env, check=True) # noqa: S603 + + +def _project_path(project: str) -> Path: + try: + relative = PROJECT_PATHS[project] + except KeyError as error: + raise ValueError(f"unknown project: {project}") from error + return REPO_ROOT / relative + + +def _output_path(project: str, directory: str) -> Path: + repo_root = Path(os.path.abspath(REPO_ROOT)) + project_root = Path(os.path.abspath(_project_path(project))) + if project_root != repo_root and repo_root not in project_root.parents: + raise ValueError(f"project must be within {repo_root}: {project_root}") + output_root = project_root / ".moon-out" + output = Path(os.path.abspath(output_root / directory)) + if output != output_root and output_root not in output.parents: + raise ValueError(f"output must be within {output_root}: {output}") + current = output + while current != repo_root: + if current.is_symlink(): + raise ValueError(f"output path must not traverse a symlink: {current}") + current = current.parent + return output + + +def _reset_output(output: Path) -> None: + if output.exists(): + if output.is_symlink() or not output.is_dir(): + raise ValueError(f"refusing to replace non-directory output: {output}") + shutil.rmtree(output) + output.mkdir(parents=True) + + +def _find_one(directory: Path, pattern: str, description: str) -> Path: + selected = sorted(path for path in directory.glob(pattern) if path.is_file()) + if len(selected) != 1: + raise RuntimeError(f"expected one {description} in {directory}, found {len(selected)}") + return selected[0] + + +def _find_one_in(directories: list[Path], pattern: str, description: str) -> Path: + for directory in directories: + selected = sorted(path for path in directory.glob(pattern) if path.is_file()) + if len(selected) == 1: + return selected[0] + if len(selected) > 1: + raise RuntimeError(f"expected one {description} in {directory}, found {len(selected)}") + searched = ", ".join(str(path) for path in directories) + raise RuntimeError(f"expected one {description}; searched {searched}") + + +def _artifact_wheel(project: str, lane: str) -> Path: + if project == "pathfinder": + directories = [_output_path(project, "wheel-pure"), _project_path(project)] + elif project == "bindings": + environment = os.environ.get("CUDA_BINDINGS_ARTIFACTS_DIR") + directories = [_output_path(project, f"wheel-{lane}")] + if lane == "previous": + directories.append(_project_path(project) / "dist-prev") + elif environment: + directories.append(Path(environment)) + directories.append(_project_path(project) / "dist") + elif project == "core": + environment = os.environ.get("CUDA_CORE_ARTIFACTS_DIR") + directories = [_output_path(project, f"wheel-{lane}")] + if environment: + directories.append(Path(environment)) + directories.append(_project_path(project) / "dist") + elif project == "metapackage": + directories = [_output_path(project, "wheel-pure"), REPO_ROOT, _project_path(project)] + else: + raise ValueError(f"project does not produce wheel artifacts: {project}") + return _find_one_in(directories, "*.whl", f"{project} {lane} wheel") + + +def _copy_files(source: Path, output: Path, patterns: tuple[str, ...]) -> None: + selected = sorted({path for pattern in patterns for path in source.glob(pattern) if path.is_file()}) + if not selected: + raise RuntimeError(f"no matching files found in {source}") + _reset_output(output) + for source_path in selected: + shutil.copy2(source_path, output / source_path.name) + + +def _gate(args: argparse.Namespace) -> None: + if args.marker not in GATE_MARKERS: + raise ValueError(f"unknown CI gate marker: {args.marker}") + output = _output_path("ci", f"ci-gates/{args.marker}") + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text("true\n", encoding="utf-8") + + +def _pixi_run(project: str, task: str, *, environment: str | None, extra: list[str]) -> None: + manifest = _project_path(project) / "pixi.toml" + if not manifest.is_file(): + raise FileNotFoundError(f"Pixi manifest not found: {manifest}") + pixi = shutil.which("pixi") + if pixi is None: + raise RuntimeError("pixi is required for this task but was not found on PATH") + command = [pixi, "run", "--manifest-path", str(manifest)] + selected_environment = environment or os.environ.get("PIXI_ENVIRONMENT_NAME") + if selected_environment: + command.extend(["--environment", selected_environment]) + command.append(task) + command.extend(extra) + _run(command) + + +def _pure_wheel(args: argparse.Namespace) -> None: + if args.project not in {"pathfinder", "metapackage"}: + raise ValueError("pure-wheel only supports pathfinder and metapackage") + project_root = _project_path(args.project) + output = _output_path(args.project, "wheel-pure") + _reset_output(output) + _run( + [ + sys.executable, + "-m", + "pip", + "wheel", + "--verbose", + "--no-deps", + "--wheel-dir", + str(output), + ".", + ], + cwd=project_root, + ) + _find_one(output, "*.whl", f"{args.project} wheel") + + +def _cuda_major(lane: str) -> str: + variable = "BUILD_CUDA_MAJOR" if lane == "current" else "BUILD_PREV_CUDA_MAJOR" + value = os.environ.get(variable, "") + if value: + return value + if lane == "current": + version = os.environ.get("BUILD_CUDA_VER") or os.environ.get("CUDA_VER", "") + if version: + return version.split(".", maxsplit=1)[0] + raise RuntimeError(f"{variable} is required for the {lane} CUDA lane") + + +def _constraint_uri(path: Path, *, in_linux_container: bool) -> str: + resolved = path.resolve() + if in_linux_container: + return f"file:///host{resolved.as_posix()}" + return resolved.as_uri() + + +def _constraint_environment( + project: str, + lane: str, + *, + cibuildwheel: bool, + from_sdist: bool = False, +) -> dict[str, str]: + if project not in {"bindings", "core"}: + return os.environ.copy() + + constraints = _output_path(project, f"constraints-{lane}") + _reset_output(constraints) + constraint_file = constraints / "build.txt" + linux_container = cibuildwheel and os.name != "nt" + pathfinder_wheel = ( + _find_one(_output_path("pathfinder", "sdist"), "*.whl", "cuda.pathfinder sdist wheel") + if from_sdist + else _artifact_wheel("pathfinder", "pure") + ) + requirements = [("cuda-pathfinder", pathfinder_wheel)] + if project == "core": + bindings_wheel = ( + _find_one(_output_path("bindings", "sdist"), "*.whl", "cuda.bindings sdist wheel") + if from_sdist + else _artifact_wheel("bindings", lane) + ) + requirements.append( + ( + "cuda-bindings", + bindings_wheel, + ) + ) + constraint_file.write_text( + "".join( + f"{distribution} @ {_constraint_uri(wheel, in_linux_container=linux_container)}\n" + for distribution, wheel in requirements + ), + encoding="utf-8", + ) + + environment = os.environ.copy() + host_constraint = str(constraint_file.resolve()) + environment["PIP_BUILD_CONSTRAINT"] = host_constraint + environment["PIP_CONSTRAINT"] = host_constraint + if project == "core": + environment["CUDA_CORE_BUILD_MAJOR"] = _cuda_major(lane) + + if cibuildwheel: + setting = "CIBW_ENVIRONMENT_WINDOWS" if os.name == "nt" else "CIBW_ENVIRONMENT_LINUX" + container_constraint = f"/host{constraint_file.resolve().as_posix()}" if linux_container else host_constraint + additions = [ + f'PIP_BUILD_CONSTRAINT="{container_constraint}"', + f'PIP_CONSTRAINT="{container_constraint}"', + ] + if project == "core": + additions.append(f"CUDA_CORE_BUILD_MAJOR={_cuda_major(lane)}") + environment[setting] = " ".join(filter(None, [environment.get(setting, ""), *additions])) + return environment + + +def _ensure_owned(output: Path) -> None: + if os.name == "nt": + return + owners = {path.stat().st_uid for path in output.rglob("*")} + if not owners or owners == {os.getuid()}: + return + sudo = shutil.which("sudo") + if sudo is None: + raise RuntimeError(f"cibuildwheel output is not owned by this user and sudo was not found: {output}") + _run([sudo, "chown", "-R", f"{os.getuid()}:{os.getgid()}", str(output)]) + + +def _native_wheel(args: argparse.Namespace) -> None: + if args.project not in {"bindings", "core"}: + raise ValueError("native-wheel only supports bindings and core") + if args.project == "bindings" and args.lane != "current": + raise ValueError("cuda.bindings is only built in the current lane") + + output = _output_path(args.project, f"wheel-{args.lane}") + _reset_output(output) + environment = _constraint_environment(args.project, args.lane, cibuildwheel=True) + _run( + [ + sys.executable, + "-m", + "cibuildwheel", + "--output-dir", + str(output), + str(_project_path(args.project)), + ], + env=environment, + ) + _ensure_owned(output) + wheel = _find_one(output, "*.whl", f"{args.project} {args.lane} wheel") + if args.project == "core": + renamed = wheel.with_name(f"{wheel.stem}.cu{_cuda_major(args.lane)}.whl") + wheel.rename(renamed) + + +def _sdist(args: argparse.Namespace) -> None: + project_root = _project_path(args.project) + output = _output_path(args.project, "sdist") + _reset_output(output) + environment = ( + _constraint_environment(args.project, "current", cibuildwheel=False, from_sdist=True) + if args.project in {"bindings", "core"} + else os.environ.copy() + ) + _run( + [sys.executable, "-m", "build", "--sdist", "--outdir", str(output), str(project_root)], + env=environment, + ) + archive = _find_one(output, "*.tar.gz", f"{args.project} source distribution") + _run( + [ + sys.executable, + "-m", + "pip", + "wheel", + "--no-deps", + "--wheel-dir", + str(output), + str(archive), + ], + env=environment, + ) + _find_one(output, "*.whl", f"{args.project} wheel from source distribution") + + +def _merge_core_wheels(_args: argparse.Namespace) -> None: + current = _find_one(_output_path("core", "wheel-current"), "*.whl", "current cuda.core wheel") + previous = _find_one(_output_path("core", "wheel-previous"), "*.whl", "previous cuda.core wheel") + output = _output_path("core", "wheel-merged") + _reset_output(output) + _run( + [ + sys.executable, + str(REPO_ROOT / "ci" / "tools" / "merge_cuda_core_wheels.py"), + str(current), + str(previous), + "--output-dir", + str(output), + ] + ) + _find_one(output, "*.whl", "merged cuda.core wheel") + + +def _pixi_test(args: argparse.Namespace) -> None: + _pixi_run(args.project, args.task, environment=args.environment, extra=args.extra) + + +def _pixi_docs(args: argparse.Namespace) -> None: + _pixi_run(args.project, DOC_TASKS[args.project], environment="docs", extra=[]) + + +def _docs_ci(_args: argparse.Namespace) -> None: + bash = shutil.which("bash") + if bash is None: + raise RuntimeError("bash is required to build the combined documentation") + latest_only = (os.environ.get("CUDA_PYTHON_DOCS_LATEST_ONLY") or "true").lower() + if latest_only not in {"0", "1", "false", "true"}: + raise ValueError("CUDA_PYTHON_DOCS_LATEST_ONLY must be true, false, 1, or 0") + arguments = [bash, "build_all_docs.sh"] + if latest_only in {"1", "true"}: + arguments.append("latest-only") + docs_root = _project_path("metapackage") / "docs" + _run(arguments, cwd=docs_root) + source = docs_root / "build" / "html" + if not source.is_dir(): + raise RuntimeError(f"combined documentation output not found: {source}") + output = _output_path("root", "docs") + _reset_output(output) + shutil.copytree(source, output, dirs_exist_ok=True) + + +def _cython_test_assets(args: argparse.Namespace) -> None: + source = _project_path(args.project) / "tests" / "cython" + wheels = [_artifact_wheel("pathfinder", "pure")] + if args.project == "bindings": + wheels.append(_artifact_wheel("bindings", "current")) + else: + wheels.extend( + [ + _artifact_wheel("bindings", "current"), + _artifact_wheel("core", "current"), + ] + ) + _run( + [ + sys.executable, + "-m", + "pip", + "install", + *(str(wheel) for wheel in wheels), + "--group", + f"{_project_path(args.project) / 'pyproject.toml'}:test", + ] + ) + bash = shutil.which("bash") + if bash is None: + raise RuntimeError("bash is required to build Cython test extensions") + _run([bash, "build_tests.sh"], cwd=source) + output = _output_path(args.project, "cython-tests") + _copy_files(source, output, ("test_*.so", "test_*.pyd", "test_*.dylib")) + + +def _core_test_binaries(_args: argparse.Namespace) -> None: + source = _project_path("core") / "tests" / "test_binaries" + _run([sys.executable, str(source / "build_test_binaries.py")]) + output = _output_path("core", "test-binaries") + _copy_files(source, output, ("*.o", "*.a", "*.lib")) + + +def _stage_files(source: Path, destination: Path, pattern: str) -> None: + files = sorted(path for path in source.glob(pattern) if path.is_file()) + if not files: + raise RuntimeError(f"no files matching {pattern} found in {source}") + destination.mkdir(parents=True, exist_ok=True) + for path in files: + shutil.copy2(path, destination / path.name) + + +def _installed_test(args: argparse.Namespace) -> None: + pathfinder_wheel = _artifact_wheel("pathfinder", "pure") + if pathfinder_wheel.parent != _project_path("pathfinder"): + _stage_files(pathfinder_wheel.parent, _project_path("pathfinder"), pathfinder_wheel.name) + environment = os.environ.copy() + if args.project in {"bindings", "core"}: + environment.setdefault("CUDA_BINDINGS_ARTIFACTS_DIR", str(_output_path("bindings", "wheel-current"))) + _stage_files( + _output_path(args.project, "cython-tests"), + _project_path(args.project) / "tests" / "cython", + "test_*.*", + ) + if args.project == "core": + environment.setdefault("CUDA_CORE_ARTIFACTS_DIR", str(_output_path("core", "wheel-merged"))) + _stage_files( + _output_path("core", "test-binaries"), + _project_path("core") / "tests" / "test_binaries", + "*.*", + ) + bash = shutil.which("bash") + if bash is None: + raise RuntimeError("bash is required by ci/tools/run-tests but was not found on PATH") + _run([bash, str(REPO_ROOT / "ci" / "tools" / "run-tests"), args.project], env=environment) + + +def _metapackage_install_test(_args: argparse.Namespace) -> None: + wheels = [ + _artifact_wheel("pathfinder", "pure"), + _artifact_wheel("bindings", "current"), + _artifact_wheel("core", "merged"), + ] + metapackage = _artifact_wheel("metapackage", "pure") + requirement = str(metapackage) + if os.environ.get("LOCAL_CTK", "1") != "1": + requirement += "[all]" + _run( + [ + sys.executable, + "-m", + "pip", + "install", + "--only-binary=:all:", + *(str(wheel) for wheel in wheels), + requirement, + ] + ) + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + subparsers = parser.add_subparsers(dest="command", required=True) + + gate = subparsers.add_parser("gate", help="write one affected CI allocation marker") + gate.add_argument("marker", choices=tuple(sorted(GATE_MARKERS))) + gate.set_defaults(handler=_gate) + + pure_wheel = subparsers.add_parser("pure-wheel", help="build one pure-Python wheel") + pure_wheel.add_argument("project", choices=("pathfinder", "metapackage")) + pure_wheel.set_defaults(handler=_pure_wheel) + + native_wheel = subparsers.add_parser("native-wheel", help="build one cibuildwheel wheel") + native_wheel.add_argument("project", choices=("bindings", "core")) + native_wheel.add_argument("--lane", choices=("current", "previous"), required=True) + native_wheel.set_defaults(handler=_native_wheel) + + sdist = subparsers.add_parser("sdist", help="build an sdist and verify its wheel build") + sdist.add_argument("project", choices=PACKAGE_PROJECTS) + sdist.set_defaults(handler=_sdist) + + merge_wheels = subparsers.add_parser("merge-core-wheels", help="merge current and previous CUDA wheels") + merge_wheels.set_defaults(handler=_merge_core_wheels) + + pixi_test = subparsers.add_parser("pixi-test", help="run an existing Pixi test or benchmark task") + pixi_test.add_argument("project", choices=("pathfinder", "bindings", "core", "bindings-benchmarks")) + pixi_test.add_argument("--task", default="test") + pixi_test.add_argument("--environment") + pixi_test.add_argument("extra", nargs="*") + pixi_test.set_defaults(handler=_pixi_test) + + pixi_docs = subparsers.add_parser("pixi-docs", help="run an existing Pixi documentation task") + pixi_docs.add_argument("project", choices=tuple(DOC_TASKS)) + pixi_docs.set_defaults(handler=_pixi_docs) + + docs_ci = subparsers.add_parser("docs-ci", help="build and stage the combined CI documentation") + docs_ci.set_defaults(handler=_docs_ci) + + cython_assets = subparsers.add_parser("cython-test-assets", help="build and stage Cython tests") + cython_assets.add_argument("project", choices=CYTHON_PROJECTS) + cython_assets.set_defaults(handler=_cython_test_assets) + + core_binaries = subparsers.add_parser("core-test-binaries", help="build and stage cuda.core test binaries") + core_binaries.set_defaults(handler=_core_test_binaries) + + installed_test = subparsers.add_parser("installed-test", help="run an installed-wheel package test suite") + installed_test.add_argument("project", choices=("pathfinder", "bindings", "core")) + installed_test.set_defaults(handler=_installed_test) + + metapackage_test = subparsers.add_parser( + "metapackage-install-test", help="verify the local cuda-python wheel set is installable" + ) + metapackage_test.set_defaults(handler=_metapackage_install_test) + + return parser + + +def main() -> None: + args = _parser().parse_args() + if getattr(args, "extra", None) and args.extra[0] == "--": + args.extra = args.extra[1:] + args.handler(args) + + +if __name__ == "__main__": + main() diff --git a/ci/tools/moon_fingerprint.py b/ci/tools/moon_fingerprint.py new file mode 100644 index 00000000000..743eeab4292 --- /dev/null +++ b/ci/tools/moon_fingerprint.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Print a deterministic build-environment fingerprint for a Moon task.""" + +from __future__ import annotations + +import argparse +import hashlib +import importlib.metadata +import json +import os +import platform +import subprocess +import sysconfig +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] +SCM_MATCH = { + "pathfinder": "cuda-pathfinder-v*[0-9]*", + "bindings": "v*[0-9]*", + "core": "cuda-core-v*[0-9]*", + "metapackage": "v*[0-9]*", +} +SCM_DISTRIBUTION = { + "pathfinder": "CUDA_PATHFINDER", + "bindings": "CUDA_BINDINGS", + "core": "CUDA_CORE", + "metapackage": "CUDA_PYTHON", +} +SCM_GLOBAL_VARIABLES = ( + "SETUPTOOLS_SCM_PRETEND_METADATA", + "SETUPTOOLS_SCM_PRETEND_VERSION", + "SOURCE_DATE_EPOCH", + "VCS_VERSIONING_PRETEND_METADATA", + "VCS_VERSIONING_PRETEND_VERSION", +) +SCM_DISTRIBUTION_VARIABLES = ( + "SETUPTOOLS_SCM_OVERRIDES_FOR_{distribution}", + "SETUPTOOLS_SCM_PRETEND_METADATA_FOR_{distribution}", + "SETUPTOOLS_SCM_PRETEND_VERSION_FOR_{distribution}", + "VCS_VERSIONING_PRETEND_METADATA_FOR_{distribution}", + "VCS_VERSIONING_PRETEND_VERSION_FOR_{distribution}", +) +LANE_VARIABLES = ( + "BUILD_CUDA_MAJOR", + "BUILD_CUDA_VER", + "BUILD_PREV_CUDA_MAJOR", + "CIBW_ARCHS", + "CIBW_BUILD", + "CIBW_ENABLE", + "CUDA_CORE_BUILD_MAJOR", + "CUDA_PATH", + "CUDA_PYTHON_LANE", + "CUDA_VER", + "HOST_PLATFORM", + "PY_VER", +) +PYTHON_TOOLS = ("build", "cibuildwheel", "packaging", "pip", "setuptools", "setuptools-scm", "wheel") + + +def _git_describe(pattern: str) -> str: + result = subprocess.run( # noqa: S603 - fixed git command with a package-defined tag pattern. + ["git", "describe", "--dirty", "--tags", "--long", "--match", pattern], # noqa: S607 + cwd=REPO_ROOT, + check=True, + stdout=subprocess.PIPE, + text=True, + ) + return result.stdout.strip() + + +def _distribution_version(name: str) -> str: + try: + return importlib.metadata.version(name) + except importlib.metadata.PackageNotFoundError: + return "" + + +def _scm_environment(project: str) -> dict[str, str]: + distribution = SCM_DISTRIBUTION[project] + distribution_variables = tuple(name.format(distribution=distribution) for name in SCM_DISTRIBUTION_VARIABLES) + variables = (*SCM_GLOBAL_VARIABLES, *distribution_variables) + return {name: os.environ.get(name, "") for name in variables} + + +def _scm_identity(project: str) -> dict[str, object]: + environment = _scm_environment(project) + pretend_variables = ["SETUPTOOLS_SCM_PRETEND_VERSION"] + # cuda_python/setup.py calls get_version without a distribution name, so + # setuptools-scm cannot apply its distribution-specific override there. + if project != "metapackage": + pretend_variables.append(f"SETUPTOOLS_SCM_PRETEND_VERSION_FOR_{SCM_DISTRIBUTION[project]}") + describe = ( + "" + if any(environment[name] for name in pretend_variables) + else _git_describe(SCM_MATCH[project]) + ) + return {"describe": describe, "environment": environment} + + +def fingerprint(project: str, lane: str) -> str: + payload: dict[str, object] = { + "lane": lane, + "project": project, + "python": { + "implementation": platform.python_implementation(), + "soabi": sysconfig.get_config_var("SOABI") or "", + "version": platform.python_version(), + }, + "python_tools": {name: _distribution_version(name) for name in PYTHON_TOOLS}, + "scm": _scm_identity(project), + } + if lane != "portable": + payload.update( + { + "environment": {name: os.environ.get(name, "") for name in LANE_VARIABLES}, + "platform": {"machine": platform.machine(), "system": platform.system()}, + } + ) + encoded = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode() + return hashlib.sha256(encoded).hexdigest() + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("project", choices=tuple(SCM_MATCH)) + parser.add_argument("lane", choices=("portable", "native", "previous", "sdist", "test-assets")) + args = parser.parse_args() + print(fingerprint(args.project, args.lane)) + + +if __name__ == "__main__": + main() diff --git a/ci/tools/tests/test_moon_ci.py b/ci/tools/tests/test_moon_ci.py new file mode 100644 index 00000000000..1541d7f1acc --- /dev/null +++ b/ci/tools/tests/test_moon_ci.py @@ -0,0 +1,117 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +# These tests intentionally use stdlib unittest so the cheap CI planner job has +# no third-party Python dependency. +# ruff: noqa: PT009, PT027 + +from __future__ import annotations + +import tempfile +import unittest +from argparse import Namespace +from pathlib import Path +from unittest.mock import patch + +from ci.tools.moon_ci import _gate, _output_path +from ci.tools.moon_fingerprint import _scm_identity, fingerprint + + +class MoonCIOutputPathTest(unittest.TestCase): + def setUp(self) -> None: + self.temporary_directory = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary_directory.cleanup) + self.repo = Path(self.temporary_directory.name) + (self.repo / "project").mkdir() + self.patches = ( + patch("ci.tools.moon_ci.REPO_ROOT", self.repo), + patch.dict( + "ci.tools.moon_ci.PROJECT_PATHS", + {"pathfinder": Path("project"), "ci": Path("ci")}, + clear=True, + ), + ) + for active_patch in self.patches: + active_patch.start() + self.addCleanup(active_patch.stop) + + def test_confines_output_to_the_project_output_root(self) -> None: + output = _output_path("pathfinder", "wheel") + + self.assertEqual(output, self.repo / "project" / ".moon-out" / "wheel") + with self.assertRaisesRegex(ValueError, "output must be within"): + _output_path("pathfinder", "../dist") + with self.assertRaisesRegex(ValueError, "output must be within"): + _output_path("pathfinder", "../../outside") + + def test_rejects_symlinked_output_ancestors(self) -> None: + outside = self.repo / "outside" + outside.mkdir() + (self.repo / "project" / ".moon-out").symlink_to(outside, target_is_directory=True) + + with self.assertRaisesRegex(ValueError, "must not traverse a symlink"): + _output_path("pathfinder", "wheel") + + def test_rejects_projects_outside_the_workspace(self) -> None: + with ( + patch.dict("ci.tools.moon_ci.PROJECT_PATHS", {"pathfinder": Path("../outside")}), + self.assertRaisesRegex(ValueError, "project must be within"), + ): + _output_path("pathfinder", "wheel") + + def test_gate_writes_only_a_declared_marker(self) -> None: + (self.repo / "ci").mkdir() + _gate(Namespace(marker="build-linux-64")) + + marker = self.repo / "ci" / ".moon-out" / "ci-gates" / "build-linux-64" + self.assertEqual(marker.read_text(encoding="utf-8"), "true\n") + with self.assertRaisesRegex(ValueError, "unknown CI gate marker"): + _gate(Namespace(marker="anything-else")) + + +class MoonFingerprintTest(unittest.TestCase): + @patch("ci.tools.moon_fingerprint._git_describe") + def test_distribution_pretend_version_replaces_git_identity(self, git_describe) -> None: + with patch.dict( + "os.environ", + {"SETUPTOOLS_SCM_PRETEND_VERSION_FOR_CUDA_BINDINGS": "13.2.0"}, + clear=True, + ): + identity = _scm_identity("bindings") + + self.assertEqual(identity["describe"], "") + self.assertEqual( + identity["environment"]["SETUPTOOLS_SCM_PRETEND_VERSION_FOR_CUDA_BINDINGS"], + "13.2.0", + ) + git_describe.assert_not_called() + + @patch("ci.tools.moon_fingerprint._git_describe", return_value="v13.2.0-1-gabc") + def test_portable_fingerprint_includes_ambient_python(self, git_describe) -> None: + with ( + patch.dict("os.environ", {}, clear=True), + patch( + "ci.tools.moon_fingerprint.platform.python_version", + side_effect=("3.12.11", "3.13.7"), + ), + ): + first = fingerprint("metapackage", "portable") + second = fingerprint("metapackage", "portable") + + self.assertNotEqual(first, second) + self.assertEqual(git_describe.call_count, 2) + + @patch("ci.tools.moon_fingerprint._git_describe", return_value="cuda-core-v1.0.0-1-gabc") + def test_reproducibility_environment_changes_fingerprint(self, git_describe) -> None: + with patch.dict("os.environ", {"SOURCE_DATE_EPOCH": "1"}, clear=True): + first = fingerprint("core", "native") + with patch.dict("os.environ", {"SOURCE_DATE_EPOCH": "2"}, clear=True): + second = fingerprint("core", "native") + + self.assertNotEqual(first, second) + self.assertEqual(git_describe.call_count, 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/ci/tools/tests/test_moon_workspace.py b/ci/tools/tests/test_moon_workspace.py new file mode 100644 index 00000000000..d5e3ff33505 --- /dev/null +++ b/ci/tools/tests/test_moon_workspace.py @@ -0,0 +1,370 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +# These tests intentionally use stdlib unittest so the allocation job has no +# third-party Python dependency. +# ruff: noqa: PT009 + +from __future__ import annotations + +import json +import os +import shutil +import subprocess +import unittest +from pathlib import Path +from typing import Any + +REPO_ROOT = Path(__file__).resolve().parents[3] +EXPECTED_PROJECTS = { + "root": ".", + "ci": "ci", + "pathfinder": "cuda_pathfinder", + "bindings": "cuda_bindings", + "core": "cuda_core", + "metapackage": "cuda_python", + "test-helpers": "cuda_python_test_helpers", + "bindings-benchmarks": "benchmarks/cuda_bindings", +} +GATE_MARKERS = { + "force-all", + "force-all-unowned", + "build-portable", + "build-linux-64", + "build-linux-aarch64", + "build-windows", + "test-sdist-linux", + "test-sdist-windows", + "test-linux", + "test-windows", + "docs", + "core-api", + "build-pathfinder", + "build-bindings", + "build-core", + "build-metapackage", + "test-pathfinder", + "test-bindings", + "test-core", + "test-metapackage", +} +TAG_TARGETS = { + "ci-wheel-pure": {"pathfinder:wheel-pure", "metapackage:wheel-pure"}, + "ci-wheel-current": {"bindings:wheel-current", "core:wheel-current"}, + "ci-wheel-previous": {"core:wheel-previous"}, + "ci-wheel-merge": {"core:wheel-merge"}, + "ci-build-test-assets": { + "bindings:cython-test-assets", + "core:cython-test-assets", + "core:test-binaries", + }, + "ci-sdist": {"pathfinder:sdist", "bindings:sdist", "core:sdist", "metapackage:sdist"}, + "ci-test-linux": { + "pathfinder:test-installed-linux", + "pathfinder:test-installed-linux-strict", + "bindings:test-installed-linux", + "core:test-installed-linux", + "metapackage:test-installed-linux", + "bindings-benchmarks:smoke-linux", + }, + "ci-test-windows": { + "pathfinder:test-installed-windows", + "pathfinder:test-installed-windows-strict", + "bindings:test-installed-windows", + "core:test-installed-windows", + "metapackage:test-installed-windows", + }, + "ci-docs": {"root:docs-ci"}, +} +CACHED_OUTPUTS = { + "pathfinder:wheel-pure": ".moon-out/wheel-pure", + "pathfinder:sdist": ".moon-out/sdist", + "bindings:wheel-current": ".moon-out/wheel-current", + "bindings:sdist": ".moon-out/sdist", + "bindings:cython-test-assets": ".moon-out/cython-tests", + "core:wheel-current": ".moon-out/wheel-current", + "core:wheel-previous": ".moon-out/wheel-previous", + "core:wheel-merge": ".moon-out/wheel-merged", + "core:sdist": ".moon-out/sdist", + "core:cython-test-assets": ".moon-out/cython-tests", + "core:test-binaries": ".moon-out/test-binaries", + "metapackage:wheel-pure": ".moon-out/wheel-pure", + "metapackage:sdist": ".moon-out/sdist", +} + + +class MoonWorkspaceContractTest(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.moon = os.environ.get("MOON_BIN") or shutil.which("moon") + if not cls.moon: + raise unittest.SkipTest("Moon is not installed; set MOON_BIN to test the workspace") + cls.tasks = cls.moon_json("tasks", "--json") + cls.by_target = {task["target"]: task for task in cls.tasks} + + @classmethod + def moon_json(cls, *arguments: str) -> Any: + result = subprocess.run( # noqa: S603 - the binary is explicitly selected above. + [cls.moon, *arguments], + cwd=REPO_ROOT, + check=True, + text=True, + stdout=subprocess.PIPE, + ) + return json.loads(result.stdout) + + def test_projects_use_only_the_system_toolchain(self) -> None: + projects = self.moon_json("projects", "--json") + by_id = {project["id"]: project for project in projects} + self.assertEqual( + {project_id: project["source"] for project_id, project in by_id.items()}, + EXPECTED_PROJECTS, + ) + for project in by_id.values(): + self.assertEqual(project["language"], "unknown") + self.assertEqual(project["toolchains"], ["system"]) + + def test_ci_gates_are_real_uncached_marker_tasks(self) -> None: + expected_targets = {f"ci:gate-{marker}" for marker in GATE_MARKERS} + tagged = {task["target"] for task in self.tasks if "ci-gate" in task.get("tags", [])} + self.assertEqual(tagged, expected_targets) + for marker in GATE_MARKERS: + task = self.by_target[f"ci:gate-{marker}"] + self.assertEqual(task["command"], "python") + self.assertEqual(task["args"], ["ci/tools/moon_ci.py", "gate", marker]) + self.assertFalse(task["options"]["cache"]) + self.assertFalse(task["options"]["internal"]) + self.assertTrue(task["options"]["runInCI"]) + self.assertEqual(task["outputs"], [{"file": f".moon-out/ci-gates/{marker}"}]) + + def test_ci_tags_select_the_intended_real_tasks(self) -> None: + for tag, expected in TAG_TARGETS.items(): + selected = {task["target"] for task in self.tasks if tag in task.get("tags", [])} + self.assertEqual(selected, expected, tag) + for target in selected: + self.assertNotEqual(self.by_target[target]["command"], "noop") + self.assertTrue(self.by_target[target]["options"]["runInCI"]) + + def test_cached_producers_have_explicit_non_overlapping_outputs(self) -> None: + cached = {task["target"] for task in self.tasks if task["options"]["cache"]} + self.assertEqual(cached, set(CACHED_OUTPUTS)) + destinations: set[tuple[str, str]] = set() + for target, output in CACHED_OUTPUTS.items(): + task = self.by_target[target] + self.assertEqual(task["outputs"], [{"file": output}]) + self.assertTrue(task.get("inputs")) + self.assertTrue(task.get("checks")) + self.assertNotIn("CUDA_PYTHON_TOOL_VERSIONS", task.get("env") or {}) + self.assertIn({"file": "/ci/tools/moon_fingerprint.py"}, task["inputs"]) + project = target.split(":", maxsplit=1)[0] + self.assertNotIn((project, output), destinations) + destinations.add((project, output)) + + def test_tests_and_docs_are_uncached(self) -> None: + ci_test_targets = set().union( + TAG_TARGETS["ci-test-linux"], TAG_TARGETS["ci-test-windows"], TAG_TARGETS["ci-docs"] + ) + for target in ci_test_targets: + self.assertFalse(self.by_target[target]["options"]["cache"]) + + def test_cross_runner_tasks_do_not_execute_producer_dependencies(self) -> None: + for tag in ( + "ci-test-linux", + "ci-test-windows", + "ci-build-test-assets", + "ci-wheel-current", + "ci-wheel-previous", + ): + for target in TAG_TARGETS[tag]: + self.assertFalse(self.by_target[target].get("deps"), target) + self.assertFalse(self.by_target["core:wheel-merge"].get("deps")) + + def test_native_producers_hash_downloaded_wheel_bytes(self) -> None: + required_globs = { + "bindings:wheel-current": {"/cuda_pathfinder/.moon-out/wheel-pure/*.whl"}, + "core:wheel-current": { + "/cuda_pathfinder/.moon-out/wheel-pure/*.whl", + "/cuda_bindings/.moon-out/wheel-current/*.whl", + }, + "core:wheel-previous": { + "/cuda_pathfinder/.moon-out/wheel-pure/*.whl", + "/cuda_bindings/.moon-out/wheel-previous/*.whl", + }, + } + for target, expected in required_globs.items(): + configured = { + item["glob"] for item in self.by_target[target]["inputs"] if isinstance(item, dict) and "glob" in item + } + self.assertTrue(expected.issubset(configured), target) + + def test_metapackage_install_smoke_tracks_runtime_inputs(self) -> None: + for target in ("metapackage:test-installed-linux", "metapackage:test-installed-windows"): + inputs = self.by_target[target]["inputs"] + self.assertIn({"project": "pathfinder", "group": "package"}, inputs) + self.assertIn({"project": "bindings", "group": "package"}, inputs) + self.assertIn({"project": "core", "group": "package"}, inputs) + input_globs = {item["glob"] for item in inputs if isinstance(item, dict) and "glob" in item} + self.assertIn("/cuda_core/.moon-out/wheel-merged/*.whl", input_globs) + self.assertIn( + {"project": "core", "group": "package"}, + self.by_target["ci:gate-test-metapackage"]["inputs"], + ) + + def test_cross_runner_producer_inputs_reach_exact_consumers(self) -> None: + portable_workflow = {"file": "/.github/workflows/build-pure-wheel.yml"} + portable_consumers = { + "bindings:wheel-current", + "core:wheel-current", + "core:wheel-previous", + "core:wheel-merge", + "bindings:cython-test-assets", + "core:cython-test-assets", + "pathfinder:test-installed-linux", + "pathfinder:test-installed-linux-strict", + "pathfinder:test-installed-windows", + "pathfinder:test-installed-windows-strict", + "bindings:test-installed-linux", + "bindings:test-installed-windows", + "core:test-installed-linux", + "core:test-installed-windows", + "metapackage:test-installed-linux", + "metapackage:test-installed-windows", + } + for target in portable_consumers: + self.assertIn(portable_workflow, self.by_target[target]["inputs"], target) + + native_workflow = {"file": "/.github/workflows/build-wheel.yml"} + native_test_consumers = { + "bindings:test-installed-linux", + "bindings:test-installed-windows", + "core:test-installed-linux", + "core:test-installed-windows", + "metapackage:test-installed-linux", + "metapackage:test-installed-windows", + } + for target in native_test_consumers: + self.assertIn(native_workflow, self.by_target[target]["inputs"], target) + + merge_helper = {"file": "/ci/tools/merge_cuda_core_wheels.py"} + merge_test_consumers = { + "core:test-installed-linux", + "core:test-installed-windows", + "metapackage:test-installed-linux", + "metapackage:test-installed-windows", + } + for target in merge_test_consumers: + self.assertIn(merge_helper, self.by_target[target]["inputs"], target) + + def test_cross_runner_producer_inputs_reach_matching_gates(self) -> None: + gate_expectations = { + "@group(build-portable)": { + "gate-build-portable", + "gate-build-linux-64", + "gate-build-linux-aarch64", + "gate-build-windows", + "gate-build-pathfinder", + "gate-build-bindings", + "gate-build-core", + "gate-build-metapackage", + "gate-test-linux", + "gate-test-windows", + "gate-test-pathfinder", + "gate-test-bindings", + "gate-test-core", + "gate-test-metapackage", + }, + "@group(build-native-common)": { + "gate-build-linux-64", + "gate-build-linux-aarch64", + "gate-build-windows", + "gate-build-bindings", + "gate-build-core", + "gate-test-linux", + "gate-test-windows", + "gate-test-bindings", + "gate-test-core", + "gate-test-metapackage", + }, + "@group(build-native-core)": { + "gate-build-linux-64", + "gate-build-linux-aarch64", + "gate-build-windows", + "gate-build-core", + "gate-test-linux", + "gate-test-windows", + "gate-test-core", + "gate-test-metapackage", + }, + } + for producer_group, expected_gates in gate_expectations.items(): + actual_gates = { + task["id"] + for task in self.tasks + if "ci-gate" in task.get("tags", []) and producer_group in task["inputs"] + } + self.assertEqual(actual_gates, expected_gates, producer_group) + + def test_installed_test_runner_does_not_select_metapackage_smoke(self) -> None: + runner_group = "@group(test-library-runner)" + for gate in ("gate-test-pathfinder", "gate-test-bindings", "gate-test-core"): + self.assertIn(runner_group, self.by_target[f"ci:{gate}"]["inputs"]) + self.assertNotIn(runner_group, self.by_target["ci:gate-test-metapackage"]["inputs"]) + + def test_platform_test_tasks_track_provider_setup(self) -> None: + linux_targets = TAG_TARGETS["ci-test-linux"] + windows_targets = TAG_TARGETS["ci-test-windows"] + for target in linux_targets: + inputs = self.by_target[target]["inputs"] + self.assertIn({"file": "/ci/tools/guess_latest.sh"}, inputs, target) + self.assertIn({"file": "/ci/tools/install_gpu_driver.sh"}, inputs, target) + for target in windows_targets: + inputs = self.by_target[target]["inputs"] + self.assertIn({"file": "/ci/tools/configure_driver_mode.ps1"}, inputs, target) + self.assertIn({"file": "/ci/tools/install_gpu_driver.ps1"}, inputs, target) + for target in ("bindings:test-installed-linux", "core:test-installed-linux"): + self.assertIn({"file": "/ci/tools/setup-sanitizer"}, self.by_target[target]["inputs"]) + + def test_docs_gate_and_task_share_package_owned_groups(self) -> None: + docs = self.by_target["root:docs-ci"]["inputs"] + gate = self.by_target["ci:gate-docs"]["inputs"] + external_groups = [item for item in docs if isinstance(item, dict) and "project" in item] + for group in external_groups: + self.assertIn(group, gate) + + def test_core_merge_changes_materialize_all_core_wheel_phases(self) -> None: + merge_helper = {"file": "/ci/tools/merge_cuda_core_wheels.py"} + for target in ("core:wheel-current", "core:wheel-previous", "core:wheel-merge"): + self.assertIn(merge_helper, self.by_target[target]["inputs"]) + + def test_local_pixi_tasks_remain_available_and_skip_ci(self) -> None: + for target in ( + "pathfinder:test", + "bindings:test", + "core:test", + "pathfinder:docs", + "bindings:docs", + "core:docs", + "bindings-benchmarks:bench", + ): + task = self.by_target[target] + self.assertIn("pixi-", " ".join(task.get("args", []))) + self.assertFalse(task["options"]["runInCI"]) + + def test_workspace_disables_python_and_dependency_management(self) -> None: + workspace = (REPO_ROOT / ".moon" / "workspace.yml").read_text(encoding="utf-8") + self.assertIn("versionConstraint: '=2.5.1'", workspace) + self.assertIn("installDependencies: false", workspace) + self.assertIn("syncProjects: false", workspace) + self.assertIn("syncWorkspace: false", workspace) + self.assertIn("verifyIntegrity: true", workspace) + self.assertFalse((REPO_ROOT / ".moon" / "toolchains.yml").exists()) + + def test_generated_cache_and_output_roots_are_ignored(self) -> None: + ignore = (REPO_ROOT / ".gitignore").read_text(encoding="utf-8").splitlines() + self.assertIn(".moon/cache/", ignore) + self.assertIn(".moon-out/", ignore) + + +if __name__ == "__main__": + unittest.main() diff --git a/cuda_bindings/moon.yml b/cuda_bindings/moon.yml new file mode 100644 index 00000000000..296b0d32522 --- /dev/null +++ b/cuda_bindings/moon.yml @@ -0,0 +1,201 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +$schema: https://moonrepo.dev/schemas/v2/project.json + +language: unknown +layer: library +dependsOn: + - id: pathfinder + scope: production + - id: test-helpers + scope: development +toolchains: + default: system + +taskOptions: + cache: false + runFromWorkspaceRoot: true + runInCI: false + +fileGroups: + package: + - '.git_archival.txt' + - 'cuda/**/*' + - 'build_hooks.py' + - 'DESCRIPTION.rst' + - 'LICENSE' + - 'MANIFEST.in' + - 'pyproject.toml' + - 'setup.py' + tests: + - 'examples/**/*' + - 'tests/**/*' + - 'pixi.toml' + - 'pixi.lock' + docs: + - 'docs/**/*' + - 'pixi.toml' + - 'pixi.lock' + +tasks: + wheel-current: + command: python + args: [ci/tools/moon_ci.py, native-wheel, bindings, --lane, current] + env: + BUILD_CUDA_VER: '${BUILD_CUDA_VER}' + CIBW_BUILD: '${CIBW_BUILD}' + CUDA_PATH: '${CUDA_PATH}' + HOST_PLATFORM: '${HOST_PLATFORM}' + PY_VER: '${PY_VER}' + inputs: + - '@group(package)' + - {project: pathfinder, group: package} + - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' + - '/ci/tools/moon_ci.py' + - '/ci/tools/moon_fingerprint.py' + - '/ci/tools/env-vars' + - '/ci/versions.yml' + - '/.github/workflows/build-pure-wheel.yml' + - '/.github/workflows/build-wheel.yml' + outputs: + - '.moon-out/wheel-current' + checks: + - check: fingerprint + script: python ci/tools/moon_fingerprint.py bindings native + hash: stdout + tags: [ci-wheel-current] + type: build + options: + cache: true + cacheKey: wheel-current-v2 + runInCI: true + + sdist: + command: python + args: [ci/tools/moon_ci.py, sdist, bindings] + deps: + - pathfinder:sdist + env: + BUILD_CUDA_VER: '${BUILD_CUDA_VER}' + CUDA_PATH: '${CUDA_PATH}' + HOST_PLATFORM: '${HOST_PLATFORM}' + PY_VER: '${PY_VER}' + inputs: + - '@group(package)' + - {project: pathfinder, group: package} + - '/ci/tools/moon_ci.py' + - '/ci/tools/moon_fingerprint.py' + - '/.github/workflows/test-sdist-linux.yml' + - '/.github/workflows/test-sdist-windows.yml' + outputs: + - '.moon-out/sdist' + checks: + - check: fingerprint + script: python ci/tools/moon_fingerprint.py bindings sdist + hash: stdout + tags: [ci-sdist] + type: build + options: + cache: true + cacheKey: sdist-v2 + runInCI: true + + cython-test-assets: + command: python + args: [ci/tools/moon_ci.py, cython-test-assets, bindings] + inputs: + - '@group(package)' + - 'tests/cython/**/*' + - {project: pathfinder, group: package} + - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' + - '/cuda_bindings/.moon-out/wheel-current/*.whl' + - '/ci/tools/moon_ci.py' + - '/ci/tools/moon_fingerprint.py' + - '/.github/workflows/build-pure-wheel.yml' + - '/.github/workflows/build-wheel.yml' + outputs: + - '.moon-out/cython-tests' + checks: + - check: fingerprint + script: python ci/tools/moon_fingerprint.py bindings test-assets + hash: stdout + tags: [ci-build-test-assets] + type: build + options: + cache: true + cacheKey: cython-test-assets-v2 + runInCI: true + + test: + command: python + args: [ci/tools/moon_ci.py, pixi-test, bindings] + inputs: + - '@group(package)' + - '@group(tests)' + - '/cuda_pathfinder/**/*' + - '/cuda_python_test_helpers/**/*' + - '/benchmarks/cuda_bindings/**/*' + - '/ci/test-matrix.yml' + - '/ci/tools/env-vars' + - '/tests/**/*' + - '/ci/tools/moon_ci.py' + type: test + + test-installed-linux: + command: python + args: [ci/tools/moon_ci.py, installed-test, bindings] + inputs: + - '@group(package)' + - '@group(tests)' + - {project: pathfinder, group: package} + - '/cuda_python_test_helpers/**/*' + - '/ci/tools/moon_ci.py' + - '/ci/tools/run-tests' + - '/ci/test-matrix.yml' + - '/ci/tools/env-vars' + - '/tests/**/*' + - '/.github/workflows/build-pure-wheel.yml' + - '/.github/workflows/build-wheel.yml' + - '/ci/tools/guess_latest.sh' + - '/ci/tools/install_gpu_driver.sh' + - '/ci/tools/setup-sanitizer' + - '/.github/workflows/test-wheel-linux.yml' + tags: [ci-test-linux] + type: test + options: + runInCI: true + + test-installed-windows: + command: python + args: [ci/tools/moon_ci.py, installed-test, bindings] + inputs: + - '@group(package)' + - '@group(tests)' + - {project: pathfinder, group: package} + - '/cuda_python_test_helpers/**/*' + - '/ci/tools/moon_ci.py' + - '/ci/tools/run-tests' + - '/ci/test-matrix.yml' + - '/ci/tools/env-vars' + - '/tests/**/*' + - '/.github/workflows/build-pure-wheel.yml' + - '/.github/workflows/build-wheel.yml' + - '/ci/tools/configure_driver_mode.ps1' + - '/ci/tools/install_gpu_driver.ps1' + - '/.github/workflows/test-wheel-windows.yml' + tags: [ci-test-windows] + type: test + options: + runInCI: true + + docs: + command: python + args: [ci/tools/moon_ci.py, pixi-docs, bindings] + inputs: + - '@group(package)' + - '@group(docs)' + - '/cuda_pathfinder/**/*' + - '/ci/tools/moon_ci.py' + - '/.github/workflows/build-docs.yml' diff --git a/cuda_core/moon.yml b/cuda_core/moon.yml new file mode 100644 index 00000000000..43090601c1b --- /dev/null +++ b/cuda_core/moon.yml @@ -0,0 +1,309 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +$schema: https://moonrepo.dev/schemas/v2/project.json + +language: unknown +layer: library +dependsOn: + - id: pathfinder + scope: production + - id: bindings + scope: production + - id: test-helpers + scope: development +toolchains: + default: system + +taskOptions: + cache: false + runFromWorkspaceRoot: true + runInCI: false + +fileGroups: + package: + - '.git_archival.txt' + - 'cuda/**/*' + - 'build_hooks.py' + - 'DESCRIPTION.rst' + - 'LICENSE' + - 'MANIFEST.in' + - 'NOTICE' + - 'pyproject.toml' + - 'setup.py' + tests: + - 'examples/**/*' + - 'tests/**/*' + - 'pixi.toml' + - 'pixi.lock' + - 'pytest.ini' + docs: + - 'docs/**/*' + - 'pixi.toml' + - 'pixi.lock' + +tasks: + wheel-current: + command: python + args: [ci/tools/moon_ci.py, native-wheel, core, --lane, current] + env: + BUILD_CUDA_MAJOR: '${BUILD_CUDA_MAJOR}' + BUILD_CUDA_VER: '${BUILD_CUDA_VER}' + CIBW_BUILD: '${CIBW_BUILD}' + CUDA_PATH: '${CUDA_PATH}' + HOST_PLATFORM: '${HOST_PLATFORM}' + PY_VER: '${PY_VER}' + inputs: + - '@group(package)' + - {project: pathfinder, group: package} + - {project: bindings, group: package} + - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' + - '/cuda_bindings/.moon-out/wheel-current/*.whl' + - '/ci/tools/moon_ci.py' + - '/ci/tools/moon_fingerprint.py' + - '/ci/tools/env-vars' + - '/ci/tools/merge_cuda_core_wheels.py' + - '/ci/versions.yml' + - '/.github/workflows/build-pure-wheel.yml' + - '/.github/workflows/build-wheel.yml' + outputs: + - '.moon-out/wheel-current' + checks: + - check: fingerprint + script: python ci/tools/moon_fingerprint.py core native + hash: stdout + tags: [ci-wheel-current] + type: build + options: + cache: true + cacheKey: wheel-current-v2 + runInCI: true + + wheel-previous: + command: python + args: [ci/tools/moon_ci.py, native-wheel, core, --lane, previous] + env: + BUILD_PREV_CUDA_MAJOR: '${BUILD_PREV_CUDA_MAJOR}' + CIBW_BUILD: '${CIBW_BUILD}' + CUDA_PATH: '${CUDA_PATH}' + HOST_PLATFORM: '${HOST_PLATFORM}' + PY_VER: '${PY_VER}' + inputs: + - '@group(package)' + - {project: pathfinder, group: package} + - {project: bindings, group: package} + - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' + - '/cuda_bindings/.moon-out/wheel-previous/*.whl' + - '/ci/tools/moon_ci.py' + - '/ci/tools/moon_fingerprint.py' + - '/ci/tools/env-vars' + - '/ci/tools/merge_cuda_core_wheels.py' + - '/ci/versions.yml' + - '/.github/workflows/build-pure-wheel.yml' + - '/.github/workflows/build-wheel.yml' + outputs: + - '.moon-out/wheel-previous' + checks: + - check: fingerprint + script: python ci/tools/moon_fingerprint.py core previous + hash: stdout + tags: [ci-wheel-previous] + type: build + options: + cache: true + cacheKey: wheel-previous-v2 + runInCI: true + + wheel-merge: + command: python + args: [ci/tools/moon_ci.py, merge-core-wheels] + inputs: + - '@group(package)' + - {project: pathfinder, group: package} + - {project: bindings, group: package} + - '/cuda_core/.moon-out/wheel-current/*.whl' + - '/cuda_core/.moon-out/wheel-previous/*.whl' + - '/ci/tools/moon_ci.py' + - '/ci/tools/moon_fingerprint.py' + - '/ci/tools/merge_cuda_core_wheels.py' + - '/.github/workflows/build-pure-wheel.yml' + - '/.github/workflows/build-wheel.yml' + outputs: + - '.moon-out/wheel-merged' + checks: + - check: fingerprint + script: python ci/tools/moon_fingerprint.py core native + hash: stdout + tags: [ci-wheel-merge] + type: build + options: + cache: true + cacheKey: wheel-merge-v2 + runInCI: true + + sdist: + command: python + args: [ci/tools/moon_ci.py, sdist, core] + deps: + - pathfinder:sdist + - bindings:sdist + env: + BUILD_CUDA_MAJOR: '${BUILD_CUDA_MAJOR}' + BUILD_CUDA_VER: '${BUILD_CUDA_VER}' + CUDA_PATH: '${CUDA_PATH}' + HOST_PLATFORM: '${HOST_PLATFORM}' + PY_VER: '${PY_VER}' + inputs: + - '@group(package)' + - {project: pathfinder, group: package} + - {project: bindings, group: package} + - '/ci/tools/moon_ci.py' + - '/ci/tools/moon_fingerprint.py' + - '/.github/workflows/test-sdist-linux.yml' + - '/.github/workflows/test-sdist-windows.yml' + outputs: + - '.moon-out/sdist' + checks: + - check: fingerprint + script: python ci/tools/moon_fingerprint.py core sdist + hash: stdout + tags: [ci-sdist] + type: build + options: + cache: true + cacheKey: sdist-v2 + runInCI: true + + cython-test-assets: + command: python + args: [ci/tools/moon_ci.py, cython-test-assets, core] + inputs: + - '@group(package)' + - 'tests/cython/**/*' + - {project: pathfinder, group: package} + - {project: bindings, group: package} + - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' + - '/cuda_bindings/.moon-out/wheel-current/*.whl' + - '/cuda_core/.moon-out/wheel-current/*.whl' + - '/ci/tools/moon_ci.py' + - '/ci/tools/moon_fingerprint.py' + - '/.github/workflows/build-pure-wheel.yml' + - '/.github/workflows/build-wheel.yml' + outputs: + - '.moon-out/cython-tests' + checks: + - check: fingerprint + script: python ci/tools/moon_fingerprint.py core test-assets + hash: stdout + tags: [ci-build-test-assets] + type: build + options: + cache: true + cacheKey: cython-test-assets-v2 + runInCI: true + + test-binaries: + command: python + args: [ci/tools/moon_ci.py, core-test-binaries] + env: + CUDA_PATH: '${CUDA_PATH}' + HOST_PLATFORM: '${HOST_PLATFORM}' + inputs: + - '@group(package)' + - {project: pathfinder, group: package} + - {project: bindings, group: package} + - 'tests/test_binaries/build_test_binaries.py' + - 'tests/test_binaries/saxpy.cu' + - '/ci/tools/moon_ci.py' + - '/ci/tools/moon_fingerprint.py' + - '/.github/workflows/build-wheel.yml' + outputs: + - '.moon-out/test-binaries' + checks: + - check: fingerprint + script: python ci/tools/moon_fingerprint.py core test-assets + hash: stdout + tags: [ci-build-test-assets] + type: build + options: + cache: true + cacheKey: test-binaries-v2 + runInCI: true + + test: + command: python + args: [ci/tools/moon_ci.py, pixi-test, core] + inputs: + - '@group(package)' + - '@group(tests)' + - '/cuda_pathfinder/**/*' + - '/cuda_bindings/**/*' + - '/cuda_python_test_helpers/**/*' + - '/ci/test-matrix.yml' + - '/ci/tools/env-vars' + - '/tests/**/*' + - '/ci/tools/moon_ci.py' + type: test + + test-installed-linux: + command: python + args: [ci/tools/moon_ci.py, installed-test, core] + inputs: + - '@group(package)' + - '@group(tests)' + - {project: pathfinder, group: package} + - {project: bindings, group: package} + - '/cuda_python_test_helpers/**/*' + - '/ci/tools/moon_ci.py' + - '/ci/tools/run-tests' + - '/ci/test-matrix.yml' + - '/ci/tools/env-vars' + - '/tests/**/*' + - '/ci/tools/merge_cuda_core_wheels.py' + - '/.github/workflows/build-pure-wheel.yml' + - '/.github/workflows/build-wheel.yml' + - '/ci/tools/guess_latest.sh' + - '/ci/tools/install_gpu_driver.sh' + - '/ci/tools/setup-sanitizer' + - '/.github/workflows/test-wheel-linux.yml' + tags: [ci-test-linux] + type: test + options: + runInCI: true + + test-installed-windows: + command: python + args: [ci/tools/moon_ci.py, installed-test, core] + inputs: + - '@group(package)' + - '@group(tests)' + - {project: pathfinder, group: package} + - {project: bindings, group: package} + - '/cuda_python_test_helpers/**/*' + - '/ci/tools/moon_ci.py' + - '/ci/tools/run-tests' + - '/ci/test-matrix.yml' + - '/ci/tools/env-vars' + - '/tests/**/*' + - '/ci/tools/merge_cuda_core_wheels.py' + - '/.github/workflows/build-pure-wheel.yml' + - '/.github/workflows/build-wheel.yml' + - '/ci/tools/configure_driver_mode.ps1' + - '/ci/tools/install_gpu_driver.ps1' + - '/.github/workflows/test-wheel-windows.yml' + tags: [ci-test-windows] + type: test + options: + runInCI: true + + docs: + command: python + args: [ci/tools/moon_ci.py, pixi-docs, core] + inputs: + - '@group(package)' + - '@group(docs)' + - '/cuda_pathfinder/**/*' + - '/cuda_bindings/**/*' + - '/ci/tools/moon_ci.py' + - '/.github/workflows/build-docs.yml' diff --git a/cuda_pathfinder/moon.yml b/cuda_pathfinder/moon.yml new file mode 100644 index 00000000000..66455d2a04a --- /dev/null +++ b/cuda_pathfinder/moon.yml @@ -0,0 +1,177 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +$schema: https://moonrepo.dev/schemas/v2/project.json + +language: unknown +layer: library +toolchains: + default: system + +taskOptions: + cache: false + runFromWorkspaceRoot: true + runInCI: false + +fileGroups: + package: + - '.git_archival.txt' + - 'cuda/**/*' + - 'DESCRIPTION.rst' + - 'LICENSE' + - 'pyproject.toml' + tests: + - 'examples/**/*' + - 'tests/**/*' + - 'pixi.toml' + - 'pixi.lock' + docs: + - 'docs/**/*' + - 'pixi.toml' + - 'pixi.lock' + +tasks: + test: + command: python + args: + - ci/tools/moon_ci.py + - pixi-test + - pathfinder + inputs: + - '@group(package)' + - '@group(tests)' + - '/ci/tools/moon_ci.py' + type: test + + test-installed-linux: + command: python + args: [ci/tools/moon_ci.py, installed-test, pathfinder] + inputs: + - '@group(package)' + - '@group(tests)' + - '/ci/tools/moon_ci.py' + - '/ci/tools/run-tests' + - '/ci/test-matrix.yml' + - '/ci/tools/env-vars' + - '/tests/**/*' + - '/.github/workflows/build-pure-wheel.yml' + - '/ci/tools/guess_latest.sh' + - '/ci/tools/install_gpu_driver.sh' + - '/.github/workflows/test-wheel-linux.yml' + tags: [ci-test-linux] + type: test + options: + runInCI: true + + test-installed-linux-strict: + command: python + args: [ci/tools/moon_ci.py, installed-test, pathfinder] + inputs: + - '@group(package)' + - '@group(tests)' + - '/ci/tools/moon_ci.py' + - '/ci/tools/run-tests' + - '/ci/test-matrix.yml' + - '/ci/tools/env-vars' + - '/tests/**/*' + - '/.github/workflows/build-pure-wheel.yml' + - '/ci/tools/guess_latest.sh' + - '/ci/tools/install_gpu_driver.sh' + - '/.github/workflows/test-wheel-linux.yml' + tags: [ci-test-linux] + type: test + options: + runInCI: true + + test-installed-windows: + command: python + args: [ci/tools/moon_ci.py, installed-test, pathfinder] + inputs: + - '@group(package)' + - '@group(tests)' + - '/ci/tools/moon_ci.py' + - '/ci/tools/run-tests' + - '/ci/test-matrix.yml' + - '/ci/tools/env-vars' + - '/tests/**/*' + - '/.github/workflows/build-pure-wheel.yml' + - '/ci/tools/configure_driver_mode.ps1' + - '/ci/tools/install_gpu_driver.ps1' + - '/.github/workflows/test-wheel-windows.yml' + tags: [ci-test-windows] + type: test + options: + runInCI: true + + test-installed-windows-strict: + command: python + args: [ci/tools/moon_ci.py, installed-test, pathfinder] + inputs: + - '@group(package)' + - '@group(tests)' + - '/ci/tools/moon_ci.py' + - '/ci/tools/run-tests' + - '/ci/test-matrix.yml' + - '/ci/tools/env-vars' + - '/tests/**/*' + - '/.github/workflows/build-pure-wheel.yml' + - '/ci/tools/configure_driver_mode.ps1' + - '/ci/tools/install_gpu_driver.ps1' + - '/.github/workflows/test-wheel-windows.yml' + tags: [ci-test-windows] + type: test + options: + runInCI: true + + docs: + command: python + args: [ci/tools/moon_ci.py, pixi-docs, pathfinder] + inputs: + - '@group(package)' + - '@group(docs)' + - '/ci/tools/moon_ci.py' + - '/.github/workflows/build-docs.yml' + + wheel-pure: + command: python + args: [ci/tools/moon_ci.py, pure-wheel, pathfinder] + inputs: + - '@group(package)' + - '/ci/tools/moon_ci.py' + - '/ci/tools/moon_fingerprint.py' + - '/.github/workflows/build-pure-wheel.yml' + outputs: + - '.moon-out/wheel-pure' + checks: + - check: fingerprint + script: python ci/tools/moon_fingerprint.py pathfinder portable + hash: stdout + tags: [ci-wheel-pure] + type: build + options: + cache: true + cacheKey: wheel-pure-v2 + runInCI: true + + sdist: + command: python + args: [ci/tools/moon_ci.py, sdist, pathfinder] + inputs: + - '@group(package)' + - '/ci/tools/moon_ci.py' + - '/ci/tools/moon_fingerprint.py' + - '/.github/workflows/test-sdist-linux.yml' + - '/.github/workflows/test-sdist-windows.yml' + outputs: + - '.moon-out/sdist' + checks: + - check: fingerprint + script: python ci/tools/moon_fingerprint.py pathfinder sdist + hash: stdout + tags: [ci-sdist] + type: build + options: + cache: true + cacheKey: sdist-v2 + runInCI: true diff --git a/cuda_python/moon.yml b/cuda_python/moon.yml new file mode 100644 index 00000000000..d1abf7bbfe7 --- /dev/null +++ b/cuda_python/moon.yml @@ -0,0 +1,122 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +$schema: https://moonrepo.dev/schemas/v2/project.json + +language: unknown +layer: library +dependsOn: + - pathfinder + - bindings + - core +toolchains: + default: system + +taskOptions: + cache: false + runFromWorkspaceRoot: true + runInCI: false + +fileGroups: + package: + - 'DESCRIPTION.rst' + - 'LICENSE' + - 'pyproject.toml' + - 'setup.py' + docs: + - 'docs/**/*' + +tasks: + wheel-pure: + command: python + args: [ci/tools/moon_ci.py, pure-wheel, metapackage] + inputs: + - '@group(package)' + - {project: bindings, group: package} + - '/ci/tools/moon_ci.py' + - '/ci/tools/moon_fingerprint.py' + - '/.github/workflows/build-pure-wheel.yml' + outputs: + - '.moon-out/wheel-pure' + checks: + - check: fingerprint + script: python ci/tools/moon_fingerprint.py metapackage portable + hash: stdout + tags: [ci-wheel-pure] + type: build + options: + cache: true + cacheKey: wheel-pure-v2 + runInCI: true + + sdist: + command: python + args: [ci/tools/moon_ci.py, sdist, metapackage] + inputs: + - '@group(package)' + - {project: bindings, group: package} + - '/ci/tools/moon_ci.py' + - '/ci/tools/moon_fingerprint.py' + - '/.github/workflows/test-sdist-linux.yml' + - '/.github/workflows/test-sdist-windows.yml' + outputs: + - '.moon-out/sdist' + checks: + - check: fingerprint + script: python ci/tools/moon_fingerprint.py metapackage sdist + hash: stdout + tags: [ci-sdist] + type: build + options: + cache: true + cacheKey: sdist-v2 + runInCI: true + + test-installed-linux: + command: python + args: [ci/tools/moon_ci.py, metapackage-install-test] + inputs: + - '@group(package)' + - {project: pathfinder, group: package} + - {project: bindings, group: package} + - {project: core, group: package} + - '/cuda_core/.moon-out/wheel-merged/*.whl' + - '/ci/tools/moon_ci.py' + - '/ci/test-matrix.yml' + - '/ci/tools/env-vars' + - '/tests/**/*' + - '/ci/tools/merge_cuda_core_wheels.py' + - '/.github/workflows/build-pure-wheel.yml' + - '/.github/workflows/build-wheel.yml' + - '/ci/tools/guess_latest.sh' + - '/ci/tools/install_gpu_driver.sh' + - '/.github/workflows/test-wheel-linux.yml' + tags: [ci-test-linux] + type: test + options: + runInCI: true + + test-installed-windows: + command: python + args: [ci/tools/moon_ci.py, metapackage-install-test] + inputs: + - '@group(package)' + - {project: pathfinder, group: package} + - {project: bindings, group: package} + - {project: core, group: package} + - '/cuda_core/.moon-out/wheel-merged/*.whl' + - '/ci/tools/moon_ci.py' + - '/ci/test-matrix.yml' + - '/ci/tools/env-vars' + - '/tests/**/*' + - '/ci/tools/merge_cuda_core_wheels.py' + - '/.github/workflows/build-pure-wheel.yml' + - '/.github/workflows/build-wheel.yml' + - '/ci/tools/configure_driver_mode.ps1' + - '/ci/tools/install_gpu_driver.ps1' + - '/.github/workflows/test-wheel-windows.yml' + tags: [ci-test-windows] + type: test + options: + runInCI: true diff --git a/cuda_python_test_helpers/moon.yml b/cuda_python_test_helpers/moon.yml new file mode 100644 index 00000000000..0ae9a55ffa5 --- /dev/null +++ b/cuda_python_test_helpers/moon.yml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +$schema: https://moonrepo.dev/schemas/v2/project.json + +language: unknown +layer: library +toolchains: + default: system diff --git a/moon.yml b/moon.yml new file mode 100644 index 00000000000..70a68374dd6 --- /dev/null +++ b/moon.yml @@ -0,0 +1,70 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +$schema: https://moonrepo.dev/schemas/v2/project.json + +language: unknown +layer: automation +dependsOn: + - id: pathfinder + scope: development + - id: bindings + scope: development + - id: core + scope: development + - id: metapackage + scope: development +toolchains: + default: system + +taskOptions: + cache: false + runInCI: false + +tasks: + test: + deps: + - pathfinder:test + - bindings:test + - core:test + inputs: [] + options: + runDepsInParallel: false + + docs: + deps: + - pathfinder:docs + - bindings:docs + - core:docs + inputs: [] + options: + runDepsInParallel: false + + docs-ci: + command: python + args: [ci/tools/moon_ci.py, docs-ci] + env: + CUDA_PYTHON_DOCS_LATEST_ONLY: '${CUDA_PYTHON_DOCS_LATEST_ONLY}' + inputs: + - {project: pathfinder, group: package} + - {project: pathfinder, group: docs} + - {project: bindings, group: package} + - {project: bindings, group: docs} + - {project: core, group: package} + - {project: core, group: docs} + - {project: metapackage, group: package} + - {project: metapackage, group: docs} + - '/ci/tools/moon_ci.py' + - '/.github/workflows/build-docs.yml' + outputs: + - '.moon-out/docs' + tags: [ci-docs] + options: + runInCI: true + + pure-wheel: + deps: + - pathfinder:wheel-pure + - metapackage:wheel-pure + inputs: [] diff --git a/toolshed/check_spdx.py b/toolshed/check_spdx.py index d4c9430673c..54c62caee50 100644 --- a/toolshed/check_spdx.py +++ b/toolshed/check_spdx.py @@ -23,6 +23,7 @@ TOP_LEVEL_DIRS_LICENSE_IDENTIFIERS = { ".agents": "Apache-2.0", ".github": "Apache-2.0", + ".moon": "Apache-2.0", "benchmarks": "Apache-2.0", "ci": "Apache-2.0", "cuda_bindings": "Apache-2.0", From d715d34b94cbb59bf731f02ecedbd68028dcb149 Mon Sep 17 00:00:00 2001 From: Keith Kraus Date: Tue, 18 Aug 2026 14:01:51 -0400 Subject: [PATCH 2/6] ci: make Moon task graph authoritative --- .github/workflows/build-docs.yml | 90 ++--- .github/workflows/build-pure-wheel.yml | 36 +- .github/workflows/build-wheel.yml | 408 +++++++---------------- .github/workflows/ci-nightly.yml | 166 +++++++-- .github/workflows/ci.yml | 295 ++++++++-------- .github/workflows/test-sdist-linux.yml | 21 +- .github/workflows/test-sdist-windows.yml | 21 +- .github/workflows/test-wheel-linux.yml | 240 ++----------- .github/workflows/test-wheel-windows.yml | 229 ++----------- CONTRIBUTING.md | 17 +- benchmarks/cuda_bindings/moon.yml | 54 +-- ci/moon.yml | 357 ++------------------ ci/tools/moon_ci.py | 231 +++++++------ ci/tools/moon_fingerprint.py | 46 ++- ci/tools/tests/test_moon_ci.py | 144 +++++++- ci/tools/tests/test_moon_workspace.py | 395 ++++++++++------------ cuda_bindings/moon.yml | 53 ++- cuda_core/moon.yml | 136 +++++++- cuda_pathfinder/moon.yml | 104 +++++- cuda_python/moon.yml | 38 ++- cuda_python_test_helpers/moon.yml | 22 ++ moon.yml | 27 +- 22 files changed, 1414 insertions(+), 1716 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 12c2098ec1e..d3a7fd85a39 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -40,16 +40,6 @@ on: required: false default: ${{ github.sha }} type: string - moon-base: - description: "Base revision used by Moon affected checks" - required: false - default: "" - type: string - force-all: - description: "Force selected Moon tasks" - required: false - default: false - type: boolean is-release: description: "Are we building release docs?" required: false @@ -159,38 +149,31 @@ jobs: echo "CUDA_BINDINGS_ARTIFACT_NAME=${CUDA_BINDINGS_ARTIFACT_BASENAME}-${FILE_HASH}" >> $GITHUB_ENV echo "CUDA_BINDINGS_ARTIFACTS_DIR=$(realpath "$REPO_DIR/cuda_bindings/dist")" >> $GITHUB_ENV - - name: Download cuda-python build artifacts + - name: Download portable Moon lane + if: ${{ !inputs.is-release }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: cuda-python-wheel + name: moon-lane-build-portable path: . run-id: ${{ inputs.portable-run-id || inputs.run-id }} github-token: ${{ github.token }} - - name: Display structure of downloaded cuda-python artifacts - run: | - pwd - ls -lahR . - - - name: Download cuda-pathfinder build artifacts + - name: Download portable release artifacts + if: ${{ inputs.is-release }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: cuda-pathfinder-wheel - path: ./cuda_pathfinder + pattern: "cuda-*-wheel" + path: . + merge-multiple: false run-id: ${{ inputs.portable-run-id || inputs.run-id }} github-token: ${{ github.token }} - - name: Display structure of downloaded cuda-pathfinder artifacts - run: | - pwd - ls -lahR cuda_pathfinder - - - name: Download cuda.bindings build artifacts + - name: Download native Moon lane if: ${{ !inputs.is-release }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }} - path: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} + name: moon-lane-build-linux-64-py312 + path: . run-id: ${{ inputs.run-id }} github-token: ${{ github.token }} @@ -204,20 +187,6 @@ jobs: run-id: ${{ inputs.run-id }} github-token: ${{ github.token }} - - name: Display structure of downloaded cuda.bindings artifacts - run: | - pwd - ls -lahR $CUDA_BINDINGS_ARTIFACTS_DIR - - - name: Download cuda.core build artifacts - if: ${{ !inputs.is-release }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ env.CUDA_CORE_ARTIFACT_NAME }} - path: ${{ env.CUDA_CORE_ARTIFACTS_DIR }} - run-id: ${{ inputs.run-id }} - github-token: ${{ github.token }} - - name: Download cuda.core build artifacts if: ${{ inputs.is-release }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -229,27 +198,25 @@ jobs: github-token: ${{ github.token }} - name: Display structure of downloaded cuda.core build artifacts + if: ${{ inputs.is-release }} run: | pwd ls -lahR $CUDA_CORE_ARTIFACTS_DIR - name: Install all packages run: | - pushd cuda_pathfinder - pip install *.whl - popd - - pushd "${CUDA_BINDINGS_ARTIFACTS_DIR}" - pip install *.whl - popd - - pushd "${CUDA_CORE_ARTIFACTS_DIR}" - pip install *.whl - popd - - # Subpackages are already installed from CI artifacts above. - # --no-deps avoids re-resolving cuda-core from PyPI during tag releases. - pip install --no-deps cuda_python*.whl + if [[ "${{ inputs.is-release }}" == "true" ]]; then + pip install cuda-pathfinder-wheel/*.whl + pip install "${CUDA_BINDINGS_ARTIFACTS_DIR}"/*.whl + pip install "${CUDA_CORE_ARTIFACTS_DIR}"/*.whl + # Subpackages are already installed from release artifacts above. + pip install --no-deps cuda-python-wheel/*.whl + else + pip install cuda_pathfinder/.moon-out/wheel-pure/*.whl + pip install cuda_bindings/.moon-out/wheel-current/*.whl + pip install cuda_core/.moon-out/wheel-merged/*.whl + pip install --no-deps cuda_python/.moon-out/wheel-pure/*.whl + fi # This step sets the PR_NUMBER/BUILD_LATEST/BUILD_PREVIEW env vars. - name: Get PR number @@ -264,12 +231,13 @@ jobs: - name: Build all docs if: ${{ inputs.component == 'all' }} - env: - MOON_BASE: ${{ inputs.moon-base }} - MOON_HEAD: ${{ github.sha }} run: | if [[ "${{ inputs.is-release }}" == "false" ]]; then - moon ci root:docs-ci --upstream none --downstream none ${{ inputs.force-all && '--force' || '' }} + # Render context differs between main, PR previews, and releases, + # so docs outputs are intentionally rebuilt instead of restored + # from a cross-run cache. Moon still builds the four components in + # parallel before assembling the aggregate site. + moon ci root:docs-ci --force --upstream deep --downstream none mv .moon-out/docs/* artifacts/docs/ else pushd cuda_python/docs/ diff --git a/.github/workflows/build-pure-wheel.yml b/.github/workflows/build-pure-wheel.yml index 302b9f860ad..1d616df67df 100644 --- a/.github/workflows/build-pure-wheel.yml +++ b/.github/workflows/build-pure-wheel.yml @@ -7,12 +7,6 @@ name: "CI: Build portable wheels with Moon" on: workflow_call: inputs: - build-pathfinder: - required: true - type: boolean - build-metapackage: - required: true - type: boolean moon-base: required: true type: string @@ -66,26 +60,8 @@ jobs: if: ${{ inputs.baseline-run-id != '' && !inputs.force-all }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: moon-cache-build-portable - path: .moon/cache - github-token: ${{ github.token }} - run-id: ${{ inputs.baseline-run-id }} - - - name: Restore unchanged cuda.pathfinder wheel - if: ${{ !inputs.build-pathfinder && inputs.baseline-run-id != '' }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: cuda-pathfinder-wheel - path: cuda_pathfinder/.moon-out/wheel-pure - github-token: ${{ github.token }} - run-id: ${{ inputs.baseline-run-id }} - - - name: Restore unchanged cuda-python metapackage wheel - if: ${{ !inputs.build-metapackage && inputs.baseline-run-id != '' }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: cuda-python-wheel - path: cuda_python/.moon-out/wheel-pure + name: moon-lane-build-portable + path: . github-token: ${{ github.token }} run-id: ${{ inputs.baseline-run-id }} @@ -100,7 +76,7 @@ jobs: if [[ "${MOON_FORCE_ALL}" == "true" ]]; then args+=(--force) fi - moon ci ':#ci-wheel-pure' --downstream none "${args[@]}" + moon ci ':#ci-wheel-pure' --upstream deep --downstream none "${args[@]}" - name: Validate portable wheels run: | @@ -129,14 +105,16 @@ jobs: # Moon documents hashes/ and outputs/ as the portable subset of its # local cache. GitHub artifacts provide trusted exact-run transport; # Moon remains responsible for hashes, hits, and output hydration. - - name: Upload portable Moon cache + - name: Upload portable Moon lane if: ${{ always() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: moon-cache-build-portable + name: moon-lane-build-portable path: | .moon/cache/hashes .moon/cache/outputs + cuda_pathfinder/.moon-out/wheel-pure + cuda_python/.moon-out/wheel-pure if-no-files-found: error include-hidden-files: true overwrite: true diff --git a/.github/workflows/build-wheel.yml b/.github/workflows/build-wheel.yml index 8ae90a3c941..a1f69d28e66 100644 --- a/.github/workflows/build-wheel.yml +++ b/.github/workflows/build-wheel.yml @@ -14,30 +14,10 @@ on: prev-cuda-version: required: true type: string - build-bindings: - required: false - type: boolean - default: true - build-core: - required: false - type: boolean - default: true - test-bindings: - required: false - type: boolean - default: true - test-core: - required: false - type: boolean - default: true baseline-run-id: required: false type: string default: "" - baseline-sha: - required: false - type: string - default: "" portable-run-id: description: "Workflow run containing the selected portable wheels" required: true @@ -65,15 +45,15 @@ jobs: strategy: fail-fast: false matrix: - python-version: - - "3.10" - - "3.11" - - "3.12" - - "3.13" - - "3.14" - - "3.14t" - - "3.15" - - "3.15t" + include: + - {python-version: "3.10", python-version-formatted: "310"} + - {python-version: "3.11", python-version-formatted: "311"} + - {python-version: "3.12", python-version-formatted: "312"} + - {python-version: "3.13", python-version-formatted: "313"} + - {python-version: "3.14", python-version-formatted: "314"} + - {python-version: "3.14t", python-version-formatted: "314t"} + - {python-version: "3.15", python-version-formatted: "315"} + - {python-version: "3.15t", python-version-formatted: "315t"} name: py${{ matrix.python-version }} runs-on: ${{ (inputs.host-platform == 'linux-64' && 'linux-amd64-cpu8') || (inputs.host-platform == 'linux-aarch64' && 'linux-arm64-cpu8') || @@ -88,7 +68,7 @@ jobs: filter: blob:none - name: Install latest rapidsai/sccache - if: ${{ startsWith(inputs.host-platform, 'linux') && (inputs.build-bindings || inputs.build-core) }} + if: ${{ startsWith(inputs.host-platform, 'linux') }} run: | curl -fsSL "https://github.com/rapidsai/sccache/releases/latest/download/sccache-$(uname -m)-unknown-linux-musl.tar.gz" \ | sudo tar -C /usr/local/bin -xvzf - --wildcards --strip-components=1 -x '*/sccache' @@ -96,7 +76,6 @@ jobs: # xref: https://github.com/orgs/community/discussions/42856#discussioncomment-7678867 - name: Adding addtional GHA cache-related env vars - if: ${{ inputs.build-bindings || inputs.build-core }} uses: actions/github-script@v9 with: script: | @@ -133,19 +112,19 @@ jobs: if: ${{ inputs.baseline-run-id != '' && !inputs.force-all }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: moon-cache-build-${{ inputs.host-platform }}-py${{ matrix.python-version }} - path: .moon/cache + name: moon-lane-build-${{ inputs.host-platform }}-py${{ matrix.python-version-formatted }} + path: . github-token: ${{ github.token }} run-id: ${{ inputs.baseline-run-id }} - name: Set up MSVC - if: ${{ startsWith(inputs.host-platform, 'win') && (inputs.build-bindings || inputs.build-core || inputs.test-bindings || inputs.test-core) }} + if: ${{ startsWith(inputs.host-platform, 'win') }} uses: step-security/msvc-dev-cmd@22c98154b708dbd743e6f27a933cf6ceba3305c4 # v1.13.1 - name: Set up yq # GitHub made an unprofessional decision to not provide it in their Windows VMs, # see https://github.com/actions/runner-images/issues/7443. - if: ${{ startsWith(inputs.host-platform, 'win') && inputs.build-core }} + if: ${{ startsWith(inputs.host-platform, 'win') }} env: YQ_VERSION: v4.52.5 YQ_SHA256: 47594981f3848a4b4447494adeca9555f908f7cf0a89c4da3fd0243a4631da1c @@ -180,11 +159,11 @@ jobs: - name: Install externally managed build tools run: python -m pip install "cibuildwheel==4.1.1" twine wheel - - name: Download cuda.pathfinder wheel from the portable producer + - name: Download portable Moon lane uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: cuda-pathfinder-wheel - path: cuda_pathfinder/.moon-out/wheel-pure + name: moon-lane-build-portable + path: . github-token: ${{ github.token }} run-id: ${{ inputs.portable-run-id }} @@ -199,98 +178,13 @@ jobs: ls -lahR cuda_pathfinder/.moon-out/wheel-pure - name: Set up mini CTK - if: ${{ inputs.build-bindings || inputs.build-core || inputs.test-bindings || inputs.test-core }} uses: ./.github/actions/fetch_ctk continue-on-error: false with: host-platform: ${{ inputs.host-platform }} cuda-version: ${{ inputs.cuda-version }} - - name: Build cuda.bindings wheel - if: ${{ inputs.build-bindings }} - run: moon ci bindings:wheel-current --upstream none --downstream none ${{ inputs.force-all && '--force' || '' }} - env: - CUDA_PYTHON_LANE: ${{ inputs.host-platform }}-py${{ matrix.python-version }}-cuda${{ inputs.cuda-version }} - MOON_BASE: ${{ inputs.moon-base }} - MOON_HEAD: ${{ github.sha }} - CIBW_BUILD: ${{ env.CIBW_BUILD }} - CIBW_BEFORE_BUILD_LINUX: 'python -m pip install --upgrade "pip>=25.3"' - CIBW_BEFORE_BUILD_WINDOWS: 'python -m pip install --upgrade "pip>=25.3" delvewheel' - # TODO: remove cpython-prerelease once 3.15 is officially supported - # Allow CPython pre-release builds (currently 3.15 / 3.15t). This is a - # no-op for stable Python versions because CIBW_BUILD still filters - # the target version. - CIBW_ENABLE: cpython-prerelease - # CIBW mounts the host filesystem under /host - CIBW_ENVIRONMENT_LINUX: > - CUDA_PATH=/host/${{ env.CUDA_PATH }} - CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} - CC="/host/${{ env.SCCACHE_PATH }} cc" - CXX="/host/${{ env.SCCACHE_PATH }} c++" - SCCACHE_GHA_ENABLED=true - ACTIONS_RUNTIME_TOKEN=${{ env.ACTIONS_RUNTIME_TOKEN }} - ACTIONS_RUNTIME_URL=${{ env.ACTIONS_RUNTIME_URL }} - ACTIONS_RESULTS_URL=${{ env.ACTIONS_RESULTS_URL }} - ACTIONS_CACHE_URL=${{ env.ACTIONS_CACHE_URL }} - ACTIONS_CACHE_SERVICE_V2=${{ env.ACTIONS_CACHE_SERVICE_V2 }} - SCCACHE_DIR=/host/${{ env.SCCACHE_DIR }} - SCCACHE_CACHE_SIZE=${{ env.SCCACHE_CACHE_SIZE }} - CIBW_ENVIRONMENT_WINDOWS: > - CUDA_PATH="$(cygpath -w ${{ env.CUDA_PATH }})" - CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} - # check cache stats before leaving cibuildwheel - CIBW_BEFORE_TEST_LINUX: > - "/host/${{ env.SCCACHE_PATH }}" --show-adv-stats && - "/host/${{ env.SCCACHE_PATH }}" --show-stats --stats-format=json > /host/${{ github.workspace }}/sccache_bindings.json - # force the test stage to be run (so that before-test is not skipped) - # TODO: we might want to think twice on adding this, it does a lot of - # things before reaching this command. - CIBW_TEST_COMMAND: > - echo "ok!" - - - name: Report sccache stats (cuda.bindings) - if: ${{ inputs.build-bindings && inputs.host-platform != 'win-64' }} - uses: ./.github/actions/sccache-summary - with: - json-file: sccache_bindings.json - label: "cuda.bindings" - build-step: "Build cuda.bindings wheel" - - - name: Download reusable cuda.bindings wheel - if: ${{ !inputs.build-bindings }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ env.CUDA_BINDINGS_ARTIFACT_BASENAME }}-${{ inputs.baseline-sha }} - path: cuda_bindings/.moon-out/wheel-current - github-token: ${{ github.token }} - run-id: ${{ inputs.baseline-run-id }} - - - name: List the cuda.bindings artifacts directory - run: | - if [[ "${{ inputs.host-platform }}" == win* ]]; then - export CHOWN=chown - else - export CHOWN="sudo chown" - fi - $CHOWN -R $(whoami) cuda_bindings/.moon-out/wheel-current - ls -lahR cuda_bindings/.moon-out/wheel-current - - - name: Check cuda.bindings wheel - if: ${{ inputs.build-bindings }} - run: | - twine check --strict cuda_bindings/.moon-out/wheel-current/*.whl - - - name: Upload cuda.bindings build artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }} - path: cuda_bindings/.moon-out/wheel-current/*.whl - if-no-files-found: error - overwrite: true - - - name: Build cuda.core wheel - if: ${{ inputs.build-core }} - run: moon ci core:wheel-current --upstream none --downstream none ${{ inputs.force-all && '--force' || '' }} + - name: Build current native wheels with Moon env: CUDA_PYTHON_LANE: ${{ inputs.host-platform }}-py${{ matrix.python-version }}-cuda${{ inputs.cuda-version }} MOON_BASE: ${{ inputs.moon-base }} @@ -307,7 +201,6 @@ jobs: CIBW_ENVIRONMENT_LINUX: > CUDA_PATH=/host/${{ env.CUDA_PATH }} CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} - CUDA_CORE_BUILD_MAJOR=${{ env.BUILD_CUDA_MAJOR }} CC="/host/${{ env.SCCACHE_PATH }} cc" CXX="/host/${{ env.SCCACHE_PATH }} c++" SCCACHE_GHA_ENABLED=true @@ -321,99 +214,42 @@ jobs: CIBW_ENVIRONMENT_WINDOWS: > CUDA_PATH="$(cygpath -w ${{ env.CUDA_PATH }})" CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} - CUDA_CORE_BUILD_MAJOR=${{ env.BUILD_CUDA_MAJOR }} - # check cache stats before leaving cibuildwheel - CIBW_BEFORE_TEST_LINUX: > - "/host${{ env.SCCACHE_PATH }}" --show-adv-stats && - "/host${{ env.SCCACHE_PATH }}" --show-stats --stats-format=json > /host/${{ github.workspace }}/sccache_core.json - # force the test stage to be run (so that before-test is not skipped) - # TODO: we might want to think twice on adding this, it does a lot of - # things before reaching this command. - CIBW_TEST_COMMAND: > - echo "ok!" - - - name: Report sccache stats (cuda.core) - if: ${{ inputs.build-core && inputs.host-platform != 'win-64' }} - uses: ./.github/actions/sccache-summary - with: - json-file: sccache_core.json - label: "cuda.core" - build-step: "Build cuda.core wheel" - - - name: List the current cuda.core artifacts - if: ${{ inputs.build-core }} + CIBW_BEFORE_TEST_LINUX: '"/host/${{ env.SCCACHE_PATH }}" --show-adv-stats' + # Run the test stage so the sccache summary hook is not skipped. + CIBW_TEST_COMMAND: 'echo "ok!"' run: | - if [[ "${{ inputs.host-platform }}" == win* ]]; then - export CHOWN=chown - else - export CHOWN="sudo chown" + args=() + if [[ "${{ inputs.force-all }}" == "true" ]]; then + args+=(--force) fi - $CHOWN -R $(whoami) cuda_core/.moon-out/wheel-current - ls -lahR cuda_core/.moon-out/wheel-current - - - name: Download reusable cuda.core wheel - if: ${{ !inputs.build-core }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ env.CUDA_CORE_ARTIFACT_BASENAME }}-${{ inputs.baseline-sha }} - path: cuda_core/.moon-out/wheel-merged - github-token: ${{ github.token }} - run-id: ${{ inputs.baseline-run-id }} - - - name: Stage reusable cuda.core wheel for Cython test assets - if: ${{ !inputs.build-core && inputs.test-core }} - run: | - mkdir -p cuda_core/.moon-out/wheel-current - cp cuda_core/.moon-out/wheel-merged/*.whl cuda_core/.moon-out/wheel-current/ - - - name: Restore reusable cuda.bindings Cython test assets - if: ${{ inputs.test-bindings && inputs.baseline-run-id != '' && !inputs.force-all }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ env.CUDA_BINDINGS_ARTIFACT_BASENAME }}-${{ inputs.baseline-sha }}-tests - path: cuda_bindings/.moon-out/cython-tests - github-token: ${{ github.token }} - run-id: ${{ inputs.baseline-run-id }} - - - name: Restore reusable cuda.core Cython test assets - if: ${{ inputs.test-core && inputs.baseline-run-id != '' && !inputs.force-all }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ env.CUDA_CORE_ARTIFACT_BASENAME }}-${{ inputs.baseline-sha }}-tests - path: cuda_core/.moon-out/cython-tests - github-token: ${{ github.token }} - run-id: ${{ inputs.baseline-run-id }} - - - name: Restore reusable cuda.core test binaries - if: ${{ inputs.test-core && inputs.baseline-run-id != '' && !inputs.force-all }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ env.CUDA_CORE_ARTIFACT_BASENAME }}-${{ inputs.baseline-sha }}-test-binaries - path: cuda_core/.moon-out/test-binaries - github-token: ${{ github.token }} - run-id: ${{ inputs.baseline-run-id }} + # These tasks are a true build dependency, so execute their affected + # checks in order without pulling an unchanged upstream task into the + # second invocation. + moon ci bindings:wheel-current --upstream none --downstream none "${args[@]}" + moon ci core:wheel-current --upstream none --downstream none "${args[@]}" - name: Set up Python id: setup-python2 - if: ${{ inputs.test-bindings || inputs.test-core }} uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ matrix.python-version }} # TODO: remove allow-prereleases once 3.15 is officially supported allow-prereleases: ${{ startsWith(matrix.python-version, '3.15') }} + - name: Install target-Python build tools + run: python -m pip install "cibuildwheel==4.1.1" twine wheel + - name: Enable Scientific Python Nightly Wheels for Python 3.15 - if: ${{ (inputs.test-bindings || inputs.test-core) && startsWith(matrix.python-version, '3.15') }} + if: ${{ startsWith(matrix.python-version, '3.15') }} run: | echo "PIP_EXTRA_INDEX_URL=https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" >> "$GITHUB_ENV" echo "PIP_ONLY_BINARY=numpy" >> "$GITHUB_ENV" - name: verify free-threaded build - if: ${{ (inputs.test-bindings || inputs.test-core) && endsWith(matrix.python-version, 't') }} + if: ${{ endsWith(matrix.python-version, 't') }} run: python -c 'import sys; assert not sys._is_gil_enabled()' - name: Set up Python include paths - if: ${{ inputs.test-bindings || inputs.test-core }} run: | if [[ "${{ inputs.host-platform }}" == linux* ]]; then echo "CPLUS_INCLUDE_PATH=${Python3_ROOT_DIR}/include/python${{ matrix.python-version }}" >> $GITHUB_ENV @@ -423,55 +259,31 @@ jobs: # For caching echo "PY_EXT_SUFFIX=$(python -c "import sysconfig; print(sysconfig.get_config_var('EXT_SUFFIX'))")" >> $GITHUB_ENV - - name: Install cuda.pathfinder (required for next step) - if: ${{ inputs.test-bindings || inputs.test-core }} - run: | - pip install cuda_pathfinder/.moon-out/wheel-pure/*.whl - - name: Hide GNU link.exe so Meson finds MSVC link.exe - if: ${{ startsWith(inputs.host-platform, 'win') && (inputs.test-bindings || inputs.test-core) }} + if: ${{ startsWith(inputs.host-platform, 'win') }} run: | if [ -f "/c/Program Files/Git/usr/bin/link.exe" ]; then mv "/c/Program Files/Git/usr/bin/link.exe" "/c/Program Files/Git/usr/bin/link.exe.bak" fi - - name: Build cuda.bindings Cython tests - if: ${{ inputs.test-bindings }} + - name: Build target-Python test assets with Moon env: CUDA_PYTHON_LANE: ${{ inputs.host-platform }}-py${{ matrix.python-version }}-cuda${{ inputs.cuda-version }} MOON_BASE: ${{ inputs.moon-base }} MOON_HEAD: ${{ github.sha }} - run: moon ci bindings:cython-test-assets --upstream none --downstream none ${{ inputs.force-all && '--force' || '' }} - - - name: Upload cuda.bindings Cython tests - if: ${{ inputs.test-bindings }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }}-tests - path: cuda_bindings/.moon-out/cython-tests/test_*${{ env.PY_EXT_SUFFIX }} - if-no-files-found: error - overwrite: true - - - name: Build cuda.core Cython tests - if: ${{ inputs.test-core }} - env: - CUDA_PYTHON_LANE: ${{ inputs.host-platform }}-py${{ matrix.python-version }}-cuda${{ inputs.cuda-version }} - MOON_BASE: ${{ inputs.moon-base }} - MOON_HEAD: ${{ github.sha }} - run: moon ci core:cython-test-assets --upstream none --downstream none ${{ inputs.force-all && '--force' || '' }} - - - name: Upload cuda.core Cython tests - if: ${{ inputs.test-core }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-tests - path: cuda_core/.moon-out/cython-tests/test_*${{ env.PY_EXT_SUFFIX }} - if-no-files-found: error - overwrite: true + run: | + args=() + if [[ "${{ inputs.force-all }}" == "true" ]]; then + args+=(--force) + fi + # Install the shared test groups before Moon fingerprints the compiler + # and resolved Cython/NumPy versions for the cached binary outputs. + moon ci test-helpers:prepare-test-assets --force --upstream none --downstream none + moon ci bindings:cython-test-assets core:cython-test-assets \ + --upstream none --downstream none "${args[@]}" # Note: This overwrites CUDA_PATH etc - - name: Set up mini CTK - if: ${{ inputs.build-core || inputs.test-core }} + - name: Set up previous mini CTK uses: ./.github/actions/fetch_ctk continue-on-error: false with: @@ -479,28 +291,7 @@ jobs: cuda-version: ${{ inputs.prev-cuda-version }} cuda-path: "./cuda_toolkit_prev" - - name: Build cuda.core test binaries - if: ${{ inputs.test-core }} - env: - CUDA_PYTHON_LANE: ${{ inputs.host-platform }}-py${{ matrix.python-version }}-cuda${{ inputs.prev-cuda-version }} - MOON_BASE: ${{ inputs.moon-base }} - MOON_HEAD: ${{ github.sha }} - run: moon ci core:test-binaries --upstream none --downstream none ${{ inputs.force-all && '--force' || '' }} - - - name: Upload cuda.core test binaries - if: ${{ inputs.test-core }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-test-binaries - path: | - cuda_core/.moon-out/test-binaries/*.o - cuda_core/.moon-out/test-binaries/*.a - cuda_core/.moon-out/test-binaries/*.lib - if-no-files-found: error - overwrite: true - - name: Download cuda.bindings build artifacts from the prior branch - if: ${{ inputs.build-core }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | @@ -524,13 +315,12 @@ jobs: gh run download $LATEST_PRIOR_RUN_ID -p ${OLD_BASENAME} -R NVIDIA/cuda-python rm -rf ${OLD_BASENAME}-tests # exclude cython test artifacts ls -al $OLD_BASENAME + rm -rf "${PREV_BINDINGS_DIR}" mkdir -p "${PREV_BINDINGS_DIR}" mv $OLD_BASENAME/*.whl "${PREV_BINDINGS_DIR}" rmdir $OLD_BASENAME - - name: Build cuda.core wheel - if: ${{ inputs.build-core }} - run: moon ci core:wheel-previous --upstream none --downstream none ${{ inputs.force-all && '--force' || '' }} + - name: Build previous-CTK outputs and merge wheels with Moon env: CUDA_PYTHON_LANE: ${{ inputs.host-platform }}-py${{ matrix.python-version }}-cuda${{ inputs.prev-cuda-version }} MOON_BASE: ${{ inputs.moon-base }} @@ -561,48 +351,67 @@ jobs: CIBW_ENVIRONMENT_WINDOWS: > CUDA_PATH="$(cygpath -w ${{ env.CUDA_PATH }})" CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} - CUDA_CORE_BUILD_MAJOR=${{ env.BUILD_PREV_CUDA_MAJOR }} - # check cache stats before leaving cibuildwheel - CIBW_BEFORE_TEST_LINUX: > - "/host${{ env.SCCACHE_PATH }}" --show-adv-stats && - "/host${{ env.SCCACHE_PATH }}" --show-stats --stats-format=json > /host/${{ github.workspace }}/sccache_core_prev.json - # force the test stage to be run (so that before-test is not skipped) - # TODO: we might want to think twice on adding this, it does a lot of - # things before reaching this command. - CIBW_TEST_COMMAND: > - echo "ok!" - - - name: Report sccache stats (cuda.core prev) - if: ${{ inputs.build-core && inputs.host-platform != 'win-64' }} - uses: ./.github/actions/sccache-summary - with: - json-file: sccache_core_prev.json - label: "cuda.core (prev CTK)" - build-step: "Build cuda.core wheel" + CIBW_BEFORE_TEST_LINUX: '"/host/${{ env.SCCACHE_PATH }}" --show-adv-stats' + CIBW_TEST_COMMAND: 'echo "ok!"' + run: | + args=() + if [[ "${{ inputs.force-all }}" == "true" ]]; then + args+=(--force) + fi + # The previous-CTK wheel and test binaries are independent, so Moon + # builds them in parallel. The merge then consumes the selected or + # restored current and previous wheel outputs. + moon ci core:wheel-previous core:test-binaries --upstream none --downstream none "${args[@]}" + moon ci core:wheel-merge --upstream none --downstream none "${args[@]}" - - name: List the previous cuda.core artifacts - if: ${{ inputs.build-core }} + - name: Validate native lane outputs run: | if [[ "${{ inputs.host-platform }}" == win* ]]; then - export CHOWN=chown + CHOWN=chown else - export CHOWN="sudo chown" + CHOWN="sudo chown" fi - $CHOWN -R $(whoami) cuda_core/.moon-out/wheel-previous - ls -lahR cuda_core/.moon-out/wheel-previous + $CHOWN -R "$(whoami)" cuda_bindings/.moon-out cuda_core/.moon-out + test "$(find cuda_bindings/.moon-out/wheel-current -maxdepth 1 -name '*.whl' -type f | wc -l)" -eq 1 + test "$(find cuda_core/.moon-out/wheel-merged -maxdepth 1 -name '*.whl' -type f | wc -l)" -eq 1 + twine check --strict \ + cuda_bindings/.moon-out/wheel-current/*.whl \ + cuda_core/.moon-out/wheel-merged/*.whl - - name: Merge cuda.core wheels - if: ${{ inputs.build-core }} - env: - CUDA_PYTHON_LANE: ${{ inputs.host-platform }}-py${{ matrix.python-version }}-cuda${{ inputs.cuda-version }}-${{ inputs.prev-cuda-version }} - MOON_BASE: ${{ inputs.moon-base }} - MOON_HEAD: ${{ github.sha }} - run: moon ci core:wheel-merge --upstream none --downstream none ${{ inputs.force-all && '--force' || '' }} + - name: Upload cuda.bindings build artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }} + path: cuda_bindings/.moon-out/wheel-current/*.whl + if-no-files-found: error + overwrite: true - - name: Check cuda.core wheel - if: ${{ inputs.build-core }} - run: | - twine check --strict cuda_core/.moon-out/wheel-merged/*.whl + - name: Upload cuda.bindings Cython tests + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }}-tests + path: cuda_bindings/.moon-out/cython-tests/test_*${{ env.PY_EXT_SUFFIX }} + if-no-files-found: error + overwrite: true + + - name: Upload cuda.core Cython tests + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-tests + path: cuda_core/.moon-out/cython-tests/test_*${{ env.PY_EXT_SUFFIX }} + if-no-files-found: error + overwrite: true + + - name: Upload cuda.core test binaries + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-test-binaries + path: | + cuda_core/.moon-out/test-binaries/*.o + cuda_core/.moon-out/test-binaries/*.a + cuda_core/.moon-out/test-binaries/*.lib + if-no-files-found: error + overwrite: true - name: Upload cuda.core build artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -612,14 +421,21 @@ jobs: if-no-files-found: error overwrite: true - - name: Upload Moon cache for this build lane + - name: Upload native Moon lane if: ${{ always() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: moon-cache-build-${{ inputs.host-platform }}-py${{ matrix.python-version }} + name: moon-lane-build-${{ inputs.host-platform }}-py${{ matrix.python-version-formatted }} path: | .moon/cache/hashes .moon/cache/outputs + cuda_bindings/.moon-out/wheel-current + cuda_bindings/.moon-out/cython-tests + cuda_core/.moon-out/wheel-current + cuda_core/.moon-out/wheel-previous + cuda_core/.moon-out/wheel-merged + cuda_core/.moon-out/cython-tests + cuda_core/.moon-out/test-binaries if-no-files-found: error include-hidden-files: true overwrite: true diff --git a/.github/workflows/ci-nightly.yml b/.github/workflows/ci-nightly.yml index 0188ebf2524..7ccd3b87b54 100644 --- a/.github/workflows/ci-nightly.yml +++ b/.github/workflows/ci-nightly.yml @@ -26,7 +26,7 @@ on: inputs: run-id: description: > - Override the CI run ID to download artifacts from. + Override the Moon-native CI run ID to download lane artifacts from. Leave empty to auto-detect the latest successful main run. type: string default: '' @@ -50,10 +50,24 @@ jobs: find-wheels: runs-on: ubuntu-latest + permissions: + actions: read + contents: read outputs: RUN_ID: ${{ steps.find.outputs.run_id }} HEAD_SHA: ${{ steps.find.outputs.head_sha }} CUDA_BUILD_VER: ${{ steps.find.outputs.cuda_build_ver }} + PYTORCH_LINUX_64_MATRIX: ${{ steps.matrices.outputs.pytorch_linux_64 }} + PYTORCH_LINUX_ARM64_MATRIX: ${{ steps.matrices.outputs.pytorch_linux_arm64 }} + PYTORCH_WINDOWS_MATRIX: ${{ steps.matrices.outputs.pytorch_windows }} + NUMBA_LINUX_64_MATRIX: ${{ steps.matrices.outputs.numba_linux_64 }} + NUMBA_LINUX_ARM64_MATRIX: ${{ steps.matrices.outputs.numba_linux_arm64 }} + NUMBA_WINDOWS_MATRIX: ${{ steps.matrices.outputs.numba_windows }} + MLIR_LINUX_64_MATRIX: ${{ steps.matrices.outputs.mlir_linux_64 }} + MLIR_WINDOWS_MATRIX: ${{ steps.matrices.outputs.mlir_windows }} + CORE_LINUX_64_MATRIX: ${{ steps.matrices.outputs.core_linux_64 }} + CORE_WINDOWS_MATRIX: ${{ steps.matrices.outputs.core_windows }} + STANDARD_LINUX_ARM64_MATRIX: ${{ steps.matrices.outputs.standard_linux_arm64 }} steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -65,16 +79,91 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | + expected=(moon-lane-build-portable) + python_versions=(310 311 312 313 314 314t 315 315t) + host_platforms=(linux-64 linux-aarch64 win-64) + for host_platform in "${host_platforms[@]}"; do + for python_version in "${python_versions[@]}"; do + expected+=("moon-lane-build-${host_platform}-py${python_version}") + done + done + + has_moon_lanes() { + local run_id=$1 + local artifacts artifact_name artifact_valid + artifacts="$( + gh api --paginate --slurp \ + "repos/${{ github.repository }}/actions/runs/${run_id}/artifacts?per_page=100" \ + | jq -c '[.[].artifacts[]]' + )" + for artifact_name in "${expected[@]}"; do + artifact_valid="$( + jq \ + --arg name "${artifact_name}" \ + '[.[] | select(.name == $name)] | + length == 1 and + (.[0].expired | not) and + .[0].size_in_bytes > 0 and + ((.[0].digest // "") | + test("^sha256:[0-9a-fA-F]{64}$"))' \ + <<< "${artifacts}" + )" + if [[ "${artifact_valid}" != "true" ]]; then + return 1 + fi + done + } + if [[ -n "${{ inputs.run-id }}" ]]; then RUN_ID="${{ inputs.run-id }}" - HEAD_SHA=$(gh run view "$RUN_ID" \ - -R "${{ github.repository }}" \ - --json headSha | jq -r '.headSha') + RUN="$(gh api "repos/${{ github.repository }}/actions/runs/${RUN_ID}")" + if ! jq -e ' + .head_branch == "main" and + .event == "push" and + .conclusion == "success" and + .path == ".github/workflows/ci.yml" and + .head_repository.full_name == "${{ github.repository }}" + ' <<< "${RUN}" > /dev/null; then + echo "::error::Run ${RUN_ID} is not a successful trusted push run on main." + exit 1 + fi + HEAD_SHA="$(jq -r '.head_sha' <<< "${RUN}")" + if ! has_moon_lanes "${RUN_ID}"; then + echo "::error::Run ${RUN_ID} does not contain the required Moon lane artifacts." + exit 1 + fi else - # lookup-run-id --branch --head-sha prints two lines: run_id then head_sha - OUTPUT=$(./ci/tools/lookup-run-id --branch main --head-sha "${{ github.repository }}" "CI") - RUN_ID=$(echo "$OUTPUT" | sed -n '1p') - HEAD_SHA=$(echo "$OUTPUT" | sed -n '2p') + RUN_ID="" + HEAD_SHA="" + RUNS="$(gh run list \ + --repo "${{ github.repository }}" \ + --workflow ci.yml \ + --branch main \ + --event push \ + --status success \ + --limit 20 \ + --json databaseId,headSha,headBranch,event,conclusion,createdAt)" + while IFS=$'\t' read -r candidate candidate_sha; do + if has_moon_lanes "${candidate}"; then + RUN_ID="${candidate}" + HEAD_SHA="${candidate_sha}" + break + fi + done < <( + jq -r ' + sort_by(.createdAt) | reverse | .[] | + select( + .headBranch == "main" and + .event == "push" and + .conclusion == "success" + ) | + [.databaseId, .headSha] | @tsv + ' <<< "${RUNS}" + ) + if [[ -z "${RUN_ID}" ]]; then + echo "::error::No recent successful main CI run contains the required Moon lane artifacts." + exit 1 + fi fi if [[ -z "$HEAD_SHA" || "$HEAD_SHA" == "null" ]]; then @@ -97,6 +186,45 @@ jobs: echo "head_sha=$HEAD_SHA" >> $GITHUB_OUTPUT echo "cuda_build_ver=$CUDA_BUILD_VER" >> $GITHUB_OUTPUT + - name: Compute nightly test matrices + id: matrices + run: | + emit_matrix() { + local output_name="$1" + local os="$2" + local arch="$3" + local mode="$4" + local matrix + matrix=$(yq -o=json ".${os}.nightly" ci/test-matrix.yml | jq -c \ + --arg arch "${arch}" --arg mode "${mode}" ' + map(select(.ARCH == $arch and .ENV.MODE == $mode)) + | if length == 0 then + error("empty nightly matrix") + elif any(.[]; + .DRIVER != "latest" and + .DRIVER != "earliest" and + .FLAVOR == "wsl") then + error("Custom DRIVER is not supported with FLAVOR=wsl") + else + {include: map(. + {RUNNER_DRIVER: ( + if .DRIVER == "latest" or .DRIVER == "earliest" + then .DRIVER else "latest" end)})} + end') + echo "${output_name}=${matrix}" >> "$GITHUB_OUTPUT" + } + + emit_matrix pytorch_linux_64 linux amd64 nightly-pytorch + emit_matrix pytorch_linux_arm64 linux arm64 nightly-pytorch + emit_matrix pytorch_windows windows amd64 nightly-pytorch + emit_matrix numba_linux_64 linux amd64 nightly-numba-cuda + emit_matrix numba_linux_arm64 linux arm64 nightly-numba-cuda + emit_matrix numba_windows windows amd64 nightly-numba-cuda + emit_matrix mlir_linux_64 linux amd64 nightly-numba-cuda-mlir + emit_matrix mlir_windows windows amd64 nightly-numba-cuda-mlir + emit_matrix core_linux_64 linux amd64 nightly-cuda-core + emit_matrix core_windows windows amd64 nightly-cuda-core + emit_matrix standard_linux_arm64 linux arm64 nightly-standard + # ── PyTorch interop tests ── test-pytorch-linux: @@ -115,7 +243,7 @@ jobs: run-id: ${{ needs.find-wheels.outputs.RUN_ID }} sha: ${{ needs.find-wheels.outputs.HEAD_SHA }} test-mode: nightly-pytorch - matrix_filter: 'map(select(.ENV.MODE == "nightly-pytorch"))' + matrix: ${{ needs.find-wheels.outputs.PYTORCH_LINUX_64_MATRIX }} test-pytorch-linux-aarch64: name: "Nightly PyTorch (linux-aarch64)" @@ -133,7 +261,7 @@ jobs: run-id: ${{ needs.find-wheels.outputs.RUN_ID }} sha: ${{ needs.find-wheels.outputs.HEAD_SHA }} test-mode: nightly-pytorch - matrix_filter: 'map(select(.ENV.MODE == "nightly-pytorch"))' + matrix: ${{ needs.find-wheels.outputs.PYTORCH_LINUX_ARM64_MATRIX }} test-pytorch-windows: name: "Nightly PyTorch (win-64)" @@ -151,7 +279,7 @@ jobs: run-id: ${{ needs.find-wheels.outputs.RUN_ID }} sha: ${{ needs.find-wheels.outputs.HEAD_SHA }} test-mode: nightly-pytorch - matrix_filter: 'map(select(.ENV.MODE == "nightly-pytorch"))' + matrix: ${{ needs.find-wheels.outputs.PYTORCH_WINDOWS_MATRIX }} # ── numba-cuda tests ── @@ -171,7 +299,7 @@ jobs: run-id: ${{ needs.find-wheels.outputs.RUN_ID }} sha: ${{ needs.find-wheels.outputs.HEAD_SHA }} test-mode: nightly-numba-cuda - matrix_filter: 'map(select(.ENV.MODE == "nightly-numba-cuda"))' + matrix: ${{ needs.find-wheels.outputs.NUMBA_LINUX_64_MATRIX }} test-numba-cuda-linux-aarch64: name: "Nightly numba-cuda (linux-aarch64)" @@ -189,7 +317,7 @@ jobs: run-id: ${{ needs.find-wheels.outputs.RUN_ID }} sha: ${{ needs.find-wheels.outputs.HEAD_SHA }} test-mode: nightly-numba-cuda - matrix_filter: 'map(select(.ENV.MODE == "nightly-numba-cuda"))' + matrix: ${{ needs.find-wheels.outputs.NUMBA_LINUX_ARM64_MATRIX }} test-numba-cuda-windows: name: "Nightly numba-cuda (win-64)" @@ -207,7 +335,7 @@ jobs: run-id: ${{ needs.find-wheels.outputs.RUN_ID }} sha: ${{ needs.find-wheels.outputs.HEAD_SHA }} test-mode: nightly-numba-cuda - matrix_filter: 'map(select(.ENV.MODE == "nightly-numba-cuda"))' + matrix: ${{ needs.find-wheels.outputs.NUMBA_WINDOWS_MATRIX }} # ── numba-cuda-mlir tests ── @@ -227,7 +355,7 @@ jobs: run-id: ${{ needs.find-wheels.outputs.RUN_ID }} sha: ${{ needs.find-wheels.outputs.HEAD_SHA }} test-mode: nightly-numba-cuda-mlir - matrix_filter: 'map(select(.ENV.MODE == "nightly-numba-cuda-mlir"))' + matrix: ${{ needs.find-wheels.outputs.MLIR_LINUX_64_MATRIX }} test-numba-cuda-mlir-windows: name: "Nightly numba-cuda-mlir (win-64)" @@ -245,7 +373,7 @@ jobs: run-id: ${{ needs.find-wheels.outputs.RUN_ID }} sha: ${{ needs.find-wheels.outputs.HEAD_SHA }} test-mode: nightly-numba-cuda-mlir - matrix_filter: 'map(select(.ENV.MODE == "nightly-numba-cuda-mlir"))' + matrix: ${{ needs.find-wheels.outputs.MLIR_WINDOWS_MATRIX }} # ── Released cuda-core against main pathfinder/bindings ── @@ -265,7 +393,7 @@ jobs: run-id: ${{ needs.find-wheels.outputs.RUN_ID }} sha: ${{ needs.find-wheels.outputs.HEAD_SHA }} test-mode: nightly-cuda-core - matrix_filter: 'map(select(.ENV.MODE == "nightly-cuda-core"))' + matrix: ${{ needs.find-wheels.outputs.CORE_LINUX_64_MATRIX }} test-cuda-core-windows: name: "Nightly cuda-core (win-64)" @@ -283,7 +411,7 @@ jobs: run-id: ${{ needs.find-wheels.outputs.RUN_ID }} sha: ${{ needs.find-wheels.outputs.HEAD_SHA }} test-mode: nightly-cuda-core - matrix_filter: 'map(select(.ENV.MODE == "nightly-cuda-core"))' + matrix: ${{ needs.find-wheels.outputs.CORE_WINDOWS_MATRIX }} # ── Standard tests on nightly-only runners ── @@ -303,7 +431,7 @@ jobs: run-id: ${{ needs.find-wheels.outputs.RUN_ID }} sha: ${{ needs.find-wheels.outputs.HEAD_SHA }} test-mode: standard - matrix_filter: 'map(select(.ENV.MODE == "nightly-standard"))' + matrix: ${{ needs.find-wheels.outputs.STANDARD_LINUX_ARM64_MATRIX }} # ── Status check ── diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a212524e44f..2d0e12d0641 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ on: jobs: # This is the only unconditional planning runner. Moon owns affected - # detection and writes marker outputs before any expensive runner is started. + # detection and identifies runner classes before expensive jobs are started. gate: name: Plan affected CI lanes runs-on: ubuntu-latest @@ -44,25 +44,20 @@ jobs: moon-head: ${{ steps.baseline.outputs.moon-head }} moon-base-run-id: ${{ steps.baseline.outputs.moon-base-run-id }} moon-base-sha: ${{ steps.baseline.outputs.moon-base-sha }} - moon-force-all: ${{ steps.markers.outputs.force-all }} - build-portable: ${{ steps.markers.outputs.build-portable }} - build-linux-64: ${{ steps.markers.outputs.build-linux-64 }} - build-linux-aarch64: ${{ steps.markers.outputs.build-linux-aarch64 }} - build-windows: ${{ steps.markers.outputs.build-windows }} - test-sdist-linux: ${{ steps.markers.outputs.test-sdist-linux }} - test-sdist-windows: ${{ steps.markers.outputs.test-sdist-windows }} - test-linux: ${{ steps.markers.outputs.test-linux }} - test-windows: ${{ steps.markers.outputs.test-windows }} - docs: ${{ steps.markers.outputs.docs }} - core-api: ${{ steps.markers.outputs.core-api }} - build-pathfinder: ${{ steps.markers.outputs.build-pathfinder }} - build-bindings: ${{ steps.markers.outputs.build-bindings }} - build-core: ${{ steps.markers.outputs.build-core }} - build-metapackage: ${{ steps.markers.outputs.build-metapackage }} - test-pathfinder: ${{ steps.markers.outputs.test-pathfinder }} - test-bindings: ${{ steps.markers.outputs.test-bindings }} - test-core: ${{ steps.markers.outputs.test-core }} - test-metapackage: ${{ steps.markers.outputs.test-metapackage }} + moon-force-all: ${{ steps.lanes.outputs.force-all }} + build-portable: ${{ steps.lanes.outputs.build-portable }} + build-linux-64: ${{ steps.lanes.outputs.build-linux-64 }} + build-linux-aarch64: ${{ steps.lanes.outputs.build-linux-aarch64 }} + build-windows: ${{ steps.lanes.outputs.build-windows }} + sdist-linux: ${{ steps.lanes.outputs.sdist-linux }} + sdist-windows: ${{ steps.lanes.outputs.sdist-windows }} + test-linux: ${{ steps.lanes.outputs.test-linux }} + test-windows: ${{ steps.lanes.outputs.test-windows }} + docs: ${{ steps.lanes.outputs.docs }} + quality: ${{ steps.lanes.outputs.quality }} + test-linux-64-matrix: ${{ steps.matrices.outputs.test-linux-64 }} + test-linux-aarch64-matrix: ${{ steps.matrices.outputs.test-linux-aarch64 }} + test-windows-matrix: ${{ steps.matrices.outputs.test-windows }} defaults: run: shell: bash --noprofile --norc -euo pipefail {0} @@ -112,7 +107,6 @@ jobs: IS_PR: ${{ startsWith(github.ref_name, 'pull-request/') }} SKIP: ${{ steps.directives.outputs.skip }} BASE_REF: ${{ steps.directives.outputs.base-ref }} - CUDA_BUILD_VER: ${{ steps.vars.outputs.cuda-build-ver }} run: | head="$(git rev-parse HEAD)" base="${head}" @@ -160,27 +154,15 @@ jobs: )" expected=( - cuda-pathfinder-wheel - cuda-python-wheel - moon-cache-build-portable - moon-cache-sdist-linux-64 - moon-cache-sdist-win-64 + moon-lane-build-portable + moon-lane-sdist-linux-64 + moon-lane-sdist-win-64 ) - python_versions=(3.10 3.11 3.12 3.13 3.14 3.14t 3.15 3.15t) + python_versions=(310 311 312 313 314 314t 315 315t) host_platforms=(linux-64 linux-aarch64 win-64) for host_platform in "${host_platforms[@]}"; do for python_version in "${python_versions[@]}"; do - python_version_formatted="${python_version//./}" - bindings_artifact="cuda-bindings-python${python_version_formatted}-cuda${CUDA_BUILD_VER}-${host_platform}-${base}" - core_artifact="cuda-core-python${python_version_formatted}-${host_platform}-${base}" - expected+=( - "${bindings_artifact}" - "${bindings_artifact}-tests" - "${core_artifact}" - "${core_artifact}-tests" - "${core_artifact}-test-binaries" - ) - expected+=("moon-cache-build-${host_platform}-py${python_version}") + expected+=("moon-lane-build-${host_platform}-py${python_version}") done done @@ -230,72 +212,108 @@ jobs: auto-install: false auto-setup: false - - name: Validate Moon CI contracts - if: ${{ steps.directives.outputs.skip != 'true' }} - run: python -m unittest ci.tools.tests.test_moon_ci ci.tools.tests.test_moon_workspace - - - name: Materialize affected CI markers with Moon + - name: Query affected Moon tasks if: ${{ steps.directives.outputs.skip != 'true' }} env: MOON_BASE: ${{ steps.baseline.outputs.moon-base }} MOON_HEAD: ${{ steps.baseline.outputs.moon-head }} - MOON_FORCE_ALL: ${{ steps.baseline.outputs.moon-force-all }} + FORCE_ALL: ${{ steps.baseline.outputs.moon-force-all }} run: | - args=() - if [[ "${MOON_FORCE_ALL}" == "true" ]]; then - args+=(--force) + args=(query tasks) + if [[ "${FORCE_ALL}" == "true" ]]; then + moon "${args[@]}" > .moon-affected-tasks.json + else + moon "${args[@]}" \ + --affected \ + --upstream none \ + --downstream deep \ + > .moon-affected-tasks.json fi - moon ci ':#ci-gate' --downstream none "${args[@]}" - - name: Publish lane and module markers - id: markers + - name: Publish affected runner lanes + id: lanes if: ${{ always() }} env: SKIP: ${{ steps.directives.outputs.skip }} FORCE_ALL: ${{ steps.baseline.outputs.moon-force-all }} run: | - force_all="${FORCE_ALL}" - if [[ -f "ci/.moon-out/ci-gates/force-all" || - -f "ci/.moon-out/ci-gates/force-all-unowned" ]]; then + force_all="${FORCE_ALL:-true}" + has_tag() { + jq -e --arg tag "$1" \ + 'any(.tasks[][]; (.tags // []) | any(. == $tag))' \ + .moon-affected-tasks.json > /dev/null + } + + if [[ "${SKIP}" != "true" && + -f .moon-affected-tasks.json ]] && has_tag ci-force-all; then force_all=true fi echo "force-all=${force_all}" >> "$GITHUB_OUTPUT" - markers=( - build-portable - build-linux-64 - build-linux-aarch64 - build-windows - test-sdist-linux - test-sdist-windows - test-linux - test-windows - docs - core-api - build-pathfinder - build-bindings - build-core - build-metapackage - test-pathfinder - test-bindings - test-core - test-metapackage + lanes=( + build-portable:runner-build-portable + build-linux-64:runner-build-linux-64 + build-linux-aarch64:runner-build-linux-aarch64 + build-windows:runner-build-windows + sdist-linux:runner-sdist-linux + sdist-windows:runner-sdist-windows + test-linux:runner-test-linux + test-windows:runner-test-windows + docs:runner-docs + quality:runner-quality ) - for marker in "${markers[@]}"; do + for lane in "${lanes[@]}"; do + output="${lane%%:*}" + tag="${lane#*:}" selected=false if [[ "${SKIP}" != "true" ]] && { [[ "${force_all}" == "true" ]] || - [[ -f "ci/.moon-out/ci-gates/${marker}" ]]; }; then + has_tag "${tag}"; }; then selected=true fi - echo "${marker}=${selected}" >> "$GITHUB_OUTPUT" + echo "${output}=${selected}" >> "$GITHUB_OUTPUT" done - api-check-core-vs-release: - name: API check (cuda_core vs. latest release) + - name: Build GPU test matrices + id: matrices + if: ${{ steps.directives.outputs.skip != 'true' }} + run: | + build_matrix() { + local platform=$1 + local arch=$2 + yq -o json ".${platform}.\"pull-request\"" ci/test-matrix.yml \ + | jq -c --arg arch "${arch}" ' + map(select(.ARCH == $arch)) + | if length == 0 then + error("Empty " + $arch + " GPU matrix") + elif any(.[]; + .DRIVER != "latest" and + .DRIVER != "earliest" and + .FLAVOR == "wsl") then + error("Custom DRIVER is not supported with FLAVOR=wsl") + else + {include: map(. + { + RUNNER_DRIVER: ( + if .DRIVER == "latest" or .DRIVER == "earliest" + then .DRIVER + else "latest" + end + ) + })} + end' + } + + { + echo "test-linux-64=$(build_matrix linux amd64)" + echo "test-linux-aarch64=$(build_matrix linux arm64)" + echo "test-windows=$(build_matrix windows amd64)" + } >> "$GITHUB_OUTPUT" + + quality: + name: Moon contracts and API compatibility if: >- ${{ !fromJSON(needs.gate.outputs.skip) && - fromJSON(needs.gate.outputs.core-api) }} + fromJSON(needs.gate.outputs.quality) }} runs-on: ubuntu-latest needs: - gate @@ -305,8 +323,21 @@ jobs: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: - fetch-depth: 1 + fetch-depth: 0 filter: blob:none + persist-credentials: false + + - name: Set up uv + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + enable-cache: false + + - name: Set up Moon + uses: moonrepo/setup-toolchain@261c62cb5b0f580c7be7c8cd0f023a2e96756095 # v0.6.4 + with: + moon-version: "2.5.1" + auto-install: false + auto-setup: false - name: Find latest release tag id: latest-tag @@ -323,47 +354,26 @@ jobs: fi echo "tag=${tag}" >> "$GITHUB_OUTPUT" - - name: Fetch release tag + - name: Fetch comparison refs shell: bash --noprofile --norc -euo pipefail {0} run: | git fetch --depth=1 --filter=blob:none origin \ "refs/tags/${{ steps.latest-tag.outputs.tag }}:refs/tags/${{ steps.latest-tag.outputs.tag }}" + git fetch --depth=1 --filter=blob:none origin \ + "${{ needs.gate.outputs.moon-base }}" - - name: Check cuda_core public API - uses: ./.github/actions/griffe-api-check - with: - package-name: cuda.core - package-dir: cuda_core - merge-base: ${{ steps.latest-tag.outputs.tag }} - - api-check-core-vs-base: - name: API check (cuda_core vs. merge base) - if: >- - ${{ startsWith(github.ref_name, 'pull-request/') && - !fromJSON(needs.gate.outputs.skip) && - fromJSON(needs.gate.outputs.core-api) }} - runs-on: ubuntu-latest - needs: - - gate - permissions: - contents: read - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - fetch-depth: 1 - filter: blob:none - - - name: Fetch merge base commit + - name: Run affected quality tasks with Moon shell: bash --noprofile --norc -euo pipefail {0} - run: git fetch --depth=1 --filter=blob:none origin "${{ needs.gate.outputs.moon-base }}" - - - name: Check cuda_core public API - uses: ./.github/actions/griffe-api-check - with: - package-name: cuda.core - package-dir: cuda_core - merge-base: ${{ needs.gate.outputs.moon-base }} + env: + CUDA_CORE_API_MERGE_BASE: ${{ needs.gate.outputs.moon-base }} + CUDA_CORE_API_RELEASE_BASE: ${{ steps.latest-tag.outputs.tag }} + MOON_BASE: ${{ needs.gate.outputs.moon-base }} + MOON_HEAD: ${{ github.sha }} + run: >- + moon ci ':#ci-quality' + --upstream none + --downstream none + ${{ fromJSON(needs.gate.outputs.moon-force-all) && '--force' || '' }} build-portable: name: Build portable wheels @@ -380,15 +390,13 @@ jobs: contents: read uses: ./.github/workflows/build-pure-wheel.yml with: - build-pathfinder: ${{ fromJSON(needs.gate.outputs.build-pathfinder) }} - build-metapackage: ${{ fromJSON(needs.gate.outputs.build-metapackage) }} moon-base: ${{ needs.gate.outputs.moon-base }} baseline-run-id: ${{ needs.gate.outputs.moon-base-run-id }} force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} # Native builds remain split by platform so their tests can start as soon as # the corresponding platform finishes. Each reusable workflow owns its - # eight-version Python matrix and per-row Moon cache artifact. + # eight-version Python matrix and per-row Moon lane bundle. build-linux-64: name: Build linux-64, CUDA ${{ needs.gate.outputs.cuda-build-ver }} needs: @@ -412,12 +420,7 @@ jobs: host-platform: linux-64 cuda-version: ${{ needs.gate.outputs.cuda-build-ver }} prev-cuda-version: ${{ needs.gate.outputs.cuda-prev-build-ver }} - build-bindings: ${{ fromJSON(needs.gate.outputs.build-bindings) }} - build-core: ${{ fromJSON(needs.gate.outputs.build-core) }} - test-bindings: ${{ fromJSON(needs.gate.outputs.test-bindings) }} - test-core: ${{ fromJSON(needs.gate.outputs.test-core) }} baseline-run-id: ${{ needs.gate.outputs.moon-base-run-id }} - baseline-sha: ${{ needs.gate.outputs.moon-base-sha }} portable-run-id: ${{ needs.build-portable.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} moon-base: ${{ needs.gate.outputs.moon-base }} force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} @@ -444,12 +447,7 @@ jobs: host-platform: linux-aarch64 cuda-version: ${{ needs.gate.outputs.cuda-build-ver }} prev-cuda-version: ${{ needs.gate.outputs.cuda-prev-build-ver }} - build-bindings: ${{ fromJSON(needs.gate.outputs.build-bindings) }} - build-core: ${{ fromJSON(needs.gate.outputs.build-core) }} - test-bindings: ${{ fromJSON(needs.gate.outputs.test-bindings) }} - test-core: ${{ fromJSON(needs.gate.outputs.test-core) }} baseline-run-id: ${{ needs.gate.outputs.moon-base-run-id }} - baseline-sha: ${{ needs.gate.outputs.moon-base-sha }} portable-run-id: ${{ needs.build-portable.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} moon-base: ${{ needs.gate.outputs.moon-base }} force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} @@ -476,12 +474,7 @@ jobs: host-platform: win-64 cuda-version: ${{ needs.gate.outputs.cuda-build-ver }} prev-cuda-version: ${{ needs.gate.outputs.cuda-prev-build-ver }} - build-bindings: ${{ fromJSON(needs.gate.outputs.build-bindings) }} - build-core: ${{ fromJSON(needs.gate.outputs.build-core) }} - test-bindings: ${{ fromJSON(needs.gate.outputs.test-bindings) }} - test-core: ${{ fromJSON(needs.gate.outputs.test-core) }} baseline-run-id: ${{ needs.gate.outputs.moon-base-run-id }} - baseline-sha: ${{ needs.gate.outputs.moon-base-sha }} portable-run-id: ${{ needs.build-portable.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} moon-base: ${{ needs.gate.outputs.moon-base }} force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} @@ -496,7 +489,7 @@ jobs: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.gate.outputs.skip) && !fromJSON(needs.gate.outputs.doc-only) && - fromJSON(needs.gate.outputs.test-sdist-linux) }} + fromJSON(needs.gate.outputs.sdist-linux) }} permissions: actions: read contents: read @@ -516,7 +509,7 @@ jobs: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.gate.outputs.skip) && !fromJSON(needs.gate.outputs.doc-only) && - fromJSON(needs.gate.outputs.test-sdist-windows) }} + fromJSON(needs.gate.outputs.sdist-windows) }} permissions: actions: read contents: read @@ -553,11 +546,8 @@ jobs: build-type: pull-request host-platform: linux-64 build-ctk-ver: ${{ needs.gate.outputs.cuda-build-ver }} + matrix: ${{ needs.gate.outputs.test-linux-64-matrix }} nruns: ${{ (github.event_name == 'schedule' && 5) || 1 }} - test-pathfinder: ${{ fromJSON(needs.gate.outputs.test-pathfinder) }} - test-bindings: ${{ fromJSON(needs.gate.outputs.test-bindings) }} - test-core: ${{ fromJSON(needs.gate.outputs.test-core) }} - test-python: ${{ fromJSON(needs.gate.outputs.test-metapackage) }} run-id: ${{ needs.build-linux-64.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} sha: ${{ needs.build-linux-64.result == 'success' && github.sha || needs.gate.outputs.moon-base-sha }} portable-run-id: ${{ needs.build-portable.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} @@ -589,11 +579,8 @@ jobs: build-type: pull-request host-platform: linux-aarch64 build-ctk-ver: ${{ needs.gate.outputs.cuda-build-ver }} + matrix: ${{ needs.gate.outputs.test-linux-aarch64-matrix }} nruns: ${{ (github.event_name == 'schedule' && 5) || 1 }} - test-pathfinder: ${{ fromJSON(needs.gate.outputs.test-pathfinder) }} - test-bindings: ${{ fromJSON(needs.gate.outputs.test-bindings) }} - test-core: ${{ fromJSON(needs.gate.outputs.test-core) }} - test-python: ${{ fromJSON(needs.gate.outputs.test-metapackage) }} run-id: ${{ needs.build-linux-aarch64.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} sha: ${{ needs.build-linux-aarch64.result == 'success' && github.sha || needs.gate.outputs.moon-base-sha }} portable-run-id: ${{ needs.build-portable.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} @@ -625,11 +612,8 @@ jobs: build-type: pull-request host-platform: win-64 build-ctk-ver: ${{ needs.gate.outputs.cuda-build-ver }} + matrix: ${{ needs.gate.outputs.test-windows-matrix }} nruns: ${{ (github.event_name == 'schedule' && 5) || 1 }} - test-pathfinder: ${{ fromJSON(needs.gate.outputs.test-pathfinder) }} - test-bindings: ${{ fromJSON(needs.gate.outputs.test-bindings) }} - test-core: ${{ fromJSON(needs.gate.outputs.test-core) }} - test-python: ${{ fromJSON(needs.gate.outputs.test-metapackage) }} run-id: ${{ needs.build-windows.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} sha: ${{ needs.build-windows.result == 'success' && github.sha || needs.gate.outputs.moon-base-sha }} portable-run-id: ${{ needs.build-portable.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} @@ -661,10 +645,8 @@ jobs: with: is-release: ${{ github.ref_type == 'tag' }} run-id: ${{ needs.build-linux-64.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} - sha: ${{ needs.build-linux-64.result == 'success' && github.sha || needs.gate.outputs.moon-base-sha }} + sha: ${{ github.sha }} portable-run-id: ${{ needs.build-portable.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} - moon-base: ${{ needs.gate.outputs.moon-base }} - force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} precommit-windows: name: Pre-commit on Windows @@ -701,8 +683,7 @@ jobs: permissions: {} needs: - gate - - api-check-core-vs-release - - api-check-core-vs-base + - quality - build-portable - build-linux-64 - build-linux-aarch64 @@ -772,8 +753,8 @@ jobs: test_linux=false test_windows=false if [[ "${doc_only}" != "true" ]]; then - sdist_linux="${{ needs.gate.outputs.test-sdist-linux }}" - sdist_windows="${{ needs.gate.outputs.test-sdist-windows }}" + sdist_linux="${{ needs.gate.outputs.sdist-linux }}" + sdist_windows="${{ needs.gate.outputs.sdist-windows }}" test_linux="${{ needs.gate.outputs.test-linux }}" test_windows="${{ needs.gate.outputs.test-windows }}" fi @@ -783,13 +764,7 @@ jobs: check_result test-linux-aarch64 "$(expected_for "${test_linux}")" "${{ needs.test-linux-aarch64.result }}" check_result test-windows "$(expected_for "${test_windows}")" "${{ needs.test-windows.result }}" - core_api="${{ needs.gate.outputs.core-api }}" - core_api_base=false - if [[ "${GITHUB_REF_NAME}" == pull-request/* ]]; then - core_api_base="${core_api}" - fi - check_result api-check-core-vs-release "$(expected_for "${core_api}")" "${{ needs.api-check-core-vs-release.result }}" - check_result api-check-core-vs-base "$(expected_for "${core_api_base}")" "${{ needs.api-check-core-vs-base.result }}" + check_result quality "$(expected_for "${{ needs.gate.outputs.quality }}")" "${{ needs.quality.result }}" check_result doc "$(expected_for "${{ needs.gate.outputs.docs }}")" "${{ needs.doc.result }}" check_result precommit-windows success "${{ needs.precommit-windows.result }}" diff --git a/.github/workflows/test-sdist-linux.yml b/.github/workflows/test-sdist-linux.yml index 16519b8d55d..438ee6ef86d 100644 --- a/.github/workflows/test-sdist-linux.yml +++ b/.github/workflows/test-sdist-linux.yml @@ -95,12 +95,12 @@ jobs: SHA: ${{ github.sha }} run: ./ci/tools/env-vars build - - name: Restore trusted exact-base Moon cache + - name: Restore trusted exact-base Moon lane if: ${{ inputs.baseline-run-id != '' && !inputs.force-all }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: moon-cache-sdist-${{ inputs.host-platform }} - path: .moon/cache + name: moon-lane-sdist-${{ inputs.host-platform }} + path: . github-token: ${{ github.token }} run-id: ${{ inputs.baseline-run-id }} @@ -119,7 +119,12 @@ jobs: if [[ "${MOON_FORCE_ALL}" == "true" ]]; then args+=(--force) fi - moon ci ':#ci-sdist' --downstream none "${args[@]}" + # Preserve affected granularity while retaining the package build + # order. cuda.core and the metapackage are independent after their + # shared prerequisites and can run in parallel. + moon ci pathfinder:sdist --upstream none --downstream none "${args[@]}" + moon ci bindings:sdist --upstream none --downstream none "${args[@]}" + moon ci core:sdist metapackage:sdist --upstream none --downstream none "${args[@]}" - name: Validate sdist outputs run: | @@ -141,14 +146,18 @@ jobs: # GitHub transports Moon's portable cache between trusted exact runs; # Moon remains responsible for hashes, hits, and output hydration. - - name: Upload portable Moon cache + - name: Upload sdist Moon lane if: ${{ always() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: moon-cache-sdist-${{ inputs.host-platform }} + name: moon-lane-sdist-${{ inputs.host-platform }} path: | .moon/cache/hashes .moon/cache/outputs + cuda_pathfinder/.moon-out/sdist + cuda_bindings/.moon-out/sdist + cuda_core/.moon-out/sdist + cuda_python/.moon-out/sdist if-no-files-found: error include-hidden-files: true overwrite: true diff --git a/.github/workflows/test-sdist-windows.yml b/.github/workflows/test-sdist-windows.yml index 96d7dc6a070..ca73effe125 100644 --- a/.github/workflows/test-sdist-windows.yml +++ b/.github/workflows/test-sdist-windows.yml @@ -92,12 +92,12 @@ jobs: SHA: ${{ github.sha }} run: ./ci/tools/env-vars build - - name: Restore trusted exact-base Moon cache + - name: Restore trusted exact-base Moon lane if: ${{ inputs.baseline-run-id != '' && !inputs.force-all }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: moon-cache-sdist-${{ inputs.host-platform }} - path: .moon/cache + name: moon-lane-sdist-${{ inputs.host-platform }} + path: . github-token: ${{ github.token }} run-id: ${{ inputs.baseline-run-id }} @@ -107,7 +107,12 @@ jobs: if [[ "${MOON_FORCE_ALL}" == "true" ]]; then args+=(--force) fi - moon ci ':#ci-sdist' --downstream none "${args[@]}" + # Preserve affected granularity while retaining the package build + # order. cuda.core and the metapackage are independent after their + # shared prerequisites and can run in parallel. + moon ci pathfinder:sdist --upstream none --downstream none "${args[@]}" + moon ci bindings:sdist --upstream none --downstream none "${args[@]}" + moon ci core:sdist metapackage:sdist --upstream none --downstream none "${args[@]}" - name: Validate sdist outputs run: | @@ -125,14 +130,18 @@ jobs: # GitHub transports Moon's portable cache between trusted exact runs; # Moon remains responsible for hashes, hits, and output hydration. - - name: Upload portable Moon cache + - name: Upload sdist Moon lane if: ${{ always() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: moon-cache-sdist-${{ inputs.host-platform }} + name: moon-lane-sdist-${{ inputs.host-platform }} path: | .moon/cache/hashes .moon/cache/outputs + cuda_pathfinder/.moon-out/sdist + cuda_bindings/.moon-out/sdist + cuda_core/.moon-out/sdist + cuda_python/.moon-out/sdist if-no-files-found: error include-hidden-files: true overwrite: true diff --git a/.github/workflows/test-wheel-linux.yml b/.github/workflows/test-wheel-linux.yml index 6b898dfe494..b37467a4e8f 100644 --- a/.github/workflows/test-wheel-linux.yml +++ b/.github/workflows/test-wheel-linux.yml @@ -16,24 +16,13 @@ on: build-ctk-ver: type: string required: true - matrix_filter: + matrix: + description: "Precomputed test matrix JSON" type: string - default: "." + required: true nruns: type: number default: 1 - test-pathfinder: - type: boolean - default: true - test-bindings: - type: boolean - default: true - test-core: - type: boolean - default: true - test-python: - type: boolean - default: true run-id: description: > Workflow run ID to download artifacts from. @@ -71,63 +60,16 @@ defaults: shell: bash --noprofile --norc -xeuo pipefail {0} jobs: - compute-matrix: - runs-on: ubuntu-latest - env: - BUILD_TYPE: ${{ inputs.build-type }} - ARCH: ${{ (inputs.host-platform == 'linux-64' && 'amd64') || - (inputs.host-platform == 'linux-aarch64' && 'arm64') }} - outputs: - MATRIX: ${{ steps.compute-matrix.outputs.MATRIX }} - OLD_BRANCH: ${{ steps.compute-matrix.outputs.OLD_BRANCH }} - steps: - - name: Checkout ${{ github.event.repository.name }} - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Validate Test Type - run: | - if [[ "$BUILD_TYPE" != "pull-request" ]] && [[ "$BUILD_TYPE" != "nightly" ]] && [[ "$BUILD_TYPE" != "branch" ]]; then - echo "Invalid build type! Must be one of 'nightly', 'pull-request', or 'branch'." - exit 1 - fi - - - name: Compute Python Test Matrix - id: compute-matrix - run: | - # Use the nightly matrix for branch tests - MATRIX_TYPE="${BUILD_TYPE}" - if [[ "${MATRIX_TYPE}" == "branch" ]]; then - MATRIX_TYPE="nightly" - fi - - # Read base matrix from YAML file for the specific architecture - TEST_MATRIX=$(yq -o json ".linux[\"${MATRIX_TYPE}\"] | map(select(.ARCH == \"${ARCH}\"))" ci/test-matrix.yml) - - # Apply matrix filter; reject custom DRIVER + FLAVOR=wsl (the - # in-container driver swap doesn't work under WSL); add a - # RUNNER_DRIVER field that maps any custom version back to - # 'latest' (the install script swaps the driver itself, so we - # need to land on the runner that ships with the most recent - # pre-installed driver); wrap in include structure. - MATRIX=$(echo "$TEST_MATRIX" | jq -c '${{ inputs.matrix_filter }} | if any(.[]; .DRIVER != "latest" and .DRIVER != "earliest" and .FLAVOR == "wsl") then "Error: custom DRIVER is not supported with FLAVOR=wsl\n" | halt_error(1) else . end | map(. + {RUNNER_DRIVER: (if .DRIVER == "latest" or .DRIVER == "earliest" then .DRIVER else "latest" end)}) | if (. | length) > 0 then {include: .} else "Error: Empty matrix\n" | halt_error(1) end') - - echo "MATRIX=${MATRIX}" | tee --append "${GITHUB_OUTPUT}" - - # This job has yq already installed, so let's do it here - OLD_BRANCH=$(yq '.backport_branch' ci/versions.yml) - echo "OLD_BRANCH=${OLD_BRANCH}" >> "$GITHUB_OUTPUT" - test: name: Python ${{ matrix.PY_VER }}, CUDA ${{ matrix.CUDA_VER }} (${{ (matrix.LOCAL_CTK == '1' && 'local') || 'wheels' }}), GPU ${{ matrix.GPU }}${{ matrix.GPU_COUNT != '1' && format(' (x{0})', matrix.GPU_COUNT) || '' }}${{ matrix.FLAVOR && format(', {0}', matrix.FLAVOR) || '' }}${{ matrix.ENV.TORCH_VER && format(', {0}+{1}', matrix.ENV.TORCH_VER, matrix.ENV.TORCH_CUDA) || '' }}${{ matrix.ENV.MODE == 'nightly-numba-cuda' && ', latest' || '' }} timeout-minutes: 60 - needs: compute-matrix env: CUDA_PYTHON_LANE: test-${{ inputs.host-platform }}-py${{ matrix.PY_VER }}-cuda${{ matrix.CUDA_VER }} MOON_BASE: ${{ inputs.moon-base }} MOON_HEAD: ${{ github.sha }} strategy: fail-fast: false - matrix: ${{ fromJSON(needs.compute-matrix.outputs.MATRIX) }} + matrix: ${{ fromJSON(inputs.matrix) }} runs-on: "${{ matrix.FLAVOR || 'linux' }}-${{ matrix.ARCH }}-gpu-${{ matrix.GPU }}-${{ matrix.RUNNER_DRIVER }}-${{ matrix.GPU_COUNT }}" # TODO: remove continue-on-error once 3.15 is officially supported continue-on-error: ${{ startsWith(matrix.PY_VER, '3.15') }} @@ -191,7 +133,7 @@ jobs: LOCAL_CTK: ${{ matrix.LOCAL_CTK }} PY_VER: ${{ matrix.PY_VER }} SHA: ${{ inputs.sha || github.sha }} - SKIP_BINDINGS_TEST_OVERRIDE: ${{ !inputs.test-bindings && '1' || '0' }} + SKIP_BINDINGS_TEST_OVERRIDE: "0" run: ./ci/tools/env-vars test - name: Apply extra matrix environment variables @@ -200,37 +142,32 @@ jobs: MATRIX_ENV: ${{ toJSON(matrix.ENV) }} run: echo "$MATRIX_ENV" | jq -r 'to_entries[] | "\(.key)=\(.value)"' >> "$GITHUB_ENV" - - name: Download cuda-pathfinder build artifacts - if: ${{ inputs.test-pathfinder || inputs.test-bindings || inputs.test-core || inputs.test-python }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: cuda-pathfinder-wheel - path: ./cuda_pathfinder - run-id: ${{ inputs.portable-run-id || inputs.run-id || github.run_id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Download cuda-python build artifacts - if: ${{ inputs.test-python && env.BINDINGS_SOURCE == 'main' }} + - name: Download portable Moon lane uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: cuda-python-wheel + name: moon-lane-build-portable path: . run-id: ${{ inputs.portable-run-id || inputs.run-id || github.run_id }} github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Download cuda.bindings build artifacts - if: ${{ (inputs.test-bindings || inputs.test-core || inputs.test-python) && - env.BINDINGS_SOURCE == 'main' }} + - name: Download native Moon lane uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }} - path: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} + name: moon-lane-build-${{ inputs.host-platform }}-py${{ env.PYTHON_VERSION_FORMATTED }} + path: . run-id: ${{ inputs.run-id || github.run_id }} github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Download cuda-python & cuda.bindings build artifacts from the prior branch - if: ${{ (inputs.test-bindings || inputs.test-core || inputs.test-python) && - env.BINDINGS_SOURCE == 'backport' }} + - name: Stage Moon lane outputs for legacy test tooling + run: | + cp cuda_pathfinder/.moon-out/wheel-pure/*.whl cuda_pathfinder/ + echo "CUDA_CORE_ARTIFACTS_DIR=${GITHUB_WORKSPACE}/cuda_core/.moon-out/wheel-merged" >> "$GITHUB_ENV" + if [[ "${BINDINGS_SOURCE}" == "main" ]]; then + echo "CUDA_BINDINGS_ARTIFACTS_DIR=${GITHUB_WORKSPACE}/cuda_bindings/.moon-out/wheel-current" >> "$GITHUB_ENV" + fi + + - name: Download cuda.bindings build artifacts from the prior branch + if: ${{ env.BINDINGS_SOURCE == 'backport' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | @@ -244,7 +181,7 @@ jobs: && apt update \ && apt install gh -y - OLD_BRANCH=${{ needs.compute-matrix.outputs.OLD_BRANCH }} + OLD_BRANCH=$(yq '.backport_branch' ci/versions.yml) OLD_BASENAME="cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda*-${{ inputs.host-platform }}*" LATEST_PRIOR_RUN_ID=$(./ci/tools/lookup-run-id --branch "${OLD_BRANCH}" NVIDIA/cuda-python "CI") @@ -255,85 +192,6 @@ jobs: mv $OLD_BASENAME/*.whl "${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}"/ rmdir $OLD_BASENAME - if ${{ inputs.test-python }}; then - gh run download $LATEST_PRIOR_RUN_ID -p cuda-python-wheel -R NVIDIA/cuda-python - ls -al cuda-python-wheel - mv cuda-python-wheel/*.whl . - rmdir cuda-python-wheel - fi - - - name: Display structure of downloaded cuda-python artifacts - if: ${{ inputs.test-python && env.BINDINGS_SOURCE != 'published' }} - run: | - pwd - ls -lah cuda_python*.whl cuda_pathfinder/ - - - name: Display structure of downloaded cuda.bindings artifacts - if: ${{ (inputs.test-bindings || inputs.test-core || inputs.test-python) && - env.BINDINGS_SOURCE != 'published' }} - run: | - pwd - ls -lahR $CUDA_BINDINGS_ARTIFACTS_DIR - - - name: Download cuda.bindings Cython tests - if: ${{ inputs.test-bindings && env.SKIP_CYTHON_TEST == '0' }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }}-tests - path: cuda_bindings/.moon-out/cython-tests - run-id: ${{ inputs.run-id || github.run_id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Display structure of downloaded cuda.bindings Cython tests - if: ${{ inputs.test-bindings && env.SKIP_CYTHON_TEST == '0' }} - run: | - pwd - ls -lahR cuda_bindings/.moon-out/cython-tests - - - name: Download cuda.core build artifacts - if: ${{ inputs.test-core || (inputs.test-python && env.BINDINGS_SOURCE == 'main') }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ env.CUDA_CORE_ARTIFACT_NAME }} - path: ${{ env.CUDA_CORE_ARTIFACTS_DIR }} - run-id: ${{ inputs.run-id || github.run_id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Display structure of downloaded cuda.core build artifacts - if: ${{ inputs.test-core || (inputs.test-python && env.BINDINGS_SOURCE == 'main') }} - run: | - pwd - ls -lahR $CUDA_CORE_ARTIFACTS_DIR - - - name: Download cuda.core Cython tests - if: ${{ inputs.test-core }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-tests - path: cuda_core/.moon-out/cython-tests - run-id: ${{ inputs.run-id || github.run_id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Display structure of downloaded cuda.core Cython tests - if: ${{ inputs.test-core }} - run: | - pwd - ls -lahR cuda_core/.moon-out/cython-tests - - - name: Download cuda.core test binaries - if: ${{ inputs.test-core }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-test-binaries - path: cuda_core/.moon-out/test-binaries - run-id: ${{ inputs.run-id || github.run_id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Display structure of downloaded cuda.core test binaries - if: ${{ inputs.test-core }} - run: | - pwd - ls -lahR cuda_core/.moon-out/test-binaries - name: Set up Python ${{ matrix.PY_VER }} uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 @@ -346,8 +204,7 @@ jobs: AGENT_TOOLSDIRECTORY: "/opt/hostedtoolcache" - name: Enable Scientific Python Nightly Wheels for Python 3.15 - if: ${{ (inputs.test-bindings || inputs.test-core || inputs.test-python) && - startsWith(matrix.PY_VER, '3.15') }} + if: ${{ startsWith(matrix.PY_VER, '3.15') }} run: | echo "PIP_EXTRA_INDEX_URL=https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" >> "$GITHUB_ENV" echo "PIP_ONLY_BINARY=numpy" >> "$GITHUB_ENV" @@ -361,7 +218,7 @@ jobs: cuda-version: ${{ matrix.CUDA_VER }} - name: Set up latest cuda_sanitizer_api - if: ${{ (inputs.test-bindings || inputs.test-core) && env.SETUP_SANITIZER == '1' }} + if: ${{ env.SETUP_SANITIZER == '1' }} uses: ./.github/actions/fetch_ctk continue-on-error: false with: @@ -370,59 +227,22 @@ jobs: cuda-components: "cuda_sanitizer_api" - name: Set up compute-sanitizer - if: ${{ inputs.test-bindings || inputs.test-core }} run: setup-sanitizer - name: Set up test repetition on nightly runs run: echo "PYTEST_ADDOPTS=\"--count=${{ inputs.nruns }}\"" >> "$GITHUB_ENV" - # ── Standard test steps (skipped for nightly modes) ── - - name: Run cuda.pathfinder tests with see_what_works - if: ${{ inputs.test-mode == 'standard' && inputs.test-pathfinder }} - env: - CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS: see_what_works - CUDA_PATHFINDER_TEST_FIND_NVIDIA_HEADERS_STRICTNESS: see_what_works - CUDA_PATHFINDER_TEST_FIND_NVIDIA_BITCODE_LIB_STRICTNESS: see_what_works - run: moon ci pathfinder:test-installed-linux --upstream none --downstream none ${{ (inputs.force-all || inputs.build-type != 'pull-request') && '--force' || '' }} - - - name: Run cuda.bindings tests - if: ${{ inputs.test-mode == 'standard' && inputs.test-bindings && env.SKIP_CUDA_BINDINGS_TEST == '0' }} - env: - CUDA_VER: ${{ matrix.CUDA_VER }} - LOCAL_CTK: ${{ matrix.LOCAL_CTK }} - run: moon ci bindings:test-installed-linux --upstream none --downstream none ${{ (inputs.force-all || inputs.build-type != 'pull-request') && '--force' || '' }} - - - name: Run cuda.bindings benchmarks (smoke test) - if: ${{ inputs.test-mode == 'standard' && inputs.test-bindings && env.SKIP_CUDA_BINDINGS_TEST == '0' }} - run: moon ci bindings-benchmarks:smoke-linux --upstream none --downstream none ${{ (inputs.force-all || inputs.build-type != 'pull-request') && '--force' || '' }} - - - name: Run cuda.core tests - if: ${{ inputs.test-mode == 'standard' && inputs.test-core }} + - name: Run standard tests with Moon + if: ${{ inputs.test-mode == 'standard' }} env: CUDA_VER: ${{ matrix.CUDA_VER }} LOCAL_CTK: ${{ matrix.LOCAL_CTK }} - run: moon ci core:test-installed-linux --upstream none --downstream none ${{ (inputs.force-all || inputs.build-type != 'pull-request') && '--force' || '' }} - - - name: Ensure cuda-python installable - if: ${{ inputs.test-mode == 'standard' && inputs.test-python && env.BINDINGS_SOURCE == 'main' }} - run: moon ci metapackage:test-installed-linux --upstream none --downstream none ${{ (inputs.force-all || inputs.build-type != 'pull-request') && '--force' || '' }} - - - name: Install cuda.pathfinder extra wheels for testing - if: ${{ inputs.test-mode == 'standard' && inputs.test-pathfinder }} run: | - set -euo pipefail - pushd cuda_pathfinder - pip install --only-binary=:all: -v ./*.whl --group "test-cu${TEST_CUDA_MAJOR}" - pip list - popd - - - name: Run cuda.pathfinder tests with all_must_work - if: ${{ inputs.test-mode == 'standard' && inputs.test-pathfinder }} - env: - CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS: all_must_work - CUDA_PATHFINDER_TEST_FIND_NVIDIA_HEADERS_STRICTNESS: all_must_work - CUDA_PATHFINDER_TEST_FIND_NVIDIA_BITCODE_LIB_STRICTNESS: all_must_work - run: moon ci pathfinder:test-installed-linux-strict --upstream none --downstream none ${{ (inputs.force-all || inputs.build-type != 'pull-request') && '--force' || '' }} + args=() + if [[ "${{ inputs.force-all }}" == "true" || "${{ inputs.build-type }}" != "pull-request" ]]; then + args+=(--force) + fi + moon ci ':#ci-test-linux' --upstream deep --downstream none "${args[@]}" # ── Nightly: install wheels + optional dep together ── - name: Install cuda-python wheels + PyTorch diff --git a/.github/workflows/test-wheel-windows.yml b/.github/workflows/test-wheel-windows.yml index af377de8074..20b7b00cc10 100644 --- a/.github/workflows/test-wheel-windows.yml +++ b/.github/workflows/test-wheel-windows.yml @@ -16,24 +16,13 @@ on: build-ctk-ver: type: string required: true - matrix_filter: + matrix: + description: "Precomputed test matrix JSON" type: string - default: "." + required: true nruns: type: number default: 1 - test-pathfinder: - type: boolean - default: true - test-bindings: - type: boolean - default: true - test-core: - type: boolean - default: true - test-python: - type: boolean - default: true run-id: description: > Workflow run ID to download artifacts from. @@ -67,58 +56,17 @@ on: default: false jobs: - compute-matrix: - runs-on: ubuntu-latest - defaults: - run: - shell: bash --noprofile --norc -xeuo pipefail {0} - env: - BUILD_TYPE: ${{ inputs.build-type }} - ARCH: ${{ (inputs.host-platform == 'win-64' && 'amd64') }} - outputs: - MATRIX: ${{ steps.compute-matrix.outputs.MATRIX }} - steps: - - name: Checkout ${{ github.event.repository.name }} - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Validate Test Type - run: | - if [[ "$BUILD_TYPE" != "pull-request" ]] && [[ "$BUILD_TYPE" != "nightly" ]] && [[ "$BUILD_TYPE" != "branch" ]]; then - echo "Invalid build type! Must be one of 'nightly', 'pull-request', or 'branch'." - exit 1 - fi - - name: Compute Python Test Matrix - id: compute-matrix - run: | - # Use the nightly matrix for branch tests - MATRIX_TYPE="${BUILD_TYPE}" - if [[ "${MATRIX_TYPE}" == "branch" ]]; then - MATRIX_TYPE="nightly" - fi - - # Read base matrix from YAML file for the specific architecture - TEST_MATRIX=$(yq -o json ".windows[\"${MATRIX_TYPE}\"] | map(select(.ARCH == \"${ARCH}\"))" ci/test-matrix.yml) - - # Apply matrix filter; add a RUNNER_DRIVER field that maps any - # custom DRIVER version back to 'latest' (install_gpu_driver.ps1 - # swaps the driver itself, so the runner must be the one that - # ships the most recent pre-installed driver); wrap in include. - MATRIX=$(echo "$TEST_MATRIX" | jq -c '${{ inputs.matrix_filter }} | map(. + {RUNNER_DRIVER: (if .DRIVER == "latest" or .DRIVER == "earliest" then .DRIVER else "latest" end)}) | if (. | length) > 0 then {include: .} else "Error: Empty matrix\n" | halt_error(1) end') - - echo "MATRIX=${MATRIX}" | tee --append "${GITHUB_OUTPUT}" - test: name: Python ${{ matrix.PY_VER }}, CUDA ${{ matrix.CUDA_VER }} (${{ (matrix.LOCAL_CTK == '1' && 'local') || 'wheels' }}), GPU ${{ matrix.GPU }}${{ matrix.GPU_COUNT != '1' && format(' (x{0})', matrix.GPU_COUNT) || '' }} (${{ matrix.DRIVER_MODE }})${{ matrix.ENV.TORCH_VER && format(', {0}+{1}', matrix.ENV.TORCH_VER, matrix.ENV.TORCH_CUDA) || '' }}${{ matrix.ENV.MODE == 'nightly-numba-cuda' && ', latest' || '' }} timeout-minutes: 60 # The build stage could fail but we want the CI to keep moving. - needs: compute-matrix env: CUDA_PYTHON_LANE: test-${{ inputs.host-platform }}-py${{ matrix.PY_VER }}-cuda${{ matrix.CUDA_VER }} MOON_BASE: ${{ inputs.moon-base }} MOON_HEAD: ${{ github.sha }} strategy: fail-fast: false - matrix: ${{ fromJSON(needs.compute-matrix.outputs.MATRIX) }} + matrix: ${{ fromJSON(inputs.matrix) }} if: ${{ github.repository_owner == 'nvidia' && !cancelled() }} # TODO: remove continue-on-error once 3.15 is officially supported continue-on-error: ${{ startsWith(matrix.PY_VER, '3.15') }} @@ -178,7 +126,7 @@ jobs: LOCAL_CTK: ${{ matrix.LOCAL_CTK }} PY_VER: ${{ matrix.PY_VER }} SHA: ${{ inputs.sha || github.sha }} - SKIP_BINDINGS_TEST_OVERRIDE: ${{ !inputs.test-bindings && '1' || '0' }} + SKIP_BINDINGS_TEST_OVERRIDE: "0" shell: bash --noprofile --norc -xeuo pipefail {0} run: ./ci/tools/env-vars test @@ -189,37 +137,33 @@ jobs: shell: bash --noprofile --norc -xeuo pipefail {0} run: echo "$MATRIX_ENV" | jq -r 'to_entries[] | "\(.key)=\(.value)"' >> "$GITHUB_ENV" - - name: Download cuda-pathfinder build artifacts - if: ${{ inputs.test-pathfinder || inputs.test-bindings || inputs.test-core || inputs.test-python }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: cuda-pathfinder-wheel - path: ./cuda_pathfinder - run-id: ${{ inputs.portable-run-id || inputs.run-id || github.run_id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Download cuda-python build artifacts - if: ${{ inputs.test-python && env.BINDINGS_SOURCE == 'main' }} + - name: Download portable Moon lane uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: cuda-python-wheel + name: moon-lane-build-portable path: . run-id: ${{ inputs.portable-run-id || inputs.run-id || github.run_id }} github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Download cuda.bindings build artifacts - if: ${{ (inputs.test-bindings || inputs.test-core || inputs.test-python) && - env.BINDINGS_SOURCE == 'main' }} + - name: Download native Moon lane uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }} - path: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} + name: moon-lane-build-${{ inputs.host-platform }}-py${{ env.PYTHON_VERSION_FORMATTED }} + path: . run-id: ${{ inputs.run-id || github.run_id }} github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Download cuda-python & cuda.bindings build artifacts from the prior branch - if: ${{ (inputs.test-bindings || inputs.test-core || inputs.test-python) && - env.BINDINGS_SOURCE == 'backport' }} + - name: Stage Moon lane outputs for legacy test tooling + shell: bash --noprofile --norc -xeuo pipefail {0} + run: | + cp cuda_pathfinder/.moon-out/wheel-pure/*.whl cuda_pathfinder/ + echo "CUDA_CORE_ARTIFACTS_DIR=${GITHUB_WORKSPACE}/cuda_core/.moon-out/wheel-merged" >> "$GITHUB_ENV" + if [[ "${BINDINGS_SOURCE}" == "main" ]]; then + echo "CUDA_BINDINGS_ARTIFACTS_DIR=${GITHUB_WORKSPACE}/cuda_bindings/.moon-out/wheel-current" >> "$GITHUB_ENV" + fi + + - name: Download cuda.bindings build artifacts from the prior branch + if: ${{ env.BINDINGS_SOURCE == 'backport' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} shell: bash --noprofile --norc -xeuo pipefail {0} @@ -235,85 +179,6 @@ jobs: mv $OLD_BASENAME/*.whl "${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}"/ rmdir $OLD_BASENAME - if ${{ inputs.test-python }}; then - gh run download $LATEST_PRIOR_RUN_ID -p cuda-python-wheel -R NVIDIA/cuda-python - ls -al cuda-python-wheel - mv cuda-python-wheel/*.whl . - rmdir cuda-python-wheel - fi - - - name: Display structure of downloaded cuda-python artifacts - if: ${{ inputs.test-python && env.BINDINGS_SOURCE != 'published' }} - run: | - Get-Location - Get-ChildItem cuda_python*.whl | Select-Object Mode, LastWriteTime, Length, FullName - - - name: Display structure of downloaded cuda.bindings artifacts - if: ${{ (inputs.test-bindings || inputs.test-core || inputs.test-python) && - env.BINDINGS_SOURCE != 'published' }} - run: | - Get-Location - Get-ChildItem -Recurse -Force $env:CUDA_BINDINGS_ARTIFACTS_DIR | Select-Object Mode, LastWriteTime, Length, FullName - - - name: Download cuda.bindings Cython tests - if: ${{ inputs.test-bindings && env.SKIP_CYTHON_TEST == '0' }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }}-tests - path: cuda_bindings/.moon-out/cython-tests - run-id: ${{ inputs.run-id || github.run_id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Display structure of downloaded cuda.bindings Cython tests - if: ${{ inputs.test-bindings && env.SKIP_CYTHON_TEST == '0' }} - run: | - Get-Location - Get-ChildItem -Recurse -Force cuda_bindings/.moon-out/cython-tests | Select-Object Mode, LastWriteTime, Length, FullName - - - name: Download cuda.core build artifacts - if: ${{ inputs.test-core || (inputs.test-python && env.BINDINGS_SOURCE == 'main') }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ env.CUDA_CORE_ARTIFACT_NAME }} - path: ${{ env.CUDA_CORE_ARTIFACTS_DIR }} - run-id: ${{ inputs.run-id || github.run_id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Display structure of downloaded cuda.core build artifacts - if: ${{ inputs.test-core || (inputs.test-python && env.BINDINGS_SOURCE == 'main') }} - run: | - Get-Location - Get-ChildItem -Recurse -Force $env:CUDA_CORE_ARTIFACTS_DIR | Select-Object Mode, LastWriteTime, Length, FullName - - - name: Download cuda.core Cython tests - if: ${{ inputs.test-core }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-tests - path: cuda_core/.moon-out/cython-tests - run-id: ${{ inputs.run-id || github.run_id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Display structure of downloaded cuda.core Cython tests - if: ${{ inputs.test-core }} - run: | - Get-Location - Get-ChildItem -Recurse -Force cuda_core/.moon-out/cython-tests | Select-Object Mode, LastWriteTime, Length, FullName - - - name: Download cuda.core test binaries - if: ${{ inputs.test-core }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-test-binaries - path: cuda_core/.moon-out/test-binaries - run-id: ${{ inputs.run-id || github.run_id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Display structure of downloaded cuda.core test binaries - if: ${{ inputs.test-core }} - run: | - Get-Location - Get-ChildItem -Recurse -Force cuda_core/.moon-out/test-binaries | Select-Object Mode, LastWriteTime, Length, FullName - name: Set up Python ${{ matrix.PY_VER }} uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 @@ -323,8 +188,7 @@ jobs: allow-prereleases: ${{ startsWith(matrix.PY_VER, '3.15') }} - name: Enable Scientific Python Nightly Wheels for Python 3.15 - if: ${{ (inputs.test-bindings || inputs.test-core || inputs.test-python) && - startsWith(matrix.PY_VER, '3.15') }} + if: ${{ startsWith(matrix.PY_VER, '3.15') }} shell: bash --noprofile --norc -xeuo pipefail {0} run: | echo "PIP_EXTRA_INDEX_URL=https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" >> "$GITHUB_ENV" @@ -351,53 +215,18 @@ jobs: shell: bash --noprofile --norc -xeuo pipefail {0} run: echo "PYTEST_ADDOPTS=\"--count=${{ inputs.nruns }}\"" >> "$GITHUB_ENV" - # ── Standard test steps (skipped for nightly modes) ── - - name: Run cuda.pathfinder tests with see_what_works - if: ${{ inputs.test-mode == 'standard' && inputs.test-pathfinder }} - env: - CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS: see_what_works - CUDA_PATHFINDER_TEST_FIND_NVIDIA_HEADERS_STRICTNESS: see_what_works - CUDA_PATHFINDER_TEST_FIND_NVIDIA_BITCODE_LIB_STRICTNESS: see_what_works - shell: bash --noprofile --norc -xeuo pipefail {0} - run: moon ci pathfinder:test-installed-windows --upstream none --downstream none ${{ (inputs.force-all || inputs.build-type != 'pull-request') && '--force' || '' }} - - - name: Run cuda.bindings tests - if: ${{ inputs.test-mode == 'standard' && inputs.test-bindings && env.SKIP_CUDA_BINDINGS_TEST == '0' }} - env: - CUDA_VER: ${{ matrix.CUDA_VER }} - LOCAL_CTK: ${{ matrix.LOCAL_CTK }} - shell: bash --noprofile --norc -xeuo pipefail {0} - run: moon ci bindings:test-installed-windows --upstream none --downstream none ${{ (inputs.force-all || inputs.build-type != 'pull-request') && '--force' || '' }} - - - name: Run cuda.core tests - if: ${{ inputs.test-mode == 'standard' && inputs.test-core }} + - name: Run standard tests with Moon + if: ${{ inputs.test-mode == 'standard' }} env: CUDA_VER: ${{ matrix.CUDA_VER }} LOCAL_CTK: ${{ matrix.LOCAL_CTK }} shell: bash --noprofile --norc -xeuo pipefail {0} - run: moon ci core:test-installed-windows --upstream none --downstream none ${{ (inputs.force-all || inputs.build-type != 'pull-request') && '--force' || '' }} - - - name: Ensure cuda-python installable - if: ${{ inputs.test-mode == 'standard' && inputs.test-python && env.BINDINGS_SOURCE == 'main' }} - run: moon ci metapackage:test-installed-windows --upstream none --downstream none ${{ (inputs.force-all || inputs.build-type != 'pull-request') && '--force' || '' }} - - - name: Install cuda.pathfinder extra wheels for testing - if: ${{ inputs.test-mode == 'standard' && inputs.test-pathfinder }} - shell: bash --noprofile --norc -xeuo pipefail {0} run: | - pushd cuda_pathfinder - pip install --only-binary=:all: -v ./*.whl --group "test-cu${TEST_CUDA_MAJOR}" - pip list - popd - - - name: Run cuda.pathfinder tests with all_must_work - if: ${{ inputs.test-mode == 'standard' && inputs.test-pathfinder }} - env: - CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS: all_must_work - CUDA_PATHFINDER_TEST_FIND_NVIDIA_HEADERS_STRICTNESS: all_must_work - CUDA_PATHFINDER_TEST_FIND_NVIDIA_BITCODE_LIB_STRICTNESS: all_must_work - shell: bash --noprofile --norc -xeuo pipefail {0} - run: moon ci pathfinder:test-installed-windows-strict --upstream none --downstream none ${{ (inputs.force-all || inputs.build-type != 'pull-request') && '--force' || '' }} + args=() + if [[ "${{ inputs.force-all }}" == "true" || "${{ inputs.build-type }}" != "pull-request" ]]; then + args+=(--force) + fi + moon ci ':#ci-test-windows' --upstream deep --downstream none "${args[@]}" # ── Nightly: install wheels + optional dep together ── - name: Install Visual C++ Redistributable (required by PyTorch on Windows) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 868758e9eaf..bd6051e2ac6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -271,7 +271,7 @@ $ moon run : $ moon run metapackage:wheel-pure $ moon run root:test $ moon run root:pure-wheel -$ MOON_BASE=origin/main MOON_HEAD=HEAD moon ci ':#ci-test-linux' --downstream none +$ MOON_BASE=origin/main MOON_HEAD=HEAD moon ci ':#ci-test-linux' --upstream deep --downstream none ``` CI workers follow Moon's CI model: after GitHub Actions provisions the required Python, CUDA toolkit, compiler, or @@ -280,13 +280,20 @@ execution, cache hits, and output hydration. GitHub Actions retains heterogeneou and release or Pages publishing. The explicit `--downstream none` keeps work from crossing those runner-class boundaries; upstream dependencies remain part of the Moon task graph where they share a cache and environment. +The current and previous CUDA variants of `cuda.core` are intentionally separate tasks because Moon does not +provision or switch CUDA toolkits. For a local merged wheel, activate the current toolkit and run the current wheel +tasks, stage the matching previous-branch `cuda.bindings` wheel, activate the previous toolkit and run +`core:wheel-previous`, then run `core:wheel-merge`. CI follows the same staged sequence and lets Moon parallelize +independent work within each phase. + For ephemeral runners, CI uploads the portable `.moon/cache/hashes` and `.moon/cache/outputs` directories as ordinary immutable GitHub workflow artifacts. A later producer restores the lane-qualified artifact from the successful trusted `main` run at the exact merge-base commit, then runs `moon ci`; GitHub transports the local cache -while Moon alone interprets its hashes and hydrates task outputs. Conventional wheel artifacts remain the -cross-runner input to GPU tests and release tooling. Missing or incomplete cache artifacts conservatively allocate -the producer runners and start with an empty cache. Generated `.moon/cache` and `.moon-out` directories are ignored -by Git. +while Moon alone interprets its hashes and hydrates task outputs. Lane bundles also carry Moon's canonical task +outputs between heterogeneous build and test runners, while conventional named wheel artifacts remain available for +release tooling. Context-sensitive documentation is rebuilt as four parallel Moon tasks whenever its runner is +selected. Missing or incomplete cache artifacts conservatively allocate the producer runners and start with an empty +cache. Generated `.moon/cache` and `.moon-out` directories are ignored by Git. ### CI Pipeline Flow diff --git a/benchmarks/cuda_bindings/moon.yml b/benchmarks/cuda_bindings/moon.yml index 37c39fe0e25..baee002fe98 100644 --- a/benchmarks/cuda_bindings/moon.yml +++ b/benchmarks/cuda_bindings/moon.yml @@ -28,41 +28,37 @@ fileGroups: - 'run_pyperf.py' - 'pixi.toml' - 'pixi.lock' + tests: + - 'runner/**/*' + - 'benchmarks/**/*' + - 'tests/**/*' + - 'pixi.toml' + - 'pixi.lock' tasks: bench: - command: python - args: [ci/tools/moon_ci.py, pixi-test, bindings-benchmarks, --task, bench, --environment, source] + command: pixi + args: [run, --manifest-path, benchmarks/cuda_bindings/pixi.toml, --environment, source, bench] inputs: - '@group(benchmarks)' - '/cuda_bindings/**/*' - - '/ci/tools/moon_ci.py' smoke: - command: python + command: pixi args: - - ci/tools/moon_ci.py - - pixi-test - - bindings-benchmarks - - --task - - bench-smoke-test + - run + - --manifest-path + - benchmarks/cuda_bindings/pixi.toml - --environment - source + - bench-smoke-test inputs: - '@group(benchmarks)' - '/cuda_bindings/**/*' - - '/ci/tools/moon_ci.py' smoke-linux: command: python - args: - - ci/tools/moon_ci.py - - pixi-test - - bindings-benchmarks - - --task - - bench-smoke-test - - --environment - - source + args: [ci/tools/moon_ci.py, bindings-benchmark-smoke] inputs: - '@group(benchmarks)' - {project: bindings, group: package} @@ -74,7 +70,27 @@ tasks: - '/ci/tools/install_gpu_driver.sh' - '/tests/**/*' - '/.github/workflows/test-wheel-linux.yml' - tags: [ci-test-linux] + tags: [ci-test-linux, runner-test-linux] + type: test + options: + mutex: ci-python-gpu + os: linux + runInCI: true + + unit-test: + command: uvx + args: + - --no-managed-python + - --no-python-downloads + - --from + - pytest + - pytest + - --noconftest + - benchmarks/cuda_bindings/tests + inputs: + - '@group(tests)' + tags: [ci-quality, runner-quality] type: test options: + os: linux runInCI: true diff --git a/ci/moon.yml b/ci/moon.yml index 968db00ef08..bb659ac4e31 100644 --- a/ci/moon.yml +++ b/ci/moon.yml @@ -6,17 +6,6 @@ $schema: https://moonrepo.dev/schemas/v2/project.json language: unknown layer: automation -dependsOn: - - id: pathfinder - scope: development - - id: bindings - scope: development - - id: core - scope: development - - id: metapackage - scope: development - - id: bindings-benchmarks - scope: development toolchains: default: system @@ -26,9 +15,8 @@ taskOptions: runFromWorkspaceRoot: true runInCI: true -# Runner allocation is modeled as ordinary affected Moon tasks. Package-owned -# file groups remain the source of truth; these tasks only map them onto the -# heterogeneous runner classes that GitHub Actions must allocate. +# Runner allocation tags live on the real build and test tasks. This project +# only owns the two conservative catch-all inputs and the Moon contract test. fileGroups: orchestration: - '/.moon/**/*' @@ -48,46 +36,6 @@ fileGroups: - '/pytest.ini' - '/ruff.toml' - test-common: - - '/tests/**/*' - - '/ci/test-matrix.yml' - - '/ci/tools/env-vars' - test-library-runner: - - '/ci/tools/run-tests' - test-helpers: - - '/cuda_python_test_helpers/**/*' - test-assets-bindings: - - '/cuda_bindings/tests/cython/**/*' - test-assets-core: - - '/cuda_core/tests/cython/**/*' - - '/cuda_core/tests/test_binaries/**/*' - - build-portable: - - '/.github/workflows/build-pure-wheel.yml' - build-native-common: - - '/.github/workflows/build-wheel.yml' - - '/.github/actions/fetch_ctk/**/*' - - '/ci/versions.yml' - - '/ci/tools/env-vars' - build-native-core: - - '/ci/tools/merge_cuda_core_wheels.py' - sdist-linux: - - '/.github/workflows/test-sdist-linux.yml' - sdist-windows: - - '/.github/workflows/test-sdist-windows.yml' - test-linux-common: - - '/.github/workflows/test-wheel-linux.yml' - - '/ci/tools/guess_latest.sh' - - '/ci/tools/install_gpu_driver.sh' - test-linux-native: - - '/ci/tools/setup-sanitizer' - test-windows-common: - - '/.github/workflows/test-wheel-windows.yml' - - '/ci/tools/configure_driver_mode.ps1' - - '/ci/tools/install_gpu_driver.ps1' - docs: - - '/.github/workflows/build-docs.yml' - # A new or otherwise unowned path must never silently suppress CI. Known # source, test, docs, and platform inputs are excluded because their precise # gates handle them; anything left forces the complete pipeline. @@ -119,7 +67,14 @@ fileGroups: - '!/.github/workflows/test-wheel-linux.yml' - '!/.github/workflows/test-wheel-windows.yml' - '!/.moon/**/*' - - '!/benchmarks/cuda_bindings/**/*' + - '!/benchmarks/cuda_bindings/benchmarks/**/*' + - '!/benchmarks/cuda_bindings/runner/**/*' + - '!/benchmarks/cuda_bindings/tests/**/*' + - '!/benchmarks/cuda_bindings/compare.py' + - '!/benchmarks/cuda_bindings/pixi.lock' + - '!/benchmarks/cuda_bindings/pixi.toml' + - '!/benchmarks/cuda_bindings/run_cpp.py' + - '!/benchmarks/cuda_bindings/run_pyperf.py' - '!/ci/moon.yml' - '!/ci/test-matrix.yml' - '!/ci/versions.yml' @@ -195,279 +150,29 @@ fileGroups: - '!/LICENSE' tasks: - gate-force-all: - command: python - args: [ci/tools/moon_ci.py, gate, force-all] + # These two no-op tasks are the only allocation-only nodes. The planner + # inspects their tag and never executes them. + force-all: inputs: - '@group(orchestration)' - outputs: ['.moon-out/ci-gates/force-all'] - tags: [ci-gate] + tags: [ci-force-all] - gate-force-all-unowned: - command: python - args: [ci/tools/moon_ci.py, gate, force-all-unowned] + force-all-unowned: inputs: - '@group(unowned)' - outputs: ['.moon-out/ci-gates/force-all-unowned'] - tags: [ci-gate] - - gate-build-portable: - command: python - args: [ci/tools/moon_ci.py, gate, build-portable] - inputs: - - {project: pathfinder, group: package} - - {project: bindings, group: package} - - {project: metapackage, group: package} - - '@group(build-portable)' - outputs: ['.moon-out/ci-gates/build-portable'] - tags: [ci-gate] - - gate-build-linux-64: - command: python - args: [ci/tools/moon_ci.py, gate, build-linux-64] - inputs: - - {project: pathfinder, group: package} - - {project: bindings, group: package} - - {project: core, group: package} - - '@group(build-portable)' - - '@group(build-native-common)' - - '@group(build-native-core)' - - '@group(test-assets-bindings)' - - '@group(test-assets-core)' - outputs: ['.moon-out/ci-gates/build-linux-64'] - tags: [ci-gate] - - gate-build-linux-aarch64: - command: python - args: [ci/tools/moon_ci.py, gate, build-linux-aarch64] - inputs: - - {project: pathfinder, group: package} - - {project: bindings, group: package} - - {project: core, group: package} - - '@group(build-portable)' - - '@group(build-native-common)' - - '@group(build-native-core)' - - '@group(test-assets-bindings)' - - '@group(test-assets-core)' - outputs: ['.moon-out/ci-gates/build-linux-aarch64'] - tags: [ci-gate] - - gate-build-windows: - command: python - args: [ci/tools/moon_ci.py, gate, build-windows] - inputs: - - {project: pathfinder, group: package} - - {project: bindings, group: package} - - {project: core, group: package} - - '@group(build-portable)' - - '@group(build-native-common)' - - '@group(build-native-core)' - - '@group(test-assets-bindings)' - - '@group(test-assets-core)' - outputs: ['.moon-out/ci-gates/build-windows'] - tags: [ci-gate] - - gate-test-sdist-linux: - command: python - args: [ci/tools/moon_ci.py, gate, test-sdist-linux] - inputs: - - {project: pathfinder, group: package} - - {project: bindings, group: package} - - {project: core, group: package} - - {project: metapackage, group: package} - - '@group(sdist-linux)' - outputs: ['.moon-out/ci-gates/test-sdist-linux'] - tags: [ci-gate] - - gate-test-sdist-windows: - command: python - args: [ci/tools/moon_ci.py, gate, test-sdist-windows] - inputs: - - {project: pathfinder, group: package} - - {project: bindings, group: package} - - {project: core, group: package} - - {project: metapackage, group: package} - - '@group(sdist-windows)' - outputs: ['.moon-out/ci-gates/test-sdist-windows'] - tags: [ci-gate] - - gate-test-linux: - command: python - args: [ci/tools/moon_ci.py, gate, test-linux] - inputs: - - {project: pathfinder, group: package} - - {project: pathfinder, group: tests} - - {project: bindings, group: package} - - {project: bindings, group: tests} - - {project: core, group: package} - - {project: core, group: tests} - - {project: metapackage, group: package} - - '@group(build-portable)' - - '@group(build-native-common)' - - '@group(build-native-core)' - - '@group(test-common)' - - '@group(test-library-runner)' - - '@group(test-helpers)' - - {project: bindings-benchmarks, group: benchmarks} - - '@group(test-linux-common)' - - '@group(test-linux-native)' - outputs: ['.moon-out/ci-gates/test-linux'] - tags: [ci-gate] - - gate-test-windows: - command: python - args: [ci/tools/moon_ci.py, gate, test-windows] - inputs: - - {project: pathfinder, group: package} - - {project: pathfinder, group: tests} - - {project: bindings, group: package} - - {project: bindings, group: tests} - - {project: core, group: package} - - {project: core, group: tests} - - {project: metapackage, group: package} - - '@group(build-portable)' - - '@group(build-native-common)' - - '@group(build-native-core)' - - '@group(test-common)' - - '@group(test-library-runner)' - - '@group(test-helpers)' - - '@group(test-windows-common)' - outputs: ['.moon-out/ci-gates/test-windows'] - tags: [ci-gate] - - gate-docs: - command: python - args: [ci/tools/moon_ci.py, gate, docs] - inputs: - - {project: pathfinder, group: package} - - {project: pathfinder, group: docs} - - {project: bindings, group: package} - - {project: bindings, group: docs} - - {project: core, group: package} - - {project: core, group: docs} - - {project: metapackage, group: package} - - {project: metapackage, group: docs} - - '@group(docs)' - outputs: ['.moon-out/ci-gates/docs'] - tags: [ci-gate] - - gate-core-api: - command: python - args: [ci/tools/moon_ci.py, gate, core-api] - inputs: - - '/cuda_core/cuda/core/**/*' - outputs: ['.moon-out/ci-gates/core-api'] - tags: [ci-gate] - - gate-build-pathfinder: - command: python - args: [ci/tools/moon_ci.py, gate, build-pathfinder] - inputs: - - {project: pathfinder, group: package} - - '@group(build-portable)' - outputs: ['.moon-out/ci-gates/build-pathfinder'] - tags: [ci-gate] - - gate-build-bindings: - command: python - args: [ci/tools/moon_ci.py, gate, build-bindings] - inputs: - - {project: pathfinder, group: package} - - {project: bindings, group: package} - - '@group(build-portable)' - - '@group(build-native-common)' - outputs: ['.moon-out/ci-gates/build-bindings'] - tags: [ci-gate] - - gate-build-core: - command: python - args: [ci/tools/moon_ci.py, gate, build-core] - inputs: - - {project: pathfinder, group: package} - - {project: bindings, group: package} - - {project: core, group: package} - - '@group(build-portable)' - - '@group(build-native-common)' - - '@group(build-native-core)' - outputs: ['.moon-out/ci-gates/build-core'] - tags: [ci-gate] - - gate-build-metapackage: - command: python - args: [ci/tools/moon_ci.py, gate, build-metapackage] - inputs: - - {project: bindings, group: package} - - {project: metapackage, group: package} - - '@group(build-portable)' - outputs: ['.moon-out/ci-gates/build-metapackage'] - tags: [ci-gate] - - gate-test-pathfinder: - command: python - args: [ci/tools/moon_ci.py, gate, test-pathfinder] - inputs: - - {project: pathfinder, group: package} - - {project: pathfinder, group: tests} - - '@group(build-portable)' - - '@group(test-common)' - - '@group(test-library-runner)' - - '@group(test-linux-common)' - - '@group(test-windows-common)' - outputs: ['.moon-out/ci-gates/test-pathfinder'] - tags: [ci-gate] - - gate-test-bindings: - command: python - args: [ci/tools/moon_ci.py, gate, test-bindings] - inputs: - - {project: pathfinder, group: package} - - {project: bindings, group: package} - - {project: bindings, group: tests} - - '@group(build-portable)' - - '@group(build-native-common)' - - '@group(test-common)' - - '@group(test-library-runner)' - - '@group(test-helpers)' - - {project: bindings-benchmarks, group: benchmarks} - - '@group(test-linux-common)' - - '@group(test-linux-native)' - - '@group(test-windows-common)' - outputs: ['.moon-out/ci-gates/test-bindings'] - tags: [ci-gate] - - gate-test-core: - command: python - args: [ci/tools/moon_ci.py, gate, test-core] - inputs: - - {project: pathfinder, group: package} - - {project: bindings, group: package} - - {project: core, group: package} - - {project: core, group: tests} - - '@group(build-portable)' - - '@group(build-native-common)' - - '@group(build-native-core)' - - '@group(test-common)' - - '@group(test-library-runner)' - - '@group(test-helpers)' - - '@group(test-linux-common)' - - '@group(test-linux-native)' - - '@group(test-windows-common)' - outputs: ['.moon-out/ci-gates/test-core'] - tags: [ci-gate] - - gate-test-metapackage: - command: python - args: [ci/tools/moon_ci.py, gate, test-metapackage] - inputs: - - {project: pathfinder, group: package} - - {project: bindings, group: package} - - {project: core, group: package} - - {project: metapackage, group: package} - - '@group(build-portable)' - - '@group(build-native-common)' - - '@group(build-native-core)' - - '@group(test-common)' - - '@group(test-linux-common)' - - '@group(test-windows-common)' - outputs: ['.moon-out/ci-gates/test-metapackage'] - tags: [ci-gate] + tags: [ci-force-all] + + quality-moon-contracts: + command: python + args: [-m, unittest, ci.tools.tests.test_moon_ci, ci.tools.tests.test_moon_workspace] + inputs: + - '/.moon/**/*' + - '/**/moon.yml' + - '/ci/tools/moon_ci.py' + - '/ci/tools/moon_fingerprint.py' + - '/ci/tools/tests/test_moon_ci.py' + - '/ci/tools/tests/test_moon_workspace.py' + tags: [ci-quality, runner-quality] + type: test + options: + os: linux diff --git a/ci/tools/moon_ci.py b/ci/tools/moon_ci.py index 1b6dd6097a1..d575f0d8f9d 100644 --- a/ci/tools/moon_ci.py +++ b/ci/tools/moon_ci.py @@ -23,42 +23,14 @@ REPO_ROOT = Path(__file__).resolve().parents[2] PROJECT_PATHS = { "root": Path("."), - "ci": Path("ci"), "pathfinder": Path("cuda_pathfinder"), "bindings": Path("cuda_bindings"), "core": Path("cuda_core"), "metapackage": Path("cuda_python"), "bindings-benchmarks": Path("benchmarks/cuda_bindings"), } -DOC_TASKS = { - "pathfinder": "build-docs", - "bindings": "build-docs", - "core": "docs-build", -} PACKAGE_PROJECTS = ("pathfinder", "bindings", "core", "metapackage") CYTHON_PROJECTS = ("bindings", "core") -GATE_MARKERS = { - "force-all", - "force-all-unowned", - "build-portable", - "build-linux-64", - "build-linux-aarch64", - "build-windows", - "test-sdist-linux", - "test-sdist-windows", - "test-linux", - "test-windows", - "docs", - "core-api", - "build-pathfinder", - "build-bindings", - "build-core", - "build-metapackage", - "test-pathfinder", - "test-bindings", - "test-core", - "test-metapackage", -} def _run( @@ -155,30 +127,6 @@ def _copy_files(source: Path, output: Path, patterns: tuple[str, ...]) -> None: shutil.copy2(source_path, output / source_path.name) -def _gate(args: argparse.Namespace) -> None: - if args.marker not in GATE_MARKERS: - raise ValueError(f"unknown CI gate marker: {args.marker}") - output = _output_path("ci", f"ci-gates/{args.marker}") - output.parent.mkdir(parents=True, exist_ok=True) - output.write_text("true\n", encoding="utf-8") - - -def _pixi_run(project: str, task: str, *, environment: str | None, extra: list[str]) -> None: - manifest = _project_path(project) / "pixi.toml" - if not manifest.is_file(): - raise FileNotFoundError(f"Pixi manifest not found: {manifest}") - pixi = shutil.which("pixi") - if pixi is None: - raise RuntimeError("pixi is required for this task but was not found on PATH") - command = [pixi, "run", "--manifest-path", str(manifest)] - selected_environment = environment or os.environ.get("PIXI_ENVIRONMENT_NAME") - if selected_environment: - command.extend(["--environment", selected_environment]) - command.append(task) - command.extend(extra) - _run(command) - - def _pure_wheel(args: argparse.Namespace) -> None: if args.project not in {"pathfinder", "metapackage"}: raise ValueError("pure-wheel only supports pathfinder and metapackage") @@ -369,62 +317,129 @@ def _merge_core_wheels(_args: argparse.Namespace) -> None: def _pixi_test(args: argparse.Namespace) -> None: - _pixi_run(args.project, args.task, environment=args.environment, extra=args.extra) + pixi = shutil.which("pixi") + if pixi is None: + raise RuntimeError("pixi is required for this task but was not found on PATH") + command = [pixi, "run", "--manifest-path", str(_project_path(args.project) / "pixi.toml")] + # A nested Pixi invocation otherwise falls back to the package's default + # environment instead of the cu12/cu13 environment selected at the root. + environment = os.environ.get("PIXI_ENVIRONMENT_NAME") + if environment: + command.extend(["--environment", environment]) + command.append("test") + _run(command) -def _pixi_docs(args: argparse.Namespace) -> None: - _pixi_run(args.project, DOC_TASKS[args.project], environment="docs", extra=[]) +def _docs_arguments() -> list[str]: + latest_only = (os.environ.get("CUDA_PYTHON_DOCS_LATEST_ONLY") or "true").lower() + if latest_only not in {"0", "1", "false", "true"}: + raise ValueError("CUDA_PYTHON_DOCS_LATEST_ONLY must be true, false, 1, or 0") + return ["latest-only"] if latest_only in {"1", "true"} else [] -def _docs_ci(_args: argparse.Namespace) -> None: +def _docs_component(args: argparse.Namespace) -> None: bash = shutil.which("bash") if bash is None: - raise RuntimeError("bash is required to build the combined documentation") - latest_only = (os.environ.get("CUDA_PYTHON_DOCS_LATEST_ONLY") or "true").lower() - if latest_only not in {"0", "1", "false", "true"}: - raise ValueError("CUDA_PYTHON_DOCS_LATEST_ONLY must be true, false, 1, or 0") - arguments = [bash, "build_all_docs.sh"] - if latest_only in {"1", "true"}: - arguments.append("latest-only") - docs_root = _project_path("metapackage") / "docs" - _run(arguments, cwd=docs_root) + raise RuntimeError("bash is required to build documentation") + docs_root = _project_path(args.project) / "docs" + build = docs_root / "build" + if build.exists(): + if build.is_symlink() or not build.is_dir(): + raise ValueError(f"refusing to replace non-directory docs output: {build}") + shutil.rmtree(build) + _run([bash, "build_docs.sh", *_docs_arguments()], cwd=docs_root) source = docs_root / "build" / "html" - if not source.is_dir(): - raise RuntimeError(f"combined documentation output not found: {source}") - output = _output_path("root", "docs") + if source.is_symlink() or not source.is_dir(): + raise RuntimeError(f"documentation output not found: {source}") + output = _output_path(args.project, "docs-ci") _reset_output(output) shutil.copytree(source, output, dirs_exist_ok=True) +def _docs_assemble(_args: argparse.Namespace) -> None: + output = _output_path("root", "docs") + _reset_output(output) + shutil.copytree(_output_path("metapackage", "docs-ci"), output, dirs_exist_ok=True) + for project, destination in ( + ("bindings", "cuda-bindings"), + ("core", "cuda-core"), + ("pathfinder", "cuda-pathfinder"), + ): + source = _output_path(project, "docs-ci") + if source.is_symlink() or not source.is_dir(): + raise RuntimeError(f"documentation component output not found: {source}") + shutil.copytree(source, output / destination) + + +def _prepare_test_assets(_args: argparse.Namespace) -> None: + wheels = [ + _artifact_wheel("pathfinder", "pure"), + _artifact_wheel("bindings", "current"), + _artifact_wheel("core", "current"), + ] + groups = [ + _project_path("bindings") / "pyproject.toml", + _project_path("core") / "pyproject.toml", + ] + command = [sys.executable, "-m", "pip", "install", *(str(wheel) for wheel in wheels)] + for pyproject in groups: + command.extend(["--group", f"{pyproject}:test"]) + _run(command) + + def _cython_test_assets(args: argparse.Namespace) -> None: source = _project_path(args.project) / "tests" / "cython" - wheels = [_artifact_wheel("pathfinder", "pure")] - if args.project == "bindings": - wheels.append(_artifact_wheel("bindings", "current")) - else: - wheels.extend( - [ - _artifact_wheel("bindings", "current"), - _artifact_wheel("core", "current"), - ] - ) + bash = shutil.which("bash") + if bash is None: + raise RuntimeError("bash is required to build Cython test extensions") + _run([bash, "build_tests.sh"], cwd=source) + output = _output_path(args.project, "cython-tests") + _copy_files(source, output, ("test_*.so", "test_*.pyd", "test_*.dylib")) + + +def _prepare_pathfinder_strict(_args: argparse.Namespace) -> None: + cuda_major = os.environ.get("TEST_CUDA_MAJOR", "") + if not cuda_major.isdigit(): + raise RuntimeError("TEST_CUDA_MAJOR must be a numeric CUDA major version") _run( [ sys.executable, "-m", "pip", "install", - *(str(wheel) for wheel in wheels), + "--only-binary=:all:", + "--verbose", + str(_artifact_wheel("pathfinder", "pure")), "--group", - f"{_project_path(args.project) / 'pyproject.toml'}:test", + f"{_project_path('pathfinder') / 'pyproject.toml'}:test-cu{cuda_major}", ] ) - bash = shutil.which("bash") - if bash is None: - raise RuntimeError("bash is required to build Cython test extensions") - _run([bash, "build_tests.sh"], cwd=source) - output = _output_path(args.project, "cython-tests") - _copy_files(source, output, ("test_*.so", "test_*.pyd", "test_*.dylib")) + _run([sys.executable, "-m", "pip", "list"]) + + +def _bindings_benchmark_smoke(_args: argparse.Namespace) -> None: + if os.environ.get("SKIP_CUDA_BINDINGS_TEST") == "1": + print("Skipping cuda.bindings benchmarks for this declared compatibility lane.", flush=True) + return + _run( + [ + sys.executable, + "-m", + "pip", + "install", + str(_artifact_wheel("pathfinder", "pure")), + str(_artifact_wheel("bindings", "current")), + "pyperf", + ] + ) + _run( + [ + sys.executable, + str(_project_path("bindings-benchmarks") / "run_pyperf.py"), + "--debug-single-value", + ], + cwd=_project_path("bindings-benchmarks"), + ) def _core_test_binaries(_args: argparse.Namespace) -> None: @@ -444,6 +459,9 @@ def _stage_files(source: Path, destination: Path, pattern: str) -> None: def _installed_test(args: argparse.Namespace) -> None: + if args.project == "bindings" and os.environ.get("SKIP_CUDA_BINDINGS_TEST") == "1": + print("Skipping cuda.bindings tests for this declared compatibility lane.", flush=True) + return pathfinder_wheel = _artifact_wheel("pathfinder", "pure") if pathfinder_wheel.parent != _project_path("pathfinder"): _stage_files(pathfinder_wheel.parent, _project_path("pathfinder"), pathfinder_wheel.name) @@ -469,6 +487,9 @@ def _installed_test(args: argparse.Namespace) -> None: def _metapackage_install_test(_args: argparse.Namespace) -> None: + if os.environ.get("BINDINGS_SOURCE") != "main": + print("Skipping the metapackage smoke test because BINDINGS_SOURCE is not main.", flush=True) + return wheels = [ _artifact_wheel("pathfinder", "pure"), _artifact_wheel("bindings", "current"), @@ -495,10 +516,6 @@ def _parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description=__doc__) subparsers = parser.add_subparsers(dest="command", required=True) - gate = subparsers.add_parser("gate", help="write one affected CI allocation marker") - gate.add_argument("marker", choices=tuple(sorted(GATE_MARKERS))) - gate.set_defaults(handler=_gate) - pure_wheel = subparsers.add_parser("pure-wheel", help="build one pure-Python wheel") pure_wheel.add_argument("project", choices=("pathfinder", "metapackage")) pure_wheel.set_defaults(handler=_pure_wheel) @@ -515,24 +532,36 @@ def _parser() -> argparse.ArgumentParser: merge_wheels = subparsers.add_parser("merge-core-wheels", help="merge current and previous CUDA wheels") merge_wheels.set_defaults(handler=_merge_core_wheels) - pixi_test = subparsers.add_parser("pixi-test", help="run an existing Pixi test or benchmark task") - pixi_test.add_argument("project", choices=("pathfinder", "bindings", "core", "bindings-benchmarks")) - pixi_test.add_argument("--task", default="test") - pixi_test.add_argument("--environment") - pixi_test.add_argument("extra", nargs="*") + pixi_test = subparsers.add_parser("pixi-test", help="run a package test in the caller-selected Pixi environment") + pixi_test.add_argument("project", choices=("pathfinder", "bindings", "core")) pixi_test.set_defaults(handler=_pixi_test) - pixi_docs = subparsers.add_parser("pixi-docs", help="run an existing Pixi documentation task") - pixi_docs.add_argument("project", choices=tuple(DOC_TASKS)) - pixi_docs.set_defaults(handler=_pixi_docs) + docs_component = subparsers.add_parser("docs-component", help="build and stage one documentation component") + docs_component.add_argument("project", choices=PACKAGE_PROJECTS) + docs_component.set_defaults(handler=_docs_component) - docs_ci = subparsers.add_parser("docs-ci", help="build and stage the combined CI documentation") - docs_ci.set_defaults(handler=_docs_ci) + docs_assemble = subparsers.add_parser("docs-assemble", help="assemble staged documentation components") + docs_assemble.set_defaults(handler=_docs_assemble) + + prepare_assets = subparsers.add_parser( + "prepare-test-assets", help="install the shared inputs for native test-asset builds" + ) + prepare_assets.set_defaults(handler=_prepare_test_assets) cython_assets = subparsers.add_parser("cython-test-assets", help="build and stage Cython tests") cython_assets.add_argument("project", choices=CYTHON_PROJECTS) cython_assets.set_defaults(handler=_cython_test_assets) + pathfinder_strict = subparsers.add_parser( + "prepare-pathfinder-strict", help="install CUDA-specific pathfinder test dependencies" + ) + pathfinder_strict.set_defaults(handler=_prepare_pathfinder_strict) + + benchmark_smoke = subparsers.add_parser( + "bindings-benchmark-smoke", help="run the bindings benchmark smoke test when the lane supports it" + ) + benchmark_smoke.set_defaults(handler=_bindings_benchmark_smoke) + core_binaries = subparsers.add_parser("core-test-binaries", help="build and stage cuda.core test binaries") core_binaries.set_defaults(handler=_core_test_binaries) @@ -550,8 +579,6 @@ def _parser() -> argparse.ArgumentParser: def main() -> None: args = _parser().parse_args() - if getattr(args, "extra", None) and args.extra[0] == "--": - args.extra = args.extra[1:] args.handler(args) diff --git a/ci/tools/moon_fingerprint.py b/ci/tools/moon_fingerprint.py index 743eeab4292..6acb39d2b48 100644 --- a/ci/tools/moon_fingerprint.py +++ b/ci/tools/moon_fingerprint.py @@ -14,6 +14,8 @@ import json import os import platform +import shlex +import shutil import subprocess import sysconfig from pathlib import Path @@ -49,17 +51,23 @@ "BUILD_CUDA_MAJOR", "BUILD_CUDA_VER", "BUILD_PREV_CUDA_MAJOR", + "CC", "CIBW_ARCHS", "CIBW_BUILD", "CIBW_ENABLE", + "CL", + "CPLUS_INCLUDE_PATH", + "CXX", "CUDA_CORE_BUILD_MAJOR", "CUDA_PATH", "CUDA_PYTHON_LANE", "CUDA_VER", "HOST_PLATFORM", + "PY_EXT_SUFFIX", "PY_VER", ) PYTHON_TOOLS = ("build", "cibuildwheel", "packaging", "pip", "setuptools", "setuptools-scm", "wheel") +TEST_ASSET_PYTHON_TOOLS = ("Cython", "numpy") def _git_describe(pattern: str) -> str: @@ -80,6 +88,39 @@ def _distribution_version(name: str) -> str: return "" +def _configured_compilers() -> set[str]: + commands = {"cc", "c++", "cl", "nvcc"} + for variable, config_var in (("CC", "CC"), ("CXX", "CXX")): + configured = os.environ.get(variable) or sysconfig.get_config_var(config_var) or "" + try: + tokens = shlex.split(configured, posix=os.name != "nt") + except ValueError: + tokens = [] + commands.update(token for token in tokens if token and not token.startswith("-")) + return commands + + +def _native_tool_identities() -> dict[str, dict[str, object]]: + identities: dict[str, dict[str, object]] = {} + for command in sorted(_configured_compilers()): + executable = shutil.which(command) + if executable is None: + continue + result = subprocess.run( # noqa: S603 - commands are resolved compiler executables, not shell input. + [executable, "--version"], + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + timeout=10, + ) + identities[command] = { + "output": result.stdout.strip(), + "returncode": result.returncode, + } + return identities + + def _scm_environment(project: str) -> dict[str, str]: distribution = SCM_DISTRIBUTION[project] distribution_variables = tuple(name.format(distribution=distribution) for name in SCM_DISTRIBUTION_VARIABLES) @@ -103,6 +144,7 @@ def _scm_identity(project: str) -> dict[str, object]: def fingerprint(project: str, lane: str) -> str: + python_tools = PYTHON_TOOLS + (TEST_ASSET_PYTHON_TOOLS if lane == "test-assets" else ()) payload: dict[str, object] = { "lane": lane, "project": project, @@ -111,7 +153,7 @@ def fingerprint(project: str, lane: str) -> str: "soabi": sysconfig.get_config_var("SOABI") or "", "version": platform.python_version(), }, - "python_tools": {name: _distribution_version(name) for name in PYTHON_TOOLS}, + "python_tools": {name: _distribution_version(name) for name in python_tools}, "scm": _scm_identity(project), } if lane != "portable": @@ -121,6 +163,8 @@ def fingerprint(project: str, lane: str) -> str: "platform": {"machine": platform.machine(), "system": platform.system()}, } ) + if lane == "test-assets": + payload["native_tools"] = _native_tool_identities() encoded = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode() return hashlib.sha256(encoded).hexdigest() diff --git a/ci/tools/tests/test_moon_ci.py b/ci/tools/tests/test_moon_ci.py index 1541d7f1acc..90eddbfe22f 100644 --- a/ci/tools/tests/test_moon_ci.py +++ b/ci/tools/tests/test_moon_ci.py @@ -2,20 +2,29 @@ # # SPDX-License-Identifier: Apache-2.0 -# These tests intentionally use stdlib unittest so the cheap CI planner job has -# no third-party Python dependency. +# These tests intentionally use stdlib unittest so Moon's contract task does +# not need a separately managed Python test environment. # ruff: noqa: PT009, PT027 from __future__ import annotations +import sys import tempfile import unittest from argparse import Namespace from pathlib import Path from unittest.mock import patch -from ci.tools.moon_ci import _gate, _output_path -from ci.tools.moon_fingerprint import _scm_identity, fingerprint +from ci.tools.moon_ci import ( + _bindings_benchmark_smoke, + _docs_arguments, + _installed_test, + _metapackage_install_test, + _output_path, + _pixi_test, + _prepare_pathfinder_strict, +) +from ci.tools.moon_fingerprint import _native_tool_identities, _scm_identity, fingerprint class MoonCIOutputPathTest(unittest.TestCase): @@ -28,7 +37,7 @@ def setUp(self) -> None: patch("ci.tools.moon_ci.REPO_ROOT", self.repo), patch.dict( "ci.tools.moon_ci.PROJECT_PATHS", - {"pathfinder": Path("project"), "ci": Path("ci")}, + {"pathfinder": Path("project")}, clear=True, ), ) @@ -60,14 +69,16 @@ def test_rejects_projects_outside_the_workspace(self) -> None: ): _output_path("pathfinder", "wheel") - def test_gate_writes_only_a_declared_marker(self) -> None: - (self.repo / "ci").mkdir() - _gate(Namespace(marker="build-linux-64")) - - marker = self.repo / "ci" / ".moon-out" / "ci-gates" / "build-linux-64" - self.assertEqual(marker.read_text(encoding="utf-8"), "true\n") - with self.assertRaisesRegex(ValueError, "unknown CI gate marker"): - _gate(Namespace(marker="anything-else")) + def test_docs_latest_only_defaults_to_enabled_and_validates_values(self) -> None: + with patch.dict("os.environ", {}, clear=True): + self.assertEqual(_docs_arguments(), ["latest-only"]) + with patch.dict("os.environ", {"CUDA_PYTHON_DOCS_LATEST_ONLY": "false"}, clear=True): + self.assertEqual(_docs_arguments(), []) + with ( + patch.dict("os.environ", {"CUDA_PYTHON_DOCS_LATEST_ONLY": "sometimes"}, clear=True), + self.assertRaisesRegex(ValueError, "must be true"), + ): + _docs_arguments() class MoonFingerprintTest(unittest.TestCase): @@ -112,6 +123,113 @@ def test_reproducibility_environment_changes_fingerprint(self, git_describe) -> self.assertNotEqual(first, second) self.assertEqual(git_describe.call_count, 2) + @patch("ci.tools.moon_fingerprint._git_describe", return_value="v13.2.0-1-gabc") + @patch("ci.tools.moon_fingerprint._native_tool_identities") + def test_test_asset_fingerprint_tracks_resolved_build_tools(self, native_tools, git_describe) -> None: + versions = {"Cython": "3.1.0", "numpy": "2.3.0"} + + def distribution_version(name: str) -> str: + return versions.get(name, "fixed") + + native_tools.return_value = {"cc": {"output": "cc 1", "returncode": 0}} + with patch("ci.tools.moon_fingerprint._distribution_version", side_effect=distribution_version): + first = fingerprint("bindings", "test-assets") + versions["Cython"] = "3.1.1" + second = fingerprint("bindings", "test-assets") + native_tools.return_value = {"cc": {"output": "cc 2", "returncode": 0}} + third = fingerprint("bindings", "test-assets") + + self.assertNotEqual(first, second) + self.assertNotEqual(second, third) + self.assertEqual(git_describe.call_count, 3) + + @patch("ci.tools.moon_fingerprint.subprocess.run") + @patch("ci.tools.moon_fingerprint.shutil.which") + @patch("ci.tools.moon_fingerprint._configured_compilers", return_value={"cc", "missing"}) + def test_native_tool_identity_uses_resolved_executables(self, compilers, which, run) -> None: + which.side_effect = lambda command: "/tools/cc" if command == "cc" else None + run.return_value = Namespace(stdout="cc 1.2\n", returncode=0) + + self.assertEqual( + _native_tool_identities(), + {"cc": {"output": "cc 1.2", "returncode": 0}}, + ) + run.assert_called_once_with( + ["/tools/cc", "--version"], + check=False, + stdout=-1, + stderr=-2, + text=True, + timeout=10, + ) + compilers.assert_called_once_with() + + +class MoonCIConditionalTest(unittest.TestCase): + def test_local_pixi_test_forwards_the_selected_environment(self) -> None: + with ( + patch.dict("os.environ", {"PIXI_ENVIRONMENT_NAME": "cu12"}, clear=True), + patch("ci.tools.moon_ci.shutil.which", return_value="/tools/pixi"), + patch("ci.tools.moon_ci._run") as run, + ): + _pixi_test(Namespace(project="core")) + + command = run.call_args.args[0] + self.assertEqual(command[0], "/tools/pixi") + self.assertIn("cuda_core/pixi.toml", command[3]) + self.assertEqual(command[-3:], ["--environment", "cu12", "test"]) + + def test_declared_unsupported_bindings_lane_skips_benchmark_before_pixi_lookup(self) -> None: + with ( + patch.dict("os.environ", {"SKIP_CUDA_BINDINGS_TEST": "1"}, clear=True), + patch("ci.tools.moon_ci._artifact_wheel") as artifact_wheel, + ): + _bindings_benchmark_smoke(Namespace()) + + artifact_wheel.assert_not_called() + + def test_benchmark_smoke_uses_the_prepared_system_python(self) -> None: + pathfinder = Path("pathfinder.whl") + bindings = Path("bindings.whl") + with ( + patch.dict("os.environ", {}, clear=True), + patch("ci.tools.moon_ci._artifact_wheel", side_effect=(pathfinder, bindings)), + patch("ci.tools.moon_ci._run") as run, + ): + _bindings_benchmark_smoke(Namespace()) + + self.assertEqual( + run.call_args_list[0].args[0], + [sys.executable, "-m", "pip", "install", str(pathfinder), str(bindings), "pyperf"], + ) + self.assertEqual(run.call_args_list[1].args[0][0], sys.executable) + self.assertEqual(run.call_args_list[1].args[0][-1], "--debug-single-value") + + def test_declared_unsupported_bindings_lane_skips_before_artifact_lookup(self) -> None: + with ( + patch.dict("os.environ", {"SKIP_CUDA_BINDINGS_TEST": "1"}, clear=True), + patch("ci.tools.moon_ci._artifact_wheel") as artifact_wheel, + ): + _installed_test(Namespace(project="bindings")) + + artifact_wheel.assert_not_called() + + def test_non_main_bindings_lane_skips_metapackage_before_artifact_lookup(self) -> None: + with ( + patch.dict("os.environ", {"BINDINGS_SOURCE": "published"}, clear=True), + patch("ci.tools.moon_ci._artifact_wheel") as artifact_wheel, + ): + _metapackage_install_test(Namespace()) + + artifact_wheel.assert_not_called() + + def test_pathfinder_strict_preparation_requires_numeric_cuda_major(self) -> None: + with ( + patch.dict("os.environ", {"TEST_CUDA_MAJOR": "latest"}, clear=True), + self.assertRaisesRegex(RuntimeError, "numeric CUDA major"), + ): + _prepare_pathfinder_strict(Namespace()) + if __name__ == "__main__": unittest.main() diff --git a/ci/tools/tests/test_moon_workspace.py b/ci/tools/tests/test_moon_workspace.py index d5e3ff33505..4f4cbadf946 100644 --- a/ci/tools/tests/test_moon_workspace.py +++ b/ci/tools/tests/test_moon_workspace.py @@ -2,8 +2,8 @@ # # SPDX-License-Identifier: Apache-2.0 -# These tests intentionally use stdlib unittest so the allocation job has no -# third-party Python dependency. +# These tests intentionally use stdlib unittest so Moon's contract task does +# not need a separately managed Python test environment. # ruff: noqa: PT009 from __future__ import annotations @@ -27,41 +27,17 @@ "test-helpers": "cuda_python_test_helpers", "bindings-benchmarks": "benchmarks/cuda_bindings", } -GATE_MARKERS = { - "force-all", - "force-all-unowned", - "build-portable", - "build-linux-64", - "build-linux-aarch64", - "build-windows", - "test-sdist-linux", - "test-sdist-windows", - "test-linux", - "test-windows", - "docs", - "core-api", - "build-pathfinder", - "build-bindings", - "build-core", - "build-metapackage", - "test-pathfinder", - "test-bindings", - "test-core", - "test-metapackage", -} -TAG_TARGETS = { +EXECUTION_TAG_TARGETS = { "ci-wheel-pure": {"pathfinder:wheel-pure", "metapackage:wheel-pure"}, "ci-wheel-current": {"bindings:wheel-current", "core:wheel-current"}, - "ci-wheel-previous": {"core:wheel-previous"}, - "ci-wheel-merge": {"core:wheel-merge"}, - "ci-build-test-assets": { + "ci-build-cython-assets": { "bindings:cython-test-assets", "core:cython-test-assets", - "core:test-binaries", }, "ci-sdist": {"pathfinder:sdist", "bindings:sdist", "core:sdist", "metapackage:sdist"}, "ci-test-linux": { "pathfinder:test-installed-linux", + "pathfinder:prepare-strict-linux", "pathfinder:test-installed-linux-strict", "bindings:test-installed-linux", "core:test-installed-linux", @@ -70,13 +46,47 @@ }, "ci-test-windows": { "pathfinder:test-installed-windows", + "pathfinder:prepare-strict-windows", "pathfinder:test-installed-windows-strict", "bindings:test-installed-windows", "core:test-installed-windows", "metapackage:test-installed-windows", }, - "ci-docs": {"root:docs-ci"}, + "ci-docs": { + "pathfinder:docs-ci", + "bindings:docs-ci", + "core:docs-ci", + "metapackage:docs-ci", + "root:docs-ci", + }, + "ci-quality": { + "ci:quality-moon-contracts", + "core:quality-api-base", + "core:quality-api-release", + "bindings-benchmarks:unit-test", + }, +} +RUNNER_TAG_TARGETS = { + "runner-build-portable": EXECUTION_TAG_TARGETS["ci-wheel-pure"], + "runner-build-linux-64": { + "bindings:wheel-current", + "core:wheel-current", + "bindings:cython-test-assets", + "core:cython-test-assets", + "core:wheel-previous", + "core:test-binaries", + "core:wheel-merge", + }, + "runner-sdist-linux": EXECUTION_TAG_TARGETS["ci-sdist"], + "runner-sdist-windows": EXECUTION_TAG_TARGETS["ci-sdist"], + "runner-test-linux": EXECUTION_TAG_TARGETS["ci-test-linux"], + "runner-test-windows": EXECUTION_TAG_TARGETS["ci-test-windows"], + "runner-docs": EXECUTION_TAG_TARGETS["ci-docs"], + "runner-quality": EXECUTION_TAG_TARGETS["ci-quality"], } +RUNNER_TAG_TARGETS["runner-build-linux-aarch64"] = RUNNER_TAG_TARGETS["runner-build-linux-64"] +RUNNER_TAG_TARGETS["runner-build-windows"] = RUNNER_TAG_TARGETS["runner-build-linux-64"] + CACHED_OUTPUTS = { "pathfinder:wheel-pure": ".moon-out/wheel-pure", "pathfinder:sdist": ".moon-out/sdist", @@ -92,6 +102,7 @@ "metapackage:wheel-pure": ".moon-out/wheel-pure", "metapackage:sdist": ".moon-out/sdist", } +FINGERPRINTED_TARGETS = set(CACHED_OUTPUTS) class MoonWorkspaceContractTest(unittest.TestCase): @@ -117,34 +128,60 @@ def moon_json(cls, *arguments: str) -> Any: def test_projects_use_only_the_system_toolchain(self) -> None: projects = self.moon_json("projects", "--json") by_id = {project["id"]: project for project in projects} - self.assertEqual( - {project_id: project["source"] for project_id, project in by_id.items()}, - EXPECTED_PROJECTS, - ) + self.assertEqual({project_id: project["source"] for project_id, project in by_id.items()}, EXPECTED_PROJECTS) for project in by_id.values(): self.assertEqual(project["language"], "unknown") self.assertEqual(project["toolchains"], ["system"]) - def test_ci_gates_are_real_uncached_marker_tasks(self) -> None: - expected_targets = {f"ci:gate-{marker}" for marker in GATE_MARKERS} - tagged = {task["target"] for task in self.tasks if "ci-gate" in task.get("tags", [])} - self.assertEqual(tagged, expected_targets) - for marker in GATE_MARKERS: - task = self.by_target[f"ci:gate-{marker}"] - self.assertEqual(task["command"], "python") - self.assertEqual(task["args"], ["ci/tools/moon_ci.py", "gate", marker]) + def test_only_force_all_tasks_are_allocation_only(self) -> None: + self.assertFalse({task["target"] for task in self.tasks if "ci-gate" in task.get("tags", [])}) + forced = {task["target"] for task in self.tasks if "ci-force-all" in task.get("tags", [])} + self.assertEqual(forced, {"ci:force-all", "ci:force-all-unowned"}) + for target in forced: + task = self.by_target[target] + self.assertEqual(task["command"], "noop") self.assertFalse(task["options"]["cache"]) - self.assertFalse(task["options"]["internal"]) self.assertTrue(task["options"]["runInCI"]) - self.assertEqual(task["outputs"], [{"file": f".moon-out/ci-gates/{marker}"}]) + self.assertFalse(task.get("outputs")) - def test_ci_tags_select_the_intended_real_tasks(self) -> None: - for tag, expected in TAG_TARGETS.items(): + def test_benchmark_inputs_are_owned_without_hiding_new_paths(self) -> None: + def affected(path: str) -> set[str]: + result = subprocess.run( # noqa: S603 - the binary is explicitly selected in setUpClass. + [ + self.moon, + "query", + "tasks", + "--affected", + "stdin", + "--upstream", + "none", + "--downstream", + "deep", + ], + cwd=REPO_ROOT, + check=True, + input=path, + text=True, + stdout=subprocess.PIPE, + ) + queried = json.loads(result.stdout) + return {task["target"] for project in queried["tasks"].values() for task in project.values()} + + self.assertIn( + "bindings-benchmarks:unit-test", + affected("benchmarks/cuda_bindings/tests/test_runner.py"), + ) + self.assertIn( + "ci:force-all-unowned", + affected("benchmarks/cuda_bindings/new_helper.py"), + ) + + def test_execution_and_runner_tags_select_real_tasks(self) -> None: + for tag, expected in {**EXECUTION_TAG_TARGETS, **RUNNER_TAG_TARGETS}.items(): selected = {task["target"] for task in self.tasks if tag in task.get("tags", [])} self.assertEqual(selected, expected, tag) for target in selected: - self.assertNotEqual(self.by_target[target]["command"], "noop") - self.assertTrue(self.by_target[target]["options"]["runInCI"]) + self.assertTrue(self.by_target[target]["options"]["runInCI"], target) def test_cached_producers_have_explicit_non_overlapping_outputs(self) -> None: cached = {task["target"] for task in self.tasks if task["options"]["cache"]} @@ -154,201 +191,123 @@ def test_cached_producers_have_explicit_non_overlapping_outputs(self) -> None: task = self.by_target[target] self.assertEqual(task["outputs"], [{"file": output}]) self.assertTrue(task.get("inputs")) - self.assertTrue(task.get("checks")) - self.assertNotIn("CUDA_PYTHON_TOOL_VERSIONS", task.get("env") or {}) - self.assertIn({"file": "/ci/tools/moon_fingerprint.py"}, task["inputs"]) project = target.split(":", maxsplit=1)[0] self.assertNotIn((project, output), destinations) destinations.add((project, output)) + for target in FINGERPRINTED_TARGETS: + task = self.by_target[target] + self.assertTrue(task.get("checks"), target) + self.assertIn({"file": "/ci/tools/moon_fingerprint.py"}, task["inputs"]) - def test_tests_and_docs_are_uncached(self) -> None: - ci_test_targets = set().union( - TAG_TARGETS["ci-test-linux"], TAG_TARGETS["ci-test-windows"], TAG_TARGETS["ci-docs"] - ) - for target in ci_test_targets: - self.assertFalse(self.by_target[target]["options"]["cache"]) - - def test_cross_runner_tasks_do_not_execute_producer_dependencies(self) -> None: - for tag in ( - "ci-test-linux", - "ci-test-windows", - "ci-build-test-assets", - "ci-wheel-current", - "ci-wheel-previous", - ): - for target in TAG_TARGETS[tag]: - self.assertFalse(self.by_target[target].get("deps"), target) - self.assertFalse(self.by_target["core:wheel-merge"].get("deps")) - - def test_native_producers_hash_downloaded_wheel_bytes(self) -> None: - required_globs = { - "bindings:wheel-current": {"/cuda_pathfinder/.moon-out/wheel-pure/*.whl"}, - "core:wheel-current": { - "/cuda_pathfinder/.moon-out/wheel-pure/*.whl", - "/cuda_bindings/.moon-out/wheel-current/*.whl", + def test_same_environment_build_dependencies_use_output_bytes(self) -> None: + expected = { + "core:wheel-current": {"bindings:wheel-current"}, + "bindings:sdist": {"pathfinder:sdist"}, + "core:sdist": {"pathfinder:sdist", "bindings:sdist"}, + "metapackage:sdist": {"bindings:sdist"}, + "root:docs-ci": { + "pathfinder:docs-ci", + "bindings:docs-ci", + "core:docs-ci", + "metapackage:docs-ci", }, - "core:wheel-previous": { - "/cuda_pathfinder/.moon-out/wheel-pure/*.whl", - "/cuda_bindings/.moon-out/wheel-previous/*.whl", - }, - } - for target, expected in required_globs.items(): - configured = { - item["glob"] for item in self.by_target[target]["inputs"] if isinstance(item, dict) and "glob" in item - } - self.assertTrue(expected.issubset(configured), target) - - def test_metapackage_install_smoke_tracks_runtime_inputs(self) -> None: - for target in ("metapackage:test-installed-linux", "metapackage:test-installed-windows"): - inputs = self.by_target[target]["inputs"] - self.assertIn({"project": "pathfinder", "group": "package"}, inputs) - self.assertIn({"project": "bindings", "group": "package"}, inputs) - self.assertIn({"project": "core", "group": "package"}, inputs) - input_globs = {item["glob"] for item in inputs if isinstance(item, dict) and "glob" in item} - self.assertIn("/cuda_core/.moon-out/wheel-merged/*.whl", input_globs) - self.assertIn( - {"project": "core", "group": "package"}, - self.by_target["ci:gate-test-metapackage"]["inputs"], - ) - - def test_cross_runner_producer_inputs_reach_exact_consumers(self) -> None: - portable_workflow = {"file": "/.github/workflows/build-pure-wheel.yml"} - portable_consumers = { - "bindings:wheel-current", - "core:wheel-current", - "core:wheel-previous", - "core:wheel-merge", - "bindings:cython-test-assets", - "core:cython-test-assets", - "pathfinder:test-installed-linux", - "pathfinder:test-installed-linux-strict", - "pathfinder:test-installed-windows", - "pathfinder:test-installed-windows-strict", - "bindings:test-installed-linux", - "bindings:test-installed-windows", - "core:test-installed-linux", - "core:test-installed-windows", - "metapackage:test-installed-linux", - "metapackage:test-installed-windows", } - for target in portable_consumers: - self.assertIn(portable_workflow, self.by_target[target]["inputs"], target) + for target, dependencies in expected.items(): + configured = {dep["target"] for dep in self.by_target[target]["deps"]} + self.assertEqual(configured, dependencies, target) + self.assertTrue(all(dep["cacheStrategy"] == "outputs" for dep in self.by_target[target]["deps"]), target) - native_workflow = {"file": "/.github/workflows/build-wheel.yml"} - native_test_consumers = { - "bindings:test-installed-linux", - "bindings:test-installed-windows", - "core:test-installed-linux", - "core:test-installed-windows", - "metapackage:test-installed-linux", - "metapackage:test-installed-windows", - } - for target in native_test_consumers: - self.assertIn(native_workflow, self.by_target[target]["inputs"], target) + def test_native_asset_preparation_is_shared(self) -> None: + prep = self.by_target["test-helpers:prepare-test-assets"] + self.assertFalse(prep["options"]["cache"]) + for target in ("bindings:cython-test-assets", "core:cython-test-assets"): + deps = {dep["target"] for dep in self.by_target[target]["deps"]} + self.assertEqual(deps, {"test-helpers:prepare-test-assets"}) - merge_helper = {"file": "/ci/tools/merge_cuda_core_wheels.py"} - merge_test_consumers = { - "core:test-installed-linux", - "core:test-installed-windows", - "metapackage:test-installed-linux", - "metapackage:test-installed-windows", - } - for target in merge_test_consumers: - self.assertIn(merge_helper, self.by_target[target]["inputs"], target) + def test_platform_test_tasks_are_serialized_and_os_scoped(self) -> None: + for tag, operating_system in (("ci-test-linux", "linux"), ("ci-test-windows", "windows")): + for target in EXECUTION_TAG_TARGETS[tag]: + options = self.by_target[target]["options"] + self.assertEqual(options.get("mutex"), "ci-python-gpu", target) + self.assertEqual(options.get("os"), [operating_system], target) - def test_cross_runner_producer_inputs_reach_matching_gates(self) -> None: - gate_expectations = { - "@group(build-portable)": { - "gate-build-portable", - "gate-build-linux-64", - "gate-build-linux-aarch64", - "gate-build-windows", - "gate-build-pathfinder", - "gate-build-bindings", - "gate-build-core", - "gate-build-metapackage", - "gate-test-linux", - "gate-test-windows", - "gate-test-pathfinder", - "gate-test-bindings", - "gate-test-core", - "gate-test-metapackage", - }, - "@group(build-native-common)": { - "gate-build-linux-64", - "gate-build-linux-aarch64", - "gate-build-windows", - "gate-build-bindings", - "gate-build-core", - "gate-test-linux", - "gate-test-windows", - "gate-test-bindings", - "gate-test-core", - "gate-test-metapackage", - }, - "@group(build-native-core)": { - "gate-build-linux-64", - "gate-build-linux-aarch64", - "gate-build-windows", - "gate-build-core", - "gate-test-linux", - "gate-test-windows", - "gate-test-core", - "gate-test-metapackage", - }, - } - for producer_group, expected_gates in gate_expectations.items(): - actual_gates = { - task["id"] - for task in self.tasks - if "ci-gate" in task.get("tags", []) and producer_group in task["inputs"] - } - self.assertEqual(actual_gates, expected_gates, producer_group) + # Package tests install their own prerequisites. Cross-package deps + # would force unaffected test suites to run merely to serialize the + # shared interpreter; the mutex provides that serialization instead. + for target in EXECUTION_TAG_TARGETS["ci-test-linux"] | EXECUTION_TAG_TARGETS["ci-test-windows"]: + if not target.startswith("pathfinder:"): + self.assertFalse(self.by_target[target].get("deps"), target) - def test_installed_test_runner_does_not_select_metapackage_smoke(self) -> None: - runner_group = "@group(test-library-runner)" - for gate in ("gate-test-pathfinder", "gate-test-bindings", "gate-test-core"): - self.assertIn(runner_group, self.by_target[f"ci:{gate}"]["inputs"]) - self.assertNotIn(runner_group, self.by_target["ci:gate-test-metapackage"]["inputs"]) + def test_pathfinder_strictness_and_preparation_are_in_the_graph(self) -> None: + for operating_system in ("linux", "windows"): + normal = self.by_target[f"pathfinder:test-installed-{operating_system}"] + prepare = self.by_target[f"pathfinder:prepare-strict-{operating_system}"] + strict = self.by_target[f"pathfinder:test-installed-{operating_system}-strict"] + self.assertEqual(normal["env"]["CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS"], "see_what_works") + self.assertEqual(strict["env"]["CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS"], "all_must_work") + self.assertEqual({dep["target"] for dep in prepare["deps"]}, {normal["target"]}) + self.assertEqual({dep["target"] for dep in strict["deps"]}, {prepare["target"]}) def test_platform_test_tasks_track_provider_setup(self) -> None: - linux_targets = TAG_TARGETS["ci-test-linux"] - windows_targets = TAG_TARGETS["ci-test-windows"] - for target in linux_targets: + for target in EXECUTION_TAG_TARGETS["ci-test-linux"]: inputs = self.by_target[target]["inputs"] - self.assertIn({"file": "/ci/tools/guess_latest.sh"}, inputs, target) - self.assertIn({"file": "/ci/tools/install_gpu_driver.sh"}, inputs, target) - for target in windows_targets: + if "bindings-benchmarks" not in target and "prepare-strict" not in target: + self.assertIn({"file": "/ci/tools/guess_latest.sh"}, inputs, target) + self.assertIn({"file": "/ci/tools/install_gpu_driver.sh"}, inputs, target) + for target in EXECUTION_TAG_TARGETS["ci-test-windows"]: inputs = self.by_target[target]["inputs"] - self.assertIn({"file": "/ci/tools/configure_driver_mode.ps1"}, inputs, target) - self.assertIn({"file": "/ci/tools/install_gpu_driver.ps1"}, inputs, target) - for target in ("bindings:test-installed-linux", "core:test-installed-linux"): - self.assertIn({"file": "/ci/tools/setup-sanitizer"}, self.by_target[target]["inputs"]) + if "prepare-strict" not in target: + self.assertIn({"file": "/ci/tools/configure_driver_mode.ps1"}, inputs, target) + self.assertIn({"file": "/ci/tools/install_gpu_driver.ps1"}, inputs, target) - def test_docs_gate_and_task_share_package_owned_groups(self) -> None: - docs = self.by_target["root:docs-ci"]["inputs"] - gate = self.by_target["ci:gate-docs"]["inputs"] - external_groups = [item for item in docs if isinstance(item, dict) and "project" in item] - for group in external_groups: - self.assertIn(group, gate) + def test_docs_components_run_in_parallel_before_assembly(self) -> None: + docs = self.by_target["root:docs-ci"] + self.assertFalse(docs["options"]["cache"]) + self.assertTrue(docs["options"]["runDepsInParallel"]) + self.assertEqual(docs["args"], ["ci/tools/moon_ci.py", "docs-assemble"]) + root_inputs = docs["inputs"] + self.assertIn({"project": "core", "group": "package"}, root_inputs) + self.assertIn({"project": "metapackage", "group": "docs"}, root_inputs) + self.assertIn({"file": "/.github/workflows/build-pure-wheel.yml"}, root_inputs) + self.assertIn({"file": "/.github/workflows/build-wheel.yml"}, root_inputs) + for target in EXECUTION_TAG_TARGETS["ci-docs"] - {"root:docs-ci"}: + task = self.by_target[target] + self.assertFalse(task["options"]["cache"]) + self.assertEqual(task["args"][:2], ["ci/tools/moon_ci.py", "docs-component"]) + self.assertIn({"file": "/cuda_python/docs/environment-docs.yml"}, task["inputs"]) + + metapackage_inputs = self.by_target["metapackage:docs-ci"]["inputs"] + for project in ( + "pathfinder", + "bindings", + "core", + ): + self.assertIn({"project": project, "group": "package"}, metapackage_inputs) - def test_core_merge_changes_materialize_all_core_wheel_phases(self) -> None: - merge_helper = {"file": "/ci/tools/merge_cuda_core_wheels.py"} - for target in ("core:wheel-current", "core:wheel-previous", "core:wheel-merge"): - self.assertIn(merge_helper, self.by_target[target]["inputs"]) + def test_quality_tasks_use_external_refs_and_one_selector(self) -> None: + release = self.by_target["core:quality-api-release"] + base = self.by_target["core:quality-api-base"] + self.assertIn("${CUDA_CORE_API_RELEASE_BASE}", release["args"]) + self.assertIn("${CUDA_CORE_API_MERGE_BASE}", base["args"]) + self.assertEqual(self.by_target["ci:quality-moon-contracts"]["args"][:2], ["-m", "unittest"]) + for target in (release, base, self.by_target["bindings-benchmarks:unit-test"]): + self.assertEqual(target["command"], "uvx") + self.assertIn("--no-managed-python", target["args"]) + self.assertIn("--no-python-downloads", target["args"]) def test_local_pixi_tasks_remain_available_and_skip_ci(self) -> None: + for target in ("pathfinder:test", "bindings:test", "core:test"): + task = self.by_target[target] + self.assertEqual(task["args"][:2], ["ci/tools/moon_ci.py", "pixi-test"]) + self.assertFalse(task["options"]["runInCI"]) for target in ( - "pathfinder:test", - "bindings:test", - "core:test", "pathfinder:docs", "bindings:docs", "core:docs", "bindings-benchmarks:bench", ): task = self.by_target[target] - self.assertIn("pixi-", " ".join(task.get("args", []))) + self.assertEqual(task["command"], "pixi") self.assertFalse(task["options"]["runInCI"]) def test_workspace_disables_python_and_dependency_management(self) -> None: diff --git a/cuda_bindings/moon.yml b/cuda_bindings/moon.yml index 296b0d32522..348f9d3c4ec 100644 --- a/cuda_bindings/moon.yml +++ b/cuda_bindings/moon.yml @@ -65,18 +65,24 @@ tasks: - check: fingerprint script: python ci/tools/moon_fingerprint.py bindings native hash: stdout - tags: [ci-wheel-current] + tags: + - ci-wheel-current + - runner-build-linux-64 + - runner-build-linux-aarch64 + - runner-build-windows type: build options: cache: true cacheKey: wheel-current-v2 + priority: critical runInCI: true sdist: command: python args: [ci/tools/moon_ci.py, sdist, bindings] deps: - - pathfinder:sdist + - target: pathfinder:sdist + cacheStrategy: outputs env: BUILD_CUDA_VER: '${BUILD_CUDA_VER}' CUDA_PATH: '${CUDA_PATH}' @@ -95,7 +101,7 @@ tasks: - check: fingerprint script: python ci/tools/moon_fingerprint.py bindings sdist hash: stdout - tags: [ci-sdist] + tags: [ci-sdist, runner-sdist-linux, runner-sdist-windows] type: build options: cache: true @@ -105,6 +111,8 @@ tasks: cython-test-assets: command: python args: [ci/tools/moon_ci.py, cython-test-assets, bindings] + deps: + - test-helpers:prepare-test-assets inputs: - '@group(package)' - 'tests/cython/**/*' @@ -121,11 +129,16 @@ tasks: - check: fingerprint script: python ci/tools/moon_fingerprint.py bindings test-assets hash: stdout - tags: [ci-build-test-assets] + tags: + - ci-build-cython-assets + - runner-build-linux-64 + - runner-build-linux-aarch64 + - runner-build-windows type: build options: cache: true cacheKey: cython-test-assets-v2 + os: [linux, windows] runInCI: true test: @@ -162,9 +175,11 @@ tasks: - '/ci/tools/install_gpu_driver.sh' - '/ci/tools/setup-sanitizer' - '/.github/workflows/test-wheel-linux.yml' - tags: [ci-test-linux] + tags: [ci-test-linux, runner-test-linux] type: test options: + mutex: ci-python-gpu + os: linux runInCI: true test-installed-windows: @@ -185,17 +200,39 @@ tasks: - '/ci/tools/configure_driver_mode.ps1' - '/ci/tools/install_gpu_driver.ps1' - '/.github/workflows/test-wheel-windows.yml' - tags: [ci-test-windows] + tags: [ci-test-windows, runner-test-windows] type: test options: + mutex: ci-python-gpu + os: windows runInCI: true docs: + command: pixi + args: [run, --manifest-path, cuda_bindings/pixi.toml, --environment, docs, build-docs] + inputs: + - '@group(package)' + - '@group(docs)' + - {project: pathfinder, group: package} + - {project: pathfinder, group: docs} + - '/.github/workflows/build-docs.yml' + + docs-ci: command: python - args: [ci/tools/moon_ci.py, pixi-docs, bindings] + args: [ci/tools/moon_ci.py, docs-component, bindings] + env: + CUDA_PYTHON_DOCS_LATEST_ONLY: '${CUDA_PYTHON_DOCS_LATEST_ONLY}' inputs: - '@group(package)' - '@group(docs)' - - '/cuda_pathfinder/**/*' + - {project: pathfinder, group: package} + - '/cuda_python/docs/environment-docs.yml' - '/ci/tools/moon_ci.py' - '/.github/workflows/build-docs.yml' + outputs: + - '.moon-out/docs-ci' + tags: [ci-docs, runner-docs] + type: build + options: + os: linux + runInCI: true diff --git a/cuda_core/moon.yml b/cuda_core/moon.yml index 43090601c1b..3e4cd96331b 100644 --- a/cuda_core/moon.yml +++ b/cuda_core/moon.yml @@ -47,6 +47,9 @@ tasks: wheel-current: command: python args: [ci/tools/moon_ci.py, native-wheel, core, --lane, current] + deps: + - target: bindings:wheel-current + cacheStrategy: outputs env: BUILD_CUDA_MAJOR: '${BUILD_CUDA_MAJOR}' BUILD_CUDA_VER: '${BUILD_CUDA_VER}' @@ -73,11 +76,16 @@ tasks: - check: fingerprint script: python ci/tools/moon_fingerprint.py core native hash: stdout - tags: [ci-wheel-current] + tags: + - ci-wheel-current + - runner-build-linux-64 + - runner-build-linux-aarch64 + - runner-build-windows type: build options: cache: true cacheKey: wheel-current-v2 + priority: critical runInCI: true wheel-previous: @@ -98,7 +106,6 @@ tasks: - '/ci/tools/moon_ci.py' - '/ci/tools/moon_fingerprint.py' - '/ci/tools/env-vars' - - '/ci/tools/merge_cuda_core_wheels.py' - '/ci/versions.yml' - '/.github/workflows/build-pure-wheel.yml' - '/.github/workflows/build-wheel.yml' @@ -108,16 +115,23 @@ tasks: - check: fingerprint script: python ci/tools/moon_fingerprint.py core previous hash: stdout - tags: [ci-wheel-previous] + tags: + - runner-build-linux-64 + - runner-build-linux-aarch64 + - runner-build-windows type: build options: cache: true cacheKey: wheel-previous-v2 + priority: critical runInCI: true wheel-merge: command: python args: [ci/tools/moon_ci.py, merge-core-wheels] + # Current and previous CUDA toolkits are provisioned outside Moon, so the + # two input wheels are deliberately staged in separate invocations before + # this task merges their declared outputs. inputs: - '@group(package)' - {project: pathfinder, group: package} @@ -135,7 +149,10 @@ tasks: - check: fingerprint script: python ci/tools/moon_fingerprint.py core native hash: stdout - tags: [ci-wheel-merge] + tags: + - runner-build-linux-64 + - runner-build-linux-aarch64 + - runner-build-windows type: build options: cache: true @@ -146,8 +163,10 @@ tasks: command: python args: [ci/tools/moon_ci.py, sdist, core] deps: - - pathfinder:sdist - - bindings:sdist + - target: pathfinder:sdist + cacheStrategy: outputs + - target: bindings:sdist + cacheStrategy: outputs env: BUILD_CUDA_MAJOR: '${BUILD_CUDA_MAJOR}' BUILD_CUDA_VER: '${BUILD_CUDA_VER}' @@ -168,7 +187,7 @@ tasks: - check: fingerprint script: python ci/tools/moon_fingerprint.py core sdist hash: stdout - tags: [ci-sdist] + tags: [ci-sdist, runner-sdist-linux, runner-sdist-windows] type: build options: cache: true @@ -178,6 +197,8 @@ tasks: cython-test-assets: command: python args: [ci/tools/moon_ci.py, cython-test-assets, core] + deps: + - test-helpers:prepare-test-assets inputs: - '@group(package)' - 'tests/cython/**/*' @@ -196,11 +217,16 @@ tasks: - check: fingerprint script: python ci/tools/moon_fingerprint.py core test-assets hash: stdout - tags: [ci-build-test-assets] + tags: + - ci-build-cython-assets + - runner-build-linux-64 + - runner-build-linux-aarch64 + - runner-build-windows type: build options: cache: true cacheKey: cython-test-assets-v2 + os: [linux, windows] runInCI: true test-binaries: @@ -224,11 +250,15 @@ tasks: - check: fingerprint script: python ci/tools/moon_fingerprint.py core test-assets hash: stdout - tags: [ci-build-test-assets] + tags: + - runner-build-linux-64 + - runner-build-linux-aarch64 + - runner-build-windows type: build options: cache: true cacheKey: test-binaries-v2 + os: [linux, windows] runInCI: true test: @@ -267,9 +297,11 @@ tasks: - '/ci/tools/install_gpu_driver.sh' - '/ci/tools/setup-sanitizer' - '/.github/workflows/test-wheel-linux.yml' - tags: [ci-test-linux] + tags: [ci-test-linux, runner-test-linux] type: test options: + mutex: ci-python-gpu + os: linux runInCI: true test-installed-windows: @@ -292,18 +324,94 @@ tasks: - '/ci/tools/configure_driver_mode.ps1' - '/ci/tools/install_gpu_driver.ps1' - '/.github/workflows/test-wheel-windows.yml' - tags: [ci-test-windows] + tags: [ci-test-windows, runner-test-windows] type: test options: + mutex: ci-python-gpu + os: windows runInCI: true docs: + command: pixi + args: [run, --manifest-path, cuda_core/pixi.toml, --environment, docs, docs-build] + inputs: + - '@group(package)' + - '@group(docs)' + - {project: pathfinder, group: package} + - {project: pathfinder, group: docs} + - {project: bindings, group: package} + - {project: bindings, group: docs} + - '/.github/workflows/build-docs.yml' + + docs-ci: command: python - args: [ci/tools/moon_ci.py, pixi-docs, core] + args: [ci/tools/moon_ci.py, docs-component, core] + env: + CUDA_PYTHON_DOCS_LATEST_ONLY: '${CUDA_PYTHON_DOCS_LATEST_ONLY}' inputs: - '@group(package)' - '@group(docs)' - - '/cuda_pathfinder/**/*' - - '/cuda_bindings/**/*' + - {project: pathfinder, group: package} + - {project: bindings, group: package} + - '/cuda_python/docs/environment-docs.yml' - '/ci/tools/moon_ci.py' - '/.github/workflows/build-docs.yml' + outputs: + - '.moon-out/docs-ci' + tags: [ci-docs, runner-docs] + type: build + options: + os: linux + runInCI: true + + quality-api-release: + command: uvx + args: + - --no-managed-python + - --no-python-downloads + - griffe + - check + - cuda.core + - --search + - cuda_core + - --find-stubs-packages + - --against + - '${CUDA_CORE_API_RELEASE_BASE}' + - --format + - github + env: + CUDA_CORE_API_RELEASE_BASE: '${CUDA_CORE_API_RELEASE_BASE}' + inputs: + - 'cuda/core/**/*' + - '/.github/actions/griffe-api-check/action.yml' + tags: [ci-quality, runner-quality] + type: test + options: + os: linux + runInCI: true + + quality-api-base: + command: uvx + args: + - --no-managed-python + - --no-python-downloads + - griffe + - check + - cuda.core + - --search + - cuda_core + - --find-stubs-packages + - --against + - '${CUDA_CORE_API_MERGE_BASE}' + - --format + - github + env: + CUDA_CORE_API_MERGE_BASE: '${CUDA_CORE_API_MERGE_BASE}' + inputs: + - 'cuda/core/**/*' + - '/.github/actions/griffe-api-check/action.yml' + tags: [ci-quality, runner-quality] + type: test + options: + os: linux + runInCI: true diff --git a/cuda_pathfinder/moon.yml b/cuda_pathfinder/moon.yml index 66455d2a04a..25c1c40875c 100644 --- a/cuda_pathfinder/moon.yml +++ b/cuda_pathfinder/moon.yml @@ -34,10 +34,7 @@ fileGroups: tasks: test: command: python - args: - - ci/tools/moon_ci.py - - pixi-test - - pathfinder + args: [ci/tools/moon_ci.py, pixi-test, pathfinder] inputs: - '@group(package)' - '@group(tests)' @@ -59,9 +56,34 @@ tasks: - '/ci/tools/guess_latest.sh' - '/ci/tools/install_gpu_driver.sh' - '/.github/workflows/test-wheel-linux.yml' - tags: [ci-test-linux] + env: + CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS: see_what_works + CUDA_PATHFINDER_TEST_FIND_NVIDIA_HEADERS_STRICTNESS: see_what_works + CUDA_PATHFINDER_TEST_FIND_NVIDIA_BITCODE_LIB_STRICTNESS: see_what_works + tags: [ci-test-linux, runner-test-linux] type: test options: + mutex: ci-python-gpu + os: linux + runInCI: true + + prepare-strict-linux: + command: python + args: [ci/tools/moon_ci.py, prepare-pathfinder-strict] + deps: + - pathfinder:test-installed-linux + env: + TEST_CUDA_MAJOR: '${TEST_CUDA_MAJOR}' + inputs: + - '@group(package)' + - '@group(tests)' + - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' + - '/ci/tools/moon_ci.py' + - '/.github/workflows/test-wheel-linux.yml' + tags: [ci-test-linux, runner-test-linux] + options: + mutex: ci-python-gpu + os: linux runInCI: true test-installed-linux-strict: @@ -79,9 +101,17 @@ tasks: - '/ci/tools/guess_latest.sh' - '/ci/tools/install_gpu_driver.sh' - '/.github/workflows/test-wheel-linux.yml' - tags: [ci-test-linux] + deps: + - pathfinder:prepare-strict-linux + env: + CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS: all_must_work + CUDA_PATHFINDER_TEST_FIND_NVIDIA_HEADERS_STRICTNESS: all_must_work + CUDA_PATHFINDER_TEST_FIND_NVIDIA_BITCODE_LIB_STRICTNESS: all_must_work + tags: [ci-test-linux, runner-test-linux] type: test options: + mutex: ci-python-gpu + os: linux runInCI: true test-installed-windows: @@ -99,9 +129,34 @@ tasks: - '/ci/tools/configure_driver_mode.ps1' - '/ci/tools/install_gpu_driver.ps1' - '/.github/workflows/test-wheel-windows.yml' - tags: [ci-test-windows] + env: + CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS: see_what_works + CUDA_PATHFINDER_TEST_FIND_NVIDIA_HEADERS_STRICTNESS: see_what_works + CUDA_PATHFINDER_TEST_FIND_NVIDIA_BITCODE_LIB_STRICTNESS: see_what_works + tags: [ci-test-windows, runner-test-windows] type: test options: + mutex: ci-python-gpu + os: windows + runInCI: true + + prepare-strict-windows: + command: python + args: [ci/tools/moon_ci.py, prepare-pathfinder-strict] + deps: + - pathfinder:test-installed-windows + env: + TEST_CUDA_MAJOR: '${TEST_CUDA_MAJOR}' + inputs: + - '@group(package)' + - '@group(tests)' + - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' + - '/ci/tools/moon_ci.py' + - '/.github/workflows/test-wheel-windows.yml' + tags: [ci-test-windows, runner-test-windows] + options: + mutex: ci-python-gpu + os: windows runInCI: true test-installed-windows-strict: @@ -119,19 +174,45 @@ tasks: - '/ci/tools/configure_driver_mode.ps1' - '/ci/tools/install_gpu_driver.ps1' - '/.github/workflows/test-wheel-windows.yml' - tags: [ci-test-windows] + deps: + - pathfinder:prepare-strict-windows + env: + CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS: all_must_work + CUDA_PATHFINDER_TEST_FIND_NVIDIA_HEADERS_STRICTNESS: all_must_work + CUDA_PATHFINDER_TEST_FIND_NVIDIA_BITCODE_LIB_STRICTNESS: all_must_work + tags: [ci-test-windows, runner-test-windows] type: test options: + mutex: ci-python-gpu + os: windows runInCI: true docs: + command: pixi + args: [run, --manifest-path, cuda_pathfinder/pixi.toml, --environment, docs, build-docs] + inputs: + - '@group(package)' + - '@group(docs)' + - '/.github/workflows/build-docs.yml' + + docs-ci: command: python - args: [ci/tools/moon_ci.py, pixi-docs, pathfinder] + args: [ci/tools/moon_ci.py, docs-component, pathfinder] + env: + CUDA_PYTHON_DOCS_LATEST_ONLY: '${CUDA_PYTHON_DOCS_LATEST_ONLY}' inputs: - '@group(package)' - '@group(docs)' + - '/cuda_python/docs/environment-docs.yml' - '/ci/tools/moon_ci.py' - '/.github/workflows/build-docs.yml' + outputs: + - '.moon-out/docs-ci' + tags: [ci-docs, runner-docs] + type: build + options: + os: linux + runInCI: true wheel-pure: command: python @@ -147,11 +228,12 @@ tasks: - check: fingerprint script: python ci/tools/moon_fingerprint.py pathfinder portable hash: stdout - tags: [ci-wheel-pure] + tags: [ci-wheel-pure, runner-build-portable] type: build options: cache: true cacheKey: wheel-pure-v2 + priority: critical runInCI: true sdist: @@ -169,7 +251,7 @@ tasks: - check: fingerprint script: python ci/tools/moon_fingerprint.py pathfinder sdist hash: stdout - tags: [ci-sdist] + tags: [ci-sdist, runner-sdist-linux, runner-sdist-windows] type: build options: cache: true diff --git a/cuda_python/moon.yml b/cuda_python/moon.yml index d1abf7bbfe7..64e35650a4f 100644 --- a/cuda_python/moon.yml +++ b/cuda_python/moon.yml @@ -43,16 +43,20 @@ tasks: - check: fingerprint script: python ci/tools/moon_fingerprint.py metapackage portable hash: stdout - tags: [ci-wheel-pure] + tags: [ci-wheel-pure, runner-build-portable] type: build options: cache: true cacheKey: wheel-pure-v2 + priority: critical runInCI: true sdist: command: python args: [ci/tools/moon_ci.py, sdist, metapackage] + deps: + - target: bindings:sdist + cacheStrategy: outputs inputs: - '@group(package)' - {project: bindings, group: package} @@ -66,7 +70,7 @@ tasks: - check: fingerprint script: python ci/tools/moon_fingerprint.py metapackage sdist hash: stdout - tags: [ci-sdist] + tags: [ci-sdist, runner-sdist-linux, runner-sdist-windows] type: build options: cache: true @@ -92,9 +96,11 @@ tasks: - '/ci/tools/guess_latest.sh' - '/ci/tools/install_gpu_driver.sh' - '/.github/workflows/test-wheel-linux.yml' - tags: [ci-test-linux] + tags: [ci-test-linux, runner-test-linux] type: test options: + mutex: ci-python-gpu + os: linux runInCI: true test-installed-windows: @@ -116,7 +122,31 @@ tasks: - '/ci/tools/configure_driver_mode.ps1' - '/ci/tools/install_gpu_driver.ps1' - '/.github/workflows/test-wheel-windows.yml' - tags: [ci-test-windows] + tags: [ci-test-windows, runner-test-windows] type: test options: + mutex: ci-python-gpu + os: windows + runInCI: true + + docs-ci: + command: python + args: [ci/tools/moon_ci.py, docs-component, metapackage] + env: + CUDA_PYTHON_DOCS_LATEST_ONLY: '${CUDA_PYTHON_DOCS_LATEST_ONLY}' + inputs: + - '@group(package)' + - '@group(docs)' + - {project: pathfinder, group: package} + - {project: bindings, group: package} + - {project: core, group: package} + - '/cuda_python/docs/environment-docs.yml' + - '/ci/tools/moon_ci.py' + - '/.github/workflows/build-docs.yml' + outputs: + - '.moon-out/docs-ci' + tags: [ci-docs, runner-docs] + type: build + options: + os: linux runInCI: true diff --git a/cuda_python_test_helpers/moon.yml b/cuda_python_test_helpers/moon.yml index 0ae9a55ffa5..8d448da3939 100644 --- a/cuda_python_test_helpers/moon.yml +++ b/cuda_python_test_helpers/moon.yml @@ -8,3 +8,25 @@ language: unknown layer: library toolchains: default: system + +taskOptions: + cache: false + runFromWorkspaceRoot: true + runInCI: false + +tasks: + prepare-test-assets: + command: python + args: [ci/tools/moon_ci.py, prepare-test-assets] + inputs: + - '/cuda_pathfinder/pyproject.toml' + - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' + - '/cuda_bindings/pyproject.toml' + - '/cuda_bindings/.moon-out/wheel-current/*.whl' + - '/cuda_core/pyproject.toml' + - '/cuda_core/.moon-out/wheel-current/*.whl' + - '/ci/tools/moon_ci.py' + - '/.github/workflows/build-wheel.yml' + options: + os: [linux, windows] + runInCI: true diff --git a/moon.yml b/moon.yml index 70a68374dd6..27fd879ff10 100644 --- a/moon.yml +++ b/moon.yml @@ -29,8 +29,6 @@ tasks: - bindings:test - core:test inputs: [] - options: - runDepsInParallel: false docs: deps: @@ -38,14 +36,19 @@ tasks: - bindings:docs - core:docs inputs: [] - options: - runDepsInParallel: false docs-ci: command: python - args: [ci/tools/moon_ci.py, docs-ci] - env: - CUDA_PYTHON_DOCS_LATEST_ONLY: '${CUDA_PYTHON_DOCS_LATEST_ONLY}' + args: [ci/tools/moon_ci.py, docs-assemble] + deps: + - target: pathfinder:docs-ci + cacheStrategy: outputs + - target: bindings:docs-ci + cacheStrategy: outputs + - target: core:docs-ci + cacheStrategy: outputs + - target: metapackage:docs-ci + cacheStrategy: outputs inputs: - {project: pathfinder, group: package} - {project: pathfinder, group: docs} @@ -55,12 +58,20 @@ tasks: - {project: core, group: docs} - {project: metapackage, group: package} - {project: metapackage, group: docs} + - '/cuda_pathfinder/.moon-out/docs-ci/**/*' + - '/cuda_bindings/.moon-out/docs-ci/**/*' + - '/cuda_core/.moon-out/docs-ci/**/*' + - '/cuda_python/.moon-out/docs-ci/**/*' - '/ci/tools/moon_ci.py' + - '/.github/workflows/build-pure-wheel.yml' + - '/.github/workflows/build-wheel.yml' - '/.github/workflows/build-docs.yml' outputs: - '.moon-out/docs' - tags: [ci-docs] + tags: [ci-docs, runner-docs] + type: build options: + os: linux runInCI: true pure-wheel: From a96b4dcba3ea81eb7cd20de45fb167758550a5fd Mon Sep 17 00:00:00 2001 From: Keith Kraus Date: Tue, 18 Aug 2026 14:15:40 -0400 Subject: [PATCH 3/6] ci: simplify Moon workspace configuration --- .moon/workspace.yml | 11 -- ci/moon.yml | 178 -------------------------- ci/tools/tests/test_moon_workspace.py | 13 +- moon.yml | 162 +++++++++++++++++++++++ 4 files changed, 170 insertions(+), 194 deletions(-) delete mode 100644 ci/moon.yml diff --git a/.moon/workspace.yml b/.moon/workspace.yml index eb5fce4662e..c265a23cb8e 100644 --- a/.moon/workspace.yml +++ b/.moon/workspace.yml @@ -8,7 +8,6 @@ versionConstraint: '=2.5.1' projects: root: '.' - ci: 'ci' pathfinder: 'cuda_pathfinder' bindings: 'cuda_bindings' core: 'cuda_core' @@ -17,12 +16,7 @@ projects: bindings-benchmarks: 'benchmarks/cuda_bindings' vcs: - client: git - provider: github defaultBranch: main - remoteCandidates: - - origin - - upstream pipeline: installDependencies: false @@ -33,9 +27,4 @@ cache: cas: verifyIntegrity: true -experiments: - asyncAffectedTracking: false - asyncGraphBuilding: false - nativeFileHashing: false - telemetry: false diff --git a/ci/moon.yml b/ci/moon.yml deleted file mode 100644 index bb659ac4e31..00000000000 --- a/ci/moon.yml +++ /dev/null @@ -1,178 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 - -$schema: https://moonrepo.dev/schemas/v2/project.json - -language: unknown -layer: automation -toolchains: - default: system - -taskOptions: - cache: false - internal: false - runFromWorkspaceRoot: true - runInCI: true - -# Runner allocation tags live on the real build and test tasks. This project -# only owns the two conservative catch-all inputs and the Moon contract test. -fileGroups: - orchestration: - - '/.moon/**/*' - - '/moon.yml' - - '/**/moon.yml' - - '/ci/moon.yml' - - '/ci/tools/moon_ci.py' - - '/ci/tools/moon_fingerprint.py' - - '/ci/tools/env-vars' - - '/ci/versions.yml' - - '/.github/actions/**/*' - - '/.github/workflows/ci.yml' - - '/.git_archival.txt' - - '/.gitattributes' - - '/pixi.toml' - - '/pixi.lock' - - '/pytest.ini' - - '/ruff.toml' - - # A new or otherwise unowned path must never silently suppress CI. Known - # source, test, docs, and platform inputs are excluded because their precise - # gates handle them; anything left forces the complete pipeline. - unowned: - - '/**/*' - - '!/.moon/cache/**/*' - - '!/.pixi/**/*' - - '!/**/.moon-out/**/*' - - '!/**/AGENTS.md' - - '!/**/CLAUDE.md' - - '!/README.md' - - '!/CONTRIBUTING.md' - - '!/SECURITY.md' - - '!/benchmarks/cuda_bindings/README.md' - - '!/benchmarks/cuda_core/README.md' - - '!/ci/ci-pipeline.svg' - - '!/cuda_bindings/README.md' - - '!/cuda_core/README.md' - - '!/cuda_python/README.md' - - '!/toolshed/README.md' - - '!/.agents/**/*' - - '!/.github/actions/**/*' - - '!/.github/workflows/ci.yml' - - '!/.github/workflows/build-docs.yml' - - '!/.github/workflows/build-pure-wheel.yml' - - '!/.github/workflows/build-wheel.yml' - - '!/.github/workflows/test-sdist-linux.yml' - - '!/.github/workflows/test-sdist-windows.yml' - - '!/.github/workflows/test-wheel-linux.yml' - - '!/.github/workflows/test-wheel-windows.yml' - - '!/.moon/**/*' - - '!/benchmarks/cuda_bindings/benchmarks/**/*' - - '!/benchmarks/cuda_bindings/runner/**/*' - - '!/benchmarks/cuda_bindings/tests/**/*' - - '!/benchmarks/cuda_bindings/compare.py' - - '!/benchmarks/cuda_bindings/pixi.lock' - - '!/benchmarks/cuda_bindings/pixi.toml' - - '!/benchmarks/cuda_bindings/run_cpp.py' - - '!/benchmarks/cuda_bindings/run_pyperf.py' - - '!/ci/moon.yml' - - '!/ci/test-matrix.yml' - - '!/ci/versions.yml' - - '!/ci/tools/configure_driver_mode.ps1' - - '!/ci/tools/env-vars' - - '!/ci/tools/guess_latest.sh' - - '!/ci/tools/install_gpu_driver.ps1' - - '!/ci/tools/install_gpu_driver.sh' - - '!/ci/tools/merge_cuda_core_wheels.py' - - '!/ci/tools/moon_ci.py' - - '!/ci/tools/moon_fingerprint.py' - - '!/ci/tools/run-tests' - - '!/ci/tools/setup-sanitizer' - - '!/cuda_bindings/cuda/**/*' - - '!/cuda_bindings/docs/**/*' - - '!/cuda_bindings/examples/**/*' - - '!/cuda_bindings/tests/**/*' - - '!/cuda_bindings/.git_archival.txt' - - '!/cuda_bindings/DESCRIPTION.rst' - - '!/cuda_bindings/LICENSE' - - '!/cuda_bindings/MANIFEST.in' - - '!/cuda_bindings/build_hooks.py' - - '!/cuda_bindings/moon.yml' - - '!/cuda_bindings/pixi.lock' - - '!/cuda_bindings/pixi.toml' - - '!/cuda_bindings/pyproject.toml' - - '!/cuda_bindings/setup.py' - - '!/cuda_core/cuda/**/*' - - '!/cuda_core/docs/**/*' - - '!/cuda_core/examples/**/*' - - '!/cuda_core/tests/**/*' - - '!/cuda_core/.git_archival.txt' - - '!/cuda_core/DESCRIPTION.rst' - - '!/cuda_core/LICENSE' - - '!/cuda_core/MANIFEST.in' - - '!/cuda_core/NOTICE' - - '!/cuda_core/build_hooks.py' - - '!/cuda_core/moon.yml' - - '!/cuda_core/pixi.lock' - - '!/cuda_core/pixi.toml' - - '!/cuda_core/pyproject.toml' - - '!/cuda_core/pytest.ini' - - '!/cuda_core/setup.py' - - '!/cuda_pathfinder/cuda/**/*' - - '!/cuda_pathfinder/docs/**/*' - - '!/cuda_pathfinder/examples/**/*' - - '!/cuda_pathfinder/tests/**/*' - - '!/cuda_pathfinder/.git_archival.txt' - - '!/cuda_pathfinder/DESCRIPTION.rst' - - '!/cuda_pathfinder/LICENSE' - - '!/cuda_pathfinder/moon.yml' - - '!/cuda_pathfinder/pixi.lock' - - '!/cuda_pathfinder/pixi.toml' - - '!/cuda_pathfinder/pyproject.toml' - - '!/cuda_python/docs/**/*' - - '!/cuda_python/DESCRIPTION.rst' - - '!/cuda_python/LICENSE' - - '!/cuda_python/moon.yml' - - '!/cuda_python/pyproject.toml' - - '!/cuda_python/setup.py' - - '!/cuda_python_test_helpers/**/*' - - '!/moon.yml' - - '!/pixi.lock' - - '!/pixi.toml' - - '!/pytest.ini' - - '!/ruff.toml' - - '!/tests/**/*' - - '!/.git_archival.txt' - - '!/.gitattributes' - - '!/.gitignore' - - '!/.pre-commit-config.yaml' - - '!/.spdx-ignore' - - '!/LICENSE' - -tasks: - # These two no-op tasks are the only allocation-only nodes. The planner - # inspects their tag and never executes them. - force-all: - inputs: - - '@group(orchestration)' - tags: [ci-force-all] - - force-all-unowned: - inputs: - - '@group(unowned)' - tags: [ci-force-all] - - quality-moon-contracts: - command: python - args: [-m, unittest, ci.tools.tests.test_moon_ci, ci.tools.tests.test_moon_workspace] - inputs: - - '/.moon/**/*' - - '/**/moon.yml' - - '/ci/tools/moon_ci.py' - - '/ci/tools/moon_fingerprint.py' - - '/ci/tools/tests/test_moon_ci.py' - - '/ci/tools/tests/test_moon_workspace.py' - tags: [ci-quality, runner-quality] - type: test - options: - os: linux diff --git a/ci/tools/tests/test_moon_workspace.py b/ci/tools/tests/test_moon_workspace.py index 4f4cbadf946..0c7b8d59e90 100644 --- a/ci/tools/tests/test_moon_workspace.py +++ b/ci/tools/tests/test_moon_workspace.py @@ -19,7 +19,6 @@ REPO_ROOT = Path(__file__).resolve().parents[3] EXPECTED_PROJECTS = { "root": ".", - "ci": "ci", "pathfinder": "cuda_pathfinder", "bindings": "cuda_bindings", "core": "cuda_core", @@ -60,7 +59,7 @@ "root:docs-ci", }, "ci-quality": { - "ci:quality-moon-contracts", + "root:quality-moon-contracts", "core:quality-api-base", "core:quality-api-release", "bindings-benchmarks:unit-test", @@ -136,7 +135,7 @@ def test_projects_use_only_the_system_toolchain(self) -> None: def test_only_force_all_tasks_are_allocation_only(self) -> None: self.assertFalse({task["target"] for task in self.tasks if "ci-gate" in task.get("tags", [])}) forced = {task["target"] for task in self.tasks if "ci-force-all" in task.get("tags", [])} - self.assertEqual(forced, {"ci:force-all", "ci:force-all-unowned"}) + self.assertEqual(forced, {"root:force-all", "root:force-all-unowned"}) for target in forced: task = self.by_target[target] self.assertEqual(task["command"], "noop") @@ -172,7 +171,7 @@ def affected(path: str) -> set[str]: affected("benchmarks/cuda_bindings/tests/test_runner.py"), ) self.assertIn( - "ci:force-all-unowned", + "root:force-all-unowned", affected("benchmarks/cuda_bindings/new_helper.py"), ) @@ -289,13 +288,15 @@ def test_quality_tasks_use_external_refs_and_one_selector(self) -> None: base = self.by_target["core:quality-api-base"] self.assertIn("${CUDA_CORE_API_RELEASE_BASE}", release["args"]) self.assertIn("${CUDA_CORE_API_MERGE_BASE}", base["args"]) - self.assertEqual(self.by_target["ci:quality-moon-contracts"]["args"][:2], ["-m", "unittest"]) + self.assertEqual(self.by_target["root:quality-moon-contracts"]["args"][:2], ["-m", "unittest"]) for target in (release, base, self.by_target["bindings-benchmarks:unit-test"]): self.assertEqual(target["command"], "uvx") self.assertIn("--no-managed-python", target["args"]) self.assertIn("--no-python-downloads", target["args"]) def test_local_pixi_tasks_remain_available_and_skip_ci(self) -> None: + for target in ("root:test", "root:docs", "root:pure-wheel"): + self.assertFalse(self.by_target[target]["options"]["runInCI"]) for target in ("pathfinder:test", "bindings:test", "core:test"): task = self.by_target[target] self.assertEqual(task["args"][:2], ["ci/tools/moon_ci.py", "pixi-test"]) @@ -317,6 +318,8 @@ def test_workspace_disables_python_and_dependency_management(self) -> None: self.assertIn("syncProjects: false", workspace) self.assertIn("syncWorkspace: false", workspace) self.assertIn("verifyIntegrity: true", workspace) + self.assertNotIn("experiments:", workspace) + self.assertNotIn("remoteCandidates:", workspace) self.assertFalse((REPO_ROOT / ".moon" / "toolchains.yml").exists()) def test_generated_cache_and_output_roots_are_ignored(self) -> None: diff --git a/moon.yml b/moon.yml index 27fd879ff10..08912f97e54 100644 --- a/moon.yml +++ b/moon.yml @@ -22,7 +22,169 @@ taskOptions: cache: false runInCI: false +fileGroups: + orchestration: + - '/.moon/**/*' + - '/moon.yml' + - '/**/moon.yml' + - '/ci/tools/moon_ci.py' + - '/ci/tools/moon_fingerprint.py' + - '/ci/tools/env-vars' + - '/ci/versions.yml' + - '/.github/actions/**/*' + - '/.github/workflows/ci.yml' + - '/.git_archival.txt' + - '/.gitattributes' + - '/pixi.toml' + - '/pixi.lock' + - '/pytest.ini' + - '/ruff.toml' + + # A new or otherwise unowned path must never silently suppress CI. Known + # source, test, docs, and platform inputs are excluded because their precise + # gates handle them; anything left forces the complete pipeline. + unowned: + - '/**/*' + - '!/.moon/cache/**/*' + - '!/.pixi/**/*' + - '!/**/.moon-out/**/*' + - '!/**/AGENTS.md' + - '!/**/CLAUDE.md' + - '!/README.md' + - '!/CONTRIBUTING.md' + - '!/SECURITY.md' + - '!/benchmarks/cuda_bindings/README.md' + - '!/benchmarks/cuda_core/README.md' + - '!/ci/ci-pipeline.svg' + - '!/cuda_bindings/README.md' + - '!/cuda_core/README.md' + - '!/cuda_python/README.md' + - '!/toolshed/README.md' + - '!/.agents/**/*' + - '!/.github/actions/**/*' + - '!/.github/workflows/ci.yml' + - '!/.github/workflows/build-docs.yml' + - '!/.github/workflows/build-pure-wheel.yml' + - '!/.github/workflows/build-wheel.yml' + - '!/.github/workflows/test-sdist-linux.yml' + - '!/.github/workflows/test-sdist-windows.yml' + - '!/.github/workflows/test-wheel-linux.yml' + - '!/.github/workflows/test-wheel-windows.yml' + - '!/.moon/**/*' + - '!/benchmarks/cuda_bindings/benchmarks/**/*' + - '!/benchmarks/cuda_bindings/runner/**/*' + - '!/benchmarks/cuda_bindings/tests/**/*' + - '!/benchmarks/cuda_bindings/compare.py' + - '!/benchmarks/cuda_bindings/pixi.lock' + - '!/benchmarks/cuda_bindings/pixi.toml' + - '!/benchmarks/cuda_bindings/run_cpp.py' + - '!/benchmarks/cuda_bindings/run_pyperf.py' + - '!/ci/test-matrix.yml' + - '!/ci/versions.yml' + - '!/ci/tools/configure_driver_mode.ps1' + - '!/ci/tools/env-vars' + - '!/ci/tools/guess_latest.sh' + - '!/ci/tools/install_gpu_driver.ps1' + - '!/ci/tools/install_gpu_driver.sh' + - '!/ci/tools/merge_cuda_core_wheels.py' + - '!/ci/tools/moon_ci.py' + - '!/ci/tools/moon_fingerprint.py' + - '!/ci/tools/run-tests' + - '!/ci/tools/setup-sanitizer' + - '!/cuda_bindings/cuda/**/*' + - '!/cuda_bindings/docs/**/*' + - '!/cuda_bindings/examples/**/*' + - '!/cuda_bindings/tests/**/*' + - '!/cuda_bindings/.git_archival.txt' + - '!/cuda_bindings/DESCRIPTION.rst' + - '!/cuda_bindings/LICENSE' + - '!/cuda_bindings/MANIFEST.in' + - '!/cuda_bindings/build_hooks.py' + - '!/cuda_bindings/moon.yml' + - '!/cuda_bindings/pixi.lock' + - '!/cuda_bindings/pixi.toml' + - '!/cuda_bindings/pyproject.toml' + - '!/cuda_bindings/setup.py' + - '!/cuda_core/cuda/**/*' + - '!/cuda_core/docs/**/*' + - '!/cuda_core/examples/**/*' + - '!/cuda_core/tests/**/*' + - '!/cuda_core/.git_archival.txt' + - '!/cuda_core/DESCRIPTION.rst' + - '!/cuda_core/LICENSE' + - '!/cuda_core/MANIFEST.in' + - '!/cuda_core/NOTICE' + - '!/cuda_core/build_hooks.py' + - '!/cuda_core/moon.yml' + - '!/cuda_core/pixi.lock' + - '!/cuda_core/pixi.toml' + - '!/cuda_core/pyproject.toml' + - '!/cuda_core/pytest.ini' + - '!/cuda_core/setup.py' + - '!/cuda_pathfinder/cuda/**/*' + - '!/cuda_pathfinder/docs/**/*' + - '!/cuda_pathfinder/examples/**/*' + - '!/cuda_pathfinder/tests/**/*' + - '!/cuda_pathfinder/.git_archival.txt' + - '!/cuda_pathfinder/DESCRIPTION.rst' + - '!/cuda_pathfinder/LICENSE' + - '!/cuda_pathfinder/moon.yml' + - '!/cuda_pathfinder/pixi.lock' + - '!/cuda_pathfinder/pixi.toml' + - '!/cuda_pathfinder/pyproject.toml' + - '!/cuda_python/docs/**/*' + - '!/cuda_python/DESCRIPTION.rst' + - '!/cuda_python/LICENSE' + - '!/cuda_python/moon.yml' + - '!/cuda_python/pyproject.toml' + - '!/cuda_python/setup.py' + - '!/cuda_python_test_helpers/**/*' + - '!/moon.yml' + - '!/pixi.lock' + - '!/pixi.toml' + - '!/pytest.ini' + - '!/ruff.toml' + - '!/tests/**/*' + - '!/.git_archival.txt' + - '!/.gitattributes' + - '!/.gitignore' + - '!/.pre-commit-config.yaml' + - '!/.spdx-ignore' + - '!/LICENSE' + tasks: + # These two no-op tasks are the only allocation-only nodes. The planner + # inspects their tag and never executes them. + force-all: + inputs: + - '@group(orchestration)' + tags: [ci-force-all] + options: + runInCI: true + + force-all-unowned: + inputs: + - '@group(unowned)' + tags: [ci-force-all] + options: + runInCI: true + + quality-moon-contracts: + command: python + args: [-m, unittest, ci.tools.tests.test_moon_ci, ci.tools.tests.test_moon_workspace] + inputs: + - '/.moon/**/*' + - '/**/moon.yml' + - '/ci/tools/moon_ci.py' + - '/ci/tools/moon_fingerprint.py' + - '/ci/tools/tests/test_moon_ci.py' + - '/ci/tools/tests/test_moon_workspace.py' + tags: [ci-quality, runner-quality] + type: test + options: + os: linux + runInCI: true + test: deps: - pathfinder:test From b98808efb183965c1122fe1ff59238a78bb970c4 Mon Sep 17 00:00:00 2001 From: Keith Kraus Date: Tue, 18 Aug 2026 14:59:35 -0400 Subject: [PATCH 4/6] ci: make Moon tasks project-native --- .moon/workspace.yml | 1 - benchmarks/cuda_bindings/moon.yml | 96 --- ci/tools/artifacts.py | 103 +++ ci/tools/build_artifacts.py | 177 ++++++ ci/tools/merge_cuda_core_wheels.py | 44 +- ci/tools/moon_ci.py | 586 ------------------ ci/tools/prepare_test_assets.py | 31 + ci/tools/run-tests | 117 +++- ci/tools/run_pixi_test.py | 50 ++ .../{test_moon_ci.py => test_moon_tasks.py} | 161 +++-- ci/tools/tests/test_moon_workspace.py | 93 ++- cuda_bindings/docs/build_docs.sh | 47 +- cuda_bindings/moon.yml | 131 +++- cuda_bindings/tests/cython/build_tests.py | 45 +- cuda_bindings/tests/cython/build_tests.sh | 2 +- cuda_core/docs/build_docs.sh | 44 +- cuda_core/moon.yml | 59 +- cuda_core/tests/cython/build_tests.py | 45 +- cuda_core/tests/cython/build_tests.sh | 4 +- .../test_binaries/build_test_binaries.py | 34 +- cuda_pathfinder/docs/build_docs.sh | 47 +- cuda_pathfinder/moon.yml | 83 ++- cuda_python/docs/assemble_moon_docs.sh | 42 ++ cuda_python/docs/build_docs.sh | 47 +- cuda_python/moon.yml | 27 +- cuda_python_test_helpers/moon.yml | 5 +- moon.yml | 28 +- 27 files changed, 1257 insertions(+), 892 deletions(-) delete mode 100644 benchmarks/cuda_bindings/moon.yml create mode 100644 ci/tools/artifacts.py create mode 100644 ci/tools/build_artifacts.py delete mode 100644 ci/tools/moon_ci.py create mode 100644 ci/tools/prepare_test_assets.py create mode 100755 ci/tools/run_pixi_test.py rename ci/tools/tests/{test_moon_ci.py => test_moon_tasks.py} (60%) create mode 100755 cuda_python/docs/assemble_moon_docs.sh diff --git a/.moon/workspace.yml b/.moon/workspace.yml index c265a23cb8e..f0127a6ef18 100644 --- a/.moon/workspace.yml +++ b/.moon/workspace.yml @@ -13,7 +13,6 @@ projects: core: 'cuda_core' metapackage: 'cuda_python' test-helpers: 'cuda_python_test_helpers' - bindings-benchmarks: 'benchmarks/cuda_bindings' vcs: defaultBranch: main diff --git a/benchmarks/cuda_bindings/moon.yml b/benchmarks/cuda_bindings/moon.yml deleted file mode 100644 index baee002fe98..00000000000 --- a/benchmarks/cuda_bindings/moon.yml +++ /dev/null @@ -1,96 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 - -$schema: https://moonrepo.dev/schemas/v2/project.json - -language: unknown -layer: tool -dependsOn: - - id: bindings - scope: development - - id: pathfinder - scope: development -toolchains: - default: system - -taskOptions: - cache: false - runFromWorkspaceRoot: true - runInCI: false - -fileGroups: - benchmarks: - - 'benchmarks/**/*' - - 'runner/**/*' - - 'compare.py' - - 'run_cpp.py' - - 'run_pyperf.py' - - 'pixi.toml' - - 'pixi.lock' - tests: - - 'runner/**/*' - - 'benchmarks/**/*' - - 'tests/**/*' - - 'pixi.toml' - - 'pixi.lock' - -tasks: - bench: - command: pixi - args: [run, --manifest-path, benchmarks/cuda_bindings/pixi.toml, --environment, source, bench] - inputs: - - '@group(benchmarks)' - - '/cuda_bindings/**/*' - - smoke: - command: pixi - args: - - run - - --manifest-path - - benchmarks/cuda_bindings/pixi.toml - - --environment - - source - - bench-smoke-test - inputs: - - '@group(benchmarks)' - - '/cuda_bindings/**/*' - - smoke-linux: - command: python - args: [ci/tools/moon_ci.py, bindings-benchmark-smoke] - inputs: - - '@group(benchmarks)' - - {project: bindings, group: package} - - {project: pathfinder, group: package} - - '/ci/tools/moon_ci.py' - - '/ci/test-matrix.yml' - - '/ci/tools/env-vars' - - '/ci/tools/guess_latest.sh' - - '/ci/tools/install_gpu_driver.sh' - - '/tests/**/*' - - '/.github/workflows/test-wheel-linux.yml' - tags: [ci-test-linux, runner-test-linux] - type: test - options: - mutex: ci-python-gpu - os: linux - runInCI: true - - unit-test: - command: uvx - args: - - --no-managed-python - - --no-python-downloads - - --from - - pytest - - pytest - - --noconftest - - benchmarks/cuda_bindings/tests - inputs: - - '@group(tests)' - tags: [ci-quality, runner-quality] - type: test - options: - os: linux - runInCI: true diff --git a/ci/tools/artifacts.py b/ci/tools/artifacts.py new file mode 100644 index 00000000000..98a274104fe --- /dev/null +++ b/ci/tools/artifacts.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Shared path and artifact helpers for Moon CI tasks.""" + +from __future__ import annotations + +import os +import shutil +import subprocess +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] +PROJECT_PATHS = { + "root": Path("."), + "pathfinder": Path("cuda_pathfinder"), + "bindings": Path("cuda_bindings"), + "core": Path("cuda_core"), + "metapackage": Path("cuda_python"), +} + + +def run(command: list[str], *, cwd: Path = REPO_ROOT, env: dict[str, str] | None = None) -> None: + print(f"+ {subprocess.list2cmdline(command)}", flush=True) + subprocess.run(command, cwd=cwd, env=env, check=True) # noqa: S603 + + +def project_path(project: str) -> Path: + try: + relative = PROJECT_PATHS[project] + except KeyError as error: + raise ValueError(f"unknown project: {project}") from error + return REPO_ROOT / relative + + +def output_path(project: str, directory: str) -> Path: + repo_root = Path(os.path.abspath(REPO_ROOT)) + project_root = Path(os.path.abspath(project_path(project))) + if project_root != repo_root and repo_root not in project_root.parents: + raise ValueError(f"project must be within {repo_root}: {project_root}") + output_root = project_root / ".moon-out" + output = Path(os.path.abspath(output_root / directory)) + if output != output_root and output_root not in output.parents: + raise ValueError(f"output must be within {output_root}: {output}") + current = output + while current != repo_root: + if current.is_symlink(): + raise ValueError(f"output path must not traverse a symlink: {current}") + current = current.parent + return output + + +def reset_output(output: Path) -> None: + if output.exists(): + if output.is_symlink() or not output.is_dir(): + raise ValueError(f"refusing to replace non-directory output: {output}") + shutil.rmtree(output) + output.mkdir(parents=True) + + +def find_one(directory: Path, pattern: str, description: str) -> Path: + selected = sorted(path for path in directory.glob(pattern) if path.is_file()) + if len(selected) != 1: + raise RuntimeError(f"expected one {description} in {directory}, found {len(selected)}") + return selected[0] + + +def find_one_in(directories: list[Path], pattern: str, description: str) -> Path: + for directory in directories: + selected = sorted(path for path in directory.glob(pattern) if path.is_file()) + if len(selected) == 1: + return selected[0] + if len(selected) > 1: + raise RuntimeError(f"expected one {description} in {directory}, found {len(selected)}") + searched = ", ".join(str(path) for path in directories) + raise RuntimeError(f"expected one {description}; searched {searched}") + + +def artifact_wheel(project: str, lane: str) -> Path: + if project == "pathfinder": + directories = [output_path(project, "wheel-pure"), project_path(project)] + elif project == "bindings": + environment = os.environ.get("CUDA_BINDINGS_ARTIFACTS_DIR") + directories = [output_path(project, f"wheel-{lane}")] + if lane == "previous": + directories.append(project_path(project) / "dist-prev") + elif environment: + directories.append(Path(environment)) + directories.append(project_path(project) / "dist") + elif project == "core": + environment = os.environ.get("CUDA_CORE_ARTIFACTS_DIR") + directories = [output_path(project, f"wheel-{lane}")] + if environment: + directories.append(Path(environment)) + directories.append(project_path(project) / "dist") + elif project == "metapackage": + directories = [output_path(project, "wheel-pure"), REPO_ROOT, project_path(project)] + else: + raise ValueError(f"project does not produce wheel artifacts: {project}") + return find_one_in(directories, "*.whl", f"{project} {lane} wheel") diff --git a/ci/tools/build_artifacts.py b/ci/tools/build_artifacts.py new file mode 100644 index 00000000000..635b8294ca3 --- /dev/null +++ b/ci/tools/build_artifacts.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Build cacheable Python artifacts declared by the Moon project graph.""" + +from __future__ import annotations + +import argparse +import os +import shutil +import sys +from pathlib import Path + +from ci.tools.artifacts import artifact_wheel, find_one, output_path, project_path, reset_output, run + +PACKAGE_PROJECTS = ("pathfinder", "bindings", "core", "metapackage") + + +def _cuda_major(lane: str) -> str: + variable = "BUILD_CUDA_MAJOR" if lane == "current" else "BUILD_PREV_CUDA_MAJOR" + value = os.environ.get(variable, "") + if value: + return value + if lane == "current": + version = os.environ.get("BUILD_CUDA_VER") or os.environ.get("CUDA_VER", "") + if version: + return version.split(".", maxsplit=1)[0] + raise RuntimeError(f"{variable} is required for the {lane} CUDA lane") + + +def _constraint_uri(path: Path, *, in_linux_container: bool) -> str: + resolved = path.resolve() + return f"file:///host{resolved.as_posix()}" if in_linux_container else resolved.as_uri() + + +def _constraint_environment( + project: str, + lane: str, + *, + cibuildwheel: bool, + from_sdist: bool = False, +) -> dict[str, str]: + if project not in {"bindings", "core"}: + return os.environ.copy() + + constraints = output_path(project, f"constraints-{lane}") + reset_output(constraints) + constraint_file = constraints / "build.txt" + linux_container = cibuildwheel and os.name != "nt" + pathfinder_wheel = ( + find_one(output_path("pathfinder", "sdist"), "*.whl", "cuda.pathfinder sdist wheel") + if from_sdist + else artifact_wheel("pathfinder", "pure") + ) + requirements = [("cuda-pathfinder", pathfinder_wheel)] + if project == "core": + bindings_wheel = ( + find_one(output_path("bindings", "sdist"), "*.whl", "cuda.bindings sdist wheel") + if from_sdist + else artifact_wheel("bindings", lane) + ) + requirements.append(("cuda-bindings", bindings_wheel)) + constraint_file.write_text( + "".join( + f"{distribution} @ {_constraint_uri(wheel, in_linux_container=linux_container)}\n" + for distribution, wheel in requirements + ), + encoding="utf-8", + ) + + environment = os.environ.copy() + host_constraint = str(constraint_file.resolve()) + environment["PIP_BUILD_CONSTRAINT"] = host_constraint + environment["PIP_CONSTRAINT"] = host_constraint + if project == "core": + environment["CUDA_CORE_BUILD_MAJOR"] = _cuda_major(lane) + if cibuildwheel: + setting = "CIBW_ENVIRONMENT_WINDOWS" if os.name == "nt" else "CIBW_ENVIRONMENT_LINUX" + container_constraint = f"/host{constraint_file.resolve().as_posix()}" if linux_container else host_constraint + additions = [ + f'PIP_BUILD_CONSTRAINT="{container_constraint}"', + f'PIP_CONSTRAINT="{container_constraint}"', + ] + if project == "core": + additions.append(f"CUDA_CORE_BUILD_MAJOR={_cuda_major(lane)}") + environment[setting] = " ".join(filter(None, [environment.get(setting, ""), *additions])) + return environment + + +def _ensure_owned(output: Path) -> None: + if os.name == "nt": + return + owners = {path.stat().st_uid for path in output.rglob("*")} + if not owners or owners == {os.getuid()}: + return + sudo = shutil.which("sudo") + if sudo is None: + raise RuntimeError(f"cibuildwheel output is not owned by this user and sudo was not found: {output}") + run([sudo, "chown", "-R", f"{os.getuid()}:{os.getgid()}", str(output)]) + + +def _pure_wheel(project: str) -> None: + if project not in {"pathfinder", "metapackage"}: + raise ValueError("pure-wheel only supports pathfinder and metapackage") + output = output_path(project, "wheel-pure") + reset_output(output) + run( + [sys.executable, "-m", "pip", "wheel", "--verbose", "--no-deps", "--wheel-dir", str(output), "."], + cwd=project_path(project), + ) + find_one(output, "*.whl", f"{project} wheel") + + +def _native_wheel(project: str, lane: str) -> None: + if project not in {"bindings", "core"}: + raise ValueError("native-wheel only supports bindings and core") + if project == "bindings" and lane != "current": + raise ValueError("cuda.bindings is only built in the current lane") + output = output_path(project, f"wheel-{lane}") + reset_output(output) + environment = _constraint_environment(project, lane, cibuildwheel=True) + run( + [sys.executable, "-m", "cibuildwheel", "--output-dir", str(output), str(project_path(project))], + env=environment, + ) + _ensure_owned(output) + wheel = find_one(output, "*.whl", f"{project} {lane} wheel") + if project == "core": + wheel.rename(wheel.with_name(f"{wheel.stem}.cu{_cuda_major(lane)}.whl")) + + +def _sdist(project: str) -> None: + project_root = project_path(project) + output = output_path(project, "sdist") + reset_output(output) + environment = ( + _constraint_environment(project, "current", cibuildwheel=False, from_sdist=True) + if project in {"bindings", "core"} + else os.environ.copy() + ) + run([sys.executable, "-m", "build", "--sdist", "--outdir", str(output), str(project_root)], env=environment) + archive = find_one(output, "*.tar.gz", f"{project} source distribution") + run( + [sys.executable, "-m", "pip", "wheel", "--no-deps", "--wheel-dir", str(output), str(archive)], + env=environment, + ) + find_one(output, "*.whl", f"{project} wheel from source distribution") + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + subparsers = parser.add_subparsers(dest="command", required=True) + pure = subparsers.add_parser("pure-wheel") + pure.add_argument("project", choices=("pathfinder", "metapackage")) + native = subparsers.add_parser("native-wheel") + native.add_argument("project", choices=("bindings", "core")) + native.add_argument("--lane", choices=("current", "previous"), required=True) + sdist = subparsers.add_parser("sdist") + sdist.add_argument("project", choices=PACKAGE_PROJECTS) + return parser + + +def main() -> None: + args = _parser().parse_args() + if args.command == "pure-wheel": + _pure_wheel(args.project) + elif args.command == "native-wheel": + _native_wheel(args.project, args.lane) + else: + _sdist(args.project) + + +if __name__ == "__main__": + main() diff --git a/ci/tools/merge_cuda_core_wheels.py b/ci/tools/merge_cuda_core_wheels.py index 23a8a21289f..008d812bd38 100644 --- a/ci/tools/merge_cuda_core_wheels.py +++ b/ci/tools/merge_cuda_core_wheels.py @@ -28,6 +28,22 @@ import zipfile from pathlib import Path +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def _validated_moon_output(path: Path) -> Path: + repo_root = Path(os.path.abspath(REPO_ROOT)) + output_root = repo_root / "cuda_core" / ".moon-out" + output = Path(os.path.abspath(path if path.is_absolute() else repo_root / path)) + if output_root not in output.parents: + raise ValueError(f"clean output must be below {output_root}: {output}") + current = output + while current != repo_root: + if current.is_symlink(): + raise ValueError(f"output path must not traverse a symlink: {current}") + current = current.parent + return output + def run_command(cmd: list[str], cwd: Path | None = None, env: dict = os.environ) -> subprocess.CompletedProcess: """Run a command with error handling.""" @@ -210,8 +226,15 @@ def merge_wheels(wheels: list[Path], output_dir: Path, show_wheel_contents: bool def main(): """Main merge script.""" parser = argparse.ArgumentParser(description="Merge CUDA-specific wheels into a single multi-CUDA wheel") - parser.add_argument("wheels", nargs="+", help="Paths to the CUDA-specific wheels to merge") + parser.add_argument("wheels", nargs="*", help="Paths to the CUDA-specific wheels to merge") + parser.add_argument( + "--wheel-dir", + action="append", + default=[], + help="Directory containing exactly one input wheel (may be repeated)", + ) parser.add_argument("--output-dir", "-o", default="dist", help="Output directory for merged wheel") + parser.add_argument("--clean-output", action="store_true", help="Remove the output directory before merging") args = parser.parse_args() @@ -230,11 +253,25 @@ def main(): sys.exit(1) wheels.append(wheel) + for directory_value in args.wheel_dir: + directory = Path(directory_value) + selected = sorted(path for path in directory.glob("*.whl") if path.is_file()) + if len(selected) != 1: + print(f"Error: Expected one wheel in {directory}, found {len(selected)}", file=sys.stderr) + sys.exit(1) + wheels.append(selected[0]) + if not wheels: print("Error: No wheels provided", file=sys.stderr) sys.exit(1) - output_dir = Path(args.output_dir) + output_dir = _validated_moon_output(Path(args.output_dir)) if args.clean_output else Path(args.output_dir) + if args.clean_output and output_dir.exists(): + if output_dir.is_symlink() or not output_dir.is_dir(): + print(f"Error: Refusing to replace non-directory output: {output_dir}", file=sys.stderr) + sys.exit(1) + shutil.rmtree(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) # Check that we have wheel tool available try: @@ -245,6 +282,9 @@ def main(): # Merge the wheels merged_wheel = merge_wheels(wheels, output_dir) + output_wheels = sorted(path for path in output_dir.glob("*.whl") if path.is_file()) + if len(output_wheels) != 1: + raise RuntimeError(f"expected one merged wheel in {output_dir}, found {len(output_wheels)}") print(f"\nMerge complete! Output: {merged_wheel}") diff --git a/ci/tools/moon_ci.py b/ci/tools/moon_ci.py deleted file mode 100644 index d575f0d8f9d..00000000000 --- a/ci/tools/moon_ci.py +++ /dev/null @@ -1,586 +0,0 @@ -#!/usr/bin/env python3 - -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 - -"""Cross-platform commands used by Moon's local and CI task graph. - -Moon intentionally uses the system toolchain for this repository. These -commands consume the Python environment prepared by a contributor or CI and -continue to delegate local development tasks to Pixi. -""" - -from __future__ import annotations - -import argparse -import os -import shutil -import subprocess -import sys -from pathlib import Path - -REPO_ROOT = Path(__file__).resolve().parents[2] -PROJECT_PATHS = { - "root": Path("."), - "pathfinder": Path("cuda_pathfinder"), - "bindings": Path("cuda_bindings"), - "core": Path("cuda_core"), - "metapackage": Path("cuda_python"), - "bindings-benchmarks": Path("benchmarks/cuda_bindings"), -} -PACKAGE_PROJECTS = ("pathfinder", "bindings", "core", "metapackage") -CYTHON_PROJECTS = ("bindings", "core") - - -def _run( - command: list[str], - *, - cwd: Path = REPO_ROOT, - env: dict[str, str] | None = None, -) -> None: - print(f"+ {subprocess.list2cmdline(command)}", flush=True) - subprocess.run(command, cwd=cwd, env=env, check=True) # noqa: S603 - - -def _project_path(project: str) -> Path: - try: - relative = PROJECT_PATHS[project] - except KeyError as error: - raise ValueError(f"unknown project: {project}") from error - return REPO_ROOT / relative - - -def _output_path(project: str, directory: str) -> Path: - repo_root = Path(os.path.abspath(REPO_ROOT)) - project_root = Path(os.path.abspath(_project_path(project))) - if project_root != repo_root and repo_root not in project_root.parents: - raise ValueError(f"project must be within {repo_root}: {project_root}") - output_root = project_root / ".moon-out" - output = Path(os.path.abspath(output_root / directory)) - if output != output_root and output_root not in output.parents: - raise ValueError(f"output must be within {output_root}: {output}") - current = output - while current != repo_root: - if current.is_symlink(): - raise ValueError(f"output path must not traverse a symlink: {current}") - current = current.parent - return output - - -def _reset_output(output: Path) -> None: - if output.exists(): - if output.is_symlink() or not output.is_dir(): - raise ValueError(f"refusing to replace non-directory output: {output}") - shutil.rmtree(output) - output.mkdir(parents=True) - - -def _find_one(directory: Path, pattern: str, description: str) -> Path: - selected = sorted(path for path in directory.glob(pattern) if path.is_file()) - if len(selected) != 1: - raise RuntimeError(f"expected one {description} in {directory}, found {len(selected)}") - return selected[0] - - -def _find_one_in(directories: list[Path], pattern: str, description: str) -> Path: - for directory in directories: - selected = sorted(path for path in directory.glob(pattern) if path.is_file()) - if len(selected) == 1: - return selected[0] - if len(selected) > 1: - raise RuntimeError(f"expected one {description} in {directory}, found {len(selected)}") - searched = ", ".join(str(path) for path in directories) - raise RuntimeError(f"expected one {description}; searched {searched}") - - -def _artifact_wheel(project: str, lane: str) -> Path: - if project == "pathfinder": - directories = [_output_path(project, "wheel-pure"), _project_path(project)] - elif project == "bindings": - environment = os.environ.get("CUDA_BINDINGS_ARTIFACTS_DIR") - directories = [_output_path(project, f"wheel-{lane}")] - if lane == "previous": - directories.append(_project_path(project) / "dist-prev") - elif environment: - directories.append(Path(environment)) - directories.append(_project_path(project) / "dist") - elif project == "core": - environment = os.environ.get("CUDA_CORE_ARTIFACTS_DIR") - directories = [_output_path(project, f"wheel-{lane}")] - if environment: - directories.append(Path(environment)) - directories.append(_project_path(project) / "dist") - elif project == "metapackage": - directories = [_output_path(project, "wheel-pure"), REPO_ROOT, _project_path(project)] - else: - raise ValueError(f"project does not produce wheel artifacts: {project}") - return _find_one_in(directories, "*.whl", f"{project} {lane} wheel") - - -def _copy_files(source: Path, output: Path, patterns: tuple[str, ...]) -> None: - selected = sorted({path for pattern in patterns for path in source.glob(pattern) if path.is_file()}) - if not selected: - raise RuntimeError(f"no matching files found in {source}") - _reset_output(output) - for source_path in selected: - shutil.copy2(source_path, output / source_path.name) - - -def _pure_wheel(args: argparse.Namespace) -> None: - if args.project not in {"pathfinder", "metapackage"}: - raise ValueError("pure-wheel only supports pathfinder and metapackage") - project_root = _project_path(args.project) - output = _output_path(args.project, "wheel-pure") - _reset_output(output) - _run( - [ - sys.executable, - "-m", - "pip", - "wheel", - "--verbose", - "--no-deps", - "--wheel-dir", - str(output), - ".", - ], - cwd=project_root, - ) - _find_one(output, "*.whl", f"{args.project} wheel") - - -def _cuda_major(lane: str) -> str: - variable = "BUILD_CUDA_MAJOR" if lane == "current" else "BUILD_PREV_CUDA_MAJOR" - value = os.environ.get(variable, "") - if value: - return value - if lane == "current": - version = os.environ.get("BUILD_CUDA_VER") or os.environ.get("CUDA_VER", "") - if version: - return version.split(".", maxsplit=1)[0] - raise RuntimeError(f"{variable} is required for the {lane} CUDA lane") - - -def _constraint_uri(path: Path, *, in_linux_container: bool) -> str: - resolved = path.resolve() - if in_linux_container: - return f"file:///host{resolved.as_posix()}" - return resolved.as_uri() - - -def _constraint_environment( - project: str, - lane: str, - *, - cibuildwheel: bool, - from_sdist: bool = False, -) -> dict[str, str]: - if project not in {"bindings", "core"}: - return os.environ.copy() - - constraints = _output_path(project, f"constraints-{lane}") - _reset_output(constraints) - constraint_file = constraints / "build.txt" - linux_container = cibuildwheel and os.name != "nt" - pathfinder_wheel = ( - _find_one(_output_path("pathfinder", "sdist"), "*.whl", "cuda.pathfinder sdist wheel") - if from_sdist - else _artifact_wheel("pathfinder", "pure") - ) - requirements = [("cuda-pathfinder", pathfinder_wheel)] - if project == "core": - bindings_wheel = ( - _find_one(_output_path("bindings", "sdist"), "*.whl", "cuda.bindings sdist wheel") - if from_sdist - else _artifact_wheel("bindings", lane) - ) - requirements.append( - ( - "cuda-bindings", - bindings_wheel, - ) - ) - constraint_file.write_text( - "".join( - f"{distribution} @ {_constraint_uri(wheel, in_linux_container=linux_container)}\n" - for distribution, wheel in requirements - ), - encoding="utf-8", - ) - - environment = os.environ.copy() - host_constraint = str(constraint_file.resolve()) - environment["PIP_BUILD_CONSTRAINT"] = host_constraint - environment["PIP_CONSTRAINT"] = host_constraint - if project == "core": - environment["CUDA_CORE_BUILD_MAJOR"] = _cuda_major(lane) - - if cibuildwheel: - setting = "CIBW_ENVIRONMENT_WINDOWS" if os.name == "nt" else "CIBW_ENVIRONMENT_LINUX" - container_constraint = f"/host{constraint_file.resolve().as_posix()}" if linux_container else host_constraint - additions = [ - f'PIP_BUILD_CONSTRAINT="{container_constraint}"', - f'PIP_CONSTRAINT="{container_constraint}"', - ] - if project == "core": - additions.append(f"CUDA_CORE_BUILD_MAJOR={_cuda_major(lane)}") - environment[setting] = " ".join(filter(None, [environment.get(setting, ""), *additions])) - return environment - - -def _ensure_owned(output: Path) -> None: - if os.name == "nt": - return - owners = {path.stat().st_uid for path in output.rglob("*")} - if not owners or owners == {os.getuid()}: - return - sudo = shutil.which("sudo") - if sudo is None: - raise RuntimeError(f"cibuildwheel output is not owned by this user and sudo was not found: {output}") - _run([sudo, "chown", "-R", f"{os.getuid()}:{os.getgid()}", str(output)]) - - -def _native_wheel(args: argparse.Namespace) -> None: - if args.project not in {"bindings", "core"}: - raise ValueError("native-wheel only supports bindings and core") - if args.project == "bindings" and args.lane != "current": - raise ValueError("cuda.bindings is only built in the current lane") - - output = _output_path(args.project, f"wheel-{args.lane}") - _reset_output(output) - environment = _constraint_environment(args.project, args.lane, cibuildwheel=True) - _run( - [ - sys.executable, - "-m", - "cibuildwheel", - "--output-dir", - str(output), - str(_project_path(args.project)), - ], - env=environment, - ) - _ensure_owned(output) - wheel = _find_one(output, "*.whl", f"{args.project} {args.lane} wheel") - if args.project == "core": - renamed = wheel.with_name(f"{wheel.stem}.cu{_cuda_major(args.lane)}.whl") - wheel.rename(renamed) - - -def _sdist(args: argparse.Namespace) -> None: - project_root = _project_path(args.project) - output = _output_path(args.project, "sdist") - _reset_output(output) - environment = ( - _constraint_environment(args.project, "current", cibuildwheel=False, from_sdist=True) - if args.project in {"bindings", "core"} - else os.environ.copy() - ) - _run( - [sys.executable, "-m", "build", "--sdist", "--outdir", str(output), str(project_root)], - env=environment, - ) - archive = _find_one(output, "*.tar.gz", f"{args.project} source distribution") - _run( - [ - sys.executable, - "-m", - "pip", - "wheel", - "--no-deps", - "--wheel-dir", - str(output), - str(archive), - ], - env=environment, - ) - _find_one(output, "*.whl", f"{args.project} wheel from source distribution") - - -def _merge_core_wheels(_args: argparse.Namespace) -> None: - current = _find_one(_output_path("core", "wheel-current"), "*.whl", "current cuda.core wheel") - previous = _find_one(_output_path("core", "wheel-previous"), "*.whl", "previous cuda.core wheel") - output = _output_path("core", "wheel-merged") - _reset_output(output) - _run( - [ - sys.executable, - str(REPO_ROOT / "ci" / "tools" / "merge_cuda_core_wheels.py"), - str(current), - str(previous), - "--output-dir", - str(output), - ] - ) - _find_one(output, "*.whl", "merged cuda.core wheel") - - -def _pixi_test(args: argparse.Namespace) -> None: - pixi = shutil.which("pixi") - if pixi is None: - raise RuntimeError("pixi is required for this task but was not found on PATH") - command = [pixi, "run", "--manifest-path", str(_project_path(args.project) / "pixi.toml")] - # A nested Pixi invocation otherwise falls back to the package's default - # environment instead of the cu12/cu13 environment selected at the root. - environment = os.environ.get("PIXI_ENVIRONMENT_NAME") - if environment: - command.extend(["--environment", environment]) - command.append("test") - _run(command) - - -def _docs_arguments() -> list[str]: - latest_only = (os.environ.get("CUDA_PYTHON_DOCS_LATEST_ONLY") or "true").lower() - if latest_only not in {"0", "1", "false", "true"}: - raise ValueError("CUDA_PYTHON_DOCS_LATEST_ONLY must be true, false, 1, or 0") - return ["latest-only"] if latest_only in {"1", "true"} else [] - - -def _docs_component(args: argparse.Namespace) -> None: - bash = shutil.which("bash") - if bash is None: - raise RuntimeError("bash is required to build documentation") - docs_root = _project_path(args.project) / "docs" - build = docs_root / "build" - if build.exists(): - if build.is_symlink() or not build.is_dir(): - raise ValueError(f"refusing to replace non-directory docs output: {build}") - shutil.rmtree(build) - _run([bash, "build_docs.sh", *_docs_arguments()], cwd=docs_root) - source = docs_root / "build" / "html" - if source.is_symlink() or not source.is_dir(): - raise RuntimeError(f"documentation output not found: {source}") - output = _output_path(args.project, "docs-ci") - _reset_output(output) - shutil.copytree(source, output, dirs_exist_ok=True) - - -def _docs_assemble(_args: argparse.Namespace) -> None: - output = _output_path("root", "docs") - _reset_output(output) - shutil.copytree(_output_path("metapackage", "docs-ci"), output, dirs_exist_ok=True) - for project, destination in ( - ("bindings", "cuda-bindings"), - ("core", "cuda-core"), - ("pathfinder", "cuda-pathfinder"), - ): - source = _output_path(project, "docs-ci") - if source.is_symlink() or not source.is_dir(): - raise RuntimeError(f"documentation component output not found: {source}") - shutil.copytree(source, output / destination) - - -def _prepare_test_assets(_args: argparse.Namespace) -> None: - wheels = [ - _artifact_wheel("pathfinder", "pure"), - _artifact_wheel("bindings", "current"), - _artifact_wheel("core", "current"), - ] - groups = [ - _project_path("bindings") / "pyproject.toml", - _project_path("core") / "pyproject.toml", - ] - command = [sys.executable, "-m", "pip", "install", *(str(wheel) for wheel in wheels)] - for pyproject in groups: - command.extend(["--group", f"{pyproject}:test"]) - _run(command) - - -def _cython_test_assets(args: argparse.Namespace) -> None: - source = _project_path(args.project) / "tests" / "cython" - bash = shutil.which("bash") - if bash is None: - raise RuntimeError("bash is required to build Cython test extensions") - _run([bash, "build_tests.sh"], cwd=source) - output = _output_path(args.project, "cython-tests") - _copy_files(source, output, ("test_*.so", "test_*.pyd", "test_*.dylib")) - - -def _prepare_pathfinder_strict(_args: argparse.Namespace) -> None: - cuda_major = os.environ.get("TEST_CUDA_MAJOR", "") - if not cuda_major.isdigit(): - raise RuntimeError("TEST_CUDA_MAJOR must be a numeric CUDA major version") - _run( - [ - sys.executable, - "-m", - "pip", - "install", - "--only-binary=:all:", - "--verbose", - str(_artifact_wheel("pathfinder", "pure")), - "--group", - f"{_project_path('pathfinder') / 'pyproject.toml'}:test-cu{cuda_major}", - ] - ) - _run([sys.executable, "-m", "pip", "list"]) - - -def _bindings_benchmark_smoke(_args: argparse.Namespace) -> None: - if os.environ.get("SKIP_CUDA_BINDINGS_TEST") == "1": - print("Skipping cuda.bindings benchmarks for this declared compatibility lane.", flush=True) - return - _run( - [ - sys.executable, - "-m", - "pip", - "install", - str(_artifact_wheel("pathfinder", "pure")), - str(_artifact_wheel("bindings", "current")), - "pyperf", - ] - ) - _run( - [ - sys.executable, - str(_project_path("bindings-benchmarks") / "run_pyperf.py"), - "--debug-single-value", - ], - cwd=_project_path("bindings-benchmarks"), - ) - - -def _core_test_binaries(_args: argparse.Namespace) -> None: - source = _project_path("core") / "tests" / "test_binaries" - _run([sys.executable, str(source / "build_test_binaries.py")]) - output = _output_path("core", "test-binaries") - _copy_files(source, output, ("*.o", "*.a", "*.lib")) - - -def _stage_files(source: Path, destination: Path, pattern: str) -> None: - files = sorted(path for path in source.glob(pattern) if path.is_file()) - if not files: - raise RuntimeError(f"no files matching {pattern} found in {source}") - destination.mkdir(parents=True, exist_ok=True) - for path in files: - shutil.copy2(path, destination / path.name) - - -def _installed_test(args: argparse.Namespace) -> None: - if args.project == "bindings" and os.environ.get("SKIP_CUDA_BINDINGS_TEST") == "1": - print("Skipping cuda.bindings tests for this declared compatibility lane.", flush=True) - return - pathfinder_wheel = _artifact_wheel("pathfinder", "pure") - if pathfinder_wheel.parent != _project_path("pathfinder"): - _stage_files(pathfinder_wheel.parent, _project_path("pathfinder"), pathfinder_wheel.name) - environment = os.environ.copy() - if args.project in {"bindings", "core"}: - environment.setdefault("CUDA_BINDINGS_ARTIFACTS_DIR", str(_output_path("bindings", "wheel-current"))) - _stage_files( - _output_path(args.project, "cython-tests"), - _project_path(args.project) / "tests" / "cython", - "test_*.*", - ) - if args.project == "core": - environment.setdefault("CUDA_CORE_ARTIFACTS_DIR", str(_output_path("core", "wheel-merged"))) - _stage_files( - _output_path("core", "test-binaries"), - _project_path("core") / "tests" / "test_binaries", - "*.*", - ) - bash = shutil.which("bash") - if bash is None: - raise RuntimeError("bash is required by ci/tools/run-tests but was not found on PATH") - _run([bash, str(REPO_ROOT / "ci" / "tools" / "run-tests"), args.project], env=environment) - - -def _metapackage_install_test(_args: argparse.Namespace) -> None: - if os.environ.get("BINDINGS_SOURCE") != "main": - print("Skipping the metapackage smoke test because BINDINGS_SOURCE is not main.", flush=True) - return - wheels = [ - _artifact_wheel("pathfinder", "pure"), - _artifact_wheel("bindings", "current"), - _artifact_wheel("core", "merged"), - ] - metapackage = _artifact_wheel("metapackage", "pure") - requirement = str(metapackage) - if os.environ.get("LOCAL_CTK", "1") != "1": - requirement += "[all]" - _run( - [ - sys.executable, - "-m", - "pip", - "install", - "--only-binary=:all:", - *(str(wheel) for wheel in wheels), - requirement, - ] - ) - - -def _parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser(description=__doc__) - subparsers = parser.add_subparsers(dest="command", required=True) - - pure_wheel = subparsers.add_parser("pure-wheel", help="build one pure-Python wheel") - pure_wheel.add_argument("project", choices=("pathfinder", "metapackage")) - pure_wheel.set_defaults(handler=_pure_wheel) - - native_wheel = subparsers.add_parser("native-wheel", help="build one cibuildwheel wheel") - native_wheel.add_argument("project", choices=("bindings", "core")) - native_wheel.add_argument("--lane", choices=("current", "previous"), required=True) - native_wheel.set_defaults(handler=_native_wheel) - - sdist = subparsers.add_parser("sdist", help="build an sdist and verify its wheel build") - sdist.add_argument("project", choices=PACKAGE_PROJECTS) - sdist.set_defaults(handler=_sdist) - - merge_wheels = subparsers.add_parser("merge-core-wheels", help="merge current and previous CUDA wheels") - merge_wheels.set_defaults(handler=_merge_core_wheels) - - pixi_test = subparsers.add_parser("pixi-test", help="run a package test in the caller-selected Pixi environment") - pixi_test.add_argument("project", choices=("pathfinder", "bindings", "core")) - pixi_test.set_defaults(handler=_pixi_test) - - docs_component = subparsers.add_parser("docs-component", help="build and stage one documentation component") - docs_component.add_argument("project", choices=PACKAGE_PROJECTS) - docs_component.set_defaults(handler=_docs_component) - - docs_assemble = subparsers.add_parser("docs-assemble", help="assemble staged documentation components") - docs_assemble.set_defaults(handler=_docs_assemble) - - prepare_assets = subparsers.add_parser( - "prepare-test-assets", help="install the shared inputs for native test-asset builds" - ) - prepare_assets.set_defaults(handler=_prepare_test_assets) - - cython_assets = subparsers.add_parser("cython-test-assets", help="build and stage Cython tests") - cython_assets.add_argument("project", choices=CYTHON_PROJECTS) - cython_assets.set_defaults(handler=_cython_test_assets) - - pathfinder_strict = subparsers.add_parser( - "prepare-pathfinder-strict", help="install CUDA-specific pathfinder test dependencies" - ) - pathfinder_strict.set_defaults(handler=_prepare_pathfinder_strict) - - benchmark_smoke = subparsers.add_parser( - "bindings-benchmark-smoke", help="run the bindings benchmark smoke test when the lane supports it" - ) - benchmark_smoke.set_defaults(handler=_bindings_benchmark_smoke) - - core_binaries = subparsers.add_parser("core-test-binaries", help="build and stage cuda.core test binaries") - core_binaries.set_defaults(handler=_core_test_binaries) - - installed_test = subparsers.add_parser("installed-test", help="run an installed-wheel package test suite") - installed_test.add_argument("project", choices=("pathfinder", "bindings", "core")) - installed_test.set_defaults(handler=_installed_test) - - metapackage_test = subparsers.add_parser( - "metapackage-install-test", help="verify the local cuda-python wheel set is installable" - ) - metapackage_test.set_defaults(handler=_metapackage_install_test) - - return parser - - -def main() -> None: - args = _parser().parse_args() - args.handler(args) - - -if __name__ == "__main__": - main() diff --git a/ci/tools/prepare_test_assets.py b/ci/tools/prepare_test_assets.py new file mode 100644 index 00000000000..fe6c28418e1 --- /dev/null +++ b/ci/tools/prepare_test_assets.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Install the wheel and dependency inputs used to build native test assets.""" + +from __future__ import annotations + +import argparse +import sys + +from ci.tools.artifacts import artifact_wheel, project_path, run + + +def main() -> None: + argparse.ArgumentParser(description=__doc__).parse_args() + wheels = [ + artifact_wheel("pathfinder", "pure"), + artifact_wheel("bindings", "current"), + artifact_wheel("core", "current"), + ] + command = [sys.executable, "-m", "pip", "install", *(str(wheel) for wheel in wheels)] + for project in ("bindings", "core"): + command.extend(["--group", f"{project_path(project) / 'pyproject.toml'}:test"]) + run(command) + + +if __name__ == "__main__": + main() diff --git a/ci/tools/run-tests b/ci/tools/run-tests index f9cc5a9e870..9c0a1e30af1 100755 --- a/ci/tools/run-tests +++ b/ci/tools/run-tests @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 @@ -13,23 +13,107 @@ if [[ ${#} -ne 1 ]]; then echo "Error: This script requires exactly 1 argument. You provided ${#}" exit 1 fi -if [[ "${1}" != "bindings" && "${1}" != "core" && "${1}" != "pathfinder" && "${1}" != nightly-* ]]; then - echo "Error: Invalid test module '${1}'. Must be 'bindings', 'core', 'pathfinder', or 'nightly-*'" +if [[ "${1}" != "bindings" && "${1}" != "core" && "${1}" != "pathfinder" && "${1}" != "metapackage" && "${1}" != nightly-* ]]; then + echo "Error: Invalid test module '${1}'. Must be 'bindings', 'core', 'pathfinder', 'metapackage', or 'nightly-*'" exit 1 fi test_module=${1} +repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + +select_one() { + local description=${1} + local result_name=${2} + shift 2 + local directory files + for directory in "$@"; do + [[ -z "${directory}" ]] && continue + shopt -s nullglob + files=("${directory}"/*.whl) + shopt -u nullglob + if [[ ${#files[@]} -eq 1 ]]; then + printf -v "${result_name}" '%s' "${files[0]}" + return + fi + if [[ ${#files[@]} -gt 1 ]]; then + echo "Error: Expected one ${description} in ${directory}, found ${#files[@]}" >&2 + exit 1 + fi + done + echo "Error: Expected one ${description}; searched $*" >&2 + exit 1 +} + +stage_generated() { + local source=${1} + local destination=${2} + shift 2 + local pattern selected=() stale=() path + for pattern in "$@"; do + shopt -s nullglob + for path in "${source}"/${pattern}; do + [[ -f "${path}" ]] && selected+=("${path}") + done + for path in "${destination}"/${pattern}; do + [[ -f "${path}" ]] && stale+=("${path}") + done + shopt -u nullglob + done + if [[ ${#selected[@]} -eq 0 ]]; then + echo "Error: No generated test files found in ${source}" >&2 + exit 1 + fi + mkdir -p "${destination}" + if [[ ${#stale[@]} -gt 0 ]]; then + rm -f -- "${stale[@]}" + fi + cp -- "${selected[@]}" "${destination}/" +} + +if [[ "${test_module}" == "bindings" && "${SKIP_CUDA_BINDINGS_TEST:-0}" == 1 ]]; then + echo "Skipping cuda.bindings tests for this declared compatibility lane." + exit 0 +fi + +if [[ "${test_module}" == "metapackage" ]]; then + if [[ "${BINDINGS_SOURCE:-}" != "main" ]]; then + echo "Skipping the metapackage smoke test because BINDINGS_SOURCE is not main." + exit 0 + fi + select_one "pathfinder wheel" PATHFINDER_WHL \ + "${repo_dir}/cuda_pathfinder/.moon-out/wheel-pure" "${repo_dir}/cuda_pathfinder" + select_one "bindings wheel" BINDINGS_WHL \ + "${repo_dir}/cuda_bindings/.moon-out/wheel-current" "${CUDA_BINDINGS_ARTIFACTS_DIR:-}" "${repo_dir}/cuda_bindings/dist" + select_one "merged core wheel" CORE_WHL \ + "${repo_dir}/cuda_core/.moon-out/wheel-merged" "${CUDA_CORE_ARTIFACTS_DIR:-}" "${repo_dir}/cuda_core/dist" + select_one "metapackage wheel" METAPACKAGE_WHL \ + "${repo_dir}/cuda_python/.moon-out/wheel-pure" "${repo_dir}" "${repo_dir}/cuda_python" + if [[ "${LOCAL_CTK:-1}" != 1 ]]; then + METAPACKAGE_WHL="${METAPACKAGE_WHL}[all]" + fi + python -m pip install --only-binary=:all: \ + "${PATHFINDER_WHL}" "${BINDINGS_WHL}" "${CORE_WHL}" "${METAPACKAGE_WHL}" + exit 0 +fi # For standard modes, install pathfinder up front (it is a direct dependency # of bindings, and a transitive dependency of core). Nightly modes install # all wheels together in a single pip call further below. if [[ "${test_module}" != nightly-* ]]; then + select_one "pathfinder wheel" PATHFINDER_WHL \ + "${repo_dir}/cuda_pathfinder/.moon-out/wheel-pure" "${repo_dir}/cuda_pathfinder" pushd ./cuda_pathfinder echo "Installing pathfinder wheel" - pip install ./*.whl --group test + pip install "${PATHFINDER_WHL}" --group test popd fi +if [[ "${test_module}" == "core" ]]; then + : "${CUDA_BINDINGS_ARTIFACTS_DIR:=${repo_dir}/cuda_bindings/.moon-out/wheel-current}" + : "${CUDA_CORE_ARTIFACTS_DIR:=${repo_dir}/cuda_core/.moon-out/wheel-merged}" + export CUDA_BINDINGS_ARTIFACTS_DIR CUDA_CORE_ARTIFACTS_DIR +fi + if [[ "${test_module}" == "pathfinder" ]]; then pushd ./cuda_pathfinder echo "Running pathfinder tests with " \ @@ -43,12 +127,19 @@ if [[ "${test_module}" == "pathfinder" ]]; then echo "Number of \"INFO test_\" lines: $line_count" popd elif [[ "${test_module}" == "bindings" ]]; then + : "${CUDA_BINDINGS_ARTIFACTS_DIR:=${repo_dir}/cuda_bindings/.moon-out/wheel-current}" + export CUDA_BINDINGS_ARTIFACTS_DIR + stage_generated \ + "${repo_dir}/cuda_bindings/.moon-out/cython-tests" \ + "${repo_dir}/cuda_bindings/tests/cython" \ + 'test_*.so' 'test_*.pyd' 'test_*.dylib' echo "Installing bindings wheel" pushd ./cuda_bindings + select_one "bindings wheel" BINDINGS_WHL "${CUDA_BINDINGS_ARTIFACTS_DIR}" if [[ "${LOCAL_CTK}" == 1 ]]; then - pip install "${CUDA_BINDINGS_ARTIFACTS_DIR}"/*.whl --group test + pip install "${BINDINGS_WHL}" --group test else - pip install $(ls "${CUDA_BINDINGS_ARTIFACTS_DIR}"/*.whl)[all] --group test + pip install "${BINDINGS_WHL}[all]" --group test fi echo "Running bindings tests" ${SANITIZER_CMD} pytest -rxXs -v --randomly-dont-reorganize tests/ @@ -73,7 +164,8 @@ elif [[ "${test_module}" == "core" || "${test_module}" == nightly-* ]]; then if [[ "${BINDINGS_SOURCE}" == "published" ]]; then BINDINGS_ARGS+=("cuda-bindings==${TEST_CUDA_MAJOR}.${TEST_CUDA_MINOR}.*") else - BINDINGS_ARGS=("${CUDA_BINDINGS_ARTIFACTS_DIR}"/*.whl) + select_one "bindings wheel" BINDINGS_WHL "${CUDA_BINDINGS_ARTIFACTS_DIR}" + BINDINGS_ARGS=("${BINDINGS_WHL}") if [[ "${LOCAL_CTK}" != 1 ]]; then BINDINGS_ARGS=("${BINDINGS_ARGS[0]}[all]") fi @@ -81,7 +173,8 @@ elif [[ "${test_module}" == "core" || "${test_module}" == nightly-* ]]; then # Resolve core wheel, adding the published cuda.bindings extra # when this job is resolving against wheel-installed CTK packages. - CORE_WHL=("${CUDA_CORE_ARTIFACTS_DIR}"/*.whl) + select_one "core wheel" CORE_WHL_PATH "${CUDA_CORE_ARTIFACTS_DIR}" + CORE_WHL=("${CORE_WHL_PATH}") if [[ "${LOCAL_CTK}" != 1 ]]; then CORE_WHL=("${CORE_WHL[0]}[cu${TEST_CUDA_MAJOR}]") fi @@ -94,6 +187,14 @@ elif [[ "${test_module}" == "core" || "${test_module}" == nightly-* ]]; then fi if [[ "${test_module}" == "core" ]]; then + stage_generated \ + "${repo_dir}/cuda_core/.moon-out/cython-tests" \ + "${repo_dir}/cuda_core/tests/cython" \ + 'test_*.so' 'test_*.pyd' 'test_*.dylib' + stage_generated \ + "${repo_dir}/cuda_core/.moon-out/test-binaries" \ + "${repo_dir}/cuda_core/tests/test_binaries" \ + '*.o' '*.a' '*.lib' # pushd so --group reads test dependency groups from cuda_core/pyproject.toml. pushd ./cuda_core echo "Installing bindings (source: ${BINDINGS_SOURCE})" diff --git a/ci/tools/run_pixi_test.py b/ci/tools/run_pixi_test.py new file mode 100755 index 00000000000..d41b86bda47 --- /dev/null +++ b/ci/tools/run_pixi_test.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Run a package test in the caller-selected Pixi environment.""" + +from __future__ import annotations + +import argparse +import os +import shutil +import subprocess +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] +PROJECT_PATHS = { + "pathfinder": Path("cuda_pathfinder"), + "bindings": Path("cuda_bindings"), + "core": Path("cuda_core"), +} + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("project", choices=PROJECT_PATHS) + args = parser.parse_args() + + pixi = shutil.which("pixi") + if pixi is None: + raise RuntimeError("pixi is required for this task but was not found on PATH") + + command = [ + pixi, + "run", + "--manifest-path", + str(REPO_ROOT / PROJECT_PATHS[args.project] / "pixi.toml"), + ] + environment = os.environ.get("PIXI_ENVIRONMENT_NAME") + if environment: + command.extend(["--environment", environment]) + command.append("test") + + print(f"+ {subprocess.list2cmdline(command)}", flush=True) + subprocess.run(command, cwd=REPO_ROOT, check=True) # noqa: S603 + + +if __name__ == "__main__": + main() diff --git a/ci/tools/tests/test_moon_ci.py b/ci/tools/tests/test_moon_tasks.py similarity index 60% rename from ci/tools/tests/test_moon_ci.py rename to ci/tools/tests/test_moon_tasks.py index 90eddbfe22f..296c606fda6 100644 --- a/ci/tools/tests/test_moon_ci.py +++ b/ci/tools/tests/test_moon_tasks.py @@ -2,12 +2,15 @@ # # SPDX-License-Identifier: Apache-2.0 -# These tests intentionally use stdlib unittest so Moon's contract task does -# not need a separately managed Python test environment. +# These task and helper tests intentionally use stdlib unittest so Moon's +# contract task does not need a separately managed Python test environment. # ruff: noqa: PT009, PT027 from __future__ import annotations +import os +import shutil +import subprocess import sys import tempfile import unittest @@ -15,28 +18,23 @@ from pathlib import Path from unittest.mock import patch -from ci.tools.moon_ci import ( - _bindings_benchmark_smoke, - _docs_arguments, - _installed_test, - _metapackage_install_test, - _output_path, - _pixi_test, - _prepare_pathfinder_strict, -) +from ci.tools.artifacts import output_path +from ci.tools.build_artifacts import _cuda_major +from ci.tools.merge_cuda_core_wheels import _validated_moon_output from ci.tools.moon_fingerprint import _native_tool_identities, _scm_identity, fingerprint +from ci.tools.run_pixi_test import main as run_pixi_test -class MoonCIOutputPathTest(unittest.TestCase): +class MoonArtifactOutputPathTest(unittest.TestCase): def setUp(self) -> None: self.temporary_directory = tempfile.TemporaryDirectory() self.addCleanup(self.temporary_directory.cleanup) self.repo = Path(self.temporary_directory.name) (self.repo / "project").mkdir() self.patches = ( - patch("ci.tools.moon_ci.REPO_ROOT", self.repo), + patch("ci.tools.artifacts.REPO_ROOT", self.repo), patch.dict( - "ci.tools.moon_ci.PROJECT_PATHS", + "ci.tools.artifacts.PROJECT_PATHS", {"pathfinder": Path("project")}, clear=True, ), @@ -46,13 +44,13 @@ def setUp(self) -> None: self.addCleanup(active_patch.stop) def test_confines_output_to_the_project_output_root(self) -> None: - output = _output_path("pathfinder", "wheel") + output = output_path("pathfinder", "wheel") self.assertEqual(output, self.repo / "project" / ".moon-out" / "wheel") with self.assertRaisesRegex(ValueError, "output must be within"): - _output_path("pathfinder", "../dist") + output_path("pathfinder", "../dist") with self.assertRaisesRegex(ValueError, "output must be within"): - _output_path("pathfinder", "../../outside") + output_path("pathfinder", "../../outside") def test_rejects_symlinked_output_ancestors(self) -> None: outside = self.repo / "outside" @@ -60,25 +58,31 @@ def test_rejects_symlinked_output_ancestors(self) -> None: (self.repo / "project" / ".moon-out").symlink_to(outside, target_is_directory=True) with self.assertRaisesRegex(ValueError, "must not traverse a symlink"): - _output_path("pathfinder", "wheel") + output_path("pathfinder", "wheel") def test_rejects_projects_outside_the_workspace(self) -> None: with ( - patch.dict("ci.tools.moon_ci.PROJECT_PATHS", {"pathfinder": Path("../outside")}), + patch.dict("ci.tools.artifacts.PROJECT_PATHS", {"pathfinder": Path("../outside")}), self.assertRaisesRegex(ValueError, "project must be within"), ): - _output_path("pathfinder", "wheel") - - def test_docs_latest_only_defaults_to_enabled_and_validates_values(self) -> None: - with patch.dict("os.environ", {}, clear=True): - self.assertEqual(_docs_arguments(), ["latest-only"]) - with patch.dict("os.environ", {"CUDA_PYTHON_DOCS_LATEST_ONLY": "false"}, clear=True): - self.assertEqual(_docs_arguments(), []) - with ( - patch.dict("os.environ", {"CUDA_PYTHON_DOCS_LATEST_ONLY": "sometimes"}, clear=True), - self.assertRaisesRegex(ValueError, "must be true"), - ): - _docs_arguments() + output_path("pathfinder", "wheel") + + +class MoonCleanOutputPathTest(unittest.TestCase): + def test_core_merger_only_cleans_task_owned_output_directories(self) -> None: + with tempfile.TemporaryDirectory() as temporary_directory: + repo = Path(temporary_directory) + output = repo / "cuda_core" / ".moon-out" / "wheel-merged" + with patch("ci.tools.merge_cuda_core_wheels.REPO_ROOT", repo): + self.assertEqual(_validated_moon_output(output), output) + self.assertEqual( + _validated_moon_output(Path("cuda_core/.moon-out/wheel-merged")), + output, + ) + with self.assertRaisesRegex(ValueError, "must be below"): + _validated_moon_output(repo / "cuda_core" / ".moon-out") + with self.assertRaisesRegex(ValueError, "must be below"): + _validated_moon_output(repo / "cuda_core" / "dist") class MoonFingerprintTest(unittest.TestCase): @@ -165,70 +169,65 @@ def test_native_tool_identity_uses_resolved_executables(self, compilers, which, compilers.assert_called_once_with() -class MoonCIConditionalTest(unittest.TestCase): +class MoonTaskCommandTest(unittest.TestCase): + def test_focused_tool_modules_are_directly_executable(self) -> None: + for module in ("ci.tools.build_artifacts", "ci.tools.prepare_test_assets"): + result = subprocess.run( # noqa: S603 + [sys.executable, "-m", module, "--help"], + cwd=Path(__file__).resolve().parents[3], + check=False, + capture_output=True, + text=True, + ) + self.assertEqual(result.returncode, 0, result.stderr) + def test_local_pixi_test_forwards_the_selected_environment(self) -> None: with ( patch.dict("os.environ", {"PIXI_ENVIRONMENT_NAME": "cu12"}, clear=True), - patch("ci.tools.moon_ci.shutil.which", return_value="/tools/pixi"), - patch("ci.tools.moon_ci._run") as run, + patch("sys.argv", ["run_pixi_test.py", "core"]), + patch("ci.tools.run_pixi_test.shutil.which", return_value="/tools/pixi"), + patch("ci.tools.run_pixi_test.subprocess.run") as run, ): - _pixi_test(Namespace(project="core")) + run_pixi_test() command = run.call_args.args[0] self.assertEqual(command[0], "/tools/pixi") self.assertIn("cuda_core/pixi.toml", command[3]) self.assertEqual(command[-3:], ["--environment", "cu12", "test"]) - def test_declared_unsupported_bindings_lane_skips_benchmark_before_pixi_lookup(self) -> None: - with ( - patch.dict("os.environ", {"SKIP_CUDA_BINDINGS_TEST": "1"}, clear=True), - patch("ci.tools.moon_ci._artifact_wheel") as artifact_wheel, - ): - _bindings_benchmark_smoke(Namespace()) - - artifact_wheel.assert_not_called() - - def test_benchmark_smoke_uses_the_prepared_system_python(self) -> None: - pathfinder = Path("pathfinder.whl") - bindings = Path("bindings.whl") - with ( - patch.dict("os.environ", {}, clear=True), - patch("ci.tools.moon_ci._artifact_wheel", side_effect=(pathfinder, bindings)), - patch("ci.tools.moon_ci._run") as run, - ): - _bindings_benchmark_smoke(Namespace()) - - self.assertEqual( - run.call_args_list[0].args[0], - [sys.executable, "-m", "pip", "install", str(pathfinder), str(bindings), "pyperf"], - ) - self.assertEqual(run.call_args_list[1].args[0][0], sys.executable) - self.assertEqual(run.call_args_list[1].args[0][-1], "--debug-single-value") - def test_declared_unsupported_bindings_lane_skips_before_artifact_lookup(self) -> None: - with ( - patch.dict("os.environ", {"SKIP_CUDA_BINDINGS_TEST": "1"}, clear=True), - patch("ci.tools.moon_ci._artifact_wheel") as artifact_wheel, - ): - _installed_test(Namespace(project="bindings")) - - artifact_wheel.assert_not_called() + bash = shutil.which("bash") + self.assertIsNotNone(bash) + assert bash is not None + result = subprocess.run( # noqa: S603 + [bash, "ci/tools/run-tests", "bindings"], + cwd=Path(__file__).resolve().parents[3], + env={**os.environ, "SKIP_CUDA_BINDINGS_TEST": "1"}, + check=False, + capture_output=True, + text=True, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("Skipping cuda.bindings tests", result.stdout) def test_non_main_bindings_lane_skips_metapackage_before_artifact_lookup(self) -> None: - with ( - patch.dict("os.environ", {"BINDINGS_SOURCE": "published"}, clear=True), - patch("ci.tools.moon_ci._artifact_wheel") as artifact_wheel, - ): - _metapackage_install_test(Namespace()) - - artifact_wheel.assert_not_called() + bash = shutil.which("bash") + self.assertIsNotNone(bash) + assert bash is not None + result = subprocess.run( # noqa: S603 + [bash, "ci/tools/run-tests", "metapackage"], + cwd=Path(__file__).resolve().parents[3], + env={**os.environ, "BINDINGS_SOURCE": "published"}, + check=False, + capture_output=True, + text=True, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("BINDINGS_SOURCE is not main", result.stdout) - def test_pathfinder_strict_preparation_requires_numeric_cuda_major(self) -> None: - with ( - patch.dict("os.environ", {"TEST_CUDA_MAJOR": "latest"}, clear=True), - self.assertRaisesRegex(RuntimeError, "numeric CUDA major"), - ): - _prepare_pathfinder_strict(Namespace()) + def test_native_builder_requires_the_lane_cuda_major(self) -> None: + with patch.dict("os.environ", {}, clear=True), self.assertRaisesRegex(RuntimeError, "BUILD_CUDA_MAJOR"): + _cuda_major("current") if __name__ == "__main__": diff --git a/ci/tools/tests/test_moon_workspace.py b/ci/tools/tests/test_moon_workspace.py index 0c7b8d59e90..cb2a1eeb1e5 100644 --- a/ci/tools/tests/test_moon_workspace.py +++ b/ci/tools/tests/test_moon_workspace.py @@ -24,7 +24,6 @@ "core": "cuda_core", "metapackage": "cuda_python", "test-helpers": "cuda_python_test_helpers", - "bindings-benchmarks": "benchmarks/cuda_bindings", } EXECUTION_TAG_TARGETS = { "ci-wheel-pure": {"pathfinder:wheel-pure", "metapackage:wheel-pure"}, @@ -41,7 +40,7 @@ "bindings:test-installed-linux", "core:test-installed-linux", "metapackage:test-installed-linux", - "bindings-benchmarks:smoke-linux", + "bindings:smoke-linux", }, "ci-test-windows": { "pathfinder:test-installed-windows", @@ -62,7 +61,7 @@ "root:quality-moon-contracts", "core:quality-api-base", "core:quality-api-release", - "bindings-benchmarks:unit-test", + "bindings:unit-test", }, } RUNNER_TAG_TARGETS = { @@ -143,7 +142,7 @@ def test_only_force_all_tasks_are_allocation_only(self) -> None: self.assertTrue(task["options"]["runInCI"]) self.assertFalse(task.get("outputs")) - def test_benchmark_inputs_are_owned_without_hiding_new_paths(self) -> None: + def test_precise_inputs_are_owned_without_hiding_new_paths(self) -> None: def affected(path: str) -> set[str]: result = subprocess.run( # noqa: S603 - the binary is explicitly selected in setUpClass. [ @@ -166,14 +165,26 @@ def affected(path: str) -> set[str]: queried = json.loads(result.stdout) return {task["target"] for project in queried["tasks"].values() for task in project.values()} - self.assertIn( - "bindings-benchmarks:unit-test", - affected("benchmarks/cuda_bindings/tests/test_runner.py"), - ) + known = affected("benchmarks/cuda_bindings/tests/test_runner.py") + self.assertIn("bindings:unit-test", known) + self.assertNotIn("root:force-all-unowned", known) self.assertIn( "root:force-all-unowned", affected("benchmarks/cuda_bindings/new_helper.py"), ) + quality = affected("ci/tools/tests/test_moon_tasks.py") + self.assertIn("root:quality-moon-contracts", quality) + self.assertNotIn("root:force-all-unowned", quality) + + def test_bindings_benchmark_smoke_uses_materialized_wheels(self) -> None: + task = self.by_target["bindings:smoke-linux"] + self.assertIn("${SKIP_CUDA_BINDINGS_TEST:-0}", task["script"]) + self.assertIn("cuda_pathfinder/.moon-out/wheel-pure/*.whl", task["script"]) + self.assertIn("cuda_bindings/.moon-out/wheel-current/*.whl", task["script"]) + self.assertIn("${#pathfinder_wheels[@]} -eq 1", task["script"]) + self.assertIn("${#bindings_wheels[@]} -eq 1", task["script"]) + self.assertIn("benchmarks/cuda_bindings/run_pyperf.py", task["script"]) + self.assertNotIn("moon_ci.py", str(task["inputs"])) def test_execution_and_runner_tags_select_real_tasks(self) -> None: for tag, expected in {**EXECUTION_TAG_TARGETS, **RUNNER_TAG_TARGETS}.items(): @@ -198,6 +209,52 @@ def test_cached_producers_have_explicit_non_overlapping_outputs(self) -> None: self.assertTrue(task.get("checks"), target) self.assertIn({"file": "/ci/tools/moon_fingerprint.py"}, task["inputs"]) + def test_tasks_use_focused_commands_instead_of_an_omnibus_dispatcher(self) -> None: + artifact_commands = { + "pathfinder:wheel-pure": ["pure-wheel", "pathfinder"], + "pathfinder:sdist": ["sdist", "pathfinder"], + "bindings:wheel-current": ["native-wheel", "bindings", "--lane", "current"], + "bindings:sdist": ["sdist", "bindings"], + "core:wheel-current": ["native-wheel", "core", "--lane", "current"], + "core:wheel-previous": ["native-wheel", "core", "--lane", "previous"], + "core:sdist": ["sdist", "core"], + "metapackage:wheel-pure": ["pure-wheel", "metapackage"], + "metapackage:sdist": ["sdist", "metapackage"], + } + for target, arguments in artifact_commands.items(): + task = self.by_target[target] + self.assertEqual(task["command"], "python") + self.assertEqual(task["args"], ["-m", "ci.tools.build_artifacts", *arguments]) + self.assertIn({"file": "/ci/tools/artifacts.py"}, task["inputs"]) + self.assertIn({"file": "/ci/tools/build_artifacts.py"}, task["inputs"]) + + for target, project in { + "pathfinder:test-installed-linux": "pathfinder", + "pathfinder:test-installed-linux-strict": "pathfinder", + "pathfinder:test-installed-windows": "pathfinder", + "pathfinder:test-installed-windows-strict": "pathfinder", + "bindings:test-installed-linux": "bindings", + "bindings:test-installed-windows": "bindings", + "core:test-installed-linux": "core", + "core:test-installed-windows": "core", + "metapackage:test-installed-linux": "metapackage", + "metapackage:test-installed-windows": "metapackage", + }.items(): + task = self.by_target[target] + if task["command"] != "noop": + self.assertEqual(task["command"], "bash") + self.assertEqual(task["args"], ["ci/tools/run-tests", project]) + + self.assertEqual( + self.by_target["test-helpers:prepare-test-assets"]["args"], + ["-m", "ci.tools.prepare_test_assets"], + ) + self.assertIn("--clean-output", self.by_target["core:wheel-merge"]["args"]) + self.assertIn("--output-dir", self.by_target["core:test-binaries"]["args"]) + for target in ("bindings:cython-test-assets", "core:cython-test-assets"): + self.assertEqual(self.by_target[target]["command"], "bash") + self.assertIn("--output-dir", self.by_target[target]["args"]) + def test_same_environment_build_dependencies_use_output_bytes(self) -> None: expected = { "core:wheel-current": {"bindings:wheel-current"}, @@ -250,7 +307,7 @@ def test_pathfinder_strictness_and_preparation_are_in_the_graph(self) -> None: def test_platform_test_tasks_track_provider_setup(self) -> None: for target in EXECUTION_TAG_TARGETS["ci-test-linux"]: inputs = self.by_target[target]["inputs"] - if "bindings-benchmarks" not in target and "prepare-strict" not in target: + if "prepare-strict" not in target: self.assertIn({"file": "/ci/tools/guess_latest.sh"}, inputs, target) self.assertIn({"file": "/ci/tools/install_gpu_driver.sh"}, inputs, target) for target in EXECUTION_TAG_TARGETS["ci-test-windows"]: @@ -263,7 +320,8 @@ def test_docs_components_run_in_parallel_before_assembly(self) -> None: docs = self.by_target["root:docs-ci"] self.assertFalse(docs["options"]["cache"]) self.assertTrue(docs["options"]["runDepsInParallel"]) - self.assertEqual(docs["args"], ["ci/tools/moon_ci.py", "docs-assemble"]) + self.assertEqual(docs["command"], "bash") + self.assertEqual(docs["args"], ["cuda_python/docs/assemble_moon_docs.sh"]) root_inputs = docs["inputs"] self.assertIn({"project": "core", "group": "package"}, root_inputs) self.assertIn({"project": "metapackage", "group": "docs"}, root_inputs) @@ -272,7 +330,8 @@ def test_docs_components_run_in_parallel_before_assembly(self) -> None: for target in EXECUTION_TAG_TARGETS["ci-docs"] - {"root:docs-ci"}: task = self.by_target[target] self.assertFalse(task["options"]["cache"]) - self.assertEqual(task["args"][:2], ["ci/tools/moon_ci.py", "docs-component"]) + self.assertEqual(task["command"], "bash") + self.assertEqual(task["args"][-1], "moon-ci") self.assertIn({"file": "/cuda_python/docs/environment-docs.yml"}, task["inputs"]) metapackage_inputs = self.by_target["metapackage:docs-ci"]["inputs"] @@ -289,7 +348,7 @@ def test_quality_tasks_use_external_refs_and_one_selector(self) -> None: self.assertIn("${CUDA_CORE_API_RELEASE_BASE}", release["args"]) self.assertIn("${CUDA_CORE_API_MERGE_BASE}", base["args"]) self.assertEqual(self.by_target["root:quality-moon-contracts"]["args"][:2], ["-m", "unittest"]) - for target in (release, base, self.by_target["bindings-benchmarks:unit-test"]): + for target in (release, base, self.by_target["bindings:unit-test"]): self.assertEqual(target["command"], "uvx") self.assertIn("--no-managed-python", target["args"]) self.assertIn("--no-python-downloads", target["args"]) @@ -299,18 +358,24 @@ def test_local_pixi_tasks_remain_available_and_skip_ci(self) -> None: self.assertFalse(self.by_target[target]["options"]["runInCI"]) for target in ("pathfinder:test", "bindings:test", "core:test"): task = self.by_target[target] - self.assertEqual(task["args"][:2], ["ci/tools/moon_ci.py", "pixi-test"]) + self.assertEqual(task["args"][:1], ["ci/tools/run_pixi_test.py"]) self.assertFalse(task["options"]["runInCI"]) for target in ( "pathfinder:docs", "bindings:docs", "core:docs", - "bindings-benchmarks:bench", + "bindings:bench", ): task = self.by_target[target] self.assertEqual(task["command"], "pixi") self.assertFalse(task["options"]["runInCI"]) + def test_omnibus_moon_helper_is_removed(self) -> None: + self.assertFalse((REPO_ROOT / "ci" / "tools" / "moon_ci.py").exists()) + for task in self.tasks: + self.assertNotIn({"file": "/ci/tools/moon_ci.py"}, task.get("inputs", []), task["target"]) + self.assertNotIn("ci/tools/moon_ci.py", task.get("args", []), task["target"]) + def test_workspace_disables_python_and_dependency_management(self) -> None: workspace = (REPO_ROOT / ".moon" / "workspace.yml").read_text(encoding="utf-8") self.assertIn("versionConstraint: '=2.5.1'", workspace) diff --git a/cuda_bindings/docs/build_docs.sh b/cuda_bindings/docs/build_docs.sh index 199ababce60..72530155929 100755 --- a/cuda_bindings/docs/build_docs.sh +++ b/cuda_bindings/docs/build_docs.sh @@ -5,15 +5,38 @@ set -ex +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +cd "${SCRIPT_DIR}" + +MOON_CI="0" if [[ "$#" == "0" ]]; then LATEST_ONLY="0" elif [[ "$#" == "1" && "$1" == "latest-only" ]]; then LATEST_ONLY="1" +elif [[ "$#" == "1" && "$1" == "moon-ci" ]]; then + MOON_CI="1" + DOCS_LATEST_ONLY="${CUDA_PYTHON_DOCS_LATEST_ONLY:-true}" + case "${DOCS_LATEST_ONLY,,}" in + 1|true) LATEST_ONLY="1" ;; + 0|false) LATEST_ONLY="0" ;; + *) + echo "CUDA_PYTHON_DOCS_LATEST_ONLY must be true, false, 1, or 0" >&2 + exit 1 + ;; + esac else - echo "usage: ./build_docs.sh [latest-only]" + echo "usage: ./build_docs.sh [latest-only|moon-ci]" exit 1 fi +if [[ "${MOON_CI}" == "1" ]]; then + if [[ -L build || ( -e build && ! -d build ) ]]; then + echo "refusing to replace non-directory docs build output: ${SCRIPT_DIR}/build" >&2 + exit 1 + fi + rm -rf build +fi + # SPHINX_CUDA_BINDINGS_VER is used to create a subdir under build/html # (the Makefile file for sphinx-build also honors it if defined). # If there's a post release (ex: .post1) we don't want it to show up in the @@ -58,3 +81,25 @@ fi # ensure that the Sphinx reference uses the latest docs cp build/html/latest/objects.inv build/html + +if [[ "${MOON_CI}" == "1" ]]; then + SOURCE="${SCRIPT_DIR}/build/html" + OUTPUT_ROOT="${SCRIPT_DIR}/../.moon-out" + OUTPUT="${OUTPUT_ROOT}/docs-ci" + if [[ -L "${SOURCE}" || ! -d "${SOURCE}" ]]; then + echo "documentation output not found: ${SOURCE}" >&2 + exit 1 + fi + if [[ -L "${OUTPUT_ROOT}" || ( -e "${OUTPUT_ROOT}" && ! -d "${OUTPUT_ROOT}" ) ]]; then + echo "refusing to use non-directory Moon output root: ${OUTPUT_ROOT}" >&2 + exit 1 + fi + if [[ -L "${OUTPUT}" || ( -e "${OUTPUT}" && ! -d "${OUTPUT}" ) ]]; then + echo "refusing to replace non-directory Moon docs output: ${OUTPUT}" >&2 + exit 1 + fi + mkdir -p "${OUTPUT_ROOT}" + rm -rf "${OUTPUT}" + mkdir -p "${OUTPUT}" + cp -aL "${SOURCE}/." "${OUTPUT}/" +fi diff --git a/cuda_bindings/moon.yml b/cuda_bindings/moon.yml index 348f9d3c4ec..c868047833b 100644 --- a/cuda_bindings/moon.yml +++ b/cuda_bindings/moon.yml @@ -38,11 +38,25 @@ fileGroups: - 'docs/**/*' - 'pixi.toml' - 'pixi.lock' + benchmarks: + - '/benchmarks/cuda_bindings/benchmarks/**/*' + - '/benchmarks/cuda_bindings/runner/**/*' + - '/benchmarks/cuda_bindings/compare.py' + - '/benchmarks/cuda_bindings/run_cpp.py' + - '/benchmarks/cuda_bindings/run_pyperf.py' + - '/benchmarks/cuda_bindings/pixi.toml' + - '/benchmarks/cuda_bindings/pixi.lock' + benchmark-tests: + - '/benchmarks/cuda_bindings/runner/**/*' + - '/benchmarks/cuda_bindings/benchmarks/**/*' + - '/benchmarks/cuda_bindings/tests/**/*' + - '/benchmarks/cuda_bindings/pixi.toml' + - '/benchmarks/cuda_bindings/pixi.lock' tasks: wheel-current: command: python - args: [ci/tools/moon_ci.py, native-wheel, bindings, --lane, current] + args: [-m, ci.tools.build_artifacts, native-wheel, bindings, --lane, current] env: BUILD_CUDA_VER: '${BUILD_CUDA_VER}' CIBW_BUILD: '${CIBW_BUILD}' @@ -53,7 +67,8 @@ tasks: - '@group(package)' - {project: pathfinder, group: package} - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' - - '/ci/tools/moon_ci.py' + - '/ci/tools/artifacts.py' + - '/ci/tools/build_artifacts.py' - '/ci/tools/moon_fingerprint.py' - '/ci/tools/env-vars' - '/ci/versions.yml' @@ -79,7 +94,7 @@ tasks: sdist: command: python - args: [ci/tools/moon_ci.py, sdist, bindings] + args: [-m, ci.tools.build_artifacts, sdist, bindings] deps: - target: pathfinder:sdist cacheStrategy: outputs @@ -91,7 +106,8 @@ tasks: inputs: - '@group(package)' - {project: pathfinder, group: package} - - '/ci/tools/moon_ci.py' + - '/ci/tools/artifacts.py' + - '/ci/tools/build_artifacts.py' - '/ci/tools/moon_fingerprint.py' - '/.github/workflows/test-sdist-linux.yml' - '/.github/workflows/test-sdist-windows.yml' @@ -109,8 +125,11 @@ tasks: runInCI: true cython-test-assets: - command: python - args: [ci/tools/moon_ci.py, cython-test-assets, bindings] + command: bash + args: + - cuda_bindings/tests/cython/build_tests.sh + - --output-dir + - cuda_bindings/.moon-out/cython-tests deps: - test-helpers:prepare-test-assets inputs: @@ -119,7 +138,6 @@ tasks: - {project: pathfinder, group: package} - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' - '/cuda_bindings/.moon-out/wheel-current/*.whl' - - '/ci/tools/moon_ci.py' - '/ci/tools/moon_fingerprint.py' - '/.github/workflows/build-pure-wheel.yml' - '/.github/workflows/build-wheel.yml' @@ -143,7 +161,7 @@ tasks: test: command: python - args: [ci/tools/moon_ci.py, pixi-test, bindings] + args: [ci/tools/run_pixi_test.py, bindings] inputs: - '@group(package)' - '@group(tests)' @@ -153,18 +171,97 @@ tasks: - '/ci/test-matrix.yml' - '/ci/tools/env-vars' - '/tests/**/*' - - '/ci/tools/moon_ci.py' + - '/ci/tools/run_pixi_test.py' + type: test + + bench: + command: pixi + args: [run, --manifest-path, benchmarks/cuda_bindings/pixi.toml, --environment, source, bench] + inputs: + - '@group(benchmarks)' + - '/cuda_bindings/**/*' + + smoke: + command: pixi + args: + - run + - --manifest-path + - benchmarks/cuda_bindings/pixi.toml + - --environment + - source + - bench-smoke-test + inputs: + - '@group(benchmarks)' + - '/cuda_bindings/**/*' + + smoke-linux: + script: | + set -euo pipefail + if [[ "${SKIP_CUDA_BINDINGS_TEST:-0}" == "1" ]]; then + echo "Skipping cuda.bindings benchmarks for this declared compatibility lane." + exit 0 + fi + shopt -s nullglob + pathfinder_wheels=(cuda_pathfinder/.moon-out/wheel-pure/*.whl) + bindings_wheels=(cuda_bindings/.moon-out/wheel-current/*.whl) + [[ ${#pathfinder_wheels[@]} -eq 1 ]] || { + echo "expected one pathfinder wheel, found ${#pathfinder_wheels[@]}" >&2 + exit 1 + } + [[ ${#bindings_wheels[@]} -eq 1 ]] || { + echo "expected one bindings wheel, found ${#bindings_wheels[@]}" >&2 + exit 1 + } + python -m pip install \ + "${pathfinder_wheels[0]}" \ + "${bindings_wheels[0]}" \ + pyperf + python benchmarks/cuda_bindings/run_pyperf.py --debug-single-value + inputs: + - '@group(benchmarks)' + - '@group(package)' + - {project: pathfinder, group: package} + - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' + - '/cuda_bindings/.moon-out/wheel-current/*.whl' + - '/ci/test-matrix.yml' + - '/ci/tools/env-vars' + - '/ci/tools/guess_latest.sh' + - '/ci/tools/install_gpu_driver.sh' + - '/tests/**/*' + - '/.github/workflows/test-wheel-linux.yml' + tags: [ci-test-linux, runner-test-linux] type: test + options: + mutex: ci-python-gpu + os: linux + runInCI: true + + unit-test: + command: uvx + args: + - --no-managed-python + - --no-python-downloads + - --from + - pytest + - pytest + - --noconftest + - benchmarks/cuda_bindings/tests + inputs: + - '@group(benchmark-tests)' + tags: [ci-quality, runner-quality] + type: test + options: + os: linux + runInCI: true test-installed-linux: - command: python - args: [ci/tools/moon_ci.py, installed-test, bindings] + command: bash + args: [ci/tools/run-tests, bindings] inputs: - '@group(package)' - '@group(tests)' - {project: pathfinder, group: package} - '/cuda_python_test_helpers/**/*' - - '/ci/tools/moon_ci.py' - '/ci/tools/run-tests' - '/ci/test-matrix.yml' - '/ci/tools/env-vars' @@ -183,14 +280,13 @@ tasks: runInCI: true test-installed-windows: - command: python - args: [ci/tools/moon_ci.py, installed-test, bindings] + command: bash + args: [ci/tools/run-tests, bindings] inputs: - '@group(package)' - '@group(tests)' - {project: pathfinder, group: package} - '/cuda_python_test_helpers/**/*' - - '/ci/tools/moon_ci.py' - '/ci/tools/run-tests' - '/ci/test-matrix.yml' - '/ci/tools/env-vars' @@ -218,8 +314,8 @@ tasks: - '/.github/workflows/build-docs.yml' docs-ci: - command: python - args: [ci/tools/moon_ci.py, docs-component, bindings] + command: bash + args: [cuda_bindings/docs/build_docs.sh, moon-ci] env: CUDA_PYTHON_DOCS_LATEST_ONLY: '${CUDA_PYTHON_DOCS_LATEST_ONLY}' inputs: @@ -227,7 +323,6 @@ tasks: - '@group(docs)' - {project: pathfinder, group: package} - '/cuda_python/docs/environment-docs.yml' - - '/ci/tools/moon_ci.py' - '/.github/workflows/build-docs.yml' outputs: - '.moon-out/docs-ci' diff --git a/cuda_bindings/tests/cython/build_tests.py b/cuda_bindings/tests/cython/build_tests.py index 5bde350e87b..44b2460f765 100644 --- a/cuda_bindings/tests/cython/build_tests.py +++ b/cuda_bindings/tests/cython/build_tests.py @@ -12,7 +12,9 @@ from __future__ import annotations +import argparse import os +import shutil import sys from pathlib import Path @@ -32,8 +34,32 @@ def _bindings_source_root() -> Path: return root +def _output_directory(script_dir: Path, value: str) -> Path: + project_root = script_dir.parents[1] + output_root = project_root / ".moon-out" + requested = Path(value) + output = Path(os.path.abspath(requested if requested.is_absolute() else project_root.parent / requested)) + if output_root not in output.parents: + raise ValueError(f"output must be below {output_root}: {output}") + current = output + while current != project_root: + if current.is_symlink(): + raise ValueError(f"output path must not traverse a symlink: {current}") + current = current.parent + if output.exists(): + if not output.is_dir(): + raise ValueError(f"refusing to replace non-directory output: {output}") + shutil.rmtree(output) + output.mkdir(parents=True) + return output + + def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output-dir") + args = parser.parse_args() script_dir = Path(__file__).resolve().parent + output = _output_directory(script_dir, args.output_dir) if args.output_dir else None # Avoid appending the absolute checkout path under build/temp: the # concatenated path can exceed Windows' path limit. These files are siblings. os.chdir(script_dir) @@ -51,8 +77,25 @@ def main() -> None: # pytest imports each extension by bare module name (see test_cython.py), # so build in-place next to its .pyx regardless of the invoking cwd. - sys.argv = [sys.argv[0], "build_ext", "--inplace"] + sys.argv = [sys.argv[0], "build_ext"] + if output is None: + sys.argv.append("--inplace") + else: + build_temp = output / ".build-temp" + sys.argv.extend(["--build-lib", str(output), "--build-temp", str(build_temp)]) setup(name="cuda_bindings_cython_tests", ext_modules=ext_modules) + if output is not None: + if build_temp.exists(): + shutil.rmtree(build_temp) + for source in pyx_files: + matches = [ + path + for pattern in (f"{Path(source).stem}*.so", f"{Path(source).stem}*.pyd", f"{Path(source).stem}*.dylib") + for path in output.glob(pattern) + if path.is_file() + ] + if len(matches) != 1: + raise RuntimeError(f"expected one extension for {source} in {output}, found {len(matches)}") if __name__ == "__main__": diff --git a/cuda_bindings/tests/cython/build_tests.sh b/cuda_bindings/tests/cython/build_tests.sh index 0ca0745d2b4..d6e9d2433ab 100755 --- a/cuda_bindings/tests/cython/build_tests.sh +++ b/cuda_bindings/tests/cython/build_tests.sh @@ -20,4 +20,4 @@ fi # PYTHONPATH separator handling and surfaces import errors as exceptions. # nthreads=1 inside the driver mirrors the previous `-j 1` to side-step # any process-pool issues and keep builds deterministic. -python "${SCRIPTPATH}/build_tests.py" +python "${SCRIPTPATH}/build_tests.py" "$@" diff --git a/cuda_core/docs/build_docs.sh b/cuda_core/docs/build_docs.sh index 28ae6dd07fd..33939942e51 100755 --- a/cuda_core/docs/build_docs.sh +++ b/cuda_core/docs/build_docs.sh @@ -8,15 +8,35 @@ set -ex SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) cd "${SCRIPT_DIR}" +MOON_CI="0" if [[ "$#" == "0" ]]; then LATEST_ONLY="0" elif [[ "$#" == "1" && "$1" == "latest-only" ]]; then LATEST_ONLY="1" +elif [[ "$#" == "1" && "$1" == "moon-ci" ]]; then + MOON_CI="1" + DOCS_LATEST_ONLY="${CUDA_PYTHON_DOCS_LATEST_ONLY:-true}" + case "${DOCS_LATEST_ONLY,,}" in + 1|true) LATEST_ONLY="1" ;; + 0|false) LATEST_ONLY="0" ;; + *) + echo "CUDA_PYTHON_DOCS_LATEST_ONLY must be true, false, 1, or 0" >&2 + exit 1 + ;; + esac else - echo "usage: ./build_docs.sh [latest-only]" + echo "usage: ./build_docs.sh [latest-only|moon-ci]" exit 1 fi +if [[ "${MOON_CI}" == "1" ]]; then + if [[ -L build || ( -e build && ! -d build ) ]]; then + echo "refusing to replace non-directory docs build output: ${SCRIPT_DIR}/build" >&2 + exit 1 + fi + rm -rf build +fi + # SPHINX_CUDA_CORE_VER is used to create a subdir under build/html # (the Makefile file for sphinx-build also honors it if defined) if [[ -z "${SPHINX_CUDA_CORE_VER}" ]]; then @@ -56,3 +76,25 @@ cp build/html/latest/objects.inv build/html # clean up previously auto-generated files rm -rf source/generated/ + +if [[ "${MOON_CI}" == "1" ]]; then + SOURCE="${SCRIPT_DIR}/build/html" + OUTPUT_ROOT="${SCRIPT_DIR}/../.moon-out" + OUTPUT="${OUTPUT_ROOT}/docs-ci" + if [[ -L "${SOURCE}" || ! -d "${SOURCE}" ]]; then + echo "documentation output not found: ${SOURCE}" >&2 + exit 1 + fi + if [[ -L "${OUTPUT_ROOT}" || ( -e "${OUTPUT_ROOT}" && ! -d "${OUTPUT_ROOT}" ) ]]; then + echo "refusing to use non-directory Moon output root: ${OUTPUT_ROOT}" >&2 + exit 1 + fi + if [[ -L "${OUTPUT}" || ( -e "${OUTPUT}" && ! -d "${OUTPUT}" ) ]]; then + echo "refusing to replace non-directory Moon docs output: ${OUTPUT}" >&2 + exit 1 + fi + mkdir -p "${OUTPUT_ROOT}" + rm -rf "${OUTPUT}" + mkdir -p "${OUTPUT}" + cp -aL "${SOURCE}/." "${OUTPUT}/" +fi diff --git a/cuda_core/moon.yml b/cuda_core/moon.yml index 3e4cd96331b..2f9128cd99e 100644 --- a/cuda_core/moon.yml +++ b/cuda_core/moon.yml @@ -46,7 +46,7 @@ fileGroups: tasks: wheel-current: command: python - args: [ci/tools/moon_ci.py, native-wheel, core, --lane, current] + args: [-m, ci.tools.build_artifacts, native-wheel, core, --lane, current] deps: - target: bindings:wheel-current cacheStrategy: outputs @@ -63,7 +63,8 @@ tasks: - {project: bindings, group: package} - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' - '/cuda_bindings/.moon-out/wheel-current/*.whl' - - '/ci/tools/moon_ci.py' + - '/ci/tools/artifacts.py' + - '/ci/tools/build_artifacts.py' - '/ci/tools/moon_fingerprint.py' - '/ci/tools/env-vars' - '/ci/tools/merge_cuda_core_wheels.py' @@ -90,7 +91,7 @@ tasks: wheel-previous: command: python - args: [ci/tools/moon_ci.py, native-wheel, core, --lane, previous] + args: [-m, ci.tools.build_artifacts, native-wheel, core, --lane, previous] env: BUILD_PREV_CUDA_MAJOR: '${BUILD_PREV_CUDA_MAJOR}' CIBW_BUILD: '${CIBW_BUILD}' @@ -103,7 +104,8 @@ tasks: - {project: bindings, group: package} - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' - '/cuda_bindings/.moon-out/wheel-previous/*.whl' - - '/ci/tools/moon_ci.py' + - '/ci/tools/artifacts.py' + - '/ci/tools/build_artifacts.py' - '/ci/tools/moon_fingerprint.py' - '/ci/tools/env-vars' - '/ci/versions.yml' @@ -128,7 +130,15 @@ tasks: wheel-merge: command: python - args: [ci/tools/moon_ci.py, merge-core-wheels] + args: + - ci/tools/merge_cuda_core_wheels.py + - --wheel-dir + - cuda_core/.moon-out/wheel-current + - --wheel-dir + - cuda_core/.moon-out/wheel-previous + - --output-dir + - cuda_core/.moon-out/wheel-merged + - --clean-output # Current and previous CUDA toolkits are provisioned outside Moon, so the # two input wheels are deliberately staged in separate invocations before # this task merges their declared outputs. @@ -138,7 +148,6 @@ tasks: - {project: bindings, group: package} - '/cuda_core/.moon-out/wheel-current/*.whl' - '/cuda_core/.moon-out/wheel-previous/*.whl' - - '/ci/tools/moon_ci.py' - '/ci/tools/moon_fingerprint.py' - '/ci/tools/merge_cuda_core_wheels.py' - '/.github/workflows/build-pure-wheel.yml' @@ -161,7 +170,7 @@ tasks: sdist: command: python - args: [ci/tools/moon_ci.py, sdist, core] + args: [-m, ci.tools.build_artifacts, sdist, core] deps: - target: pathfinder:sdist cacheStrategy: outputs @@ -177,7 +186,8 @@ tasks: - '@group(package)' - {project: pathfinder, group: package} - {project: bindings, group: package} - - '/ci/tools/moon_ci.py' + - '/ci/tools/artifacts.py' + - '/ci/tools/build_artifacts.py' - '/ci/tools/moon_fingerprint.py' - '/.github/workflows/test-sdist-linux.yml' - '/.github/workflows/test-sdist-windows.yml' @@ -195,8 +205,11 @@ tasks: runInCI: true cython-test-assets: - command: python - args: [ci/tools/moon_ci.py, cython-test-assets, core] + command: bash + args: + - cuda_core/tests/cython/build_tests.sh + - --output-dir + - cuda_core/.moon-out/cython-tests deps: - test-helpers:prepare-test-assets inputs: @@ -207,7 +220,6 @@ tasks: - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' - '/cuda_bindings/.moon-out/wheel-current/*.whl' - '/cuda_core/.moon-out/wheel-current/*.whl' - - '/ci/tools/moon_ci.py' - '/ci/tools/moon_fingerprint.py' - '/.github/workflows/build-pure-wheel.yml' - '/.github/workflows/build-wheel.yml' @@ -231,7 +243,10 @@ tasks: test-binaries: command: python - args: [ci/tools/moon_ci.py, core-test-binaries] + args: + - cuda_core/tests/test_binaries/build_test_binaries.py + - --output-dir + - cuda_core/.moon-out/test-binaries env: CUDA_PATH: '${CUDA_PATH}' HOST_PLATFORM: '${HOST_PLATFORM}' @@ -241,7 +256,6 @@ tasks: - {project: bindings, group: package} - 'tests/test_binaries/build_test_binaries.py' - 'tests/test_binaries/saxpy.cu' - - '/ci/tools/moon_ci.py' - '/ci/tools/moon_fingerprint.py' - '/.github/workflows/build-wheel.yml' outputs: @@ -263,7 +277,7 @@ tasks: test: command: python - args: [ci/tools/moon_ci.py, pixi-test, core] + args: [ci/tools/run_pixi_test.py, core] inputs: - '@group(package)' - '@group(tests)' @@ -273,19 +287,18 @@ tasks: - '/ci/test-matrix.yml' - '/ci/tools/env-vars' - '/tests/**/*' - - '/ci/tools/moon_ci.py' + - '/ci/tools/run_pixi_test.py' type: test test-installed-linux: - command: python - args: [ci/tools/moon_ci.py, installed-test, core] + command: bash + args: [ci/tools/run-tests, core] inputs: - '@group(package)' - '@group(tests)' - {project: pathfinder, group: package} - {project: bindings, group: package} - '/cuda_python_test_helpers/**/*' - - '/ci/tools/moon_ci.py' - '/ci/tools/run-tests' - '/ci/test-matrix.yml' - '/ci/tools/env-vars' @@ -305,15 +318,14 @@ tasks: runInCI: true test-installed-windows: - command: python - args: [ci/tools/moon_ci.py, installed-test, core] + command: bash + args: [ci/tools/run-tests, core] inputs: - '@group(package)' - '@group(tests)' - {project: pathfinder, group: package} - {project: bindings, group: package} - '/cuda_python_test_helpers/**/*' - - '/ci/tools/moon_ci.py' - '/ci/tools/run-tests' - '/ci/test-matrix.yml' - '/ci/tools/env-vars' @@ -344,8 +356,8 @@ tasks: - '/.github/workflows/build-docs.yml' docs-ci: - command: python - args: [ci/tools/moon_ci.py, docs-component, core] + command: bash + args: [cuda_core/docs/build_docs.sh, moon-ci] env: CUDA_PYTHON_DOCS_LATEST_ONLY: '${CUDA_PYTHON_DOCS_LATEST_ONLY}' inputs: @@ -354,7 +366,6 @@ tasks: - {project: pathfinder, group: package} - {project: bindings, group: package} - '/cuda_python/docs/environment-docs.yml' - - '/ci/tools/moon_ci.py' - '/.github/workflows/build-docs.yml' outputs: - '.moon-out/docs-ci' diff --git a/cuda_core/tests/cython/build_tests.py b/cuda_core/tests/cython/build_tests.py index 6cebb0c6ff9..e5108872a76 100644 --- a/cuda_core/tests/cython/build_tests.py +++ b/cuda_core/tests/cython/build_tests.py @@ -12,7 +12,9 @@ from __future__ import annotations +import argparse import os +import shutil import sys from pathlib import Path @@ -32,8 +34,32 @@ def _bindings_source_root() -> Path: return root +def _output_directory(script_dir: Path, value: str) -> Path: + project_root = script_dir.parents[1] + output_root = project_root / ".moon-out" + requested = Path(value) + output = Path(os.path.abspath(requested if requested.is_absolute() else project_root.parent / requested)) + if output_root not in output.parents: + raise ValueError(f"output must be below {output_root}: {output}") + current = output + while current != project_root: + if current.is_symlink(): + raise ValueError(f"output path must not traverse a symlink: {current}") + current = current.parent + if output.exists(): + if not output.is_dir(): + raise ValueError(f"refusing to replace non-directory output: {output}") + shutil.rmtree(output) + output.mkdir(parents=True) + return output + + def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output-dir") + args = parser.parse_args() script_dir = Path(__file__).resolve().parent + output = _output_directory(script_dir, args.output_dir) if args.output_dir else None pyx_files = sorted(str(p) for p in script_dir.glob("test_*.pyx")) if not pyx_files: raise SystemExit(f"no test_*.pyx files under {script_dir}") @@ -52,8 +78,25 @@ def main() -> None: # sys.path). chdir here so the .so lands next to its .pyx regardless of the # invoking cwd. os.chdir(script_dir) - sys.argv = [sys.argv[0], "build_ext", "--inplace"] + sys.argv = [sys.argv[0], "build_ext"] + if output is None: + sys.argv.append("--inplace") + else: + build_temp = output / ".build-temp" + sys.argv.extend(["--build-lib", str(output), "--build-temp", str(build_temp)]) setup(name="cuda_core_cython_tests", ext_modules=ext_modules) + if output is not None: + if build_temp.exists(): + shutil.rmtree(build_temp) + for source in pyx_files: + matches = [ + path + for pattern in (f"{Path(source).stem}*.so", f"{Path(source).stem}*.pyd", f"{Path(source).stem}*.dylib") + for path in output.glob(pattern) + if path.is_file() + ] + if len(matches) != 1: + raise RuntimeError(f"expected one extension for {source} in {output}, found {len(matches)}") if __name__ == "__main__": diff --git a/cuda_core/tests/cython/build_tests.sh b/cuda_core/tests/cython/build_tests.sh index 7ee65c50d68..26acb0c6c2f 100755 --- a/cuda_core/tests/cython/build_tests.sh +++ b/cuda_core/tests/cython/build_tests.sh @@ -1,7 +1,7 @@ #!/bin/bash set -eo pipefail -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 UNAME=$(uname) @@ -19,4 +19,4 @@ fi # Use a Python driver so the cuda.bindings source root is resolved at # runtime and passed via Cython's include_path -- avoids platform-specific # PYTHONPATH separator handling and surfaces import errors as exceptions. -python "${SCRIPTPATH}/build_tests.py" +python "${SCRIPTPATH}/build_tests.py" "$@" diff --git a/cuda_core/tests/test_binaries/build_test_binaries.py b/cuda_core/tests/test_binaries/build_test_binaries.py index ca1ceccb268..23d5a32bf00 100644 --- a/cuda_core/tests/test_binaries/build_test_binaries.py +++ b/cuda_core/tests/test_binaries/build_test_binaries.py @@ -4,7 +4,9 @@ from __future__ import annotations +import argparse import os +import shutil import subprocess import tempfile from pathlib import Path @@ -17,17 +19,43 @@ def _run(command: list[str]) -> None: raise SystemExit(result.returncode) +def _prepare_output(script_dir: Path, value: str | None) -> Path: + if value is None: + return script_dir + project_root = script_dir.parents[1] + output_root = project_root / ".moon-out" + requested = Path(value) + output = Path(os.path.abspath(requested if requested.is_absolute() else project_root.parent / requested)) + if output_root not in output.parents: + raise ValueError(f"output must be below {output_root}: {output}") + current = output + while current != project_root: + if current.is_symlink(): + raise ValueError(f"output path must not traverse a symlink: {current}") + current = current.parent + if output.exists(): + if not output.is_dir(): + raise ValueError(f"refusing to replace non-directory output: {output}") + shutil.rmtree(output) + output.mkdir(parents=True) + return output + + def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output-dir") + args = parser.parse_args() script_dir = Path(__file__).resolve().parent source_path = script_dir / "saxpy.cu" - final_object_path = script_dir / "saxpy.o" - final_library_path = script_dir / ("saxpy.lib" if os.name == "nt" else "saxpy.a") + output = _prepare_output(script_dir, args.output_dir) + final_object_path = output / "saxpy.o" + final_library_path = output / ("saxpy.lib" if os.name == "nt" else "saxpy.a") nvcc_extra_flags = ["-std=c++17"] if os.name == "nt": nvcc_extra_flags.extend(["-Xcompiler", "/Zc:preprocessor"]) - with tempfile.TemporaryDirectory(prefix="build_test_binaries-", dir=script_dir) as temp_dir: + with tempfile.TemporaryDirectory(prefix="build_test_binaries-", dir=output) as temp_dir: temp_dir_path = Path(temp_dir) object_path = temp_dir_path / final_object_path.name library_path = temp_dir_path / final_library_path.name diff --git a/cuda_pathfinder/docs/build_docs.sh b/cuda_pathfinder/docs/build_docs.sh index 4720cd458f6..baa1ac91259 100755 --- a/cuda_pathfinder/docs/build_docs.sh +++ b/cuda_pathfinder/docs/build_docs.sh @@ -5,15 +5,38 @@ set -ex +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +cd "${SCRIPT_DIR}" + +MOON_CI="0" if [[ "$#" == "0" ]]; then LATEST_ONLY="0" elif [[ "$#" == "1" && "$1" == "latest-only" ]]; then LATEST_ONLY="1" +elif [[ "$#" == "1" && "$1" == "moon-ci" ]]; then + MOON_CI="1" + DOCS_LATEST_ONLY="${CUDA_PYTHON_DOCS_LATEST_ONLY:-true}" + case "${DOCS_LATEST_ONLY,,}" in + 1|true) LATEST_ONLY="1" ;; + 0|false) LATEST_ONLY="0" ;; + *) + echo "CUDA_PYTHON_DOCS_LATEST_ONLY must be true, false, 1, or 0" >&2 + exit 1 + ;; + esac else - echo "usage: ./build_docs.sh [latest-only]" + echo "usage: ./build_docs.sh [latest-only|moon-ci]" exit 1 fi +if [[ "${MOON_CI}" == "1" ]]; then + if [[ -L build || ( -e build && ! -d build ) ]]; then + echo "refusing to replace non-directory docs build output: ${SCRIPT_DIR}/build" >&2 + exit 1 + fi + rm -rf build +fi + # SPHINX_CUDA_PATHFINDER_VER is used to create a subdir under build/html # (the Makefile file for sphinx-build also honors it if defined). # If there's a post release (ex: .post1) we don't want it to show up in the @@ -57,3 +80,25 @@ fi # ensure that the Sphinx reference uses the latest docs cp build/html/latest/objects.inv build/html + +if [[ "${MOON_CI}" == "1" ]]; then + SOURCE="${SCRIPT_DIR}/build/html" + OUTPUT_ROOT="${SCRIPT_DIR}/../.moon-out" + OUTPUT="${OUTPUT_ROOT}/docs-ci" + if [[ -L "${SOURCE}" || ! -d "${SOURCE}" ]]; then + echo "documentation output not found: ${SOURCE}" >&2 + exit 1 + fi + if [[ -L "${OUTPUT_ROOT}" || ( -e "${OUTPUT_ROOT}" && ! -d "${OUTPUT_ROOT}" ) ]]; then + echo "refusing to use non-directory Moon output root: ${OUTPUT_ROOT}" >&2 + exit 1 + fi + if [[ -L "${OUTPUT}" || ( -e "${OUTPUT}" && ! -d "${OUTPUT}" ) ]]; then + echo "refusing to replace non-directory Moon docs output: ${OUTPUT}" >&2 + exit 1 + fi + mkdir -p "${OUTPUT_ROOT}" + rm -rf "${OUTPUT}" + mkdir -p "${OUTPUT}" + cp -aL "${SOURCE}/." "${OUTPUT}/" +fi diff --git a/cuda_pathfinder/moon.yml b/cuda_pathfinder/moon.yml index 25c1c40875c..864feab9059 100644 --- a/cuda_pathfinder/moon.yml +++ b/cuda_pathfinder/moon.yml @@ -34,20 +34,19 @@ fileGroups: tasks: test: command: python - args: [ci/tools/moon_ci.py, pixi-test, pathfinder] + args: [ci/tools/run_pixi_test.py, pathfinder] inputs: - '@group(package)' - '@group(tests)' - - '/ci/tools/moon_ci.py' + - '/ci/tools/run_pixi_test.py' type: test test-installed-linux: - command: python - args: [ci/tools/moon_ci.py, installed-test, pathfinder] + command: bash + args: [ci/tools/run-tests, pathfinder] inputs: - '@group(package)' - '@group(tests)' - - '/ci/tools/moon_ci.py' - '/ci/tools/run-tests' - '/ci/test-matrix.yml' - '/ci/tools/env-vars' @@ -68,8 +67,25 @@ tasks: runInCI: true prepare-strict-linux: - command: python - args: [ci/tools/moon_ci.py, prepare-pathfinder-strict] + command: bash + args: + - -euo + - pipefail + - -c + - | + [[ "${TEST_CUDA_MAJOR}" =~ ^[0-9]+$ ]] || { + echo "TEST_CUDA_MAJOR must be a numeric CUDA major version" >&2 + exit 1 + } + shopt -s nullglob + wheels=(cuda_pathfinder/.moon-out/wheel-pure/*.whl) + [[ ${#wheels[@]} -eq 1 ]] || { + echo "expected one pathfinder wheel, found ${#wheels[@]}" >&2 + exit 1 + } + python -m pip install --only-binary=:all: --verbose "${wheels[0]}" \ + --group "cuda_pathfinder/pyproject.toml:test-cu${TEST_CUDA_MAJOR}" + python -m pip list deps: - pathfinder:test-installed-linux env: @@ -78,7 +94,6 @@ tasks: - '@group(package)' - '@group(tests)' - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' - - '/ci/tools/moon_ci.py' - '/.github/workflows/test-wheel-linux.yml' tags: [ci-test-linux, runner-test-linux] options: @@ -87,12 +102,11 @@ tasks: runInCI: true test-installed-linux-strict: - command: python - args: [ci/tools/moon_ci.py, installed-test, pathfinder] + command: bash + args: [ci/tools/run-tests, pathfinder] inputs: - '@group(package)' - '@group(tests)' - - '/ci/tools/moon_ci.py' - '/ci/tools/run-tests' - '/ci/test-matrix.yml' - '/ci/tools/env-vars' @@ -115,12 +129,11 @@ tasks: runInCI: true test-installed-windows: - command: python - args: [ci/tools/moon_ci.py, installed-test, pathfinder] + command: bash + args: [ci/tools/run-tests, pathfinder] inputs: - '@group(package)' - '@group(tests)' - - '/ci/tools/moon_ci.py' - '/ci/tools/run-tests' - '/ci/test-matrix.yml' - '/ci/tools/env-vars' @@ -141,8 +154,25 @@ tasks: runInCI: true prepare-strict-windows: - command: python - args: [ci/tools/moon_ci.py, prepare-pathfinder-strict] + command: bash + args: + - -euo + - pipefail + - -c + - | + [[ "${TEST_CUDA_MAJOR}" =~ ^[0-9]+$ ]] || { + echo "TEST_CUDA_MAJOR must be a numeric CUDA major version" >&2 + exit 1 + } + shopt -s nullglob + wheels=(cuda_pathfinder/.moon-out/wheel-pure/*.whl) + [[ ${#wheels[@]} -eq 1 ]] || { + echo "expected one pathfinder wheel, found ${#wheels[@]}" >&2 + exit 1 + } + python -m pip install --only-binary=:all: --verbose "${wheels[0]}" \ + --group "cuda_pathfinder/pyproject.toml:test-cu${TEST_CUDA_MAJOR}" + python -m pip list deps: - pathfinder:test-installed-windows env: @@ -151,7 +181,6 @@ tasks: - '@group(package)' - '@group(tests)' - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' - - '/ci/tools/moon_ci.py' - '/.github/workflows/test-wheel-windows.yml' tags: [ci-test-windows, runner-test-windows] options: @@ -160,12 +189,11 @@ tasks: runInCI: true test-installed-windows-strict: - command: python - args: [ci/tools/moon_ci.py, installed-test, pathfinder] + command: bash + args: [ci/tools/run-tests, pathfinder] inputs: - '@group(package)' - '@group(tests)' - - '/ci/tools/moon_ci.py' - '/ci/tools/run-tests' - '/ci/test-matrix.yml' - '/ci/tools/env-vars' @@ -196,15 +224,14 @@ tasks: - '/.github/workflows/build-docs.yml' docs-ci: - command: python - args: [ci/tools/moon_ci.py, docs-component, pathfinder] + command: bash + args: [cuda_pathfinder/docs/build_docs.sh, moon-ci] env: CUDA_PYTHON_DOCS_LATEST_ONLY: '${CUDA_PYTHON_DOCS_LATEST_ONLY}' inputs: - '@group(package)' - '@group(docs)' - '/cuda_python/docs/environment-docs.yml' - - '/ci/tools/moon_ci.py' - '/.github/workflows/build-docs.yml' outputs: - '.moon-out/docs-ci' @@ -216,10 +243,11 @@ tasks: wheel-pure: command: python - args: [ci/tools/moon_ci.py, pure-wheel, pathfinder] + args: [-m, ci.tools.build_artifacts, pure-wheel, pathfinder] inputs: - '@group(package)' - - '/ci/tools/moon_ci.py' + - '/ci/tools/artifacts.py' + - '/ci/tools/build_artifacts.py' - '/ci/tools/moon_fingerprint.py' - '/.github/workflows/build-pure-wheel.yml' outputs: @@ -238,10 +266,11 @@ tasks: sdist: command: python - args: [ci/tools/moon_ci.py, sdist, pathfinder] + args: [-m, ci.tools.build_artifacts, sdist, pathfinder] inputs: - '@group(package)' - - '/ci/tools/moon_ci.py' + - '/ci/tools/artifacts.py' + - '/ci/tools/build_artifacts.py' - '/ci/tools/moon_fingerprint.py' - '/.github/workflows/test-sdist-linux.yml' - '/.github/workflows/test-sdist-windows.yml' diff --git a/cuda_python/docs/assemble_moon_docs.sh b/cuda_python/docs/assemble_moon_docs.sh new file mode 100755 index 00000000000..4a8b9aad81e --- /dev/null +++ b/cuda_python/docs/assemble_moon_docs.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euxo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +REPO_ROOT=$(cd -- "${SCRIPT_DIR}/../.." && pwd) +OUTPUT_ROOT="${REPO_ROOT}/.moon-out" +OUTPUT="${OUTPUT_ROOT}/docs" + +if [[ -L "${OUTPUT_ROOT}" || ( -e "${OUTPUT_ROOT}" && ! -d "${OUTPUT_ROOT}" ) ]]; then + echo "refusing to use non-directory Moon output root: ${OUTPUT_ROOT}" >&2 + exit 1 +fi +if [[ -L "${OUTPUT}" || ( -e "${OUTPUT}" && ! -d "${OUTPUT}" ) ]]; then + echo "refusing to replace non-directory Moon docs output: ${OUTPUT}" >&2 + exit 1 +fi + +mkdir -p "${OUTPUT_ROOT}" +rm -rf "${OUTPUT}" +mkdir -p "${OUTPUT}" + +copy_component() { + local source=$1 + local destination=$2 + local source_root + source_root=$(dirname -- "${source}") + if [[ -L "${source_root}" || ! -d "${source_root}" || -L "${source}" || ! -d "${source}" ]]; then + echo "documentation component output not found: ${source}" >&2 + exit 1 + fi + mkdir -p "${destination}" + cp -aL "${source}/." "${destination}/" +} + +copy_component "${REPO_ROOT}/cuda_python/.moon-out/docs-ci" "${OUTPUT}" +copy_component "${REPO_ROOT}/cuda_bindings/.moon-out/docs-ci" "${OUTPUT}/cuda-bindings" +copy_component "${REPO_ROOT}/cuda_core/.moon-out/docs-ci" "${OUTPUT}/cuda-core" +copy_component "${REPO_ROOT}/cuda_pathfinder/.moon-out/docs-ci" "${OUTPUT}/cuda-pathfinder" diff --git a/cuda_python/docs/build_docs.sh b/cuda_python/docs/build_docs.sh index 0b2067ed60c..237cd176e73 100755 --- a/cuda_python/docs/build_docs.sh +++ b/cuda_python/docs/build_docs.sh @@ -5,15 +5,38 @@ set -ex +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +cd "${SCRIPT_DIR}" + +MOON_CI="0" if [[ "$#" == "0" ]]; then LATEST_ONLY="0" elif [[ "$#" == "1" && "$1" == "latest-only" ]]; then LATEST_ONLY="1" +elif [[ "$#" == "1" && "$1" == "moon-ci" ]]; then + MOON_CI="1" + DOCS_LATEST_ONLY="${CUDA_PYTHON_DOCS_LATEST_ONLY:-true}" + case "${DOCS_LATEST_ONLY,,}" in + 1|true) LATEST_ONLY="1" ;; + 0|false) LATEST_ONLY="0" ;; + *) + echo "CUDA_PYTHON_DOCS_LATEST_ONLY must be true, false, 1, or 0" >&2 + exit 1 + ;; + esac else - echo "usage: ./build_docs.sh [latest-only]" + echo "usage: ./build_docs.sh [latest-only|moon-ci]" exit 1 fi +if [[ "${MOON_CI}" == "1" ]]; then + if [[ -L build || ( -e build && ! -d build ) ]]; then + echo "refusing to replace non-directory docs build output: ${SCRIPT_DIR}/build" >&2 + exit 1 + fi + rm -rf build +fi + # SPHINX_CUDA_PYTHON_VER is used to create a subdir under build/html # (the Makefile file for sphinx-build also honors it if defined). # If there's a post release (ex: .post1) we don't want it to show up in the @@ -56,3 +79,25 @@ cp build/html/latest/objects.inv build/html # clean up previously auto-generated files rm -rf source/generated/ + +if [[ "${MOON_CI}" == "1" ]]; then + SOURCE="${SCRIPT_DIR}/build/html" + OUTPUT_ROOT="${SCRIPT_DIR}/../.moon-out" + OUTPUT="${OUTPUT_ROOT}/docs-ci" + if [[ -L "${SOURCE}" || ! -d "${SOURCE}" ]]; then + echo "documentation output not found: ${SOURCE}" >&2 + exit 1 + fi + if [[ -L "${OUTPUT_ROOT}" || ( -e "${OUTPUT_ROOT}" && ! -d "${OUTPUT_ROOT}" ) ]]; then + echo "refusing to use non-directory Moon output root: ${OUTPUT_ROOT}" >&2 + exit 1 + fi + if [[ -L "${OUTPUT}" || ( -e "${OUTPUT}" && ! -d "${OUTPUT}" ) ]]; then + echo "refusing to replace non-directory Moon docs output: ${OUTPUT}" >&2 + exit 1 + fi + mkdir -p "${OUTPUT_ROOT}" + rm -rf "${OUTPUT}" + mkdir -p "${OUTPUT}" + cp -aL "${SOURCE}/." "${OUTPUT}/" +fi diff --git a/cuda_python/moon.yml b/cuda_python/moon.yml index 64e35650a4f..d9e182da1ba 100644 --- a/cuda_python/moon.yml +++ b/cuda_python/moon.yml @@ -30,11 +30,12 @@ fileGroups: tasks: wheel-pure: command: python - args: [ci/tools/moon_ci.py, pure-wheel, metapackage] + args: [-m, ci.tools.build_artifacts, pure-wheel, metapackage] inputs: - '@group(package)' - {project: bindings, group: package} - - '/ci/tools/moon_ci.py' + - '/ci/tools/artifacts.py' + - '/ci/tools/build_artifacts.py' - '/ci/tools/moon_fingerprint.py' - '/.github/workflows/build-pure-wheel.yml' outputs: @@ -53,14 +54,15 @@ tasks: sdist: command: python - args: [ci/tools/moon_ci.py, sdist, metapackage] + args: [-m, ci.tools.build_artifacts, sdist, metapackage] deps: - target: bindings:sdist cacheStrategy: outputs inputs: - '@group(package)' - {project: bindings, group: package} - - '/ci/tools/moon_ci.py' + - '/ci/tools/artifacts.py' + - '/ci/tools/build_artifacts.py' - '/ci/tools/moon_fingerprint.py' - '/.github/workflows/test-sdist-linux.yml' - '/.github/workflows/test-sdist-windows.yml' @@ -78,15 +80,15 @@ tasks: runInCI: true test-installed-linux: - command: python - args: [ci/tools/moon_ci.py, metapackage-install-test] + command: bash + args: [ci/tools/run-tests, metapackage] inputs: - '@group(package)' - {project: pathfinder, group: package} - {project: bindings, group: package} - {project: core, group: package} - '/cuda_core/.moon-out/wheel-merged/*.whl' - - '/ci/tools/moon_ci.py' + - '/ci/tools/run-tests' - '/ci/test-matrix.yml' - '/ci/tools/env-vars' - '/tests/**/*' @@ -104,15 +106,15 @@ tasks: runInCI: true test-installed-windows: - command: python - args: [ci/tools/moon_ci.py, metapackage-install-test] + command: bash + args: [ci/tools/run-tests, metapackage] inputs: - '@group(package)' - {project: pathfinder, group: package} - {project: bindings, group: package} - {project: core, group: package} - '/cuda_core/.moon-out/wheel-merged/*.whl' - - '/ci/tools/moon_ci.py' + - '/ci/tools/run-tests' - '/ci/test-matrix.yml' - '/ci/tools/env-vars' - '/tests/**/*' @@ -130,8 +132,8 @@ tasks: runInCI: true docs-ci: - command: python - args: [ci/tools/moon_ci.py, docs-component, metapackage] + command: bash + args: [cuda_python/docs/build_docs.sh, moon-ci] env: CUDA_PYTHON_DOCS_LATEST_ONLY: '${CUDA_PYTHON_DOCS_LATEST_ONLY}' inputs: @@ -141,7 +143,6 @@ tasks: - {project: bindings, group: package} - {project: core, group: package} - '/cuda_python/docs/environment-docs.yml' - - '/ci/tools/moon_ci.py' - '/.github/workflows/build-docs.yml' outputs: - '.moon-out/docs-ci' diff --git a/cuda_python_test_helpers/moon.yml b/cuda_python_test_helpers/moon.yml index 8d448da3939..6ac12567a60 100644 --- a/cuda_python_test_helpers/moon.yml +++ b/cuda_python_test_helpers/moon.yml @@ -17,7 +17,7 @@ taskOptions: tasks: prepare-test-assets: command: python - args: [ci/tools/moon_ci.py, prepare-test-assets] + args: [-m, ci.tools.prepare_test_assets] inputs: - '/cuda_pathfinder/pyproject.toml' - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' @@ -25,7 +25,8 @@ tasks: - '/cuda_bindings/.moon-out/wheel-current/*.whl' - '/cuda_core/pyproject.toml' - '/cuda_core/.moon-out/wheel-current/*.whl' - - '/ci/tools/moon_ci.py' + - '/ci/tools/artifacts.py' + - '/ci/tools/prepare_test_assets.py' - '/.github/workflows/build-wheel.yml' options: os: [linux, windows] diff --git a/moon.yml b/moon.yml index 08912f97e54..16ad78e2707 100644 --- a/moon.yml +++ b/moon.yml @@ -27,7 +27,10 @@ fileGroups: - '/.moon/**/*' - '/moon.yml' - '/**/moon.yml' - - '/ci/tools/moon_ci.py' + - '/ci/tools/artifacts.py' + - '/ci/tools/build_artifacts.py' + - '/ci/tools/prepare_test_assets.py' + - '/ci/tools/run_pixi_test.py' - '/ci/tools/moon_fingerprint.py' - '/ci/tools/env-vars' - '/ci/versions.yml' @@ -87,10 +90,15 @@ fileGroups: - '!/ci/tools/install_gpu_driver.ps1' - '!/ci/tools/install_gpu_driver.sh' - '!/ci/tools/merge_cuda_core_wheels.py' - - '!/ci/tools/moon_ci.py' + - '!/ci/tools/artifacts.py' + - '!/ci/tools/build_artifacts.py' + - '!/ci/tools/prepare_test_assets.py' + - '!/ci/tools/run_pixi_test.py' - '!/ci/tools/moon_fingerprint.py' - '!/ci/tools/run-tests' - '!/ci/tools/setup-sanitizer' + - '!/ci/tools/tests/test_moon_tasks.py' + - '!/ci/tools/tests/test_moon_workspace.py' - '!/cuda_bindings/cuda/**/*' - '!/cuda_bindings/docs/**/*' - '!/cuda_bindings/examples/**/*' @@ -171,13 +179,17 @@ tasks: quality-moon-contracts: command: python - args: [-m, unittest, ci.tools.tests.test_moon_ci, ci.tools.tests.test_moon_workspace] + args: [-m, unittest, ci.tools.tests.test_moon_tasks, ci.tools.tests.test_moon_workspace] inputs: - '/.moon/**/*' - '/**/moon.yml' - - '/ci/tools/moon_ci.py' + - '/ci/tools/artifacts.py' + - '/ci/tools/build_artifacts.py' + - '/ci/tools/prepare_test_assets.py' + - '/ci/tools/run-tests' + - '/ci/tools/run_pixi_test.py' - '/ci/tools/moon_fingerprint.py' - - '/ci/tools/tests/test_moon_ci.py' + - '/ci/tools/tests/test_moon_tasks.py' - '/ci/tools/tests/test_moon_workspace.py' tags: [ci-quality, runner-quality] type: test @@ -200,8 +212,8 @@ tasks: inputs: [] docs-ci: - command: python - args: [ci/tools/moon_ci.py, docs-assemble] + command: bash + args: [cuda_python/docs/assemble_moon_docs.sh] deps: - target: pathfinder:docs-ci cacheStrategy: outputs @@ -224,7 +236,7 @@ tasks: - '/cuda_bindings/.moon-out/docs-ci/**/*' - '/cuda_core/.moon-out/docs-ci/**/*' - '/cuda_python/.moon-out/docs-ci/**/*' - - '/ci/tools/moon_ci.py' + - '/cuda_python/docs/assemble_moon_docs.sh' - '/.github/workflows/build-pure-wheel.yml' - '/.github/workflows/build-wheel.yml' - '/.github/workflows/build-docs.yml' From d7fc85e27ddd19ba0ca9cd6efc0337edc6cf4b09 Mon Sep 17 00:00:00 2001 From: Keith Kraus Date: Tue, 18 Aug 2026 17:31:55 -0400 Subject: [PATCH 5/6] ci: simplify Moon task execution --- .github/workflows/build-docs.yml | 30 +- .github/workflows/build-pure-wheel.yml | 121 ------- .github/workflows/build-wheel.yml | 78 ++-- .github/workflows/ci-nightly.yml | 2 +- .github/workflows/ci.yml | 69 +--- .github/workflows/test-wheel-linux.yml | 13 +- .github/workflows/test-wheel-windows.yml | 13 +- CONTRIBUTING.md | 20 +- ci/tools/artifacts.py | 103 ------ ci/tools/build_artifacts.py | 177 --------- ci/tools/merge_cuda_core_wheels.py | 55 +-- ci/tools/moon_fingerprint.py | 181 ---------- ci/tools/prepare_test_assets.py | 31 -- ci/tools/run_pixi_test.py | 50 --- ci/tools/tests/test_moon_tasks.py | 251 ++++--------- ci/tools/tests/test_moon_workspace.py | 226 ++++++++++-- cuda_bindings/moon.yml | 268 ++++++++++++-- cuda_core/moon.yml | 442 ++++++++++++++++++++--- cuda_pathfinder/moon.yml | 162 +++++++-- cuda_python/moon.yml | 160 +++++++- cuda_python_test_helpers/moon.yml | 39 +- moon.yml | 18 +- 22 files changed, 1331 insertions(+), 1178 deletions(-) delete mode 100644 .github/workflows/build-pure-wheel.yml delete mode 100644 ci/tools/artifacts.py delete mode 100644 ci/tools/build_artifacts.py delete mode 100644 ci/tools/moon_fingerprint.py delete mode 100644 ci/tools/prepare_test_assets.py delete mode 100755 ci/tools/run_pixi_test.py diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index d3a7fd85a39..13b5ae671a9 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -28,13 +28,6 @@ on: required: false default: ${{ github.run_id }} type: string - portable-run-id: - description: > - Workflow run ID containing cuda.pathfinder and metapackage wheels. - Falls back to run-id when empty. - required: false - default: "" - type: string sha: description: "Commit SHA used in native wheel artifact names" required: false @@ -74,7 +67,7 @@ jobs: ref: ${{ inputs.git-tag }} - name: Set up Moon - if: ${{ !inputs.is-release && inputs.component == 'all' }} + if: ${{ !inputs.is-release }} uses: moonrepo/setup-toolchain@261c62cb5b0f580c7be7c8cd0f023a2e96756095 # v0.6.4 with: moon-version: "2.5.1" @@ -149,23 +142,14 @@ jobs: echo "CUDA_BINDINGS_ARTIFACT_NAME=${CUDA_BINDINGS_ARTIFACT_BASENAME}-${FILE_HASH}" >> $GITHUB_ENV echo "CUDA_BINDINGS_ARTIFACTS_DIR=$(realpath "$REPO_DIR/cuda_bindings/dist")" >> $GITHUB_ENV - - name: Download portable Moon lane - if: ${{ !inputs.is-release }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: moon-lane-build-portable - path: . - run-id: ${{ inputs.portable-run-id || inputs.run-id }} - github-token: ${{ github.token }} - - - name: Download portable release artifacts + - name: Download universal release wheels if: ${{ inputs.is-release }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: "cuda-*-wheel" path: . merge-multiple: false - run-id: ${{ inputs.portable-run-id || inputs.run-id }} + run-id: ${{ inputs.run-id }} github-token: ${{ github.token }} - name: Download native Moon lane @@ -203,6 +187,12 @@ jobs: pwd ls -lahR $CUDA_CORE_ARTIFACTS_DIR + - name: Build or hydrate the metapackage wheel with Moon + if: ${{ !inputs.is-release }} + env: + CUDA_PYTHON_USE_STAGED_BINDINGS_VERSION: "1" + run: moon run metapackage:wheel-pure + - name: Install all packages run: | if [[ "${{ inputs.is-release }}" == "true" ]]; then @@ -231,6 +221,8 @@ jobs: - name: Build all docs if: ${{ inputs.component == 'all' }} + env: + CUDA_PYTHON_USE_STAGED_BINDINGS_VERSION: "1" run: | if [[ "${{ inputs.is-release }}" == "false" ]]; then # Render context differs between main, PR previews, and releases, diff --git a/.github/workflows/build-pure-wheel.yml b/.github/workflows/build-pure-wheel.yml deleted file mode 100644 index 1d616df67df..00000000000 --- a/.github/workflows/build-pure-wheel.yml +++ /dev/null @@ -1,121 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 - -name: "CI: Build portable wheels with Moon" - -on: - workflow_call: - inputs: - moon-base: - required: true - type: string - baseline-run-id: - required: false - default: "" - type: string - force-all: - required: false - default: false - type: boolean - -permissions: - actions: read - contents: read - -jobs: - build: - runs-on: ubuntu-latest - defaults: - run: - shell: bash --noprofile --norc -xeuo pipefail {0} - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - fetch-depth: 0 - filter: blob:none - - - name: Set up Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 - with: - python-version: "3.12" - - - name: Set up Moon - uses: moonrepo/setup-toolchain@261c62cb5b0f580c7be7c8cd0f023a2e96756095 # v0.6.4 - with: - moon-version: "2.5.1" - auto-install: false - auto-setup: false - - - name: Install externally managed build tools - run: >- - python -m pip install - "setuptools>=80" - "setuptools-scm[simple]>=8,!=10.1" - "twine" - "wheel" - - - name: Restore trusted exact-base Moon cache - if: ${{ inputs.baseline-run-id != '' && !inputs.force-all }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: moon-lane-build-portable - path: . - github-token: ${{ github.token }} - run-id: ${{ inputs.baseline-run-id }} - - - name: Build affected portable wheels with Moon - env: - CUDA_PYTHON_LANE: portable-py312 - MOON_BASE: ${{ inputs.moon-base }} - MOON_HEAD: ${{ github.sha }} - MOON_FORCE_ALL: ${{ inputs.force-all && 'true' || 'false' }} - run: | - args=() - if [[ "${MOON_FORCE_ALL}" == "true" ]]; then - args+=(--force) - fi - moon ci ':#ci-wheel-pure' --upstream deep --downstream none "${args[@]}" - - - name: Validate portable wheels - run: | - test "$(find cuda_pathfinder/.moon-out/wheel-pure -maxdepth 1 -name '*.whl' -type f | wc -l)" -eq 1 - test "$(find cuda_python/.moon-out/wheel-pure -maxdepth 1 -name '*.whl' -type f | wc -l)" -eq 1 - python -m twine check --strict \ - cuda_pathfinder/.moon-out/wheel-pure/*.whl \ - cuda_python/.moon-out/wheel-pure/*.whl - - - name: Upload cuda.pathfinder wheel - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: cuda-pathfinder-wheel - path: cuda_pathfinder/.moon-out/wheel-pure/*.whl - if-no-files-found: error - overwrite: true - - - name: Upload cuda-python metapackage wheel - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: cuda-python-wheel - path: cuda_python/.moon-out/wheel-pure/*.whl - if-no-files-found: error - overwrite: true - - # Moon documents hashes/ and outputs/ as the portable subset of its - # local cache. GitHub artifacts provide trusted exact-run transport; - # Moon remains responsible for hashes, hits, and output hydration. - - name: Upload portable Moon lane - if: ${{ always() }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: moon-lane-build-portable - path: | - .moon/cache/hashes - .moon/cache/outputs - cuda_pathfinder/.moon-out/wheel-pure - cuda_python/.moon-out/wheel-pure - if-no-files-found: error - include-hidden-files: true - overwrite: true - retention-days: 30 diff --git a/.github/workflows/build-wheel.yml b/.github/workflows/build-wheel.yml index a1f69d28e66..d62da760092 100644 --- a/.github/workflows/build-wheel.yml +++ b/.github/workflows/build-wheel.yml @@ -18,10 +18,6 @@ on: required: false type: string default: "" - portable-run-id: - description: "Workflow run containing the selected portable wheels" - required: true - type: string moon-base: description: "Base revision used by Moon affected checks" required: true @@ -159,24 +155,6 @@ jobs: - name: Install externally managed build tools run: python -m pip install "cibuildwheel==4.1.1" twine wheel - - name: Download portable Moon lane - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: moon-lane-build-portable - path: . - github-token: ${{ github.token }} - run-id: ${{ inputs.portable-run-id }} - - - name: List the cuda.pathfinder artifacts directory - run: | - if [[ "${{ inputs.host-platform }}" == win* ]]; then - export CHOWN=chown - else - export CHOWN="sudo chown" - fi - $CHOWN -R $(whoami) cuda_pathfinder/.moon-out/wheel-pure/*.whl - ls -lahR cuda_pathfinder/.moon-out/wheel-pure - - name: Set up mini CTK uses: ./.github/actions/fetch_ctk continue-on-error: false @@ -184,8 +162,9 @@ jobs: host-platform: ${{ inputs.host-platform }} cuda-version: ${{ inputs.cuda-version }} - - name: Build current native wheels with Moon + - name: Build current wheels with Moon env: + CUDA_PYTHON_USE_STAGED_BINDINGS_VERSION: "1" CUDA_PYTHON_LANE: ${{ inputs.host-platform }}-py${{ matrix.python-version }}-cuda${{ inputs.cuda-version }} MOON_BASE: ${{ inputs.moon-base }} MOON_HEAD: ${{ github.sha }} @@ -222,11 +201,15 @@ jobs: if [[ "${{ inputs.force-all }}" == "true" ]]; then args+=(--force) fi - # These tasks are a true build dependency, so execute their affected - # checks in order without pulling an unchanged upstream task into the - # second invocation. - moon ci bindings:wheel-current --upstream none --downstream none "${args[@]}" - moon ci core:wheel-current --upstream none --downstream none "${args[@]}" + # Moon owns the pathfinder -> bindings -> core build chain. + moon ci core:wheel-current --upstream deep --downstream none "${args[@]}" + # The metapackage is needed only in the Linux/Python 3.12 lane used + # by docs and release publication. Run it after bindings so its exact + # development pin is derived from the wheel that this lane produced. + if [[ "${{ inputs.host-platform }}" == "linux-64" && + "${{ matrix.python-version-formatted }}" == "312" ]]; then + moon ci metapackage:wheel-pure --upstream none --downstream none "${args[@]}" + fi - name: Set up Python id: setup-python2 @@ -371,12 +354,43 @@ jobs: else CHOWN="sudo chown" fi - $CHOWN -R "$(whoami)" cuda_bindings/.moon-out cuda_core/.moon-out + $CHOWN -R "$(whoami)" \ + cuda_pathfinder/.moon-out cuda_bindings/.moon-out cuda_core/.moon-out + test "$(find cuda_pathfinder/.moon-out/wheel-pure -maxdepth 1 -name '*.whl' -type f | wc -l)" -eq 1 test "$(find cuda_bindings/.moon-out/wheel-current -maxdepth 1 -name '*.whl' -type f | wc -l)" -eq 1 test "$(find cuda_core/.moon-out/wheel-merged -maxdepth 1 -name '*.whl' -type f | wc -l)" -eq 1 - twine check --strict \ - cuda_bindings/.moon-out/wheel-current/*.whl \ + wheels=( + cuda_pathfinder/.moon-out/wheel-pure/*.whl + cuda_bindings/.moon-out/wheel-current/*.whl cuda_core/.moon-out/wheel-merged/*.whl + ) + if [[ "${{ inputs.host-platform }}" == "linux-64" && + "${{ matrix.python-version-formatted }}" == "312" ]]; then + $CHOWN -R "$(whoami)" cuda_python/.moon-out + test "$(find cuda_python/.moon-out/wheel-pure -maxdepth 1 -name '*.whl' -type f | wc -l)" -eq 1 + wheels+=(cuda_python/.moon-out/wheel-pure/*.whl) + fi + twine check --strict "${wheels[@]}" + + # Release tooling consumes stable component artifact names. Publish the + # universal wheels once instead of racing every native matrix row. + - name: Upload cuda.pathfinder wheel + if: ${{ inputs.host-platform == 'linux-64' && matrix.python-version-formatted == '312' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: cuda-pathfinder-wheel + path: cuda_pathfinder/.moon-out/wheel-pure/*.whl + if-no-files-found: error + overwrite: true + + - name: Upload cuda-python metapackage wheel + if: ${{ inputs.host-platform == 'linux-64' && matrix.python-version-formatted == '312' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: cuda-python-wheel + path: cuda_python/.moon-out/wheel-pure/*.whl + if-no-files-found: error + overwrite: true - name: Upload cuda.bindings build artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -429,6 +443,8 @@ jobs: path: | .moon/cache/hashes .moon/cache/outputs + cuda_pathfinder/.moon-out/wheel-pure + cuda_python/.moon-out/wheel-pure cuda_bindings/.moon-out/wheel-current cuda_bindings/.moon-out/cython-tests cuda_core/.moon-out/wheel-current diff --git a/.github/workflows/ci-nightly.yml b/.github/workflows/ci-nightly.yml index 7ccd3b87b54..f3f0802fccc 100644 --- a/.github/workflows/ci-nightly.yml +++ b/.github/workflows/ci-nightly.yml @@ -79,7 +79,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - expected=(moon-lane-build-portable) + expected=() python_versions=(310 311 312 313 314 314t 315 315t) host_platforms=(linux-64 linux-aarch64 win-64) for host_platform in "${host_platforms[@]}"; do diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d0e12d0641..2b554ec5344 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,6 @@ jobs: moon-base-run-id: ${{ steps.baseline.outputs.moon-base-run-id }} moon-base-sha: ${{ steps.baseline.outputs.moon-base-sha }} moon-force-all: ${{ steps.lanes.outputs.force-all }} - build-portable: ${{ steps.lanes.outputs.build-portable }} build-linux-64: ${{ steps.lanes.outputs.build-linux-64 }} build-linux-aarch64: ${{ steps.lanes.outputs.build-linux-aarch64 }} build-windows: ${{ steps.lanes.outputs.build-windows }} @@ -154,7 +153,6 @@ jobs: )" expected=( - moon-lane-build-portable moon-lane-sdist-linux-64 moon-lane-sdist-win-64 ) @@ -251,7 +249,6 @@ jobs: echo "force-all=${force_all}" >> "$GITHUB_OUTPUT" lanes=( - build-portable:runner-build-portable build-linux-64:runner-build-linux-64 build-linux-aarch64:runner-build-linux-aarch64 build-windows:runner-build-windows @@ -375,39 +372,16 @@ jobs: --downstream none ${{ fromJSON(needs.gate.outputs.moon-force-all) && '--force' || '' }} - build-portable: - name: Build portable wheels - needs: - - gate - if: >- - ${{ github.repository_owner == 'nvidia' && - !fromJSON(needs.gate.outputs.skip) && - fromJSON(needs.gate.outputs.build-portable) && - (!fromJSON(needs.gate.outputs.doc-only) || - fromJSON(needs.gate.outputs.moon-force-all)) }} - permissions: - actions: read - contents: read - uses: ./.github/workflows/build-pure-wheel.yml - with: - moon-base: ${{ needs.gate.outputs.moon-base }} - baseline-run-id: ${{ needs.gate.outputs.moon-base-run-id }} - force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} - # Native builds remain split by platform so their tests can start as soon as # the corresponding platform finishes. Each reusable workflow owns its - # eight-version Python matrix and per-row Moon lane bundle. + # eight-version Python matrix and bundles the cheap pathfinder wheel with + # each row; the Linux/Python 3.12 row also publishes the metapackage. build-linux-64: name: Build linux-64, CUDA ${{ needs.gate.outputs.cuda-build-ver }} needs: - gate - - build-portable if: >- - ${{ always() && - github.repository_owner == 'nvidia' && - needs.gate.result == 'success' && - (needs.build-portable.result == 'success' || - needs.build-portable.result == 'skipped') && + ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.gate.outputs.skip) && fromJSON(needs.gate.outputs.build-linux-64) && (!fromJSON(needs.gate.outputs.doc-only) || @@ -421,7 +395,6 @@ jobs: cuda-version: ${{ needs.gate.outputs.cuda-build-ver }} prev-cuda-version: ${{ needs.gate.outputs.cuda-prev-build-ver }} baseline-run-id: ${{ needs.gate.outputs.moon-base-run-id }} - portable-run-id: ${{ needs.build-portable.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} moon-base: ${{ needs.gate.outputs.moon-base }} force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} @@ -429,13 +402,8 @@ jobs: name: Build linux-aarch64, CUDA ${{ needs.gate.outputs.cuda-build-ver }} needs: - gate - - build-portable if: >- - ${{ always() && - github.repository_owner == 'nvidia' && - needs.gate.result == 'success' && - (needs.build-portable.result == 'success' || - needs.build-portable.result == 'skipped') && + ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.gate.outputs.skip) && !fromJSON(needs.gate.outputs.doc-only) && fromJSON(needs.gate.outputs.build-linux-aarch64) }} @@ -448,7 +416,6 @@ jobs: cuda-version: ${{ needs.gate.outputs.cuda-build-ver }} prev-cuda-version: ${{ needs.gate.outputs.cuda-prev-build-ver }} baseline-run-id: ${{ needs.gate.outputs.moon-base-run-id }} - portable-run-id: ${{ needs.build-portable.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} moon-base: ${{ needs.gate.outputs.moon-base }} force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} @@ -456,13 +423,8 @@ jobs: name: Build win-64, CUDA ${{ needs.gate.outputs.cuda-build-ver }} needs: - gate - - build-portable if: >- - ${{ always() && - github.repository_owner == 'nvidia' && - needs.gate.result == 'success' && - (needs.build-portable.result == 'success' || - needs.build-portable.result == 'skipped') && + ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.gate.outputs.skip) && !fromJSON(needs.gate.outputs.doc-only) && fromJSON(needs.gate.outputs.build-windows) }} @@ -475,7 +437,6 @@ jobs: cuda-version: ${{ needs.gate.outputs.cuda-build-ver }} prev-cuda-version: ${{ needs.gate.outputs.cuda-prev-build-ver }} baseline-run-id: ${{ needs.gate.outputs.moon-base-run-id }} - portable-run-id: ${{ needs.build-portable.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} moon-base: ${{ needs.gate.outputs.moon-base }} force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} @@ -525,14 +486,11 @@ jobs: name: Test linux-64 needs: - gate - - build-portable - build-linux-64 if: >- ${{ always() && github.repository_owner == 'nvidia' && needs.gate.result == 'success' && - (needs.build-portable.result == 'success' || - needs.build-portable.result == 'skipped') && (needs.build-linux-64.result == 'success' || needs.build-linux-64.result == 'skipped') && !fromJSON(needs.gate.outputs.skip) && @@ -550,7 +508,6 @@ jobs: nruns: ${{ (github.event_name == 'schedule' && 5) || 1 }} run-id: ${{ needs.build-linux-64.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} sha: ${{ needs.build-linux-64.result == 'success' && github.sha || needs.gate.outputs.moon-base-sha }} - portable-run-id: ${{ needs.build-portable.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} moon-base: ${{ needs.gate.outputs.moon-base }} force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} @@ -558,14 +515,11 @@ jobs: name: Test linux-aarch64 needs: - gate - - build-portable - build-linux-aarch64 if: >- ${{ always() && github.repository_owner == 'nvidia' && needs.gate.result == 'success' && - (needs.build-portable.result == 'success' || - needs.build-portable.result == 'skipped') && (needs.build-linux-aarch64.result == 'success' || needs.build-linux-aarch64.result == 'skipped') && !fromJSON(needs.gate.outputs.skip) && @@ -583,7 +537,6 @@ jobs: nruns: ${{ (github.event_name == 'schedule' && 5) || 1 }} run-id: ${{ needs.build-linux-aarch64.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} sha: ${{ needs.build-linux-aarch64.result == 'success' && github.sha || needs.gate.outputs.moon-base-sha }} - portable-run-id: ${{ needs.build-portable.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} moon-base: ${{ needs.gate.outputs.moon-base }} force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} @@ -591,14 +544,11 @@ jobs: name: Test win-64 needs: - gate - - build-portable - build-windows if: >- ${{ always() && github.repository_owner == 'nvidia' && needs.gate.result == 'success' && - (needs.build-portable.result == 'success' || - needs.build-portable.result == 'skipped') && (needs.build-windows.result == 'success' || needs.build-windows.result == 'skipped') && !fromJSON(needs.gate.outputs.skip) && @@ -616,7 +566,6 @@ jobs: nruns: ${{ (github.event_name == 'schedule' && 5) || 1 }} run-id: ${{ needs.build-windows.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} sha: ${{ needs.build-windows.result == 'success' && github.sha || needs.gate.outputs.moon-base-sha }} - portable-run-id: ${{ needs.build-portable.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} moon-base: ${{ needs.gate.outputs.moon-base }} force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} @@ -624,14 +573,11 @@ jobs: name: Docs needs: - gate - - build-portable - build-linux-64 if: >- ${{ always() && github.repository_owner == 'nvidia' && needs.gate.result == 'success' && - (needs.build-portable.result == 'success' || - needs.build-portable.result == 'skipped') && (needs.build-linux-64.result == 'success' || needs.build-linux-64.result == 'skipped') && !fromJSON(needs.gate.outputs.skip) && @@ -646,7 +592,6 @@ jobs: is-release: ${{ github.ref_type == 'tag' }} run-id: ${{ needs.build-linux-64.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} sha: ${{ github.sha }} - portable-run-id: ${{ needs.build-portable.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} precommit-windows: name: Pre-commit on Windows @@ -684,7 +629,6 @@ jobs: needs: - gate - quality - - build-portable - build-linux-64 - build-linux-aarch64 - build-windows @@ -730,12 +674,10 @@ jobs: doc_only="${{ needs.gate.outputs.doc-only }}" force_all="${{ needs.gate.outputs.moon-force-all }}" - portable=false linux_64=false linux_aarch64=false windows=false if [[ "${doc_only}" != "true" || "${force_all}" == "true" ]]; then - portable="${{ needs.gate.outputs.build-portable }}" linux_64="${{ needs.gate.outputs.build-linux-64 }}" fi if [[ "${doc_only}" != "true" ]]; then @@ -743,7 +685,6 @@ jobs: windows="${{ needs.gate.outputs.build-windows }}" fi - check_result build-portable "$(expected_for "${portable}")" "${{ needs.build-portable.result }}" check_result build-linux-64 "$(expected_for "${linux_64}")" "${{ needs.build-linux-64.result }}" check_result build-linux-aarch64 "$(expected_for "${linux_aarch64}")" "${{ needs.build-linux-aarch64.result }}" check_result build-windows "$(expected_for "${windows}")" "${{ needs.build-windows.result }}" diff --git a/.github/workflows/test-wheel-linux.yml b/.github/workflows/test-wheel-linux.yml index b37467a4e8f..1eeebe6bcf2 100644 --- a/.github/workflows/test-wheel-linux.yml +++ b/.github/workflows/test-wheel-linux.yml @@ -29,10 +29,6 @@ on: Defaults to the current run when empty. type: string default: '' - portable-run-id: - description: "Workflow run ID containing cuda.pathfinder and metapackage wheels" - type: string - default: '' test-mode: description: > Test mode: 'standard' (default), 'nightly-pytorch', @@ -142,14 +138,6 @@ jobs: MATRIX_ENV: ${{ toJSON(matrix.ENV) }} run: echo "$MATRIX_ENV" | jq -r 'to_entries[] | "\(.key)=\(.value)"' >> "$GITHUB_ENV" - - name: Download portable Moon lane - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: moon-lane-build-portable - path: . - run-id: ${{ inputs.portable-run-id || inputs.run-id || github.run_id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Download native Moon lane uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -235,6 +223,7 @@ jobs: - name: Run standard tests with Moon if: ${{ inputs.test-mode == 'standard' }} env: + CUDA_PYTHON_USE_STAGED_BINDINGS_VERSION: "1" CUDA_VER: ${{ matrix.CUDA_VER }} LOCAL_CTK: ${{ matrix.LOCAL_CTK }} run: | diff --git a/.github/workflows/test-wheel-windows.yml b/.github/workflows/test-wheel-windows.yml index 20b7b00cc10..f11eb34c66b 100644 --- a/.github/workflows/test-wheel-windows.yml +++ b/.github/workflows/test-wheel-windows.yml @@ -29,10 +29,6 @@ on: Defaults to the current run when empty. type: string default: '' - portable-run-id: - description: "Workflow run ID containing cuda.pathfinder and metapackage wheels" - type: string - default: '' test-mode: description: > Test mode: 'standard' (default), 'nightly-pytorch', @@ -137,14 +133,6 @@ jobs: shell: bash --noprofile --norc -xeuo pipefail {0} run: echo "$MATRIX_ENV" | jq -r 'to_entries[] | "\(.key)=\(.value)"' >> "$GITHUB_ENV" - - name: Download portable Moon lane - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: moon-lane-build-portable - path: . - run-id: ${{ inputs.portable-run-id || inputs.run-id || github.run_id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Download native Moon lane uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -218,6 +206,7 @@ jobs: - name: Run standard tests with Moon if: ${{ inputs.test-mode == 'standard' }} env: + CUDA_PYTHON_USE_STAGED_BINDINGS_VERSION: "1" CUDA_VER: ${{ matrix.CUDA_VER }} LOCAL_CTK: ${{ matrix.LOCAL_CTK }} shell: bash --noprofile --norc -xeuo pipefail {0} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bd6051e2ac6..d54a89f4a42 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -286,15 +286,31 @@ tasks, stage the matching previous-branch `cuda.bindings` wheel, activate the pr `core:wheel-previous`, then run `core:wheel-merge`. CI follows the same staged sequence and lets Moon parallelize independent work within each phase. -For ephemeral runners, CI uploads the portable `.moon/cache/hashes` and `.moon/cache/outputs` directories as +The Cython test-asset tasks have a similar environment boundary. CI changes from the wheel-build interpreter to the +test interpreter before running `bindings:cython-test-assets` or `core:cython-test-assets`, so their pathfinder, +bindings, and core wheels are staged inputs rather than executable Moon dependencies. To run either task locally, +first build or copy exactly one current wheel for each package into its corresponding `.moon-out` directory. + +For ephemeral runners, CI uploads Moon's `.moon/cache/hashes` and `.moon/cache/outputs` directories as ordinary immutable GitHub workflow artifacts. A later producer restores the lane-qualified artifact from the successful trusted `main` run at the exact merge-base commit, then runs `moon ci`; GitHub transports the local cache while Moon alone interprets its hashes and hydrates task outputs. Lane bundles also carry Moon's canonical task outputs between heterogeneous build and test runners, while conventional named wheel artifacts remain available for -release tooling. Context-sensitive documentation is rebuilt as four parallel Moon tasks whenever its runner is +release tooling. Native build lanes include the inexpensive cuda-pathfinder wheel directly; the Linux/Python 3.12 +lane also carries the cuda-python metapackage used by docs and releases. Context-sensitive documentation is rebuilt as four parallel Moon tasks whenever its runner is selected. Missing or incomplete cache artifacts conservatively allocate the producer runners and start with an empty cache. Generated `.moon/cache` and `.moon-out` directories are ignored by Git. +CI sets `CUDA_PYTHON_USE_STAGED_BINDINGS_VERSION=1` when the metapackage must match a trusted staged +`cuda.bindings` development wheel. Leave this variable unset for normal local builds; `root:pure-wheel` then derives +the metapackage version from the current checkout and ignores any stale staged bindings output. + +The draft reuses only immutable cache artifacts from a successful trusted `main` run at the exact merge-base. It does +not yet treat wheel or sdist tasks as hermetic enough for a general cross-revision remote cache: their isolated build +environments still resolve ranged build dependencies and rely on runner/container compiler and repair-tool images. +Before enabling that broader cache, pin the isolated build constraints and immutable toolchain images (and include +their identities in task fingerprints), or leave those producer tasks out of the persistent cache. + ### CI Pipeline Flow ![CUDA Python CI Pipeline Flow](ci/ci-pipeline.svg) diff --git a/ci/tools/artifacts.py b/ci/tools/artifacts.py deleted file mode 100644 index 98a274104fe..00000000000 --- a/ci/tools/artifacts.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python3 - -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 - -"""Shared path and artifact helpers for Moon CI tasks.""" - -from __future__ import annotations - -import os -import shutil -import subprocess -from pathlib import Path - -REPO_ROOT = Path(__file__).resolve().parents[2] -PROJECT_PATHS = { - "root": Path("."), - "pathfinder": Path("cuda_pathfinder"), - "bindings": Path("cuda_bindings"), - "core": Path("cuda_core"), - "metapackage": Path("cuda_python"), -} - - -def run(command: list[str], *, cwd: Path = REPO_ROOT, env: dict[str, str] | None = None) -> None: - print(f"+ {subprocess.list2cmdline(command)}", flush=True) - subprocess.run(command, cwd=cwd, env=env, check=True) # noqa: S603 - - -def project_path(project: str) -> Path: - try: - relative = PROJECT_PATHS[project] - except KeyError as error: - raise ValueError(f"unknown project: {project}") from error - return REPO_ROOT / relative - - -def output_path(project: str, directory: str) -> Path: - repo_root = Path(os.path.abspath(REPO_ROOT)) - project_root = Path(os.path.abspath(project_path(project))) - if project_root != repo_root and repo_root not in project_root.parents: - raise ValueError(f"project must be within {repo_root}: {project_root}") - output_root = project_root / ".moon-out" - output = Path(os.path.abspath(output_root / directory)) - if output != output_root and output_root not in output.parents: - raise ValueError(f"output must be within {output_root}: {output}") - current = output - while current != repo_root: - if current.is_symlink(): - raise ValueError(f"output path must not traverse a symlink: {current}") - current = current.parent - return output - - -def reset_output(output: Path) -> None: - if output.exists(): - if output.is_symlink() or not output.is_dir(): - raise ValueError(f"refusing to replace non-directory output: {output}") - shutil.rmtree(output) - output.mkdir(parents=True) - - -def find_one(directory: Path, pattern: str, description: str) -> Path: - selected = sorted(path for path in directory.glob(pattern) if path.is_file()) - if len(selected) != 1: - raise RuntimeError(f"expected one {description} in {directory}, found {len(selected)}") - return selected[0] - - -def find_one_in(directories: list[Path], pattern: str, description: str) -> Path: - for directory in directories: - selected = sorted(path for path in directory.glob(pattern) if path.is_file()) - if len(selected) == 1: - return selected[0] - if len(selected) > 1: - raise RuntimeError(f"expected one {description} in {directory}, found {len(selected)}") - searched = ", ".join(str(path) for path in directories) - raise RuntimeError(f"expected one {description}; searched {searched}") - - -def artifact_wheel(project: str, lane: str) -> Path: - if project == "pathfinder": - directories = [output_path(project, "wheel-pure"), project_path(project)] - elif project == "bindings": - environment = os.environ.get("CUDA_BINDINGS_ARTIFACTS_DIR") - directories = [output_path(project, f"wheel-{lane}")] - if lane == "previous": - directories.append(project_path(project) / "dist-prev") - elif environment: - directories.append(Path(environment)) - directories.append(project_path(project) / "dist") - elif project == "core": - environment = os.environ.get("CUDA_CORE_ARTIFACTS_DIR") - directories = [output_path(project, f"wheel-{lane}")] - if environment: - directories.append(Path(environment)) - directories.append(project_path(project) / "dist") - elif project == "metapackage": - directories = [output_path(project, "wheel-pure"), REPO_ROOT, project_path(project)] - else: - raise ValueError(f"project does not produce wheel artifacts: {project}") - return find_one_in(directories, "*.whl", f"{project} {lane} wheel") diff --git a/ci/tools/build_artifacts.py b/ci/tools/build_artifacts.py deleted file mode 100644 index 635b8294ca3..00000000000 --- a/ci/tools/build_artifacts.py +++ /dev/null @@ -1,177 +0,0 @@ -#!/usr/bin/env python3 - -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 - -"""Build cacheable Python artifacts declared by the Moon project graph.""" - -from __future__ import annotations - -import argparse -import os -import shutil -import sys -from pathlib import Path - -from ci.tools.artifacts import artifact_wheel, find_one, output_path, project_path, reset_output, run - -PACKAGE_PROJECTS = ("pathfinder", "bindings", "core", "metapackage") - - -def _cuda_major(lane: str) -> str: - variable = "BUILD_CUDA_MAJOR" if lane == "current" else "BUILD_PREV_CUDA_MAJOR" - value = os.environ.get(variable, "") - if value: - return value - if lane == "current": - version = os.environ.get("BUILD_CUDA_VER") or os.environ.get("CUDA_VER", "") - if version: - return version.split(".", maxsplit=1)[0] - raise RuntimeError(f"{variable} is required for the {lane} CUDA lane") - - -def _constraint_uri(path: Path, *, in_linux_container: bool) -> str: - resolved = path.resolve() - return f"file:///host{resolved.as_posix()}" if in_linux_container else resolved.as_uri() - - -def _constraint_environment( - project: str, - lane: str, - *, - cibuildwheel: bool, - from_sdist: bool = False, -) -> dict[str, str]: - if project not in {"bindings", "core"}: - return os.environ.copy() - - constraints = output_path(project, f"constraints-{lane}") - reset_output(constraints) - constraint_file = constraints / "build.txt" - linux_container = cibuildwheel and os.name != "nt" - pathfinder_wheel = ( - find_one(output_path("pathfinder", "sdist"), "*.whl", "cuda.pathfinder sdist wheel") - if from_sdist - else artifact_wheel("pathfinder", "pure") - ) - requirements = [("cuda-pathfinder", pathfinder_wheel)] - if project == "core": - bindings_wheel = ( - find_one(output_path("bindings", "sdist"), "*.whl", "cuda.bindings sdist wheel") - if from_sdist - else artifact_wheel("bindings", lane) - ) - requirements.append(("cuda-bindings", bindings_wheel)) - constraint_file.write_text( - "".join( - f"{distribution} @ {_constraint_uri(wheel, in_linux_container=linux_container)}\n" - for distribution, wheel in requirements - ), - encoding="utf-8", - ) - - environment = os.environ.copy() - host_constraint = str(constraint_file.resolve()) - environment["PIP_BUILD_CONSTRAINT"] = host_constraint - environment["PIP_CONSTRAINT"] = host_constraint - if project == "core": - environment["CUDA_CORE_BUILD_MAJOR"] = _cuda_major(lane) - if cibuildwheel: - setting = "CIBW_ENVIRONMENT_WINDOWS" if os.name == "nt" else "CIBW_ENVIRONMENT_LINUX" - container_constraint = f"/host{constraint_file.resolve().as_posix()}" if linux_container else host_constraint - additions = [ - f'PIP_BUILD_CONSTRAINT="{container_constraint}"', - f'PIP_CONSTRAINT="{container_constraint}"', - ] - if project == "core": - additions.append(f"CUDA_CORE_BUILD_MAJOR={_cuda_major(lane)}") - environment[setting] = " ".join(filter(None, [environment.get(setting, ""), *additions])) - return environment - - -def _ensure_owned(output: Path) -> None: - if os.name == "nt": - return - owners = {path.stat().st_uid for path in output.rglob("*")} - if not owners or owners == {os.getuid()}: - return - sudo = shutil.which("sudo") - if sudo is None: - raise RuntimeError(f"cibuildwheel output is not owned by this user and sudo was not found: {output}") - run([sudo, "chown", "-R", f"{os.getuid()}:{os.getgid()}", str(output)]) - - -def _pure_wheel(project: str) -> None: - if project not in {"pathfinder", "metapackage"}: - raise ValueError("pure-wheel only supports pathfinder and metapackage") - output = output_path(project, "wheel-pure") - reset_output(output) - run( - [sys.executable, "-m", "pip", "wheel", "--verbose", "--no-deps", "--wheel-dir", str(output), "."], - cwd=project_path(project), - ) - find_one(output, "*.whl", f"{project} wheel") - - -def _native_wheel(project: str, lane: str) -> None: - if project not in {"bindings", "core"}: - raise ValueError("native-wheel only supports bindings and core") - if project == "bindings" and lane != "current": - raise ValueError("cuda.bindings is only built in the current lane") - output = output_path(project, f"wheel-{lane}") - reset_output(output) - environment = _constraint_environment(project, lane, cibuildwheel=True) - run( - [sys.executable, "-m", "cibuildwheel", "--output-dir", str(output), str(project_path(project))], - env=environment, - ) - _ensure_owned(output) - wheel = find_one(output, "*.whl", f"{project} {lane} wheel") - if project == "core": - wheel.rename(wheel.with_name(f"{wheel.stem}.cu{_cuda_major(lane)}.whl")) - - -def _sdist(project: str) -> None: - project_root = project_path(project) - output = output_path(project, "sdist") - reset_output(output) - environment = ( - _constraint_environment(project, "current", cibuildwheel=False, from_sdist=True) - if project in {"bindings", "core"} - else os.environ.copy() - ) - run([sys.executable, "-m", "build", "--sdist", "--outdir", str(output), str(project_root)], env=environment) - archive = find_one(output, "*.tar.gz", f"{project} source distribution") - run( - [sys.executable, "-m", "pip", "wheel", "--no-deps", "--wheel-dir", str(output), str(archive)], - env=environment, - ) - find_one(output, "*.whl", f"{project} wheel from source distribution") - - -def _parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser(description=__doc__) - subparsers = parser.add_subparsers(dest="command", required=True) - pure = subparsers.add_parser("pure-wheel") - pure.add_argument("project", choices=("pathfinder", "metapackage")) - native = subparsers.add_parser("native-wheel") - native.add_argument("project", choices=("bindings", "core")) - native.add_argument("--lane", choices=("current", "previous"), required=True) - sdist = subparsers.add_parser("sdist") - sdist.add_argument("project", choices=PACKAGE_PROJECTS) - return parser - - -def main() -> None: - args = _parser().parse_args() - if args.command == "pure-wheel": - _pure_wheel(args.project) - elif args.command == "native-wheel": - _native_wheel(args.project, args.lane) - else: - _sdist(args.project) - - -if __name__ == "__main__": - main() diff --git a/ci/tools/merge_cuda_core_wheels.py b/ci/tools/merge_cuda_core_wheels.py index 008d812bd38..6f95c97ec30 100644 --- a/ci/tools/merge_cuda_core_wheels.py +++ b/ci/tools/merge_cuda_core_wheels.py @@ -28,21 +28,24 @@ import zipfile from pathlib import Path -REPO_ROOT = Path(__file__).resolve().parents[2] +def _wheel_from_directory(directory: Path) -> Path: + wheels = sorted(path for path in directory.glob("*.whl") if path.is_file()) + if len(wheels) != 1: + raise ValueError(f"expected one wheel in {directory}, found {len(wheels)}") + return wheels[0] -def _validated_moon_output(path: Path) -> Path: - repo_root = Path(os.path.abspath(REPO_ROOT)) - output_root = repo_root / "cuda_core" / ".moon-out" - output = Path(os.path.abspath(path if path.is_absolute() else repo_root / path)) - if output_root not in output.parents: - raise ValueError(f"clean output must be below {output_root}: {output}") - current = output - while current != repo_root: - if current.is_symlink(): - raise ValueError(f"output path must not traverse a symlink: {current}") - current = current.parent - return output + +def _clean_output_wheels(output_dir: Path) -> None: + """Remove wheel files without recursively deleting the caller's directory.""" + if output_dir.is_symlink(): + raise ValueError(f"output path must not be a symlink: {output_dir}") + if output_dir.exists() and not output_dir.is_dir(): + raise ValueError(f"output path is not a directory: {output_dir}") + output_dir.mkdir(parents=True, exist_ok=True) + for wheel in output_dir.glob("*.whl"): + if wheel.is_file() or wheel.is_symlink(): + wheel.unlink() def run_command(cmd: list[str], cwd: Path | None = None, env: dict = os.environ) -> subprocess.CompletedProcess: @@ -234,7 +237,11 @@ def main(): help="Directory containing exactly one input wheel (may be repeated)", ) parser.add_argument("--output-dir", "-o", default="dist", help="Output directory for merged wheel") - parser.add_argument("--clean-output", action="store_true", help="Remove the output directory before merging") + parser.add_argument( + "--clean-output", + action="store_true", + help="Remove existing wheel files from the output directory before merging", + ) args = parser.parse_args() @@ -255,22 +262,24 @@ def main(): for directory_value in args.wheel_dir: directory = Path(directory_value) - selected = sorted(path for path in directory.glob("*.whl") if path.is_file()) - if len(selected) != 1: - print(f"Error: Expected one wheel in {directory}, found {len(selected)}", file=sys.stderr) + try: + wheel = _wheel_from_directory(directory) + except ValueError as error: + print(f"Error: {error}", file=sys.stderr) sys.exit(1) - wheels.append(selected[0]) + wheels.append(wheel) if not wheels: print("Error: No wheels provided", file=sys.stderr) sys.exit(1) - output_dir = _validated_moon_output(Path(args.output_dir)) if args.clean_output else Path(args.output_dir) - if args.clean_output and output_dir.exists(): - if output_dir.is_symlink() or not output_dir.is_dir(): - print(f"Error: Refusing to replace non-directory output: {output_dir}", file=sys.stderr) + output_dir = Path(args.output_dir) + if args.clean_output: + try: + _clean_output_wheels(output_dir) + except ValueError as error: + print(f"Error: {error}", file=sys.stderr) sys.exit(1) - shutil.rmtree(output_dir) output_dir.mkdir(parents=True, exist_ok=True) # Check that we have wheel tool available diff --git a/ci/tools/moon_fingerprint.py b/ci/tools/moon_fingerprint.py deleted file mode 100644 index 6acb39d2b48..00000000000 --- a/ci/tools/moon_fingerprint.py +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env python3 - -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 - -"""Print a deterministic build-environment fingerprint for a Moon task.""" - -from __future__ import annotations - -import argparse -import hashlib -import importlib.metadata -import json -import os -import platform -import shlex -import shutil -import subprocess -import sysconfig -from pathlib import Path - -REPO_ROOT = Path(__file__).resolve().parents[2] -SCM_MATCH = { - "pathfinder": "cuda-pathfinder-v*[0-9]*", - "bindings": "v*[0-9]*", - "core": "cuda-core-v*[0-9]*", - "metapackage": "v*[0-9]*", -} -SCM_DISTRIBUTION = { - "pathfinder": "CUDA_PATHFINDER", - "bindings": "CUDA_BINDINGS", - "core": "CUDA_CORE", - "metapackage": "CUDA_PYTHON", -} -SCM_GLOBAL_VARIABLES = ( - "SETUPTOOLS_SCM_PRETEND_METADATA", - "SETUPTOOLS_SCM_PRETEND_VERSION", - "SOURCE_DATE_EPOCH", - "VCS_VERSIONING_PRETEND_METADATA", - "VCS_VERSIONING_PRETEND_VERSION", -) -SCM_DISTRIBUTION_VARIABLES = ( - "SETUPTOOLS_SCM_OVERRIDES_FOR_{distribution}", - "SETUPTOOLS_SCM_PRETEND_METADATA_FOR_{distribution}", - "SETUPTOOLS_SCM_PRETEND_VERSION_FOR_{distribution}", - "VCS_VERSIONING_PRETEND_METADATA_FOR_{distribution}", - "VCS_VERSIONING_PRETEND_VERSION_FOR_{distribution}", -) -LANE_VARIABLES = ( - "BUILD_CUDA_MAJOR", - "BUILD_CUDA_VER", - "BUILD_PREV_CUDA_MAJOR", - "CC", - "CIBW_ARCHS", - "CIBW_BUILD", - "CIBW_ENABLE", - "CL", - "CPLUS_INCLUDE_PATH", - "CXX", - "CUDA_CORE_BUILD_MAJOR", - "CUDA_PATH", - "CUDA_PYTHON_LANE", - "CUDA_VER", - "HOST_PLATFORM", - "PY_EXT_SUFFIX", - "PY_VER", -) -PYTHON_TOOLS = ("build", "cibuildwheel", "packaging", "pip", "setuptools", "setuptools-scm", "wheel") -TEST_ASSET_PYTHON_TOOLS = ("Cython", "numpy") - - -def _git_describe(pattern: str) -> str: - result = subprocess.run( # noqa: S603 - fixed git command with a package-defined tag pattern. - ["git", "describe", "--dirty", "--tags", "--long", "--match", pattern], # noqa: S607 - cwd=REPO_ROOT, - check=True, - stdout=subprocess.PIPE, - text=True, - ) - return result.stdout.strip() - - -def _distribution_version(name: str) -> str: - try: - return importlib.metadata.version(name) - except importlib.metadata.PackageNotFoundError: - return "" - - -def _configured_compilers() -> set[str]: - commands = {"cc", "c++", "cl", "nvcc"} - for variable, config_var in (("CC", "CC"), ("CXX", "CXX")): - configured = os.environ.get(variable) or sysconfig.get_config_var(config_var) or "" - try: - tokens = shlex.split(configured, posix=os.name != "nt") - except ValueError: - tokens = [] - commands.update(token for token in tokens if token and not token.startswith("-")) - return commands - - -def _native_tool_identities() -> dict[str, dict[str, object]]: - identities: dict[str, dict[str, object]] = {} - for command in sorted(_configured_compilers()): - executable = shutil.which(command) - if executable is None: - continue - result = subprocess.run( # noqa: S603 - commands are resolved compiler executables, not shell input. - [executable, "--version"], - check=False, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - timeout=10, - ) - identities[command] = { - "output": result.stdout.strip(), - "returncode": result.returncode, - } - return identities - - -def _scm_environment(project: str) -> dict[str, str]: - distribution = SCM_DISTRIBUTION[project] - distribution_variables = tuple(name.format(distribution=distribution) for name in SCM_DISTRIBUTION_VARIABLES) - variables = (*SCM_GLOBAL_VARIABLES, *distribution_variables) - return {name: os.environ.get(name, "") for name in variables} - - -def _scm_identity(project: str) -> dict[str, object]: - environment = _scm_environment(project) - pretend_variables = ["SETUPTOOLS_SCM_PRETEND_VERSION"] - # cuda_python/setup.py calls get_version without a distribution name, so - # setuptools-scm cannot apply its distribution-specific override there. - if project != "metapackage": - pretend_variables.append(f"SETUPTOOLS_SCM_PRETEND_VERSION_FOR_{SCM_DISTRIBUTION[project]}") - describe = ( - "" - if any(environment[name] for name in pretend_variables) - else _git_describe(SCM_MATCH[project]) - ) - return {"describe": describe, "environment": environment} - - -def fingerprint(project: str, lane: str) -> str: - python_tools = PYTHON_TOOLS + (TEST_ASSET_PYTHON_TOOLS if lane == "test-assets" else ()) - payload: dict[str, object] = { - "lane": lane, - "project": project, - "python": { - "implementation": platform.python_implementation(), - "soabi": sysconfig.get_config_var("SOABI") or "", - "version": platform.python_version(), - }, - "python_tools": {name: _distribution_version(name) for name in python_tools}, - "scm": _scm_identity(project), - } - if lane != "portable": - payload.update( - { - "environment": {name: os.environ.get(name, "") for name in LANE_VARIABLES}, - "platform": {"machine": platform.machine(), "system": platform.system()}, - } - ) - if lane == "test-assets": - payload["native_tools"] = _native_tool_identities() - encoded = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode() - return hashlib.sha256(encoded).hexdigest() - - -def main() -> None: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("project", choices=tuple(SCM_MATCH)) - parser.add_argument("lane", choices=("portable", "native", "previous", "sdist", "test-assets")) - args = parser.parse_args() - print(fingerprint(args.project, args.lane)) - - -if __name__ == "__main__": - main() diff --git a/ci/tools/prepare_test_assets.py b/ci/tools/prepare_test_assets.py deleted file mode 100644 index fe6c28418e1..00000000000 --- a/ci/tools/prepare_test_assets.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python3 - -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 - -"""Install the wheel and dependency inputs used to build native test assets.""" - -from __future__ import annotations - -import argparse -import sys - -from ci.tools.artifacts import artifact_wheel, project_path, run - - -def main() -> None: - argparse.ArgumentParser(description=__doc__).parse_args() - wheels = [ - artifact_wheel("pathfinder", "pure"), - artifact_wheel("bindings", "current"), - artifact_wheel("core", "current"), - ] - command = [sys.executable, "-m", "pip", "install", *(str(wheel) for wheel in wheels)] - for project in ("bindings", "core"): - command.extend(["--group", f"{project_path(project) / 'pyproject.toml'}:test"]) - run(command) - - -if __name__ == "__main__": - main() diff --git a/ci/tools/run_pixi_test.py b/ci/tools/run_pixi_test.py deleted file mode 100755 index d41b86bda47..00000000000 --- a/ci/tools/run_pixi_test.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python3 - -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 - -"""Run a package test in the caller-selected Pixi environment.""" - -from __future__ import annotations - -import argparse -import os -import shutil -import subprocess -from pathlib import Path - -REPO_ROOT = Path(__file__).resolve().parents[2] -PROJECT_PATHS = { - "pathfinder": Path("cuda_pathfinder"), - "bindings": Path("cuda_bindings"), - "core": Path("cuda_core"), -} - - -def main() -> None: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("project", choices=PROJECT_PATHS) - args = parser.parse_args() - - pixi = shutil.which("pixi") - if pixi is None: - raise RuntimeError("pixi is required for this task but was not found on PATH") - - command = [ - pixi, - "run", - "--manifest-path", - str(REPO_ROOT / PROJECT_PATHS[args.project] / "pixi.toml"), - ] - environment = os.environ.get("PIXI_ENVIRONMENT_NAME") - if environment: - command.extend(["--environment", environment]) - command.append("test") - - print(f"+ {subprocess.list2cmdline(command)}", flush=True) - subprocess.run(command, cwd=REPO_ROOT, check=True) # noqa: S603 - - -if __name__ == "__main__": - main() diff --git a/ci/tools/tests/test_moon_tasks.py b/ci/tools/tests/test_moon_tasks.py index 296c606fda6..97d03e7427c 100644 --- a/ci/tools/tests/test_moon_tasks.py +++ b/ci/tools/tests/test_moon_tasks.py @@ -11,190 +11,51 @@ import os import shutil import subprocess -import sys import tempfile import unittest -from argparse import Namespace from pathlib import Path -from unittest.mock import patch - -from ci.tools.artifacts import output_path -from ci.tools.build_artifacts import _cuda_major -from ci.tools.merge_cuda_core_wheels import _validated_moon_output -from ci.tools.moon_fingerprint import _native_tool_identities, _scm_identity, fingerprint -from ci.tools.run_pixi_test import main as run_pixi_test - - -class MoonArtifactOutputPathTest(unittest.TestCase): - def setUp(self) -> None: - self.temporary_directory = tempfile.TemporaryDirectory() - self.addCleanup(self.temporary_directory.cleanup) - self.repo = Path(self.temporary_directory.name) - (self.repo / "project").mkdir() - self.patches = ( - patch("ci.tools.artifacts.REPO_ROOT", self.repo), - patch.dict( - "ci.tools.artifacts.PROJECT_PATHS", - {"pathfinder": Path("project")}, - clear=True, - ), - ) - for active_patch in self.patches: - active_patch.start() - self.addCleanup(active_patch.stop) - - def test_confines_output_to_the_project_output_root(self) -> None: - output = output_path("pathfinder", "wheel") - self.assertEqual(output, self.repo / "project" / ".moon-out" / "wheel") - with self.assertRaisesRegex(ValueError, "output must be within"): - output_path("pathfinder", "../dist") - with self.assertRaisesRegex(ValueError, "output must be within"): - output_path("pathfinder", "../../outside") +from ci.tools.merge_cuda_core_wheels import _clean_output_wheels, _wheel_from_directory - def test_rejects_symlinked_output_ancestors(self) -> None: - outside = self.repo / "outside" - outside.mkdir() - (self.repo / "project" / ".moon-out").symlink_to(outside, target_is_directory=True) - with self.assertRaisesRegex(ValueError, "must not traverse a symlink"): - output_path("pathfinder", "wheel") - - def test_rejects_projects_outside_the_workspace(self) -> None: - with ( - patch.dict("ci.tools.artifacts.PROJECT_PATHS", {"pathfinder": Path("../outside")}), - self.assertRaisesRegex(ValueError, "project must be within"), - ): - output_path("pathfinder", "wheel") +class WheelMergerInputOutputTest(unittest.TestCase): + def test_selects_exactly_one_wheel_from_a_directory(self) -> None: + with tempfile.TemporaryDirectory() as temporary_directory: + wheel_dir = Path(temporary_directory) + wheel = wheel_dir / "cuda_core.whl" + wheel.touch() + self.assertEqual(_wheel_from_directory(wheel_dir), wheel) + (wheel_dir / "another.whl").touch() + with self.assertRaisesRegex(ValueError, "expected one wheel"): + _wheel_from_directory(wheel_dir) -class MoonCleanOutputPathTest(unittest.TestCase): - def test_core_merger_only_cleans_task_owned_output_directories(self) -> None: + def test_clean_output_only_removes_wheel_files(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: - repo = Path(temporary_directory) - output = repo / "cuda_core" / ".moon-out" / "wheel-merged" - with patch("ci.tools.merge_cuda_core_wheels.REPO_ROOT", repo): - self.assertEqual(_validated_moon_output(output), output) - self.assertEqual( - _validated_moon_output(Path("cuda_core/.moon-out/wheel-merged")), - output, - ) - with self.assertRaisesRegex(ValueError, "must be below"): - _validated_moon_output(repo / "cuda_core" / ".moon-out") - with self.assertRaisesRegex(ValueError, "must be below"): - _validated_moon_output(repo / "cuda_core" / "dist") - - -class MoonFingerprintTest(unittest.TestCase): - @patch("ci.tools.moon_fingerprint._git_describe") - def test_distribution_pretend_version_replaces_git_identity(self, git_describe) -> None: - with patch.dict( - "os.environ", - {"SETUPTOOLS_SCM_PRETEND_VERSION_FOR_CUDA_BINDINGS": "13.2.0"}, - clear=True, - ): - identity = _scm_identity("bindings") - - self.assertEqual(identity["describe"], "") - self.assertEqual( - identity["environment"]["SETUPTOOLS_SCM_PRETEND_VERSION_FOR_CUDA_BINDINGS"], - "13.2.0", - ) - git_describe.assert_not_called() - - @patch("ci.tools.moon_fingerprint._git_describe", return_value="v13.2.0-1-gabc") - def test_portable_fingerprint_includes_ambient_python(self, git_describe) -> None: - with ( - patch.dict("os.environ", {}, clear=True), - patch( - "ci.tools.moon_fingerprint.platform.python_version", - side_effect=("3.12.11", "3.13.7"), - ), - ): - first = fingerprint("metapackage", "portable") - second = fingerprint("metapackage", "portable") - - self.assertNotEqual(first, second) - self.assertEqual(git_describe.call_count, 2) - - @patch("ci.tools.moon_fingerprint._git_describe", return_value="cuda-core-v1.0.0-1-gabc") - def test_reproducibility_environment_changes_fingerprint(self, git_describe) -> None: - with patch.dict("os.environ", {"SOURCE_DATE_EPOCH": "1"}, clear=True): - first = fingerprint("core", "native") - with patch.dict("os.environ", {"SOURCE_DATE_EPOCH": "2"}, clear=True): - second = fingerprint("core", "native") - - self.assertNotEqual(first, second) - self.assertEqual(git_describe.call_count, 2) - - @patch("ci.tools.moon_fingerprint._git_describe", return_value="v13.2.0-1-gabc") - @patch("ci.tools.moon_fingerprint._native_tool_identities") - def test_test_asset_fingerprint_tracks_resolved_build_tools(self, native_tools, git_describe) -> None: - versions = {"Cython": "3.1.0", "numpy": "2.3.0"} - - def distribution_version(name: str) -> str: - return versions.get(name, "fixed") - - native_tools.return_value = {"cc": {"output": "cc 1", "returncode": 0}} - with patch("ci.tools.moon_fingerprint._distribution_version", side_effect=distribution_version): - first = fingerprint("bindings", "test-assets") - versions["Cython"] = "3.1.1" - second = fingerprint("bindings", "test-assets") - native_tools.return_value = {"cc": {"output": "cc 2", "returncode": 0}} - third = fingerprint("bindings", "test-assets") - - self.assertNotEqual(first, second) - self.assertNotEqual(second, third) - self.assertEqual(git_describe.call_count, 3) - - @patch("ci.tools.moon_fingerprint.subprocess.run") - @patch("ci.tools.moon_fingerprint.shutil.which") - @patch("ci.tools.moon_fingerprint._configured_compilers", return_value={"cc", "missing"}) - def test_native_tool_identity_uses_resolved_executables(self, compilers, which, run) -> None: - which.side_effect = lambda command: "/tools/cc" if command == "cc" else None - run.return_value = Namespace(stdout="cc 1.2\n", returncode=0) - - self.assertEqual( - _native_tool_identities(), - {"cc": {"output": "cc 1.2", "returncode": 0}}, - ) - run.assert_called_once_with( - ["/tools/cc", "--version"], - check=False, - stdout=-1, - stderr=-2, - text=True, - timeout=10, - ) - compilers.assert_called_once_with() + output = Path(temporary_directory) + wheel = output / "stale.whl" + unrelated = output / "keep.txt" + wheel.touch() + unrelated.touch() + _clean_output_wheels(output) -class MoonTaskCommandTest(unittest.TestCase): - def test_focused_tool_modules_are_directly_executable(self) -> None: - for module in ("ci.tools.build_artifacts", "ci.tools.prepare_test_assets"): - result = subprocess.run( # noqa: S603 - [sys.executable, "-m", module, "--help"], - cwd=Path(__file__).resolve().parents[3], - check=False, - capture_output=True, - text=True, - ) - self.assertEqual(result.returncode, 0, result.stderr) + self.assertFalse(wheel.exists()) + self.assertTrue(unrelated.exists()) + + def test_clean_output_rejects_a_symlinked_directory(self) -> None: + with tempfile.TemporaryDirectory() as temporary_directory: + root = Path(temporary_directory) + target = root / "target" + target.mkdir() + output = root / "output" + output.symlink_to(target, target_is_directory=True) - def test_local_pixi_test_forwards_the_selected_environment(self) -> None: - with ( - patch.dict("os.environ", {"PIXI_ENVIRONMENT_NAME": "cu12"}, clear=True), - patch("sys.argv", ["run_pixi_test.py", "core"]), - patch("ci.tools.run_pixi_test.shutil.which", return_value="/tools/pixi"), - patch("ci.tools.run_pixi_test.subprocess.run") as run, - ): - run_pixi_test() + with self.assertRaisesRegex(ValueError, "must not be a symlink"): + _clean_output_wheels(output) - command = run.call_args.args[0] - self.assertEqual(command[0], "/tools/pixi") - self.assertIn("cuda_core/pixi.toml", command[3]) - self.assertEqual(command[-3:], ["--environment", "cu12", "test"]) +class MoonTaskCommandTest(unittest.TestCase): def test_declared_unsupported_bindings_lane_skips_before_artifact_lookup(self) -> None: bash = shutil.which("bash") self.assertIsNotNone(bash) @@ -225,9 +86,53 @@ def test_non_main_bindings_lane_skips_metapackage_before_artifact_lookup(self) - self.assertEqual(result.returncode, 0, result.stderr) self.assertIn("BINDINGS_SOURCE is not main", result.stdout) - def test_native_builder_requires_the_lane_cuda_major(self) -> None: - with patch.dict("os.environ", {}, clear=True), self.assertRaisesRegex(RuntimeError, "BUILD_CUDA_MAJOR"): - _cuda_major("current") + def test_metapackage_smoke_validates_all_wheels_with_the_resolver(self) -> None: + bash = shutil.which("bash") + self.assertIsNotNone(bash) + assert bash is not None + with tempfile.TemporaryDirectory() as temporary_directory: + repo = Path(temporary_directory) + script = repo / "ci" / "tools" / "run-tests" + script.parent.mkdir(parents=True) + shutil.copy2(Path(__file__).resolve().parents[1] / "run-tests", script) + + wheel_dirs = { + "cuda_pathfinder/.moon-out/wheel-pure": "pathfinder.whl", + "cuda_bindings/.moon-out/wheel-current": "bindings.whl", + "cuda_core/.moon-out/wheel-merged": "core.whl", + "cuda_python/.moon-out/wheel-pure": "metapackage.whl", + } + for relative_directory, wheel_name in wheel_dirs.items(): + directory = repo / relative_directory + directory.mkdir(parents=True) + (directory / wheel_name).touch() + + command_log = repo / "commands.txt" + fake_python = repo / "bin" / "python" + fake_python.parent.mkdir() + fake_python.write_text('#!/usr/bin/env bash\nprintf "%s\\n" "$*" >> "$COMMAND_LOG"\n') + fake_python.chmod(0o755) + result = subprocess.run( # noqa: S603 + [bash, script, "metapackage"], + cwd=repo, + env={ + **os.environ, + "BINDINGS_SOURCE": "main", + "COMMAND_LOG": str(command_log), + "LOCAL_CTK": "0", + "PATH": f"{fake_python.parent}{os.pathsep}{os.environ['PATH']}", + }, + check=False, + capture_output=True, + text=True, + ) + + self.assertEqual(result.returncode, 0, result.stderr) + commands = command_log.read_text().splitlines() + self.assertEqual(len(commands), 1) + self.assertIn("bindings.whl", commands[0]) + self.assertIn("metapackage.whl[all]", commands[0]) + self.assertNotIn("--no-deps", commands[0]) if __name__ == "__main__": diff --git a/ci/tools/tests/test_moon_workspace.py b/ci/tools/tests/test_moon_workspace.py index cb2a1eeb1e5..f66fa0b60c0 100644 --- a/ci/tools/tests/test_moon_workspace.py +++ b/ci/tools/tests/test_moon_workspace.py @@ -12,6 +12,7 @@ import os import shutil import subprocess +import tempfile import unittest from pathlib import Path from typing import Any @@ -26,7 +27,6 @@ "test-helpers": "cuda_python_test_helpers", } EXECUTION_TAG_TARGETS = { - "ci-wheel-pure": {"pathfinder:wheel-pure", "metapackage:wheel-pure"}, "ci-wheel-current": {"bindings:wheel-current", "core:wheel-current"}, "ci-build-cython-assets": { "bindings:cython-test-assets", @@ -65,8 +65,8 @@ }, } RUNNER_TAG_TARGETS = { - "runner-build-portable": EXECUTION_TAG_TARGETS["ci-wheel-pure"], "runner-build-linux-64": { + "pathfinder:wheel-pure", "bindings:wheel-current", "core:wheel-current", "bindings:cython-test-assets", @@ -178,11 +178,10 @@ def affected(path: str) -> set[str]: def test_bindings_benchmark_smoke_uses_materialized_wheels(self) -> None: task = self.by_target["bindings:smoke-linux"] - self.assertIn("${SKIP_CUDA_BINDINGS_TEST:-0}", task["script"]) + self.assertIn("printenv SKIP_CUDA_BINDINGS_TEST", task["script"]) self.assertIn("cuda_pathfinder/.moon-out/wheel-pure/*.whl", task["script"]) self.assertIn("cuda_bindings/.moon-out/wheel-current/*.whl", task["script"]) - self.assertIn("${#pathfinder_wheels[@]} -eq 1", task["script"]) - self.assertIn("${#bindings_wheels[@]} -eq 1", task["script"]) + self.assertGreaterEqual(task["script"].count("[[ $# -eq 1 ]]"), 2) self.assertIn("benchmarks/cuda_bindings/run_pyperf.py", task["script"]) self.assertNotIn("moon_ci.py", str(task["inputs"])) @@ -207,26 +206,143 @@ def test_cached_producers_have_explicit_non_overlapping_outputs(self) -> None: for target in FINGERPRINTED_TARGETS: task = self.by_target[target] self.assertTrue(task.get("checks"), target) - self.assertIn({"file": "/ci/tools/moon_fingerprint.py"}, task["inputs"]) - - def test_tasks_use_focused_commands_instead_of_an_omnibus_dispatcher(self) -> None: + scripts = [check["script"] for check in task["checks"]] + self.assertTrue(any("git describe" in script for script in scripts), target) + self.assertTrue(any("SETUPTOOLS_SCM_" in script for script in scripts), target) + self.assertTrue(any("python_implementation" in script for script in scripts), target) + self.assertFalse(task.get("inputEnv"), target) + self.assertNotIn("moon_fingerprint.py", json.dumps(task), target) + self.assertNotIn("ACTIONS_RUNTIME", "\n".join(scripts), target) + + for target in ("bindings:wheel-current", "core:wheel-current", "core:wheel-previous"): + scripts = "\n".join(check["script"] for check in self.by_target[target]["checks"]) + self.assertIn("CUDA_PYTHON_COVERAGE", scripts, target) + self.assertIn("name.startswith('CIBW_')", scripts, target) + self.assertIn("ACTIONS_VALUE=", scripts, target) + self.assertIn("hashlib.sha256", scripts, target) + + def test_artifact_commands_are_encoded_in_moon(self) -> None: artifact_commands = { - "pathfinder:wheel-pure": ["pure-wheel", "pathfinder"], - "pathfinder:sdist": ["sdist", "pathfinder"], - "bindings:wheel-current": ["native-wheel", "bindings", "--lane", "current"], - "bindings:sdist": ["sdist", "bindings"], - "core:wheel-current": ["native-wheel", "core", "--lane", "current"], - "core:wheel-previous": ["native-wheel", "core", "--lane", "previous"], - "core:sdist": ["sdist", "core"], - "metapackage:wheel-pure": ["pure-wheel", "metapackage"], - "metapackage:sdist": ["sdist", "metapackage"], + "pathfinder:wheel-pure": "python -m pip wheel", + "pathfinder:sdist": "python -m build --sdist", + "bindings:wheel-current": "python -m cibuildwheel", + "bindings:sdist": "python -m build --sdist", + "core:wheel-current": "python -m cibuildwheel", + "core:wheel-previous": "python -m cibuildwheel", + "core:sdist": "python -m build --sdist", + "metapackage:wheel-pure": "python -m pip wheel", + "metapackage:sdist": "python -m build --sdist", } - for target, arguments in artifact_commands.items(): + for target, expected_command in artifact_commands.items(): task = self.by_target[target] - self.assertEqual(task["command"], "python") - self.assertEqual(task["args"], ["-m", "ci.tools.build_artifacts", *arguments]) - self.assertIn({"file": "/ci/tools/artifacts.py"}, task["inputs"]) - self.assertIn({"file": "/ci/tools/build_artifacts.py"}, task["inputs"]) + self.assertEqual(task["command"], "bash") + self.assertFalse(task["options"]["shell"], target) + self.assertEqual(task["args"][:3], ["-euo", "pipefail", "-c"]) + self.assertIn(expected_command, task["args"][3]) + self.assertIn(".moon-out/", task["args"][3]) + self.assertIn("[[ $# -eq 1 ]]", task["args"][3]) + + metapackage = self.by_target["metapackage:wheel-pure"] + self.assertIn({"glob": "/cuda_bindings/.moon-out/wheel-current/*.whl", "cache": True}, metapackage["inputs"]) + self.assertIn("CUDA_PYTHON_USE_STAGED_BINDINGS_VERSION", metapackage["args"][3]) + self.assertIn("SETUPTOOLS_SCM_PRETEND_VERSION_FOR_CUDA_PYTHON", metapackage["args"][3]) + + def test_metapackage_uses_staged_bindings_version_only_when_requested(self) -> None: + bash = shutil.which("bash") + self.assertIsNotNone(bash) + assert bash is not None + script = self.by_target["metapackage:wheel-pure"]["args"][3] + + with tempfile.TemporaryDirectory() as temporary_directory: + workspace = Path(temporary_directory) + bindings = workspace / "cuda_bindings" / ".moon-out" / "wheel-current" + bindings.mkdir(parents=True) + (bindings / "stale.whl").touch() + + command_log = workspace / "commands.txt" + fake_python = workspace / "bin" / "python" + fake_python.parent.mkdir() + fake_python.write_text( + """#!/usr/bin/env bash +printf '%s|%s|%s\n' "$*" "${SETUPTOOLS_SCM_PRETEND_VERSION-}" "${SETUPTOOLS_SCM_PRETEND_VERSION_FOR_CUDA_PYTHON-}" >> "$COMMAND_LOG" +if [[ "$1" == "-c" ]]; then + printf '%s\n' '13.3.2.dev1' + exit 0 +fi +mkdir -p cuda_python/.moon-out/wheel-pure +touch cuda_python/.moon-out/wheel-pure/cuda_python.whl +""", + encoding="utf-8", + ) + fake_python.chmod(0o755) + + def run(mode: str | None) -> subprocess.CompletedProcess[str]: + command_log.unlink(missing_ok=True) + environment = { + **os.environ, + "COMMAND_LOG": str(command_log), + "PATH": f"{fake_python.parent}{os.pathsep}{os.environ['PATH']}", + } + environment.pop("CUDA_PYTHON_USE_STAGED_BINDINGS_VERSION", None) + if mode is not None: + environment["CUDA_PYTHON_USE_STAGED_BINDINGS_VERSION"] = mode + return subprocess.run( # noqa: S603 + [bash, "-euo", "pipefail", "-c", script], + cwd=workspace, + env=environment, + check=False, + capture_output=True, + text=True, + ) + + local = run(None) + self.assertEqual(local.returncode, 0, local.stderr) + self.assertEqual(len(command_log.read_text(encoding="utf-8").splitlines()), 1) + + staged = run("1") + self.assertEqual(staged.returncode, 0, staged.stderr) + staged_commands = command_log.read_text(encoding="utf-8").splitlines() + self.assertEqual(len(staged_commands), 2) + self.assertTrue(staged_commands[-1].endswith("|13.3.2.dev1|13.3.2.dev1")) + + invalid = run("true") + self.assertNotEqual(invalid.returncode, 0) + self.assertIn("must be unset or 1", invalid.stderr) + + def test_explicit_commands_do_not_use_moons_extra_shell_wrapper(self) -> None: + for task in self.tasks: + if task["command"] not in {"noop", "set"}: + self.assertFalse(task["options"]["shell"], task["target"]) + + def test_embedded_bash_preserves_runtime_variables_for_moon(self) -> None: + scripts: dict[str, str] = {} + for target, task in self.by_target.items(): + if task.get("script"): + scripts[target] = task["script"] + elif task.get("command") == "bash" and task.get("args", [])[:3] == ["-euo", "pipefail", "-c"]: + scripts[target] = task["args"][3] + for target, script in scripts.items(): + self.assertNotIn("${#", script, target) + self.assertNotIn("!}", script, target) + self.assertNotRegex(script, r"\$\{[^}]*\[[^}]*\}", target) + self.assertNotRegex(script, r"\$\{[^}]*%[^}]*\}", target) + self.assertNotRegex(script, r"\$\{[^}]*:-[^}]*\}", target) + self.assertNotRegex(script, r"\$[a-z_]", target) + + runtime_locals = { + "pathfinder:sdist": ("$ARCHIVE",), + "bindings:wheel-current": ("$OUTPUT", "$PATHFINDER_WHEEL"), + "bindings:sdist": ("$CONSTRAINT_FILE", "$ARCHIVE"), + "bindings:smoke-linux": ("$SKIP", "$BINDINGS_WHEEL"), + "core:wheel-current": ("$OUTPUT", "$WHEEL_WITHOUT_SUFFIX"), + "core:wheel-previous": ("$OUTPUT", "$WHEEL_WITHOUT_SUFFIX"), + "core:sdist": ("$CONSTRAINT_FILE", "$ARCHIVE"), + "metapackage:sdist": ("$ARCHIVE",), + "test-helpers:prepare-test-assets": ("$PATHFINDER_WHEEL", "$CORE_WHEEL"), + } + for target, markers in runtime_locals.items(): + for marker in markers: + self.assertIn(marker, scripts[target], target) for target, project in { "pathfinder:test-installed-linux": "pathfinder", @@ -245,10 +361,10 @@ def test_tasks_use_focused_commands_instead_of_an_omnibus_dispatcher(self) -> No self.assertEqual(task["command"], "bash") self.assertEqual(task["args"], ["ci/tools/run-tests", project]) - self.assertEqual( - self.by_target["test-helpers:prepare-test-assets"]["args"], - ["-m", "ci.tools.prepare_test_assets"], - ) + preparation = self.by_target["test-helpers:prepare-test-assets"] + self.assertEqual(preparation["command"], "bash") + self.assertEqual(preparation["args"][:3], ["-euo", "pipefail", "-c"]) + self.assertIn("python -m pip install", preparation["args"][3]) self.assertIn("--clean-output", self.by_target["core:wheel-merge"]["args"]) self.assertIn("--output-dir", self.by_target["core:test-binaries"]["args"]) for target in ("bindings:cython-test-assets", "core:cython-test-assets"): @@ -257,10 +373,15 @@ def test_tasks_use_focused_commands_instead_of_an_omnibus_dispatcher(self) -> No def test_same_environment_build_dependencies_use_output_bytes(self) -> None: expected = { + "bindings:wheel-current": {"pathfinder:wheel-pure"}, "core:wheel-current": {"bindings:wheel-current"}, + "core:wheel-previous": {"pathfinder:wheel-pure"}, "bindings:sdist": {"pathfinder:sdist"}, "core:sdist": {"pathfinder:sdist", "bindings:sdist"}, "metapackage:sdist": {"bindings:sdist"}, + "metapackage:test-installed-linux": {"metapackage:wheel-pure"}, + "metapackage:test-installed-windows": {"metapackage:wheel-pure"}, + "metapackage:docs-ci": {"metapackage:wheel-pure"}, "root:docs-ci": { "pathfinder:docs-ci", "bindings:docs-ci", @@ -291,7 +412,7 @@ def test_platform_test_tasks_are_serialized_and_os_scoped(self) -> None: # would force unaffected test suites to run merely to serialize the # shared interpreter; the mutex provides that serialization instead. for target in EXECUTION_TAG_TARGETS["ci-test-linux"] | EXECUTION_TAG_TARGETS["ci-test-windows"]: - if not target.startswith("pathfinder:"): + if not target.startswith("pathfinder:") and not target.startswith("metapackage:"): self.assertFalse(self.by_target[target].get("deps"), target) def test_pathfinder_strictness_and_preparation_are_in_the_graph(self) -> None: @@ -325,7 +446,6 @@ def test_docs_components_run_in_parallel_before_assembly(self) -> None: root_inputs = docs["inputs"] self.assertIn({"project": "core", "group": "package"}, root_inputs) self.assertIn({"project": "metapackage", "group": "docs"}, root_inputs) - self.assertIn({"file": "/.github/workflows/build-pure-wheel.yml"}, root_inputs) self.assertIn({"file": "/.github/workflows/build-wheel.yml"}, root_inputs) for target in EXECUTION_TAG_TARGETS["ci-docs"] - {"root:docs-ci"}: task = self.by_target[target] @@ -358,7 +478,10 @@ def test_local_pixi_tasks_remain_available_and_skip_ci(self) -> None: self.assertFalse(self.by_target[target]["options"]["runInCI"]) for target in ("pathfinder:test", "bindings:test", "core:test"): task = self.by_target[target] - self.assertEqual(task["args"][:1], ["ci/tools/run_pixi_test.py"]) + self.assertEqual(task["command"], "bash") + self.assertEqual(task["args"][:3], ["-euo", "pipefail", "-c"]) + self.assertIn("PIXI_ENVIRONMENT_NAME", task["args"][3]) + self.assertIn("exec pixi", task["args"][3]) self.assertFalse(task["options"]["runInCI"]) for target in ( "pathfinder:docs", @@ -370,11 +493,48 @@ def test_local_pixi_tasks_remain_available_and_skip_ci(self) -> None: self.assertEqual(task["command"], "pixi") self.assertFalse(task["options"]["runInCI"]) - def test_omnibus_moon_helper_is_removed(self) -> None: - self.assertFalse((REPO_ROOT / "ci" / "tools" / "moon_ci.py").exists()) + def test_moon_task_helpers_are_removed(self) -> None: + removed = ( + "artifacts.py", + "build_artifacts.py", + "moon_ci.py", + "moon_fingerprint.py", + "prepare_test_assets.py", + "run_pixi_test.py", + ) + for filename in removed: + self.assertFalse((REPO_ROOT / "ci" / "tools" / filename).exists(), filename) for task in self.tasks: - self.assertNotIn({"file": "/ci/tools/moon_ci.py"}, task.get("inputs", []), task["target"]) - self.assertNotIn("ci/tools/moon_ci.py", task.get("args", []), task["target"]) + serialized = json.dumps(task) + for filename in removed: + self.assertNotIn(filename, serialized, task["target"]) + + def test_universal_wheels_share_existing_runner_lanes(self) -> None: + self.assertFalse((REPO_ROOT / ".github" / "workflows" / "build-pure-wheel.yml").exists()) + self.assertFalse({task["target"] for task in self.tasks if "ci-wheel-pure" in task.get("tags", [])}) + self.assertFalse({task["target"] for task in self.tasks if "runner-build-portable" in task.get("tags", [])}) + pathfinder = self.by_target["pathfinder:wheel-pure"] + self.assertTrue( + {"runner-build-linux-64", "runner-build-linux-aarch64", "runner-build-windows"} <= set(pathfinder["tags"]) + ) + self.assertFalse( + {tag for tag in self.by_target["metapackage:wheel-pure"].get("tags", []) if tag.startswith("runner-")} + ) + self.assertNotIn( + "build-pure-wheel.yml", + "\n".join(path.read_text(encoding="utf-8") for path in REPO_ROOT.rglob("moon.yml")), + ) + native_workflow = (REPO_ROOT / ".github" / "workflows" / "build-wheel.yml").read_text(encoding="utf-8") + self.assertIn("cuda_pathfinder/.moon-out/wheel-pure", native_workflow) + self.assertIn("cuda_python/.moon-out/wheel-pure", native_workflow) + for relative_path in ( + ".github/workflows/build-wheel.yml", + ".github/workflows/build-docs.yml", + ".github/workflows/test-wheel-linux.yml", + ".github/workflows/test-wheel-windows.yml", + ): + workflow = (REPO_ROOT / relative_path).read_text(encoding="utf-8") + self.assertIn('CUDA_PYTHON_USE_STAGED_BINDINGS_VERSION: "1"', workflow, relative_path) def test_workspace_disables_python_and_dependency_management(self) -> None: workspace = (REPO_ROOT / ".moon" / "workspace.yml").read_text(encoding="utf-8") diff --git a/cuda_bindings/moon.yml b/cuda_bindings/moon.yml index c868047833b..ed2c51bfe5a 100644 --- a/cuda_bindings/moon.yml +++ b/cuda_bindings/moon.yml @@ -18,6 +18,7 @@ taskOptions: cache: false runFromWorkspaceRoot: true runInCI: false + shell: false fileGroups: package: @@ -55,8 +56,80 @@ fileGroups: tasks: wheel-current: - command: python - args: [-m, ci.tools.build_artifacts, native-wheel, bindings, --lane, current] + command: bash + args: + - -euo + - pipefail + - -c + - | + reset_directory() { + local DIRECTORY="$1" + local ROOT + ROOT=$(dirname -- "$DIRECTORY") + if [[ -L "$ROOT" || ( -e "$ROOT" && ! -d "$ROOT" ) ]]; then + echo "refusing to use non-directory output root: $ROOT" >&2 + exit 1 + fi + if [[ -L "$DIRECTORY" || ( -e "$DIRECTORY" && ! -d "$DIRECTORY" ) ]]; then + echo "refusing to replace non-directory output: $DIRECTORY" >&2 + exit 1 + fi + mkdir -p "$ROOT" + rm -rf -- "$DIRECTORY" + mkdir -p "$DIRECTORY" + } + + OUTPUT=cuda_bindings/.moon-out/wheel-current + CONSTRAINTS=cuda_bindings/.moon-out/constraints-current + reset_directory "$OUTPUT" + reset_directory "$CONSTRAINTS" + + shopt -s nullglob + set -- cuda_pathfinder/.moon-out/wheel-pure/*.whl + [[ $# -eq 1 ]] || { + echo "expected one cuda.pathfinder wheel, found $#" >&2 + exit 1 + } + PATHFINDER_WHEEL=$1 + + CONSTRAINT_FILE="$CONSTRAINTS/build.txt" + HOST_PLATFORM_RESOLVED=$(printenv HOST_PLATFORM || true) + if [[ -z "$HOST_PLATFORM_RESOLVED" ]]; then + HOST_PLATFORM_RESOLVED=$(python -c "import platform; print('win-64' if platform.system() == 'Windows' else 'linux-' + platform.machine())") + fi + if [[ "$HOST_PLATFORM_RESOLVED" == win* ]]; then + PATHFINDER_URI=$(python -c "from pathlib import Path; import sys; print(Path(sys.argv[1]).resolve().as_uri())" "$PATHFINDER_WHEEL") + CONSTRAINT_HOST=$(cygpath -w "$(pwd)/$CONSTRAINT_FILE") + printf 'cuda-pathfinder @ %s\n' "$PATHFINDER_URI" > "$CONSTRAINT_FILE" + CIBW_ENVIRONMENT_BASE=$(printenv CIBW_ENVIRONMENT_WINDOWS || true) + export CIBW_ENVIRONMENT_WINDOWS="$CIBW_ENVIRONMENT_BASE PIP_BUILD_CONSTRAINT=\"$CONSTRAINT_HOST\" PIP_CONSTRAINT=\"$CONSTRAINT_HOST\"" + else + PATHFINDER_URI=$(python -c "from pathlib import Path; import sys; print(Path('/host' + Path(sys.argv[1]).resolve().as_posix()).as_uri())" "$PATHFINDER_WHEEL") + CONSTRAINT_HOST=$(realpath "$CONSTRAINT_FILE") + CONSTRAINT_CONTAINER="/host$CONSTRAINT_HOST" + printf 'cuda-pathfinder @ %s\n' "$PATHFINDER_URI" > "$CONSTRAINT_FILE" + CIBW_ENVIRONMENT_BASE=$(printenv CIBW_ENVIRONMENT_LINUX || true) + export CIBW_ENVIRONMENT_LINUX="$CIBW_ENVIRONMENT_BASE PIP_BUILD_CONSTRAINT=\"$CONSTRAINT_CONTAINER\" PIP_CONSTRAINT=\"$CONSTRAINT_CONTAINER\"" + fi + export PIP_BUILD_CONSTRAINT="$CONSTRAINT_HOST" + export PIP_CONSTRAINT="$CONSTRAINT_HOST" + + python -m cibuildwheel --output-dir "$OUTPUT" cuda_bindings + if [[ "$HOST_PLATFORM_RESOLVED" != win* ]] && find "$OUTPUT" ! -user "$(id -u)" -print -quit | grep -q .; then + command -v sudo >/dev/null || { + echo "cibuildwheel output is not owned by this user and sudo was not found: $OUTPUT" >&2 + exit 1 + } + sudo chown -R "$(id -u):$(id -g)" "$OUTPUT" + fi + set -- "$OUTPUT"/*.whl + [[ $# -eq 1 ]] || { + echo "expected one cuda.bindings wheel, found $#" >&2 + exit 1 + } + deps: + - target: pathfinder:wheel-pure + cacheStrategy: outputs env: BUILD_CUDA_VER: '${BUILD_CUDA_VER}' CIBW_BUILD: '${CIBW_BUILD}' @@ -67,18 +140,61 @@ tasks: - '@group(package)' - {project: pathfinder, group: package} - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' - - '/ci/tools/artifacts.py' - - '/ci/tools/build_artifacts.py' - - '/ci/tools/moon_fingerprint.py' - '/ci/tools/env-vars' - '/ci/versions.yml' - - '/.github/workflows/build-pure-wheel.yml' - '/.github/workflows/build-wheel.yml' outputs: - '.moon-out/wheel-current' checks: - - check: fingerprint - script: python ci/tools/moon_fingerprint.py bindings native + - &bindings_scm_fingerprint + check: fingerprint + script: git describe --always --dirty --tags --long --match 'v*[0-9]*' + hash: stdout + - &scm_environment_fingerprint + check: fingerprint + script: >- + python -c "import hashlib, os; + names = sorted(name for name in os.environ + if name == 'SOURCE_DATE_EPOCH' or name.startswith(('SETUPTOOLS_SCM_', 'VCS_VERSIONING_'))); + payload = '\0'.join(name + '=' + os.environ[name] for name in names); + print(hashlib.sha256(payload.encode()).hexdigest())" + hash: stdout + - &native_environment_fingerprint + check: fingerprint + script: >- + python -c "import hashlib, os, re; + names = {'BUILD_CUDA_MAJOR', 'BUILD_CUDA_VER', 'BUILD_PREV_CUDA_MAJOR', + 'CC', + 'CL', 'CPLUS_INCLUDE_PATH', 'CXX', 'CUDA_CORE_BUILD_MAJOR', 'CUDA_PATH', + 'CUDA_PYTHON_COVERAGE', 'CUDA_PYTHON_LANE', 'CUDA_PYTHON_PARALLEL_LEVEL', 'CUDA_VER', + 'HOST_PLATFORM', 'PY_EXT_SUFFIX', 'PY_VER', 'SCCACHE_CACHE_SIZE', + 'SCCACHE_DIR', 'SCCACHE_PATH'}; + names.update(name for name in os.environ if name.startswith('CIBW_')); + redact = lambda value: re.sub(r'(?i)\\bACTIONS_[A-Z0-9_]+=(?:\"[^\"]*\"|\\S+)', 'ACTIONS_VALUE=', value); + payload = '\0'.join(name + '=' + redact(os.environ.get(name, '')) for name in sorted(names)); + print(hashlib.sha256(payload.encode()).hexdigest())" + hash: stdout + - &python_runtime_fingerprint + check: fingerprint + script: >- + python -c "import platform, sysconfig; + print(platform.python_implementation(), platform.python_version(), + sysconfig.get_config_var('SOABI') or '', sep='\n')" + hash: stdout + - &python_platform_fingerprint + check: fingerprint + script: >- + python -c "import platform; + print(platform.system(), platform.machine(), sep='\n')" + hash: stdout + - &python_build_tools_fingerprint + check: fingerprint + script: >- + python -c "import importlib.metadata as metadata; + names = ('build', 'cibuildwheel', 'packaging', 'pip', 'setuptools', 'setuptools-scm', 'wheel'); + normalize = lambda value: value.lower().replace('_', '-'); + versions = {normalize(dist.metadata['Name']): dist.version for dist in metadata.distributions() if dist.metadata['Name']}; + print(*(name + '=' + versions.get(name, '') for name in names), sep='\n')" hash: stdout tags: - ci-wheel-current @@ -93,8 +209,62 @@ tasks: runInCI: true sdist: - command: python - args: [-m, ci.tools.build_artifacts, sdist, bindings] + command: bash + args: + - -euo + - pipefail + - -c + - | + if [[ -L cuda_bindings/.moon-out || ( -e cuda_bindings/.moon-out && ! -d cuda_bindings/.moon-out ) ]]; then + echo "refusing to use non-directory output root: cuda_bindings/.moon-out" >&2 + exit 1 + fi + if [[ -L cuda_bindings/.moon-out/sdist || ( -e cuda_bindings/.moon-out/sdist && ! -d cuda_bindings/.moon-out/sdist ) ]]; then + echo "refusing to replace non-directory output: cuda_bindings/.moon-out/sdist" >&2 + exit 1 + fi + mkdir -p cuda_bindings/.moon-out + rm -rf -- cuda_bindings/.moon-out/sdist + mkdir -p cuda_bindings/.moon-out/sdist + + shopt -s nullglob + set -- cuda_pathfinder/.moon-out/sdist/*.whl + [[ $# -eq 1 ]] || { + echo "expected one cuda.pathfinder sdist wheel, found $#" >&2 + exit 1 + } + PATHFINDER_WHEEL=$1 + CONSTRAINT_FILE=$(mktemp) + trap 'rm -f "$CONSTRAINT_FILE"' EXIT + HOST_PLATFORM_RESOLVED=$(printenv HOST_PLATFORM || true) + if [[ -z "$HOST_PLATFORM_RESOLVED" ]]; then + HOST_PLATFORM_RESOLVED=$(python -c "import platform; print('win-64' if platform.system() == 'Windows' else 'linux-' + platform.machine())") + fi + if [[ "$HOST_PLATFORM_RESOLVED" == win* ]]; then + PATHFINDER_URI=$(python -c "from pathlib import Path; import sys; print(Path(sys.argv[1]).resolve().as_uri())" "$PATHFINDER_WHEEL") + CONSTRAINT_HOST=$(cygpath -w "$CONSTRAINT_FILE") + else + PATHFINDER_URI=$(python -c "from pathlib import Path; import sys; print(Path(sys.argv[1]).resolve().as_uri())" "$PATHFINDER_WHEEL") + CONSTRAINT_HOST=$(realpath "$CONSTRAINT_FILE") + fi + printf 'cuda-pathfinder @ %s\n' "$PATHFINDER_URI" > "$CONSTRAINT_FILE" + export PIP_BUILD_CONSTRAINT="$CONSTRAINT_HOST" + export PIP_CONSTRAINT="$CONSTRAINT_HOST" + + python -m build --sdist --outdir cuda_bindings/.moon-out/sdist cuda_bindings + set -- cuda_bindings/.moon-out/sdist/*.tar.gz + [[ $# -eq 1 ]] || { + echo "expected one cuda.bindings source distribution, found $#" >&2 + exit 1 + } + ARCHIVE=$1 + python -m pip wheel --no-deps \ + --wheel-dir cuda_bindings/.moon-out/sdist "$ARCHIVE" + set -- cuda_bindings/.moon-out/sdist/*.whl + [[ $# -eq 1 ]] || { + echo "expected one cuda.bindings wheel from source distribution, found $#" >&2 + exit 1 + } deps: - target: pathfinder:sdist cacheStrategy: outputs @@ -106,17 +276,17 @@ tasks: inputs: - '@group(package)' - {project: pathfinder, group: package} - - '/ci/tools/artifacts.py' - - '/ci/tools/build_artifacts.py' - - '/ci/tools/moon_fingerprint.py' - '/.github/workflows/test-sdist-linux.yml' - '/.github/workflows/test-sdist-windows.yml' outputs: - '.moon-out/sdist' checks: - - check: fingerprint - script: python ci/tools/moon_fingerprint.py bindings sdist - hash: stdout + - *bindings_scm_fingerprint + - *scm_environment_fingerprint + - *native_environment_fingerprint + - *python_runtime_fingerprint + - *python_platform_fingerprint + - *python_build_tools_fingerprint tags: [ci-sdist, runner-sdist-linux, runner-sdist-windows] type: build options: @@ -138,14 +308,35 @@ tasks: - {project: pathfinder, group: package} - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' - '/cuda_bindings/.moon-out/wheel-current/*.whl' - - '/ci/tools/moon_fingerprint.py' - - '/.github/workflows/build-pure-wheel.yml' - '/.github/workflows/build-wheel.yml' outputs: - '.moon-out/cython-tests' checks: + - *bindings_scm_fingerprint + - *scm_environment_fingerprint + - *native_environment_fingerprint + - *python_runtime_fingerprint + - *python_platform_fingerprint + - *python_build_tools_fingerprint + - check: fingerprint + script: >- + python -c "import importlib.metadata as metadata; + names = ('cython', 'numpy'); + normalize = lambda value: value.lower().replace('_', '-'); + versions = {normalize(dist.metadata['Name']): dist.version for dist in metadata.distributions() if dist.metadata['Name']}; + print(*(name + '=' + versions.get(name, '') for name in names), sep='\n')" + hash: stdout - check: fingerprint - script: python ci/tools/moon_fingerprint.py bindings test-assets + script: >- + python -c "import os, shlex, shutil, subprocess, sysconfig; + commands = {'cc', 'c++', 'cl', 'nvcc'}; + configured = (os.environ.get('CC') or sysconfig.get_config_var('CC') or '', + os.environ.get('CXX') or sysconfig.get_config_var('CXX') or ''); + commands.update(token for value in configured for token in shlex.split(value, posix=os.name != 'nt') if token and not token.startswith('-')); + print(*(command + '=' + str(result.returncode) + '\n' + result.stdout.strip() + for command in sorted(commands) if (path := shutil.which(command)) + for result in (subprocess.run([path, '--version'], check=False, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True, timeout=10),)), sep='\n')" hash: stdout tags: - ci-build-cython-assets @@ -160,8 +351,18 @@ tasks: runInCI: true test: - command: python - args: [ci/tools/run_pixi_test.py, bindings] + command: bash + args: + - -euo + - pipefail + - -c + - | + PIXI_ENVIRONMENT=$(printenv PIXI_ENVIRONMENT_NAME || true) + if [[ -n "$PIXI_ENVIRONMENT" ]]; then + exec pixi run --manifest-path cuda_bindings/pixi.toml \ + --environment "$PIXI_ENVIRONMENT" test + fi + exec pixi run --manifest-path cuda_bindings/pixi.toml test inputs: - '@group(package)' - '@group(tests)' @@ -171,7 +372,6 @@ tasks: - '/ci/test-matrix.yml' - '/ci/tools/env-vars' - '/tests/**/*' - - '/ci/tools/run_pixi_test.py' type: test bench: @@ -197,24 +397,27 @@ tasks: smoke-linux: script: | set -euo pipefail - if [[ "${SKIP_CUDA_BINDINGS_TEST:-0}" == "1" ]]; then + SKIP=$(printenv SKIP_CUDA_BINDINGS_TEST || printf 0) + if [[ "$SKIP" == "1" ]]; then echo "Skipping cuda.bindings benchmarks for this declared compatibility lane." exit 0 fi shopt -s nullglob - pathfinder_wheels=(cuda_pathfinder/.moon-out/wheel-pure/*.whl) - bindings_wheels=(cuda_bindings/.moon-out/wheel-current/*.whl) - [[ ${#pathfinder_wheels[@]} -eq 1 ]] || { - echo "expected one pathfinder wheel, found ${#pathfinder_wheels[@]}" >&2 + set -- cuda_pathfinder/.moon-out/wheel-pure/*.whl + [[ $# -eq 1 ]] || { + echo "expected one pathfinder wheel, found $#" >&2 exit 1 } - [[ ${#bindings_wheels[@]} -eq 1 ]] || { - echo "expected one bindings wheel, found ${#bindings_wheels[@]}" >&2 + PATHFINDER_WHEEL=$1 + set -- cuda_bindings/.moon-out/wheel-current/*.whl + [[ $# -eq 1 ]] || { + echo "expected one bindings wheel, found $#" >&2 exit 1 } + BINDINGS_WHEEL=$1 python -m pip install \ - "${pathfinder_wheels[0]}" \ - "${bindings_wheels[0]}" \ + "$PATHFINDER_WHEEL" \ + "$BINDINGS_WHEEL" \ pyperf python benchmarks/cuda_bindings/run_pyperf.py --debug-single-value inputs: @@ -235,6 +438,7 @@ tasks: mutex: ci-python-gpu os: linux runInCI: true + shell: true unit-test: command: uvx @@ -266,7 +470,6 @@ tasks: - '/ci/test-matrix.yml' - '/ci/tools/env-vars' - '/tests/**/*' - - '/.github/workflows/build-pure-wheel.yml' - '/.github/workflows/build-wheel.yml' - '/ci/tools/guess_latest.sh' - '/ci/tools/install_gpu_driver.sh' @@ -291,7 +494,6 @@ tasks: - '/ci/test-matrix.yml' - '/ci/tools/env-vars' - '/tests/**/*' - - '/.github/workflows/build-pure-wheel.yml' - '/.github/workflows/build-wheel.yml' - '/ci/tools/configure_driver_mode.ps1' - '/ci/tools/install_gpu_driver.ps1' diff --git a/cuda_core/moon.yml b/cuda_core/moon.yml index 2f9128cd99e..ab3ff4389b4 100644 --- a/cuda_core/moon.yml +++ b/cuda_core/moon.yml @@ -20,6 +20,7 @@ taskOptions: cache: false runFromWorkspaceRoot: true runInCI: false + shell: false fileGroups: package: @@ -45,8 +46,110 @@ fileGroups: tasks: wheel-current: - command: python - args: [-m, ci.tools.build_artifacts, native-wheel, core, --lane, current] + command: bash + args: + - -euo + - pipefail + - -c + - | + reset_directory() { + local DIRECTORY="$1" + local ROOT + ROOT=$(dirname -- "$DIRECTORY") + if [[ -L "$ROOT" || ( -e "$ROOT" && ! -d "$ROOT" ) ]]; then + echo "refusing to use non-directory output root: $ROOT" >&2 + exit 1 + fi + if [[ -L "$DIRECTORY" || ( -e "$DIRECTORY" && ! -d "$DIRECTORY" ) ]]; then + echo "refusing to replace non-directory output: $DIRECTORY" >&2 + exit 1 + fi + mkdir -p "$ROOT" + rm -rf -- "$DIRECTORY" + mkdir -p "$DIRECTORY" + } + + CUDA_MAJOR=$(printenv BUILD_CUDA_MAJOR || true) + if [[ -z "$CUDA_MAJOR" ]]; then + CUDA_VERSION=$(printenv BUILD_CUDA_VER || true) + if [[ -z "$CUDA_VERSION" ]]; then + CUDA_VERSION=$(printenv CUDA_VER || true) + fi + CUDA_MAJOR=$(cut -d . -f 1 <<< "$CUDA_VERSION") + fi + if [[ -z "$CUDA_MAJOR" ]] && command -v nvcc >/dev/null; then + CUDA_MAJOR=$(nvcc --version | sed -n 's/.*release \([0-9][0-9]*\).*/\1/p' | head -n 1) + fi + [[ "$CUDA_MAJOR" =~ ^[0-9]+$ ]] || { + echo "set BUILD_CUDA_MAJOR/BUILD_CUDA_VER or activate a toolkit with nvcc" >&2 + exit 1 + } + OUTPUT=cuda_core/.moon-out/wheel-current + CONSTRAINTS=cuda_core/.moon-out/constraints-current + reset_directory "$OUTPUT" + reset_directory "$CONSTRAINTS" + + shopt -s nullglob + set -- cuda_pathfinder/.moon-out/wheel-pure/*.whl + [[ $# -eq 1 ]] || { + echo "expected one cuda.pathfinder wheel, found $#" >&2 + exit 1 + } + PATHFINDER_WHEEL=$1 + set -- cuda_bindings/.moon-out/wheel-current/*.whl + [[ $# -eq 1 ]] || { + echo "expected one current cuda.bindings wheel, found $#" >&2 + exit 1 + } + BINDINGS_WHEEL=$1 + + CONSTRAINT_FILE="$CONSTRAINTS/build.txt" + HOST_PLATFORM_RESOLVED=$(printenv HOST_PLATFORM || true) + if [[ -z "$HOST_PLATFORM_RESOLVED" ]]; then + HOST_PLATFORM_RESOLVED=$(python -c "import platform; print('win-64' if platform.system() == 'Windows' else 'linux-' + platform.machine())") + fi + if [[ "$HOST_PLATFORM_RESOLVED" == win* ]]; then + PATHFINDER_URI=$(python -c "from pathlib import Path; import sys; print(Path(sys.argv[1]).resolve().as_uri())" "$PATHFINDER_WHEEL") + BINDINGS_URI=$(python -c "from pathlib import Path; import sys; print(Path(sys.argv[1]).resolve().as_uri())" "$BINDINGS_WHEEL") + CONSTRAINT_HOST=$(cygpath -w "$(pwd)/$CONSTRAINT_FILE") + { + printf 'cuda-pathfinder @ %s\n' "$PATHFINDER_URI" + printf 'cuda-bindings @ %s\n' "$BINDINGS_URI" + } > "$CONSTRAINT_FILE" + CIBW_ENVIRONMENT_BASE=$(printenv CIBW_ENVIRONMENT_WINDOWS || true) + export CIBW_ENVIRONMENT_WINDOWS="$CIBW_ENVIRONMENT_BASE PIP_BUILD_CONSTRAINT=\"$CONSTRAINT_HOST\" PIP_CONSTRAINT=\"$CONSTRAINT_HOST\" CUDA_CORE_BUILD_MAJOR=$CUDA_MAJOR" + else + PATHFINDER_URI=$(python -c "from pathlib import Path; import sys; print(Path('/host' + Path(sys.argv[1]).resolve().as_posix()).as_uri())" "$PATHFINDER_WHEEL") + BINDINGS_URI=$(python -c "from pathlib import Path; import sys; print(Path('/host' + Path(sys.argv[1]).resolve().as_posix()).as_uri())" "$BINDINGS_WHEEL") + CONSTRAINT_HOST=$(realpath "$CONSTRAINT_FILE") + CONSTRAINT_CONTAINER="/host$CONSTRAINT_HOST" + { + printf 'cuda-pathfinder @ %s\n' "$PATHFINDER_URI" + printf 'cuda-bindings @ %s\n' "$BINDINGS_URI" + } > "$CONSTRAINT_FILE" + CIBW_ENVIRONMENT_BASE=$(printenv CIBW_ENVIRONMENT_LINUX || true) + export CIBW_ENVIRONMENT_LINUX="$CIBW_ENVIRONMENT_BASE PIP_BUILD_CONSTRAINT=\"$CONSTRAINT_CONTAINER\" PIP_CONSTRAINT=\"$CONSTRAINT_CONTAINER\" CUDA_CORE_BUILD_MAJOR=$CUDA_MAJOR" + fi + export PIP_BUILD_CONSTRAINT="$CONSTRAINT_HOST" + export PIP_CONSTRAINT="$CONSTRAINT_HOST" + export CUDA_CORE_BUILD_MAJOR="$CUDA_MAJOR" + + python -m cibuildwheel --output-dir "$OUTPUT" cuda_core + if [[ "$HOST_PLATFORM_RESOLVED" != win* ]] && find "$OUTPUT" ! -user "$(id -u)" -print -quit | grep -q .; then + command -v sudo >/dev/null || { + echo "cibuildwheel output is not owned by this user and sudo was not found: $OUTPUT" >&2 + exit 1 + } + sudo chown -R "$(id -u):$(id -g)" "$OUTPUT" + fi + set -- "$OUTPUT"/*.whl + [[ $# -eq 1 ]] || { + echo "expected one current cuda.core wheel, found $#" >&2 + exit 1 + } + WHEEL=$1 + WHEEL_WITHOUT_SUFFIX=$(printf '%s\n' "$WHEEL" | sed 's/\.whl$//') + mv -- "$WHEEL" "$WHEEL_WITHOUT_SUFFIX.cu$CUDA_MAJOR.whl" deps: - target: bindings:wheel-current cacheStrategy: outputs @@ -63,19 +166,62 @@ tasks: - {project: bindings, group: package} - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' - '/cuda_bindings/.moon-out/wheel-current/*.whl' - - '/ci/tools/artifacts.py' - - '/ci/tools/build_artifacts.py' - - '/ci/tools/moon_fingerprint.py' - '/ci/tools/env-vars' - '/ci/tools/merge_cuda_core_wheels.py' - '/ci/versions.yml' - - '/.github/workflows/build-pure-wheel.yml' - '/.github/workflows/build-wheel.yml' outputs: - '.moon-out/wheel-current' checks: - - check: fingerprint - script: python ci/tools/moon_fingerprint.py core native + - &core_scm_fingerprint + check: fingerprint + script: git describe --always --dirty --tags --long --match 'cuda-core-v*[0-9]*' + hash: stdout + - &scm_environment_fingerprint + check: fingerprint + script: >- + python -c "import hashlib, os; + names = sorted(name for name in os.environ + if name == 'SOURCE_DATE_EPOCH' or name.startswith(('SETUPTOOLS_SCM_', 'VCS_VERSIONING_'))); + payload = '\0'.join(name + '=' + os.environ[name] for name in names); + print(hashlib.sha256(payload.encode()).hexdigest())" + hash: stdout + - &native_environment_fingerprint + check: fingerprint + script: >- + python -c "import hashlib, os, re; + names = {'BUILD_CUDA_MAJOR', 'BUILD_CUDA_VER', 'BUILD_PREV_CUDA_MAJOR', + 'CC', + 'CL', 'CPLUS_INCLUDE_PATH', 'CXX', 'CUDA_CORE_BUILD_MAJOR', 'CUDA_PATH', + 'CUDA_PYTHON_COVERAGE', 'CUDA_PYTHON_LANE', 'CUDA_PYTHON_PARALLEL_LEVEL', 'CUDA_VER', + 'HOST_PLATFORM', 'PY_EXT_SUFFIX', 'PY_VER', 'SCCACHE_CACHE_SIZE', + 'SCCACHE_DIR', 'SCCACHE_PATH'}; + names.update(name for name in os.environ if name.startswith('CIBW_')); + redact = lambda value: re.sub(r'(?i)\\bACTIONS_[A-Z0-9_]+=(?:\"[^\"]*\"|\\S+)', 'ACTIONS_VALUE=', value); + payload = '\0'.join(name + '=' + redact(os.environ.get(name, '')) for name in sorted(names)); + print(hashlib.sha256(payload.encode()).hexdigest())" + hash: stdout + - &python_runtime_fingerprint + check: fingerprint + script: >- + python -c "import platform, sysconfig; + print(platform.python_implementation(), platform.python_version(), + sysconfig.get_config_var('SOABI') or '', sep='\n')" + hash: stdout + - &python_platform_fingerprint + check: fingerprint + script: >- + python -c "import platform; + print(platform.system(), platform.machine(), sep='\n')" + hash: stdout + - &python_build_tools_fingerprint + check: fingerprint + script: >- + python -c "import importlib.metadata as metadata; + names = ('build', 'cibuildwheel', 'packaging', 'pip', 'setuptools', 'setuptools-scm', 'wheel'); + normalize = lambda value: value.lower().replace('_', '-'); + versions = {normalize(dist.metadata['Name']): dist.version for dist in metadata.distributions() if dist.metadata['Name']}; + print(*(name + '=' + versions.get(name, '') for name in names), sep='\n')" hash: stdout tags: - ci-wheel-current @@ -90,8 +236,103 @@ tasks: runInCI: true wheel-previous: - command: python - args: [-m, ci.tools.build_artifacts, native-wheel, core, --lane, previous] + command: bash + args: + - -euo + - pipefail + - -c + - | + reset_directory() { + local DIRECTORY="$1" + local ROOT + ROOT=$(dirname -- "$DIRECTORY") + if [[ -L "$ROOT" || ( -e "$ROOT" && ! -d "$ROOT" ) ]]; then + echo "refusing to use non-directory output root: $ROOT" >&2 + exit 1 + fi + if [[ -L "$DIRECTORY" || ( -e "$DIRECTORY" && ! -d "$DIRECTORY" ) ]]; then + echo "refusing to replace non-directory output: $DIRECTORY" >&2 + exit 1 + fi + mkdir -p "$ROOT" + rm -rf -- "$DIRECTORY" + mkdir -p "$DIRECTORY" + } + + CUDA_MAJOR=$(printenv BUILD_PREV_CUDA_MAJOR || true) + [[ "$CUDA_MAJOR" =~ ^[0-9]+$ ]] || { + echo "BUILD_PREV_CUDA_MAJOR must be a numeric CUDA major version" >&2 + exit 1 + } + OUTPUT=cuda_core/.moon-out/wheel-previous + CONSTRAINTS=cuda_core/.moon-out/constraints-previous + reset_directory "$OUTPUT" + reset_directory "$CONSTRAINTS" + + shopt -s nullglob + set -- cuda_pathfinder/.moon-out/wheel-pure/*.whl + [[ $# -eq 1 ]] || { + echo "expected one cuda.pathfinder wheel, found $#" >&2 + exit 1 + } + PATHFINDER_WHEEL=$1 + set -- cuda_bindings/.moon-out/wheel-previous/*.whl + [[ $# -eq 1 ]] || { + echo "expected one previous cuda.bindings wheel, found $#" >&2 + exit 1 + } + BINDINGS_WHEEL=$1 + + CONSTRAINT_FILE="$CONSTRAINTS/build.txt" + HOST_PLATFORM_RESOLVED=$(printenv HOST_PLATFORM || true) + if [[ -z "$HOST_PLATFORM_RESOLVED" ]]; then + HOST_PLATFORM_RESOLVED=$(python -c "import platform; print('win-64' if platform.system() == 'Windows' else 'linux-' + platform.machine())") + fi + if [[ "$HOST_PLATFORM_RESOLVED" == win* ]]; then + PATHFINDER_URI=$(python -c "from pathlib import Path; import sys; print(Path(sys.argv[1]).resolve().as_uri())" "$PATHFINDER_WHEEL") + BINDINGS_URI=$(python -c "from pathlib import Path; import sys; print(Path(sys.argv[1]).resolve().as_uri())" "$BINDINGS_WHEEL") + CONSTRAINT_HOST=$(cygpath -w "$(pwd)/$CONSTRAINT_FILE") + { + printf 'cuda-pathfinder @ %s\n' "$PATHFINDER_URI" + printf 'cuda-bindings @ %s\n' "$BINDINGS_URI" + } > "$CONSTRAINT_FILE" + CIBW_ENVIRONMENT_BASE=$(printenv CIBW_ENVIRONMENT_WINDOWS || true) + export CIBW_ENVIRONMENT_WINDOWS="$CIBW_ENVIRONMENT_BASE PIP_BUILD_CONSTRAINT=\"$CONSTRAINT_HOST\" PIP_CONSTRAINT=\"$CONSTRAINT_HOST\" CUDA_CORE_BUILD_MAJOR=$CUDA_MAJOR" + else + PATHFINDER_URI=$(python -c "from pathlib import Path; import sys; print(Path('/host' + Path(sys.argv[1]).resolve().as_posix()).as_uri())" "$PATHFINDER_WHEEL") + BINDINGS_URI=$(python -c "from pathlib import Path; import sys; print(Path('/host' + Path(sys.argv[1]).resolve().as_posix()).as_uri())" "$BINDINGS_WHEEL") + CONSTRAINT_HOST=$(realpath "$CONSTRAINT_FILE") + CONSTRAINT_CONTAINER="/host$CONSTRAINT_HOST" + { + printf 'cuda-pathfinder @ %s\n' "$PATHFINDER_URI" + printf 'cuda-bindings @ %s\n' "$BINDINGS_URI" + } > "$CONSTRAINT_FILE" + CIBW_ENVIRONMENT_BASE=$(printenv CIBW_ENVIRONMENT_LINUX || true) + export CIBW_ENVIRONMENT_LINUX="$CIBW_ENVIRONMENT_BASE PIP_BUILD_CONSTRAINT=\"$CONSTRAINT_CONTAINER\" PIP_CONSTRAINT=\"$CONSTRAINT_CONTAINER\" CUDA_CORE_BUILD_MAJOR=$CUDA_MAJOR" + fi + export PIP_BUILD_CONSTRAINT="$CONSTRAINT_HOST" + export PIP_CONSTRAINT="$CONSTRAINT_HOST" + export CUDA_CORE_BUILD_MAJOR="$CUDA_MAJOR" + + python -m cibuildwheel --output-dir "$OUTPUT" cuda_core + if [[ "$HOST_PLATFORM_RESOLVED" != win* ]] && find "$OUTPUT" ! -user "$(id -u)" -print -quit | grep -q .; then + command -v sudo >/dev/null || { + echo "cibuildwheel output is not owned by this user and sudo was not found: $OUTPUT" >&2 + exit 1 + } + sudo chown -R "$(id -u):$(id -g)" "$OUTPUT" + fi + set -- "$OUTPUT"/*.whl + [[ $# -eq 1 ]] || { + echo "expected one previous cuda.core wheel, found $#" >&2 + exit 1 + } + WHEEL=$1 + WHEEL_WITHOUT_SUFFIX=$(printf '%s\n' "$WHEEL" | sed 's/\.whl$//') + mv -- "$WHEEL" "$WHEEL_WITHOUT_SUFFIX.cu$CUDA_MAJOR.whl" + deps: + - target: pathfinder:wheel-pure + cacheStrategy: outputs env: BUILD_PREV_CUDA_MAJOR: '${BUILD_PREV_CUDA_MAJOR}' CIBW_BUILD: '${CIBW_BUILD}' @@ -104,19 +345,18 @@ tasks: - {project: bindings, group: package} - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' - '/cuda_bindings/.moon-out/wheel-previous/*.whl' - - '/ci/tools/artifacts.py' - - '/ci/tools/build_artifacts.py' - - '/ci/tools/moon_fingerprint.py' - '/ci/tools/env-vars' - '/ci/versions.yml' - - '/.github/workflows/build-pure-wheel.yml' - '/.github/workflows/build-wheel.yml' outputs: - '.moon-out/wheel-previous' checks: - - check: fingerprint - script: python ci/tools/moon_fingerprint.py core previous - hash: stdout + - *core_scm_fingerprint + - *scm_environment_fingerprint + - *native_environment_fingerprint + - *python_runtime_fingerprint + - *python_platform_fingerprint + - *python_build_tools_fingerprint tags: - runner-build-linux-64 - runner-build-linux-aarch64 @@ -148,16 +388,17 @@ tasks: - {project: bindings, group: package} - '/cuda_core/.moon-out/wheel-current/*.whl' - '/cuda_core/.moon-out/wheel-previous/*.whl' - - '/ci/tools/moon_fingerprint.py' - '/ci/tools/merge_cuda_core_wheels.py' - - '/.github/workflows/build-pure-wheel.yml' - '/.github/workflows/build-wheel.yml' outputs: - '.moon-out/wheel-merged' checks: - - check: fingerprint - script: python ci/tools/moon_fingerprint.py core native - hash: stdout + - *core_scm_fingerprint + - *scm_environment_fingerprint + - *native_environment_fingerprint + - *python_runtime_fingerprint + - *python_platform_fingerprint + - *python_build_tools_fingerprint tags: - runner-build-linux-64 - runner-build-linux-aarch64 @@ -169,8 +410,89 @@ tasks: runInCI: true sdist: - command: python - args: [-m, ci.tools.build_artifacts, sdist, core] + command: bash + args: + - -euo + - pipefail + - -c + - | + CUDA_MAJOR=$(printenv BUILD_CUDA_MAJOR || true) + if [[ -z "$CUDA_MAJOR" ]]; then + CUDA_VERSION=$(printenv BUILD_CUDA_VER || true) + if [[ -z "$CUDA_VERSION" ]]; then + CUDA_VERSION=$(printenv CUDA_VER || true) + fi + CUDA_MAJOR=$(cut -d . -f 1 <<< "$CUDA_VERSION") + fi + if [[ -z "$CUDA_MAJOR" ]] && command -v nvcc >/dev/null; then + CUDA_MAJOR=$(nvcc --version | sed -n 's/.*release \([0-9][0-9]*\).*/\1/p' | head -n 1) + fi + [[ "$CUDA_MAJOR" =~ ^[0-9]+$ ]] || { + echo "set BUILD_CUDA_MAJOR/BUILD_CUDA_VER or activate a toolkit with nvcc" >&2 + exit 1 + } + if [[ -L cuda_core/.moon-out || ( -e cuda_core/.moon-out && ! -d cuda_core/.moon-out ) ]]; then + echo "refusing to use non-directory output root: cuda_core/.moon-out" >&2 + exit 1 + fi + if [[ -L cuda_core/.moon-out/sdist || ( -e cuda_core/.moon-out/sdist && ! -d cuda_core/.moon-out/sdist ) ]]; then + echo "refusing to replace non-directory output: cuda_core/.moon-out/sdist" >&2 + exit 1 + fi + mkdir -p cuda_core/.moon-out + rm -rf -- cuda_core/.moon-out/sdist + mkdir -p cuda_core/.moon-out/sdist + + shopt -s nullglob + set -- cuda_pathfinder/.moon-out/sdist/*.whl + [[ $# -eq 1 ]] || { + echo "expected one cuda.pathfinder sdist wheel, found $#" >&2 + exit 1 + } + PATHFINDER_WHEEL=$1 + set -- cuda_bindings/.moon-out/sdist/*.whl + [[ $# -eq 1 ]] || { + echo "expected one cuda.bindings sdist wheel, found $#" >&2 + exit 1 + } + BINDINGS_WHEEL=$1 + CONSTRAINT_FILE=$(mktemp) + trap 'rm -f "$CONSTRAINT_FILE"' EXIT + HOST_PLATFORM_RESOLVED=$(printenv HOST_PLATFORM || true) + if [[ -z "$HOST_PLATFORM_RESOLVED" ]]; then + HOST_PLATFORM_RESOLVED=$(python -c "import platform; print('win-64' if platform.system() == 'Windows' else 'linux-' + platform.machine())") + fi + if [[ "$HOST_PLATFORM_RESOLVED" == win* ]]; then + PATHFINDER_URI=$(python -c "from pathlib import Path; import sys; print(Path(sys.argv[1]).resolve().as_uri())" "$PATHFINDER_WHEEL") + BINDINGS_URI=$(python -c "from pathlib import Path; import sys; print(Path(sys.argv[1]).resolve().as_uri())" "$BINDINGS_WHEEL") + CONSTRAINT_HOST=$(cygpath -w "$CONSTRAINT_FILE") + else + PATHFINDER_URI=$(python -c "from pathlib import Path; import sys; print(Path(sys.argv[1]).resolve().as_uri())" "$PATHFINDER_WHEEL") + BINDINGS_URI=$(python -c "from pathlib import Path; import sys; print(Path(sys.argv[1]).resolve().as_uri())" "$BINDINGS_WHEEL") + CONSTRAINT_HOST=$(realpath "$CONSTRAINT_FILE") + fi + { + printf 'cuda-pathfinder @ %s\n' "$PATHFINDER_URI" + printf 'cuda-bindings @ %s\n' "$BINDINGS_URI" + } > "$CONSTRAINT_FILE" + export PIP_BUILD_CONSTRAINT="$CONSTRAINT_HOST" + export PIP_CONSTRAINT="$CONSTRAINT_HOST" + export CUDA_CORE_BUILD_MAJOR="$CUDA_MAJOR" + + python -m build --sdist --outdir cuda_core/.moon-out/sdist cuda_core + set -- cuda_core/.moon-out/sdist/*.tar.gz + [[ $# -eq 1 ]] || { + echo "expected one cuda.core source distribution, found $#" >&2 + exit 1 + } + ARCHIVE=$1 + python -m pip wheel --no-deps \ + --wheel-dir cuda_core/.moon-out/sdist "$ARCHIVE" + set -- cuda_core/.moon-out/sdist/*.whl + [[ $# -eq 1 ]] || { + echo "expected one cuda.core wheel from source distribution, found $#" >&2 + exit 1 + } deps: - target: pathfinder:sdist cacheStrategy: outputs @@ -186,17 +508,17 @@ tasks: - '@group(package)' - {project: pathfinder, group: package} - {project: bindings, group: package} - - '/ci/tools/artifacts.py' - - '/ci/tools/build_artifacts.py' - - '/ci/tools/moon_fingerprint.py' - '/.github/workflows/test-sdist-linux.yml' - '/.github/workflows/test-sdist-windows.yml' outputs: - '.moon-out/sdist' checks: - - check: fingerprint - script: python ci/tools/moon_fingerprint.py core sdist - hash: stdout + - *core_scm_fingerprint + - *scm_environment_fingerprint + - *native_environment_fingerprint + - *python_runtime_fingerprint + - *python_platform_fingerprint + - *python_build_tools_fingerprint tags: [ci-sdist, runner-sdist-linux, runner-sdist-windows] type: build options: @@ -220,14 +542,37 @@ tasks: - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' - '/cuda_bindings/.moon-out/wheel-current/*.whl' - '/cuda_core/.moon-out/wheel-current/*.whl' - - '/ci/tools/moon_fingerprint.py' - - '/.github/workflows/build-pure-wheel.yml' - '/.github/workflows/build-wheel.yml' outputs: - '.moon-out/cython-tests' checks: - - check: fingerprint - script: python ci/tools/moon_fingerprint.py core test-assets + - *core_scm_fingerprint + - *scm_environment_fingerprint + - *native_environment_fingerprint + - *python_runtime_fingerprint + - *python_platform_fingerprint + - *python_build_tools_fingerprint + - &python_test_tools_fingerprint + check: fingerprint + script: >- + python -c "import importlib.metadata as metadata; + names = ('cython', 'numpy'); + normalize = lambda value: value.lower().replace('_', '-'); + versions = {normalize(dist.metadata['Name']): dist.version for dist in metadata.distributions() if dist.metadata['Name']}; + print(*(name + '=' + versions.get(name, '') for name in names), sep='\n')" + hash: stdout + - &native_tools_fingerprint + check: fingerprint + script: >- + python -c "import os, shlex, shutil, subprocess, sysconfig; + commands = {'cc', 'c++', 'cl', 'nvcc'}; + configured = (os.environ.get('CC') or sysconfig.get_config_var('CC') or '', + os.environ.get('CXX') or sysconfig.get_config_var('CXX') or ''); + commands.update(token for value in configured for token in shlex.split(value, posix=os.name != 'nt') if token and not token.startswith('-')); + print(*(command + '=' + str(result.returncode) + '\n' + result.stdout.strip() + for command in sorted(commands) if (path := shutil.which(command)) + for result in (subprocess.run([path, '--version'], check=False, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True, timeout=10),)), sep='\n')" hash: stdout tags: - ci-build-cython-assets @@ -256,14 +601,18 @@ tasks: - {project: bindings, group: package} - 'tests/test_binaries/build_test_binaries.py' - 'tests/test_binaries/saxpy.cu' - - '/ci/tools/moon_fingerprint.py' - '/.github/workflows/build-wheel.yml' outputs: - '.moon-out/test-binaries' checks: - - check: fingerprint - script: python ci/tools/moon_fingerprint.py core test-assets - hash: stdout + - *core_scm_fingerprint + - *scm_environment_fingerprint + - *native_environment_fingerprint + - *python_runtime_fingerprint + - *python_platform_fingerprint + - *python_build_tools_fingerprint + - *python_test_tools_fingerprint + - *native_tools_fingerprint tags: - runner-build-linux-64 - runner-build-linux-aarch64 @@ -276,8 +625,18 @@ tasks: runInCI: true test: - command: python - args: [ci/tools/run_pixi_test.py, core] + command: bash + args: + - -euo + - pipefail + - -c + - | + PIXI_ENVIRONMENT=$(printenv PIXI_ENVIRONMENT_NAME || true) + if [[ -n "$PIXI_ENVIRONMENT" ]]; then + exec pixi run --manifest-path cuda_core/pixi.toml \ + --environment "$PIXI_ENVIRONMENT" test + fi + exec pixi run --manifest-path cuda_core/pixi.toml test inputs: - '@group(package)' - '@group(tests)' @@ -287,7 +646,6 @@ tasks: - '/ci/test-matrix.yml' - '/ci/tools/env-vars' - '/tests/**/*' - - '/ci/tools/run_pixi_test.py' type: test test-installed-linux: @@ -304,7 +662,6 @@ tasks: - '/ci/tools/env-vars' - '/tests/**/*' - '/ci/tools/merge_cuda_core_wheels.py' - - '/.github/workflows/build-pure-wheel.yml' - '/.github/workflows/build-wheel.yml' - '/ci/tools/guess_latest.sh' - '/ci/tools/install_gpu_driver.sh' @@ -331,7 +688,6 @@ tasks: - '/ci/tools/env-vars' - '/tests/**/*' - '/ci/tools/merge_cuda_core_wheels.py' - - '/.github/workflows/build-pure-wheel.yml' - '/.github/workflows/build-wheel.yml' - '/ci/tools/configure_driver_mode.ps1' - '/ci/tools/install_gpu_driver.ps1' diff --git a/cuda_pathfinder/moon.yml b/cuda_pathfinder/moon.yml index 864feab9059..4b45d38969e 100644 --- a/cuda_pathfinder/moon.yml +++ b/cuda_pathfinder/moon.yml @@ -13,6 +13,7 @@ taskOptions: cache: false runFromWorkspaceRoot: true runInCI: false + shell: false fileGroups: package: @@ -33,12 +34,21 @@ fileGroups: tasks: test: - command: python - args: [ci/tools/run_pixi_test.py, pathfinder] + command: bash + args: + - -euo + - pipefail + - -c + - | + PIXI_ENVIRONMENT=$(printenv PIXI_ENVIRONMENT_NAME || true) + if [[ -n "$PIXI_ENVIRONMENT" ]]; then + exec pixi run --manifest-path cuda_pathfinder/pixi.toml \ + --environment "$PIXI_ENVIRONMENT" test + fi + exec pixi run --manifest-path cuda_pathfinder/pixi.toml test inputs: - '@group(package)' - '@group(tests)' - - '/ci/tools/run_pixi_test.py' type: test test-installed-linux: @@ -51,7 +61,7 @@ tasks: - '/ci/test-matrix.yml' - '/ci/tools/env-vars' - '/tests/**/*' - - '/.github/workflows/build-pure-wheel.yml' + - '/.github/workflows/build-wheel.yml' - '/ci/tools/guess_latest.sh' - '/ci/tools/install_gpu_driver.sh' - '/.github/workflows/test-wheel-linux.yml' @@ -78,12 +88,12 @@ tasks: exit 1 } shopt -s nullglob - wheels=(cuda_pathfinder/.moon-out/wheel-pure/*.whl) - [[ ${#wheels[@]} -eq 1 ]] || { - echo "expected one pathfinder wheel, found ${#wheels[@]}" >&2 + set -- cuda_pathfinder/.moon-out/wheel-pure/*.whl + [[ $# -eq 1 ]] || { + echo "expected one pathfinder wheel, found $#" >&2 exit 1 } - python -m pip install --only-binary=:all: --verbose "${wheels[0]}" \ + python -m pip install --only-binary=:all: --verbose "$1" \ --group "cuda_pathfinder/pyproject.toml:test-cu${TEST_CUDA_MAJOR}" python -m pip list deps: @@ -111,7 +121,7 @@ tasks: - '/ci/test-matrix.yml' - '/ci/tools/env-vars' - '/tests/**/*' - - '/.github/workflows/build-pure-wheel.yml' + - '/.github/workflows/build-wheel.yml' - '/ci/tools/guess_latest.sh' - '/ci/tools/install_gpu_driver.sh' - '/.github/workflows/test-wheel-linux.yml' @@ -138,7 +148,7 @@ tasks: - '/ci/test-matrix.yml' - '/ci/tools/env-vars' - '/tests/**/*' - - '/.github/workflows/build-pure-wheel.yml' + - '/.github/workflows/build-wheel.yml' - '/ci/tools/configure_driver_mode.ps1' - '/ci/tools/install_gpu_driver.ps1' - '/.github/workflows/test-wheel-windows.yml' @@ -165,12 +175,12 @@ tasks: exit 1 } shopt -s nullglob - wheels=(cuda_pathfinder/.moon-out/wheel-pure/*.whl) - [[ ${#wheels[@]} -eq 1 ]] || { - echo "expected one pathfinder wheel, found ${#wheels[@]}" >&2 + set -- cuda_pathfinder/.moon-out/wheel-pure/*.whl + [[ $# -eq 1 ]] || { + echo "expected one pathfinder wheel, found $#" >&2 exit 1 } - python -m pip install --only-binary=:all: --verbose "${wheels[0]}" \ + python -m pip install --only-binary=:all: --verbose "$1" \ --group "cuda_pathfinder/pyproject.toml:test-cu${TEST_CUDA_MAJOR}" python -m pip list deps: @@ -198,7 +208,7 @@ tasks: - '/ci/test-matrix.yml' - '/ci/tools/env-vars' - '/tests/**/*' - - '/.github/workflows/build-pure-wheel.yml' + - '/.github/workflows/build-wheel.yml' - '/ci/tools/configure_driver_mode.ps1' - '/ci/tools/install_gpu_driver.ps1' - '/.github/workflows/test-wheel-windows.yml' @@ -242,21 +252,67 @@ tasks: runInCI: true wheel-pure: - command: python - args: [-m, ci.tools.build_artifacts, pure-wheel, pathfinder] + command: bash + args: + - -euo + - pipefail + - -c + - | + if [[ -L cuda_pathfinder/.moon-out || ( -e cuda_pathfinder/.moon-out && ! -d cuda_pathfinder/.moon-out ) ]]; then + echo "refusing to use non-directory output root: cuda_pathfinder/.moon-out" >&2 + exit 1 + fi + if [[ -L cuda_pathfinder/.moon-out/wheel-pure || ( -e cuda_pathfinder/.moon-out/wheel-pure && ! -d cuda_pathfinder/.moon-out/wheel-pure ) ]]; then + echo "refusing to replace non-directory output: cuda_pathfinder/.moon-out/wheel-pure" >&2 + exit 1 + fi + mkdir -p cuda_pathfinder/.moon-out + rm -rf -- cuda_pathfinder/.moon-out/wheel-pure + mkdir -p cuda_pathfinder/.moon-out/wheel-pure + python -m pip wheel --verbose --no-deps \ + --wheel-dir cuda_pathfinder/.moon-out/wheel-pure ./cuda_pathfinder + shopt -s nullglob + set -- cuda_pathfinder/.moon-out/wheel-pure/*.whl + [[ $# -eq 1 ]] || { + echo "expected one cuda.pathfinder wheel, found $#" >&2 + exit 1 + } inputs: - '@group(package)' - - '/ci/tools/artifacts.py' - - '/ci/tools/build_artifacts.py' - - '/ci/tools/moon_fingerprint.py' - - '/.github/workflows/build-pure-wheel.yml' + - '/.github/workflows/build-wheel.yml' outputs: - '.moon-out/wheel-pure' checks: - - check: fingerprint - script: python ci/tools/moon_fingerprint.py pathfinder portable + - &pathfinder_scm_fingerprint + check: fingerprint + script: git describe --always --dirty --tags --long --match 'cuda-pathfinder-v*[0-9]*' hash: stdout - tags: [ci-wheel-pure, runner-build-portable] + - &scm_environment_fingerprint + check: fingerprint + script: >- + python -c "import hashlib, os; + names = sorted(name for name in os.environ + if name == 'SOURCE_DATE_EPOCH' or name.startswith(('SETUPTOOLS_SCM_', 'VCS_VERSIONING_'))); + payload = '\0'.join(name + '=' + os.environ[name] for name in names); + print(hashlib.sha256(payload.encode()).hexdigest())" + hash: stdout + - &python_runtime_fingerprint + check: fingerprint + script: >- + python -c "import platform, sysconfig; + print(platform.python_implementation(), platform.python_version(), + sysconfig.get_config_var('SOABI') or '', sep='\n')" + hash: stdout + - &python_build_tools_fingerprint + check: fingerprint + script: >- + python -c "import importlib.metadata as metadata; + names = ('build', 'cibuildwheel', 'packaging', 'pip', 'setuptools', 'setuptools-scm', 'wheel'); + normalize = lambda value: value.lower().replace('_', '-'); + versions = {normalize(dist.metadata['Name']): dist.version for dist in metadata.distributions() if dist.metadata['Name']}; + print(*(name + '=' + versions.get(name, '') for name in names), sep='\n')" + hash: stdout + tags: [runner-build-linux-64, runner-build-linux-aarch64, runner-build-windows] type: build options: cache: true @@ -265,20 +321,66 @@ tasks: runInCI: true sdist: - command: python - args: [-m, ci.tools.build_artifacts, sdist, pathfinder] + command: bash + args: + - -euo + - pipefail + - -c + - | + if [[ -L cuda_pathfinder/.moon-out || ( -e cuda_pathfinder/.moon-out && ! -d cuda_pathfinder/.moon-out ) ]]; then + echo "refusing to use non-directory output root: cuda_pathfinder/.moon-out" >&2 + exit 1 + fi + if [[ -L cuda_pathfinder/.moon-out/sdist || ( -e cuda_pathfinder/.moon-out/sdist && ! -d cuda_pathfinder/.moon-out/sdist ) ]]; then + echo "refusing to replace non-directory output: cuda_pathfinder/.moon-out/sdist" >&2 + exit 1 + fi + mkdir -p cuda_pathfinder/.moon-out + rm -rf -- cuda_pathfinder/.moon-out/sdist + mkdir -p cuda_pathfinder/.moon-out/sdist + python -m build --sdist --outdir cuda_pathfinder/.moon-out/sdist cuda_pathfinder + shopt -s nullglob + set -- cuda_pathfinder/.moon-out/sdist/*.tar.gz + [[ $# -eq 1 ]] || { + echo "expected one cuda.pathfinder source distribution, found $#" >&2 + exit 1 + } + ARCHIVE=$1 + python -m pip wheel --no-deps \ + --wheel-dir cuda_pathfinder/.moon-out/sdist "$ARCHIVE" + set -- cuda_pathfinder/.moon-out/sdist/*.whl + [[ $# -eq 1 ]] || { + echo "expected one cuda.pathfinder wheel from source distribution, found $#" >&2 + exit 1 + } inputs: - '@group(package)' - - '/ci/tools/artifacts.py' - - '/ci/tools/build_artifacts.py' - - '/ci/tools/moon_fingerprint.py' - '/.github/workflows/test-sdist-linux.yml' - '/.github/workflows/test-sdist-windows.yml' outputs: - '.moon-out/sdist' checks: + - *pathfinder_scm_fingerprint + - *scm_environment_fingerprint + - *python_runtime_fingerprint + - *python_build_tools_fingerprint + - check: fingerprint + script: >- + python -c "import platform; + print(platform.system(), platform.machine(), sep='\n')" + hash: stdout - check: fingerprint - script: python ci/tools/moon_fingerprint.py pathfinder sdist + script: >- + python -c "import hashlib, os; + names = ('BUILD_CUDA_MAJOR', 'BUILD_CUDA_VER', 'BUILD_PREV_CUDA_MAJOR', + 'CC', 'CIBW_BEFORE_BUILD_LINUX', 'CIBW_BEFORE_BUILD_WINDOWS', + 'CIBW_BEFORE_TEST_LINUX', 'CIBW_BUILD', 'CIBW_ENABLE', 'CIBW_TEST_COMMAND', + 'CL', 'CPLUS_INCLUDE_PATH', 'CXX', 'CUDA_CORE_BUILD_MAJOR', 'CUDA_PATH', + 'CUDA_PYTHON_COVERAGE', 'CUDA_PYTHON_LANE', 'CUDA_PYTHON_PARALLEL_LEVEL', 'CUDA_VER', + 'HOST_PLATFORM', 'PY_EXT_SUFFIX', 'PY_VER', 'SCCACHE_CACHE_SIZE', + 'SCCACHE_DIR', 'SCCACHE_PATH'); + payload = '\0'.join(name + '=' + os.environ.get(name, '') for name in names); + print(hashlib.sha256(payload.encode()).hexdigest())" hash: stdout tags: [ci-sdist, runner-sdist-linux, runner-sdist-windows] type: build diff --git a/cuda_python/moon.yml b/cuda_python/moon.yml index d9e182da1ba..4d46c43d4e3 100644 --- a/cuda_python/moon.yml +++ b/cuda_python/moon.yml @@ -17,6 +17,7 @@ taskOptions: cache: false runFromWorkspaceRoot: true runInCI: false + shell: false fileGroups: package: @@ -29,48 +30,161 @@ fileGroups: tasks: wheel-pure: - command: python - args: [-m, ci.tools.build_artifacts, pure-wheel, metapackage] + command: bash + args: + - -euo + - pipefail + - -c + - | + if [[ -L cuda_python/.moon-out || ( -e cuda_python/.moon-out && ! -d cuda_python/.moon-out ) ]]; then + echo "refusing to use non-directory output root: cuda_python/.moon-out" >&2 + exit 1 + fi + if [[ -L cuda_python/.moon-out/wheel-pure || ( -e cuda_python/.moon-out/wheel-pure && ! -d cuda_python/.moon-out/wheel-pure ) ]]; then + echo "refusing to replace non-directory output: cuda_python/.moon-out/wheel-pure" >&2 + exit 1 + fi + mkdir -p cuda_python/.moon-out + rm -rf -- cuda_python/.moon-out/wheel-pure + mkdir -p cuda_python/.moon-out/wheel-pure + + # CI explicitly opts into the bindings version from its trusted staged + # wheel. Ordinary local builds always use the checkout's SCM version, + # even when an output from an earlier build is still present. + USE_STAGED_BINDINGS_VERSION=$(printenv CUDA_PYTHON_USE_STAGED_BINDINGS_VERSION || true) + if [[ -n "$USE_STAGED_BINDINGS_VERSION" && "$USE_STAGED_BINDINGS_VERSION" != "1" ]]; then + echo "CUDA_PYTHON_USE_STAGED_BINDINGS_VERSION must be unset or 1" >&2 + exit 1 + fi + if [[ "$USE_STAGED_BINDINGS_VERSION" == "1" ]]; then + shopt -s nullglob + set -- cuda_bindings/.moon-out/wheel-current/*.whl + [[ $# -eq 1 ]] || { + echo "expected one staged cuda.bindings wheel, found $#" >&2 + exit 1 + } + BINDINGS_VERSION=$(python -c "import email, sys, zipfile; archive = zipfile.ZipFile(sys.argv[1]); names = [name for name in archive.namelist() if name.endswith('.dist-info/METADATA')]; assert len(names) == 1, names; print(email.message_from_bytes(archive.read(names[0]))['Version'])" "$1") + export SETUPTOOLS_SCM_PRETEND_VERSION="$BINDINGS_VERSION" + export SETUPTOOLS_SCM_PRETEND_VERSION_FOR_CUDA_PYTHON="$BINDINGS_VERSION" + fi + python -m pip wheel --verbose --no-deps \ + --wheel-dir cuda_python/.moon-out/wheel-pure ./cuda_python + shopt -s nullglob + set -- cuda_python/.moon-out/wheel-pure/*.whl + [[ $# -eq 1 ]] || { + echo "expected one cuda-python metapackage wheel, found $#" >&2 + exit 1 + } inputs: - '@group(package)' - {project: bindings, group: package} - - '/ci/tools/artifacts.py' - - '/ci/tools/build_artifacts.py' - - '/ci/tools/moon_fingerprint.py' - - '/.github/workflows/build-pure-wheel.yml' + - '/cuda_bindings/.moon-out/wheel-current/*.whl' + - '/.github/workflows/build-wheel.yml' outputs: - '.moon-out/wheel-pure' checks: - - check: fingerprint - script: python ci/tools/moon_fingerprint.py metapackage portable + - &metapackage_scm_fingerprint + check: fingerprint + script: git describe --always --dirty --tags --long --match 'v*[0-9]*' + hash: stdout + - &scm_environment_fingerprint + check: fingerprint + script: >- + python -c "import hashlib, os; + names = sorted(name for name in os.environ + if name in ('CUDA_PYTHON_USE_STAGED_BINDINGS_VERSION', 'SOURCE_DATE_EPOCH') + or name.startswith(('SETUPTOOLS_SCM_', 'VCS_VERSIONING_'))); + payload = '\0'.join(name + '=' + os.environ[name] for name in names); + print(hashlib.sha256(payload.encode()).hexdigest())" + hash: stdout + - &python_runtime_fingerprint + check: fingerprint + script: >- + python -c "import platform, sysconfig; + print(platform.python_implementation(), platform.python_version(), + sysconfig.get_config_var('SOABI') or '', sep='\n')" + hash: stdout + - &python_build_tools_fingerprint + check: fingerprint + script: >- + python -c "import importlib.metadata as metadata; + names = ('build', 'cibuildwheel', 'packaging', 'pip', 'setuptools', 'setuptools-scm', 'wheel'); + normalize = lambda value: value.lower().replace('_', '-'); + versions = {normalize(dist.metadata['Name']): dist.version for dist in metadata.distributions() if dist.metadata['Name']}; + print(*(name + '=' + versions.get(name, '') for name in names), sep='\n')" hash: stdout - tags: [ci-wheel-pure, runner-build-portable] type: build options: cache: true - cacheKey: wheel-pure-v2 + cacheKey: wheel-pure-v3 priority: critical runInCI: true sdist: - command: python - args: [-m, ci.tools.build_artifacts, sdist, metapackage] + command: bash + args: + - -euo + - pipefail + - -c + - | + if [[ -L cuda_python/.moon-out || ( -e cuda_python/.moon-out && ! -d cuda_python/.moon-out ) ]]; then + echo "refusing to use non-directory output root: cuda_python/.moon-out" >&2 + exit 1 + fi + if [[ -L cuda_python/.moon-out/sdist || ( -e cuda_python/.moon-out/sdist && ! -d cuda_python/.moon-out/sdist ) ]]; then + echo "refusing to replace non-directory output: cuda_python/.moon-out/sdist" >&2 + exit 1 + fi + mkdir -p cuda_python/.moon-out + rm -rf -- cuda_python/.moon-out/sdist + mkdir -p cuda_python/.moon-out/sdist + python -m build --sdist --outdir cuda_python/.moon-out/sdist cuda_python + shopt -s nullglob + set -- cuda_python/.moon-out/sdist/*.tar.gz + [[ $# -eq 1 ]] || { + echo "expected one cuda-python source distribution, found $#" >&2 + exit 1 + } + ARCHIVE=$1 + python -m pip wheel --no-deps \ + --wheel-dir cuda_python/.moon-out/sdist "$ARCHIVE" + set -- cuda_python/.moon-out/sdist/*.whl + [[ $# -eq 1 ]] || { + echo "expected one cuda-python wheel from source distribution, found $#" >&2 + exit 1 + } deps: - target: bindings:sdist cacheStrategy: outputs inputs: - '@group(package)' - {project: bindings, group: package} - - '/ci/tools/artifacts.py' - - '/ci/tools/build_artifacts.py' - - '/ci/tools/moon_fingerprint.py' - '/.github/workflows/test-sdist-linux.yml' - '/.github/workflows/test-sdist-windows.yml' outputs: - '.moon-out/sdist' checks: + - *metapackage_scm_fingerprint + - *scm_environment_fingerprint + - *python_runtime_fingerprint + - *python_build_tools_fingerprint - check: fingerprint - script: python ci/tools/moon_fingerprint.py metapackage sdist + script: >- + python -c "import platform; + print(platform.system(), platform.machine(), sep='\n')" + hash: stdout + - check: fingerprint + script: >- + python -c "import hashlib, os; + names = ('BUILD_CUDA_MAJOR', 'BUILD_CUDA_VER', 'BUILD_PREV_CUDA_MAJOR', + 'CC', 'CIBW_BEFORE_BUILD_LINUX', 'CIBW_BEFORE_BUILD_WINDOWS', + 'CIBW_BEFORE_TEST_LINUX', 'CIBW_BUILD', 'CIBW_ENABLE', 'CIBW_TEST_COMMAND', + 'CL', 'CPLUS_INCLUDE_PATH', 'CXX', 'CUDA_CORE_BUILD_MAJOR', 'CUDA_PATH', + 'CUDA_PYTHON_COVERAGE', 'CUDA_PYTHON_LANE', 'CUDA_PYTHON_PARALLEL_LEVEL', 'CUDA_VER', + 'HOST_PLATFORM', 'PY_EXT_SUFFIX', 'PY_VER', 'SCCACHE_CACHE_SIZE', + 'SCCACHE_DIR', 'SCCACHE_PATH'); + payload = '\0'.join(name + '=' + os.environ.get(name, '') for name in names); + print(hashlib.sha256(payload.encode()).hexdigest())" hash: stdout tags: [ci-sdist, runner-sdist-linux, runner-sdist-windows] type: build @@ -82,18 +196,21 @@ tasks: test-installed-linux: command: bash args: [ci/tools/run-tests, metapackage] + deps: + - target: metapackage:wheel-pure + cacheStrategy: outputs inputs: - '@group(package)' - {project: pathfinder, group: package} - {project: bindings, group: package} - {project: core, group: package} - '/cuda_core/.moon-out/wheel-merged/*.whl' + - '/cuda_python/.moon-out/wheel-pure/*.whl' - '/ci/tools/run-tests' - '/ci/test-matrix.yml' - '/ci/tools/env-vars' - '/tests/**/*' - '/ci/tools/merge_cuda_core_wheels.py' - - '/.github/workflows/build-pure-wheel.yml' - '/.github/workflows/build-wheel.yml' - '/ci/tools/guess_latest.sh' - '/ci/tools/install_gpu_driver.sh' @@ -108,18 +225,21 @@ tasks: test-installed-windows: command: bash args: [ci/tools/run-tests, metapackage] + deps: + - target: metapackage:wheel-pure + cacheStrategy: outputs inputs: - '@group(package)' - {project: pathfinder, group: package} - {project: bindings, group: package} - {project: core, group: package} - '/cuda_core/.moon-out/wheel-merged/*.whl' + - '/cuda_python/.moon-out/wheel-pure/*.whl' - '/ci/tools/run-tests' - '/ci/test-matrix.yml' - '/ci/tools/env-vars' - '/tests/**/*' - '/ci/tools/merge_cuda_core_wheels.py' - - '/.github/workflows/build-pure-wheel.yml' - '/.github/workflows/build-wheel.yml' - '/ci/tools/configure_driver_mode.ps1' - '/ci/tools/install_gpu_driver.ps1' @@ -134,6 +254,9 @@ tasks: docs-ci: command: bash args: [cuda_python/docs/build_docs.sh, moon-ci] + deps: + - target: metapackage:wheel-pure + cacheStrategy: outputs env: CUDA_PYTHON_DOCS_LATEST_ONLY: '${CUDA_PYTHON_DOCS_LATEST_ONLY}' inputs: @@ -142,6 +265,7 @@ tasks: - {project: pathfinder, group: package} - {project: bindings, group: package} - {project: core, group: package} + - '/cuda_python/.moon-out/wheel-pure/*.whl' - '/cuda_python/docs/environment-docs.yml' - '/.github/workflows/build-docs.yml' outputs: diff --git a/cuda_python_test_helpers/moon.yml b/cuda_python_test_helpers/moon.yml index 6ac12567a60..13a85531daf 100644 --- a/cuda_python_test_helpers/moon.yml +++ b/cuda_python_test_helpers/moon.yml @@ -13,11 +13,44 @@ taskOptions: cache: false runFromWorkspaceRoot: true runInCI: false + shell: false tasks: + # GitHub Actions runs this after changing to the test Python/toolkit phase. + # The canonical wheel inputs are therefore staged by the preceding build + # phase instead of being executable task dependencies in this project. prepare-test-assets: - command: python - args: [-m, ci.tools.prepare_test_assets] + command: bash + args: + - -euo + - pipefail + - -c + - | + shopt -s nullglob + set -- cuda_pathfinder/.moon-out/wheel-pure/*.whl + [[ $# -eq 1 ]] || { + echo "expected one cuda.pathfinder wheel, found $#" >&2 + exit 1 + } + PATHFINDER_WHEEL=$1 + set -- cuda_bindings/.moon-out/wheel-current/*.whl + [[ $# -eq 1 ]] || { + echo "expected one cuda.bindings wheel, found $#" >&2 + exit 1 + } + BINDINGS_WHEEL=$1 + set -- cuda_core/.moon-out/wheel-current/*.whl + [[ $# -eq 1 ]] || { + echo "expected one cuda.core wheel, found $#" >&2 + exit 1 + } + CORE_WHEEL=$1 + python -m pip install \ + "$PATHFINDER_WHEEL" \ + "$BINDINGS_WHEEL" \ + "$CORE_WHEEL" \ + --group cuda_bindings/pyproject.toml:test \ + --group cuda_core/pyproject.toml:test inputs: - '/cuda_pathfinder/pyproject.toml' - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' @@ -25,8 +58,6 @@ tasks: - '/cuda_bindings/.moon-out/wheel-current/*.whl' - '/cuda_core/pyproject.toml' - '/cuda_core/.moon-out/wheel-current/*.whl' - - '/ci/tools/artifacts.py' - - '/ci/tools/prepare_test_assets.py' - '/.github/workflows/build-wheel.yml' options: os: [linux, windows] diff --git a/moon.yml b/moon.yml index 16ad78e2707..164f6b07883 100644 --- a/moon.yml +++ b/moon.yml @@ -21,17 +21,13 @@ toolchains: taskOptions: cache: false runInCI: false + shell: false fileGroups: orchestration: - '/.moon/**/*' - '/moon.yml' - '/**/moon.yml' - - '/ci/tools/artifacts.py' - - '/ci/tools/build_artifacts.py' - - '/ci/tools/prepare_test_assets.py' - - '/ci/tools/run_pixi_test.py' - - '/ci/tools/moon_fingerprint.py' - '/ci/tools/env-vars' - '/ci/versions.yml' - '/.github/actions/**/*' @@ -67,7 +63,6 @@ fileGroups: - '!/.github/actions/**/*' - '!/.github/workflows/ci.yml' - '!/.github/workflows/build-docs.yml' - - '!/.github/workflows/build-pure-wheel.yml' - '!/.github/workflows/build-wheel.yml' - '!/.github/workflows/test-sdist-linux.yml' - '!/.github/workflows/test-sdist-windows.yml' @@ -90,11 +85,6 @@ fileGroups: - '!/ci/tools/install_gpu_driver.ps1' - '!/ci/tools/install_gpu_driver.sh' - '!/ci/tools/merge_cuda_core_wheels.py' - - '!/ci/tools/artifacts.py' - - '!/ci/tools/build_artifacts.py' - - '!/ci/tools/prepare_test_assets.py' - - '!/ci/tools/run_pixi_test.py' - - '!/ci/tools/moon_fingerprint.py' - '!/ci/tools/run-tests' - '!/ci/tools/setup-sanitizer' - '!/ci/tools/tests/test_moon_tasks.py' @@ -183,12 +173,7 @@ tasks: inputs: - '/.moon/**/*' - '/**/moon.yml' - - '/ci/tools/artifacts.py' - - '/ci/tools/build_artifacts.py' - - '/ci/tools/prepare_test_assets.py' - '/ci/tools/run-tests' - - '/ci/tools/run_pixi_test.py' - - '/ci/tools/moon_fingerprint.py' - '/ci/tools/tests/test_moon_tasks.py' - '/ci/tools/tests/test_moon_workspace.py' tags: [ci-quality, runner-quality] @@ -237,7 +222,6 @@ tasks: - '/cuda_core/.moon-out/docs-ci/**/*' - '/cuda_python/.moon-out/docs-ci/**/*' - '/cuda_python/docs/assemble_moon_docs.sh' - - '/.github/workflows/build-pure-wheel.yml' - '/.github/workflows/build-wheel.yml' - '/.github/workflows/build-docs.yml' outputs: From 7dbdfc235107d6b0498feecd720abb25431c94fe Mon Sep 17 00:00:00 2001 From: Keith Kraus Date: Wed, 19 Aug 2026 11:17:21 -0400 Subject: [PATCH 6/6] Refactor CI orchestration around Moon --- .github/workflows/build-docs.yml | 75 +- .github/workflows/build-wheel.yml | 116 ++- .github/workflows/ci-nightly.yml | 102 +-- .github/workflows/ci.yml | 281 ++++--- .github/workflows/coverage.yml | 51 +- .github/workflows/test-sdist-linux.yml | 37 +- .github/workflows/test-sdist-windows.yml | 37 +- .github/workflows/test-wheel-linux.yml | 62 +- .github/workflows/test-wheel-windows.yml | 54 +- .moon/tasks/docs.yml | 30 + .moon/tasks/installed-tests.yml | 49 ++ .moon/tasks/native-package.yml | 58 ++ .moon/tasks/pixi-package.yml | 47 ++ .moon/tasks/pure-wheel-package.yml | 76 ++ .moon/tasks/python-package.yml | 60 ++ CONTRIBUTING.md | 155 ++-- ci/.ci-pipeline-regen.md | 106 --- ci/build-constraints.txt | 20 + ci/build-matrix.yml | 17 + ci/ci-pipeline.svg | 172 ----- ci/tools/env-vars | 29 +- ci/tools/merge_cuda_core_wheels.py | 30 +- ci/tools/run-tests | 127 ++-- ci/tools/tests/test_moon_tasks.py | 88 ++- ci/tools/tests/test_moon_workspace.py | 684 ++++++++++++++++-- cuda_bindings/AGENTS.md | 2 +- cuda_bindings/docs/build_docs.sh | 102 +-- cuda_bindings/moon.yml | 252 +------ cuda_bindings/pixi.toml | 4 +- cuda_bindings/pyproject.toml | 1 + cuda_bindings/tests/conftest.py | 18 - cuda_bindings/tests/cython/build_tests.bat | 12 - cuda_bindings/tests/cython/build_tests.py | 95 +-- cuda_bindings/tests/cython/build_tests.sh | 23 - cuda_core/AGENTS.md | 2 +- cuda_core/docs/build_docs.sh | 97 +-- cuda_core/moon.yml | 493 +++---------- cuda_core/pixi.toml | 4 +- cuda_core/pytest.ini | 1 + cuda_core/tests/conftest.py | 18 - cuda_core/tests/cython/build_tests.py | 98 +-- cuda_core/tests/cython/build_tests.sh | 22 - cuda_core/tests/test_module.py | 26 +- cuda_pathfinder/docs/build_docs.sh | 101 +-- cuda_pathfinder/moon.yml | 305 +------- cuda_python/docs/assemble_moon_docs.sh | 8 +- cuda_python/docs/build_component_docs.sh | 171 +++++ cuda_python/docs/build_docs.sh | 100 +-- cuda_python/docs/environment-docs.yml | 3 +- cuda_python/moon.yml | 190 +---- .../cython_test_builder.py | 147 ++++ cuda_python_test_helpers/moon.yml | 164 ++++- moon.yml | 49 +- pytest.ini | 2 + toolshed/setup-docs-env.sh | 68 -- 55 files changed, 2315 insertions(+), 2826 deletions(-) create mode 100644 .moon/tasks/docs.yml create mode 100644 .moon/tasks/installed-tests.yml create mode 100644 .moon/tasks/native-package.yml create mode 100644 .moon/tasks/pixi-package.yml create mode 100644 .moon/tasks/pure-wheel-package.yml create mode 100644 .moon/tasks/python-package.yml delete mode 100644 ci/.ci-pipeline-regen.md create mode 100644 ci/build-constraints.txt create mode 100644 ci/build-matrix.yml delete mode 100644 ci/ci-pipeline.svg delete mode 100644 cuda_bindings/tests/cython/build_tests.bat delete mode 100755 cuda_bindings/tests/cython/build_tests.sh delete mode 100755 cuda_core/tests/cython/build_tests.sh create mode 100755 cuda_python/docs/build_component_docs.sh create mode 100644 cuda_python_test_helpers/cuda_python_test_helpers/cython_test_builder.py delete mode 100755 toolshed/setup-docs-env.sh diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 13b5ae671a9..d350b725321 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -28,11 +28,6 @@ on: required: false default: ${{ github.run_id }} type: string - sha: - description: "Commit SHA used in native wheel artifact names" - required: false - default: ${{ github.sha }} - type: string is-release: description: "Are we building release docs?" required: false @@ -99,7 +94,6 @@ jobs: environment-file: ./cuda_python/docs/environment-docs.yml miniforge-version: latest conda-remove-defaults: "true" - python-version: 3.12 - name: Check conda env run: | @@ -117,7 +111,7 @@ jobs: - name: Set environment variables run: | - PYTHON_VERSION_FORMATTED="312" # see above + PYTHON_VERSION_FORMATTED="$(python -c 'import sys; print(f"{sys.version_info.major}{sys.version_info.minor}")')" REPO_DIR=$(pwd) if [[ ${{ inputs.is-release }} == "true" ]]; then @@ -126,21 +120,18 @@ jobs: if [[ -z "${DOCS_GITHUB_REF}" ]]; then DOCS_GITHUB_REF="${GITHUB_REF_NAME}" fi + + CUDA_CORE_ARTIFACT_BASENAME="cuda-core-python${PYTHON_VERSION_FORMATTED}-linux-64" + echo "CUDA_CORE_ARTIFACT_NAME=${CUDA_CORE_ARTIFACT_BASENAME}-${FILE_HASH}" >> $GITHUB_ENV + echo "CUDA_CORE_ARTIFACTS_DIR=$(realpath "$REPO_DIR/cuda_core/dist")" >> $GITHUB_ENV + CUDA_BINDINGS_ARTIFACT_BASENAME="cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda${BUILD_CTK_VER}-linux-64" + echo "CUDA_BINDINGS_ARTIFACT_NAME=${CUDA_BINDINGS_ARTIFACT_BASENAME}-${FILE_HASH}" >> $GITHUB_ENV + echo "CUDA_BINDINGS_ARTIFACTS_DIR=$(realpath "$REPO_DIR/cuda_bindings/dist")" >> $GITHUB_ENV else - FILE_HASH="${{ inputs.sha }}" - DOCS_GITHUB_REF="${{ inputs.sha }}" + DOCS_GITHUB_REF="${GITHUB_SHA}" fi - # make outputs from the previous job as env vars - CUDA_CORE_ARTIFACT_BASENAME="cuda-core-python${PYTHON_VERSION_FORMATTED}-linux-64" echo "CUDA_PYTHON_DOCS_GITHUB_REF=${DOCS_GITHUB_REF}" >> $GITHUB_ENV - echo "CUDA_CORE_ARTIFACT_BASENAME=${CUDA_CORE_ARTIFACT_BASENAME}" >> $GITHUB_ENV - echo "CUDA_CORE_ARTIFACT_NAME=${CUDA_CORE_ARTIFACT_BASENAME}-${FILE_HASH}" >> $GITHUB_ENV - echo "CUDA_CORE_ARTIFACTS_DIR=$(realpath "$REPO_DIR/cuda_core/dist")" >> $GITHUB_ENV - CUDA_BINDINGS_ARTIFACT_BASENAME="cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda${BUILD_CTK_VER}-linux-64" - echo "CUDA_BINDINGS_ARTIFACT_BASENAME=${CUDA_BINDINGS_ARTIFACT_BASENAME}" >> $GITHUB_ENV - echo "CUDA_BINDINGS_ARTIFACT_NAME=${CUDA_BINDINGS_ARTIFACT_BASENAME}-${FILE_HASH}" >> $GITHUB_ENV - echo "CUDA_BINDINGS_ARTIFACTS_DIR=$(realpath "$REPO_DIR/cuda_bindings/dist")" >> $GITHUB_ENV - name: Download universal release wheels if: ${{ inputs.is-release }} @@ -193,20 +184,14 @@ jobs: CUDA_PYTHON_USE_STAGED_BINDINGS_VERSION: "1" run: moon run metapackage:wheel-pure - - name: Install all packages + - name: Install release packages + if: ${{ inputs.is-release }} run: | - if [[ "${{ inputs.is-release }}" == "true" ]]; then - pip install cuda-pathfinder-wheel/*.whl - pip install "${CUDA_BINDINGS_ARTIFACTS_DIR}"/*.whl - pip install "${CUDA_CORE_ARTIFACTS_DIR}"/*.whl - # Subpackages are already installed from release artifacts above. - pip install --no-deps cuda-python-wheel/*.whl - else - pip install cuda_pathfinder/.moon-out/wheel-pure/*.whl - pip install cuda_bindings/.moon-out/wheel-current/*.whl - pip install cuda_core/.moon-out/wheel-merged/*.whl - pip install --no-deps cuda_python/.moon-out/wheel-pure/*.whl - fi + pip install cuda-pathfinder-wheel/*.whl + pip install "${CUDA_BINDINGS_ARTIFACTS_DIR}"/*.whl + pip install "${CUDA_CORE_ARTIFACTS_DIR}"/*.whl + # Subpackages are already installed from release artifacts above. + pip install --no-deps cuda-python-wheel/*.whl # This step sets the PR_NUMBER/BUILD_LATEST/BUILD_PREVIEW env vars. - name: Get PR number @@ -221,8 +206,6 @@ jobs: - name: Build all docs if: ${{ inputs.component == 'all' }} - env: - CUDA_PYTHON_USE_STAGED_BINDINGS_VERSION: "1" run: | if [[ "${{ inputs.is-release }}" == "false" ]]; then # Render context differs between main, PR previews, and releases, @@ -245,23 +228,37 @@ jobs: if: ${{ inputs.component != 'all' }} run: | COMPONENT=$(echo "${{ inputs.component }}" | tr '-' '_') - pushd ${COMPONENT}/docs/ if [[ "${{ inputs.is-release }}" == "false" ]]; then - ./build_docs.sh latest-only + case "${{ inputs.component }}" in + cuda-pathfinder) MOON_PROJECT=pathfinder ;; + cuda-bindings) MOON_PROJECT=bindings ;; + cuda-core) MOON_PROJECT=core ;; + cuda-python) MOON_PROJECT=metapackage ;; + *) + echo "unsupported docs component: ${{ inputs.component }}" >&2 + exit 1 + ;; + esac + moon ci "${MOON_PROJECT}:docs-ci" --force --upstream deep --downstream none else + pushd "${COMPONENT}/docs/" ./build_docs.sh # At release time, we don't want to update the latest docs rm -rf build/html/latest + ls -l build + popd fi - ls -l build - popd if [[ "${{ inputs.component }}" != "cuda-python" ]]; then TARGET="${{ inputs.component }}" - mkdir -p artifacts/docs/${TARGET} + mkdir -p "artifacts/docs/${TARGET}" else TARGET="" fi - mv ${COMPONENT}/docs/build/html/* artifacts/docs/${TARGET} + if [[ "${{ inputs.is-release }}" == "false" ]]; then + cp -aL "${COMPONENT}/docs/build/html/." "artifacts/docs/${TARGET}/" + else + mv "${COMPONENT}"/docs/build/html/* "artifacts/docs/${TARGET}" + fi - name: Write rendered docs file list if: ${{ !inputs.is-release && github.ref_name != 'main' && !startsWith(github.ref_name, 'release/') }} diff --git a/.github/workflows/build-wheel.yml b/.github/workflows/build-wheel.yml index d62da760092..00aa0be494b 100644 --- a/.github/workflows/build-wheel.yml +++ b/.github/workflows/build-wheel.yml @@ -22,6 +22,10 @@ on: description: "Base revision used by Moon affected checks" required: true type: string + python-matrix: + description: "Python ABI rows provisioned by GitHub Actions" + required: true + type: string force-all: description: "Force selected Moon tasks when no reusable baseline exists" required: false @@ -40,20 +44,14 @@ jobs: build: strategy: fail-fast: false - matrix: - include: - - {python-version: "3.10", python-version-formatted: "310"} - - {python-version: "3.11", python-version-formatted: "311"} - - {python-version: "3.12", python-version-formatted: "312"} - - {python-version: "3.13", python-version-formatted: "313"} - - {python-version: "3.14", python-version-formatted: "314"} - - {python-version: "3.14t", python-version-formatted: "314t"} - - {python-version: "3.15", python-version-formatted: "315"} - - {python-version: "3.15t", python-version-formatted: "315t"} + matrix: ${{ fromJSON(inputs.python-matrix) }} name: py${{ matrix.python-version }} runs-on: ${{ (inputs.host-platform == 'linux-64' && 'linux-amd64-cpu8') || (inputs.host-platform == 'linux-aarch64' && 'linux-arm64-cpu8') || (inputs.host-platform == 'win-64' && 'windows-2022') }} + env: + # Only canonical outputs cross runners; upstream-none also omits hash deps. + MOON_CACHE: "off" steps: - name: Checkout ${{ github.event.repository.name }} uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -104,7 +102,7 @@ jobs: auto-install: false auto-setup: false - - name: Restore trusted exact-base Moon cache + - name: Restore trusted exact-base Moon lane if: ${{ inputs.baseline-run-id != '' && !inputs.force-all }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -153,7 +151,9 @@ jobs: env - name: Install externally managed build tools - run: python -m pip install "cibuildwheel==4.1.1" twine wheel + run: >- + python -m pip install --constraint ci/build-constraints.txt + "pip>=25.3" "cibuildwheel==4.1.1" "twine==7.0.0" wheel - name: Set up mini CTK uses: ./.github/actions/fetch_ctk @@ -165,7 +165,6 @@ jobs: - name: Build current wheels with Moon env: CUDA_PYTHON_USE_STAGED_BINDINGS_VERSION: "1" - CUDA_PYTHON_LANE: ${{ inputs.host-platform }}-py${{ matrix.python-version }}-cuda${{ inputs.cuda-version }} MOON_BASE: ${{ inputs.moon-base }} MOON_HEAD: ${{ github.sha }} CIBW_BUILD: ${{ env.CIBW_BUILD }} @@ -197,19 +196,26 @@ jobs: # Run the test stage so the sccache summary hook is not skipped. CIBW_TEST_COMMAND: 'echo "ok!"' run: | - args=() + args=(--upstream none --downstream none) if [[ "${{ inputs.force-all }}" == "true" ]]; then args+=(--force) fi - # Moon owns the pathfinder -> bindings -> core build chain. - moon ci core:wheel-current --upstream deep --downstream none "${args[@]}" - # The metapackage is needed only in the Linux/Python 3.12 lane used - # by docs and release publication. Run it after bindings so its exact - # development pin is derived from the wheel that this lane produced. + + # Moon 2.5.1 drops dependency ordering with upstream-none, so each + # producer/consumer boundary is an explicit completed phase. + moon ci pathfinder:wheel-pure "${args[@]}" + moon ci bindings:wheel-current "${args[@]}" + + # The CI-only metapackage mode reads the staged bindings wheel but + # intentionally has no executable dependency (local pure builds + # must not require a native toolkit). Keep this phase boundary; + # Moon can still build core and the metapackage in parallel. + targets=(core:wheel-current) if [[ "${{ inputs.host-platform }}" == "linux-64" && "${{ matrix.python-version-formatted }}" == "312" ]]; then - moon ci metapackage:wheel-pure --upstream none --downstream none "${args[@]}" + targets+=(metapackage:wheel-pure) fi + moon ci "${targets[@]}" "${args[@]}" - name: Set up Python id: setup-python2 @@ -220,7 +226,9 @@ jobs: allow-prereleases: ${{ startsWith(matrix.python-version, '3.15') }} - name: Install target-Python build tools - run: python -m pip install "cibuildwheel==4.1.1" twine wheel + run: >- + python -m pip install --constraint ci/build-constraints.txt + "pip>=25.3" wheel - name: Enable Scientific Python Nightly Wheels for Python 3.15 if: ${{ startsWith(matrix.python-version, '3.15') }} @@ -251,19 +259,20 @@ jobs: - name: Build target-Python test assets with Moon env: - CUDA_PYTHON_LANE: ${{ inputs.host-platform }}-py${{ matrix.python-version }}-cuda${{ inputs.cuda-version }} MOON_BASE: ${{ inputs.moon-base }} MOON_HEAD: ${{ github.sha }} run: | - args=() - if [[ "${{ inputs.force-all }}" == "true" ]]; then - args+=(--force) - fi - # Install the shared test groups before Moon fingerprints the compiler - # and resolved Cython/NumPy versions for the cached binary outputs. - moon ci test-helpers:prepare-test-assets --force --upstream none --downstream none - moon ci bindings:cython-test-assets core:cython-test-assets \ - --upstream none --downstream none "${args[@]}" + moon ci ':#ci-build-cython-assets' \ + --upstream deep --downstream none \ + ${{ inputs.force-all && '--force' || '' }} + + # cibuildwheel can target CPython 3.10, but its host interpreter must be + # Python 3.11 or newer. Keep the target interpreter through the Cython + # asset phase, then return to the same 3.12 host used for current wheels. + - name: Restore cibuildwheel host Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: "3.12" # Note: This overwrites CUDA_PATH etc - name: Set up previous mini CTK @@ -305,7 +314,6 @@ jobs: - name: Build previous-CTK outputs and merge wheels with Moon env: - CUDA_PYTHON_LANE: ${{ inputs.host-platform }}-py${{ matrix.python-version }}-cuda${{ inputs.prev-cuda-version }} MOON_BASE: ${{ inputs.moon-base }} MOON_HEAD: ${{ github.sha }} CIBW_BUILD: ${{ env.CIBW_BUILD }} @@ -337,15 +345,15 @@ jobs: CIBW_BEFORE_TEST_LINUX: '"/host/${{ env.SCCACHE_PATH }}" --show-adv-stats' CIBW_TEST_COMMAND: 'echo "ok!"' run: | - args=() + args=(--upstream none --downstream none) if [[ "${{ inputs.force-all }}" == "true" ]]; then args+=(--force) fi - # The previous-CTK wheel and test binaries are independent, so Moon - # builds them in parallel. The merge then consumes the selected or - # restored current and previous wheel outputs. - moon ci core:wheel-previous core:test-binaries --upstream none --downstream none "${args[@]}" - moon ci core:wheel-merge --upstream none --downstream none "${args[@]}" + + # The previous wheel and test binaries are independent; the merge + # starts only after that producer phase has completed. + moon ci core:wheel-previous core:test-binaries "${args[@]}" + moon ci core:wheel-merge "${args[@]}" - name: Validate native lane outputs run: | @@ -358,10 +366,12 @@ jobs: cuda_pathfinder/.moon-out cuda_bindings/.moon-out cuda_core/.moon-out test "$(find cuda_pathfinder/.moon-out/wheel-pure -maxdepth 1 -name '*.whl' -type f | wc -l)" -eq 1 test "$(find cuda_bindings/.moon-out/wheel-current -maxdepth 1 -name '*.whl' -type f | wc -l)" -eq 1 + test "$(find cuda_bindings/.moon-out/wheel-previous -maxdepth 1 -name '*.whl' -type f | wc -l)" -eq 1 test "$(find cuda_core/.moon-out/wheel-merged -maxdepth 1 -name '*.whl' -type f | wc -l)" -eq 1 wheels=( cuda_pathfinder/.moon-out/wheel-pure/*.whl cuda_bindings/.moon-out/wheel-current/*.whl + cuda_bindings/.moon-out/wheel-previous/*.whl cuda_core/.moon-out/wheel-merged/*.whl ) if [[ "${{ inputs.host-platform }}" == "linux-64" && @@ -400,33 +410,6 @@ jobs: if-no-files-found: error overwrite: true - - name: Upload cuda.bindings Cython tests - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }}-tests - path: cuda_bindings/.moon-out/cython-tests/test_*${{ env.PY_EXT_SUFFIX }} - if-no-files-found: error - overwrite: true - - - name: Upload cuda.core Cython tests - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-tests - path: cuda_core/.moon-out/cython-tests/test_*${{ env.PY_EXT_SUFFIX }} - if-no-files-found: error - overwrite: true - - - name: Upload cuda.core test binaries - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-test-binaries - path: | - cuda_core/.moon-out/test-binaries/*.o - cuda_core/.moon-out/test-binaries/*.a - cuda_core/.moon-out/test-binaries/*.lib - if-no-files-found: error - overwrite: true - - name: Upload cuda.core build artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -441,11 +424,10 @@ jobs: with: name: moon-lane-build-${{ inputs.host-platform }}-py${{ matrix.python-version-formatted }} path: | - .moon/cache/hashes - .moon/cache/outputs cuda_pathfinder/.moon-out/wheel-pure cuda_python/.moon-out/wheel-pure cuda_bindings/.moon-out/wheel-current + cuda_bindings/.moon-out/wheel-previous cuda_bindings/.moon-out/cython-tests cuda_core/.moon-out/wheel-current cuda_core/.moon-out/wheel-previous diff --git a/.github/workflows/ci-nightly.yml b/.github/workflows/ci-nightly.yml index f3f0802fccc..3e70d3370ee 100644 --- a/.github/workflows/ci-nightly.yml +++ b/.github/workflows/ci-nightly.yml @@ -78,14 +78,14 @@ jobs: id: find env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REQUESTED_RUN_ID: ${{ inputs.run-id }} run: | expected=() - python_versions=(310 311 312 313 314 314t 315 315t) host_platforms=(linux-64 linux-aarch64 win-64) for host_platform in "${host_platforms[@]}"; do - for python_version in "${python_versions[@]}"; do + while IFS= read -r python_version; do expected+=("moon-lane-build-${host_platform}-py${python_version}") - done + done < <(yq -r '.include[]."python-version-formatted"' ci/build-matrix.yml) done has_moon_lanes() { @@ -114,8 +114,12 @@ jobs: done } - if [[ -n "${{ inputs.run-id }}" ]]; then - RUN_ID="${{ inputs.run-id }}" + if [[ -n "${REQUESTED_RUN_ID}" ]]; then + if [[ ! "${REQUESTED_RUN_ID}" =~ ^[0-9]+$ ]]; then + echo "::error::run-id must contain only decimal digits." + exit 1 + fi + RUN_ID="${REQUESTED_RUN_ID}" RUN="$(gh api "repos/${{ github.repository }}/actions/runs/${RUN_ID}")" if ! jq -e ' .head_branch == "main" and @@ -237,11 +241,9 @@ jobs: secrets: inherit uses: ./.github/workflows/test-wheel-linux.yml with: - build-type: nightly host-platform: linux-64 build-ctk-ver: ${{ needs.find-wheels.outputs.CUDA_BUILD_VER }} run-id: ${{ needs.find-wheels.outputs.RUN_ID }} - sha: ${{ needs.find-wheels.outputs.HEAD_SHA }} test-mode: nightly-pytorch matrix: ${{ needs.find-wheels.outputs.PYTORCH_LINUX_64_MATRIX }} @@ -255,11 +257,9 @@ jobs: secrets: inherit uses: ./.github/workflows/test-wheel-linux.yml with: - build-type: nightly host-platform: linux-aarch64 build-ctk-ver: ${{ needs.find-wheels.outputs.CUDA_BUILD_VER }} run-id: ${{ needs.find-wheels.outputs.RUN_ID }} - sha: ${{ needs.find-wheels.outputs.HEAD_SHA }} test-mode: nightly-pytorch matrix: ${{ needs.find-wheels.outputs.PYTORCH_LINUX_ARM64_MATRIX }} @@ -273,11 +273,9 @@ jobs: secrets: inherit uses: ./.github/workflows/test-wheel-windows.yml with: - build-type: nightly host-platform: win-64 build-ctk-ver: ${{ needs.find-wheels.outputs.CUDA_BUILD_VER }} run-id: ${{ needs.find-wheels.outputs.RUN_ID }} - sha: ${{ needs.find-wheels.outputs.HEAD_SHA }} test-mode: nightly-pytorch matrix: ${{ needs.find-wheels.outputs.PYTORCH_WINDOWS_MATRIX }} @@ -293,11 +291,9 @@ jobs: secrets: inherit uses: ./.github/workflows/test-wheel-linux.yml with: - build-type: nightly host-platform: linux-64 build-ctk-ver: ${{ needs.find-wheels.outputs.CUDA_BUILD_VER }} run-id: ${{ needs.find-wheels.outputs.RUN_ID }} - sha: ${{ needs.find-wheels.outputs.HEAD_SHA }} test-mode: nightly-numba-cuda matrix: ${{ needs.find-wheels.outputs.NUMBA_LINUX_64_MATRIX }} @@ -311,11 +307,9 @@ jobs: secrets: inherit uses: ./.github/workflows/test-wheel-linux.yml with: - build-type: nightly host-platform: linux-aarch64 build-ctk-ver: ${{ needs.find-wheels.outputs.CUDA_BUILD_VER }} run-id: ${{ needs.find-wheels.outputs.RUN_ID }} - sha: ${{ needs.find-wheels.outputs.HEAD_SHA }} test-mode: nightly-numba-cuda matrix: ${{ needs.find-wheels.outputs.NUMBA_LINUX_ARM64_MATRIX }} @@ -329,11 +323,9 @@ jobs: secrets: inherit uses: ./.github/workflows/test-wheel-windows.yml with: - build-type: nightly host-platform: win-64 build-ctk-ver: ${{ needs.find-wheels.outputs.CUDA_BUILD_VER }} run-id: ${{ needs.find-wheels.outputs.RUN_ID }} - sha: ${{ needs.find-wheels.outputs.HEAD_SHA }} test-mode: nightly-numba-cuda matrix: ${{ needs.find-wheels.outputs.NUMBA_WINDOWS_MATRIX }} @@ -349,11 +341,9 @@ jobs: secrets: inherit uses: ./.github/workflows/test-wheel-linux.yml with: - build-type: nightly host-platform: linux-64 build-ctk-ver: ${{ needs.find-wheels.outputs.CUDA_BUILD_VER }} run-id: ${{ needs.find-wheels.outputs.RUN_ID }} - sha: ${{ needs.find-wheels.outputs.HEAD_SHA }} test-mode: nightly-numba-cuda-mlir matrix: ${{ needs.find-wheels.outputs.MLIR_LINUX_64_MATRIX }} @@ -367,11 +357,9 @@ jobs: secrets: inherit uses: ./.github/workflows/test-wheel-windows.yml with: - build-type: nightly host-platform: win-64 build-ctk-ver: ${{ needs.find-wheels.outputs.CUDA_BUILD_VER }} run-id: ${{ needs.find-wheels.outputs.RUN_ID }} - sha: ${{ needs.find-wheels.outputs.HEAD_SHA }} test-mode: nightly-numba-cuda-mlir matrix: ${{ needs.find-wheels.outputs.MLIR_WINDOWS_MATRIX }} @@ -387,11 +375,9 @@ jobs: secrets: inherit uses: ./.github/workflows/test-wheel-linux.yml with: - build-type: nightly host-platform: linux-64 build-ctk-ver: ${{ needs.find-wheels.outputs.CUDA_BUILD_VER }} run-id: ${{ needs.find-wheels.outputs.RUN_ID }} - sha: ${{ needs.find-wheels.outputs.HEAD_SHA }} test-mode: nightly-cuda-core matrix: ${{ needs.find-wheels.outputs.CORE_LINUX_64_MATRIX }} @@ -405,11 +391,9 @@ jobs: secrets: inherit uses: ./.github/workflows/test-wheel-windows.yml with: - build-type: nightly host-platform: win-64 build-ctk-ver: ${{ needs.find-wheels.outputs.CUDA_BUILD_VER }} run-id: ${{ needs.find-wheels.outputs.RUN_ID }} - sha: ${{ needs.find-wheels.outputs.HEAD_SHA }} test-mode: nightly-cuda-core matrix: ${{ needs.find-wheels.outputs.CORE_WINDOWS_MATRIX }} @@ -425,12 +409,12 @@ jobs: secrets: inherit uses: ./.github/workflows/test-wheel-linux.yml with: - build-type: nightly host-platform: linux-aarch64 build-ctk-ver: ${{ needs.find-wheels.outputs.CUDA_BUILD_VER }} run-id: ${{ needs.find-wheels.outputs.RUN_ID }} - sha: ${{ needs.find-wheels.outputs.HEAD_SHA }} + source-ref: ${{ needs.find-wheels.outputs.HEAD_SHA }} test-mode: standard + force-all: true matrix: ${{ needs.find-wheels.outputs.STANDARD_LINUX_ARM64_MATRIX }} # ── Status check ── @@ -456,38 +440,32 @@ jobs: steps: - name: Exit run: | - # If any dependency was cancelled or failed, that's a failure. - # - # See ci.yml for the full rationale on why we must use always() - # and explicitly check each result rather than relying on the - # default behaviour. - if ${{ needs.test-ci-tools-for-release.result == 'cancelled' || - needs.test-ci-tools-for-release.result == 'failure' || - needs.find-wheels.result != 'success' }}; then - exit 1 - fi - if ${{ needs.test-pytorch-linux.result == 'cancelled' || - needs.test-pytorch-linux.result == 'failure' || - needs.test-pytorch-linux-aarch64.result == 'cancelled' || - needs.test-pytorch-linux-aarch64.result == 'failure' || - needs.test-pytorch-windows.result == 'cancelled' || - needs.test-pytorch-windows.result == 'failure' || - needs.test-numba-cuda-linux-64.result == 'cancelled' || - needs.test-numba-cuda-linux-64.result == 'failure' || - needs.test-numba-cuda-linux-aarch64.result == 'cancelled' || - needs.test-numba-cuda-linux-aarch64.result == 'failure' || - needs.test-numba-cuda-windows.result == 'cancelled' || - needs.test-numba-cuda-windows.result == 'failure' || - needs.test-numba-cuda-mlir-linux-64.result == 'cancelled' || - needs.test-numba-cuda-mlir-linux-64.result == 'failure' || - needs.test-numba-cuda-mlir-windows.result == 'cancelled' || - needs.test-numba-cuda-mlir-windows.result == 'failure' || - needs.test-cuda-core-linux-64.result == 'cancelled' || - needs.test-cuda-core-linux-64.result == 'failure' || - needs.test-cuda-core-windows.result == 'cancelled' || - needs.test-cuda-core-windows.result == 'failure' || - needs.test-standard-linux-aarch64.result == 'cancelled' || - needs.test-standard-linux-aarch64.result == 'failure' }}; then - exit 1 - fi - exit 0 + # `always()` keeps this status job runnable after failures. Require + # every nightly dependency to have actually run and succeeded; + # an unexpected skip must not produce a green aggregate check. + results=( + "test-ci-tools-for-release:${{ needs.test-ci-tools-for-release.result }}" + "find-wheels:${{ needs.find-wheels.result }}" + "test-pytorch-linux:${{ needs.test-pytorch-linux.result }}" + "test-pytorch-linux-aarch64:${{ needs.test-pytorch-linux-aarch64.result }}" + "test-pytorch-windows:${{ needs.test-pytorch-windows.result }}" + "test-numba-cuda-linux-64:${{ needs.test-numba-cuda-linux-64.result }}" + "test-numba-cuda-linux-aarch64:${{ needs.test-numba-cuda-linux-aarch64.result }}" + "test-numba-cuda-windows:${{ needs.test-numba-cuda-windows.result }}" + "test-numba-cuda-mlir-linux-64:${{ needs.test-numba-cuda-mlir-linux-64.result }}" + "test-numba-cuda-mlir-windows:${{ needs.test-numba-cuda-mlir-windows.result }}" + "test-cuda-core-linux-64:${{ needs.test-cuda-core-linux-64.result }}" + "test-cuda-core-windows:${{ needs.test-cuda-core-windows.result }}" + "test-standard-linux-aarch64:${{ needs.test-standard-linux-aarch64.result }}" + ) + status=success + for entry in "${results[@]}"; do + name="${entry%%:*}" + result="${entry#*:}" + echo "Checking ${name}: result='${result}' (expected 'success')" + if [[ "${result}" != "success" ]]; then + echo "::error::${name} did not complete successfully" + status=failed + fi + done + [[ "${status}" == "success" ]] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b554ec5344..84e583d7f84 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,18 +38,21 @@ jobs: outputs: cuda-build-ver: ${{ steps.vars.outputs.cuda-build-ver }} cuda-prev-build-ver: ${{ steps.vars.outputs.cuda-prev-build-ver }} + build-python-matrix: ${{ steps.build-matrix.outputs.python }} skip: ${{ steps.directives.outputs.skip }} - doc-only: ${{ steps.directives.outputs.doc-only }} moon-base: ${{ steps.baseline.outputs.moon-base }} - moon-head: ${{ steps.baseline.outputs.moon-head }} moon-base-run-id: ${{ steps.baseline.outputs.moon-base-run-id }} - moon-base-sha: ${{ steps.baseline.outputs.moon-base-sha }} moon-force-all: ${{ steps.lanes.outputs.force-all }} build-linux-64: ${{ steps.lanes.outputs.build-linux-64 }} + build-linux-64-force: ${{ steps.lanes.outputs.build-linux-64-force }} build-linux-aarch64: ${{ steps.lanes.outputs.build-linux-aarch64 }} + build-linux-aarch64-force: ${{ steps.lanes.outputs.build-linux-aarch64-force }} build-windows: ${{ steps.lanes.outputs.build-windows }} + build-windows-force: ${{ steps.lanes.outputs.build-windows-force }} sdist-linux: ${{ steps.lanes.outputs.sdist-linux }} + sdist-linux-force: ${{ steps.lanes.outputs.sdist-linux-force }} sdist-windows: ${{ steps.lanes.outputs.sdist-windows }} + sdist-windows-force: ${{ steps.lanes.outputs.sdist-windows-force }} test-linux: ${{ steps.lanes.outputs.test-linux }} test-windows: ${{ steps.lanes.outputs.test-windows }} docs: ${{ steps.lanes.outputs.docs }} @@ -76,6 +79,14 @@ jobs: echo "cuda-build-ver=$(yq '.cuda.build.version' ci/versions.yml)" >> "$GITHUB_OUTPUT" echo "cuda-prev-build-ver=$(yq '.cuda.prev_build.version' ci/versions.yml)" >> "$GITHUB_OUTPUT" + - name: Read native build matrix + id: build-matrix + run: | + # The file is already shaped as a GitHub Actions matrix. Structural + # and cross-matrix invariants are enforced by Moon contract tests. + matrix="$(yq -o json -I=0 '.' ci/build-matrix.yml)" + echo "python=${matrix}" >> "$GITHUB_OUTPUT" + - name: Read PR title directives id: directives env: @@ -106,10 +117,16 @@ jobs: IS_PR: ${{ startsWith(github.ref_name, 'pull-request/') }} SKIP: ${{ steps.directives.outputs.skip }} BASE_REF: ${{ steps.directives.outputs.base-ref }} + BUILD_PYTHON_MATRIX: ${{ steps.build-matrix.outputs.python }} run: | head="$(git rev-parse HEAD)" base="${head}" base_run_id="" + baseline_build_linux_64=false + baseline_build_linux_aarch64=false + baseline_build_windows=false + baseline_sdist_linux=false + baseline_sdist_windows=false force_all=true if [[ "${SKIP}" != "true" && "${IS_PR}" == "true" ]]; then @@ -146,48 +163,59 @@ jobs: )" if [[ -n "${candidate}" ]]; then + base_run_id="${candidate}" artifacts="$( gh api --paginate --slurp \ "repos/${GITHUB_REPOSITORY}/actions/runs/${candidate}/artifacts?per_page=100" \ | jq -c '[.[].artifacts[]]' )" - expected=( - moon-lane-sdist-linux-64 - moon-lane-sdist-win-64 - ) - python_versions=(310 311 312 313 314 314t 315 315t) - host_platforms=(linux-64 linux-aarch64 win-64) - for host_platform in "${host_platforms[@]}"; do - for python_version in "${python_versions[@]}"; do - expected+=("moon-lane-build-${host_platform}-py${python_version}") - done - done - - complete=true - for artifact_name in "${expected[@]}"; do - artifact_valid="$( - jq \ - --arg name "${artifact_name}" \ - '[.[] | select(.name == $name)] | - length == 1 and - (.[0].expired | not) and - .[0].size_in_bytes > 0 and - ((.[0].digest // "") | - test("^sha256:[0-9a-fA-F]{64}$"))' \ - <<< "${artifacts}" - )" - if [[ "${artifact_valid}" != "true" ]]; then - echo "No unique valid ${artifact_name} in run ${candidate}; forcing all lanes." - complete=false - fi - done - - if [[ "${complete}" == "true" ]]; then - base_run_id="${candidate}" - force_all=false + artifact_valid() { + jq -e --arg name "$1" ' + [.[] | select(.name == $name)] | + length == 1 and + (.[0].expired | not) and + .[0].size_in_bytes > 0 and + ((.[0].digest // "") | + test("^sha256:[0-9a-fA-F]{64}$")) + ' <<< "${artifacts}" > /dev/null + } + build_lane_valid() { + local host_platform=$1 + local python_version + while IFS= read -r python_version; do + if ! artifact_valid "moon-lane-build-${host_platform}-py${python_version}"; then + return 1 + fi + done < <(jq -r '.include[]."python-version-formatted"' <<< "${BUILD_PYTHON_MATRIX}") + } + + if build_lane_valid linux-64; then + baseline_build_linux_64=true + fi + if build_lane_valid linux-aarch64; then + baseline_build_linux_aarch64=true + fi + if build_lane_valid win-64; then + baseline_build_windows=true + fi + if artifact_valid moon-lane-sdist-linux-64; then + baseline_sdist_linux=true fi + if artifact_valid moon-lane-sdist-win-64; then + baseline_sdist_windows=true + fi + echo "Exact baseline ${candidate}:" \ + "linux-64=${baseline_build_linux_64}," \ + "linux-aarch64=${baseline_build_linux_aarch64}," \ + "win-64=${baseline_build_windows}," \ + "sdist-linux=${baseline_sdist_linux}," \ + "sdist-windows=${baseline_sdist_windows}" fi + + # Missing artifacts are recovered only when an affected consumer + # needs their lane; they do not turn an unrelated PR into full CI. + force_all=false elif [[ "${SKIP}" != "true" ]]; then # Push, tag, schedule, and manual runs deliberately exercise the # full pipeline and publish a complete baseline for future PRs. @@ -196,10 +224,13 @@ jobs: { echo "moon-base=${base}" - echo "moon-head=${head}" echo "moon-base-run-id=${base_run_id}" - echo "moon-base-sha=${base}" echo "moon-force-all=${force_all}" + echo "baseline-build-linux-64=${baseline_build_linux_64}" + echo "baseline-build-linux-aarch64=${baseline_build_linux_aarch64}" + echo "baseline-build-windows=${baseline_build_windows}" + echo "baseline-sdist-linux=${baseline_sdist_linux}" + echo "baseline-sdist-windows=${baseline_sdist_windows}" } >> "$GITHUB_OUTPUT" - name: Set up Moon @@ -214,7 +245,7 @@ jobs: if: ${{ steps.directives.outputs.skip != 'true' }} env: MOON_BASE: ${{ steps.baseline.outputs.moon-base }} - MOON_HEAD: ${{ steps.baseline.outputs.moon-head }} + MOON_HEAD: ${{ github.sha }} FORCE_ALL: ${{ steps.baseline.outputs.moon-force-all }} run: | args=(query tasks) @@ -232,6 +263,12 @@ jobs: id: lanes if: ${{ always() }} env: + BASELINE_BUILD_LINUX_64: ${{ steps.baseline.outputs.baseline-build-linux-64 }} + BASELINE_BUILD_LINUX_AARCH64: ${{ steps.baseline.outputs.baseline-build-linux-aarch64 }} + BASELINE_BUILD_WINDOWS: ${{ steps.baseline.outputs.baseline-build-windows }} + BASELINE_SDIST_LINUX: ${{ steps.baseline.outputs.baseline-sdist-linux }} + BASELINE_SDIST_WINDOWS: ${{ steps.baseline.outputs.baseline-sdist-windows }} + DOC_ONLY: ${{ steps.directives.outputs.doc-only }} SKIP: ${{ steps.directives.outputs.skip }} FORCE_ALL: ${{ steps.baseline.outputs.moon-force-all }} run: | @@ -248,28 +285,78 @@ jobs: fi echo "force-all=${force_all}" >> "$GITHUB_OUTPUT" - lanes=( - build-linux-64:runner-build-linux-64 - build-linux-aarch64:runner-build-linux-aarch64 - build-windows:runner-build-windows - sdist-linux:runner-sdist-linux - sdist-windows:runner-sdist-windows - test-linux:runner-test-linux - test-windows:runner-test-windows - docs:runner-docs - quality:runner-quality - ) - for lane in "${lanes[@]}"; do - output="${lane%%:*}" - tag="${lane#*:}" - selected=false - if [[ "${SKIP}" != "true" ]] && - { [[ "${force_all}" == "true" ]] || - has_tag "${tag}"; }; then - selected=true + selected() { + [[ "${SKIP}" != "true" ]] && + { [[ "${force_all}" == "true" ]] || has_tag "$1"; } + } + force_lane() { + if [[ "${force_all}" == "true" || "$1" != "true" ]]; then + echo true + else + echo false fi - echo "${output}=${selected}" >> "$GITHUB_OUTPUT" - done + } + + build_native=false + sdist=false + test_linux=false + test_windows=false + docs=false + quality=false + if selected ci-build-native; then build_native=true; fi + if selected ci-sdist; then sdist=true; fi + if selected ci-test-linux; then test_linux=true; fi + if selected ci-test-windows; then test_windows=true; fi + if selected ci-docs; then docs=true; fi + if selected ci-quality; then quality=true; fi + + build_linux_64=false + build_linux_aarch64=false + build_windows=false + if [[ "${SKIP}" != "true" ]]; then + if [[ "${DOC_ONLY}" == "true" ]]; then + if [[ "${force_all}" == "true" || + ("${docs}" == "true" && "${BASELINE_BUILD_LINUX_64}" != "true") ]]; then + build_linux_64=true + fi + sdist=false + test_linux=false + test_windows=false + else + if [[ "${build_native}" == "true" || + (("${test_linux}" == "true" || "${docs}" == "true") && + "${BASELINE_BUILD_LINUX_64}" != "true") ]]; then + build_linux_64=true + fi + if [[ "${build_native}" == "true" || + ("${test_linux}" == "true" && + "${BASELINE_BUILD_LINUX_AARCH64}" != "true") ]]; then + build_linux_aarch64=true + fi + if [[ "${build_native}" == "true" || + ("${test_windows}" == "true" && + "${BASELINE_BUILD_WINDOWS}" != "true") ]]; then + build_windows=true + fi + fi + fi + + { + echo "build-linux-64=${build_linux_64}" + echo "build-linux-aarch64=${build_linux_aarch64}" + echo "build-windows=${build_windows}" + echo "sdist-linux=${sdist}" + echo "sdist-windows=${sdist}" + echo "test-linux=${test_linux}" + echo "test-windows=${test_windows}" + echo "docs=${docs}" + echo "quality=${quality}" + echo "build-linux-64-force=$(force_lane "${BASELINE_BUILD_LINUX_64}")" + echo "build-linux-aarch64-force=$(force_lane "${BASELINE_BUILD_LINUX_AARCH64}")" + echo "build-windows-force=$(force_lane "${BASELINE_BUILD_WINDOWS}")" + echo "sdist-linux-force=$(force_lane "${BASELINE_SDIST_LINUX}")" + echo "sdist-windows-force=$(force_lane "${BASELINE_SDIST_WINDOWS}")" + } >> "$GITHUB_OUTPUT" - name: Build GPU test matrices id: matrices @@ -383,9 +470,7 @@ jobs: if: >- ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.gate.outputs.skip) && - fromJSON(needs.gate.outputs.build-linux-64) && - (!fromJSON(needs.gate.outputs.doc-only) || - fromJSON(needs.gate.outputs.moon-force-all)) }} + fromJSON(needs.gate.outputs.build-linux-64) }} permissions: actions: read contents: read @@ -396,7 +481,8 @@ jobs: prev-cuda-version: ${{ needs.gate.outputs.cuda-prev-build-ver }} baseline-run-id: ${{ needs.gate.outputs.moon-base-run-id }} moon-base: ${{ needs.gate.outputs.moon-base }} - force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} + python-matrix: ${{ needs.gate.outputs.build-python-matrix }} + force-all: ${{ fromJSON(needs.gate.outputs.build-linux-64-force) }} build-linux-aarch64: name: Build linux-aarch64, CUDA ${{ needs.gate.outputs.cuda-build-ver }} @@ -405,7 +491,6 @@ jobs: if: >- ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.gate.outputs.skip) && - !fromJSON(needs.gate.outputs.doc-only) && fromJSON(needs.gate.outputs.build-linux-aarch64) }} permissions: actions: read @@ -417,7 +502,8 @@ jobs: prev-cuda-version: ${{ needs.gate.outputs.cuda-prev-build-ver }} baseline-run-id: ${{ needs.gate.outputs.moon-base-run-id }} moon-base: ${{ needs.gate.outputs.moon-base }} - force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} + python-matrix: ${{ needs.gate.outputs.build-python-matrix }} + force-all: ${{ fromJSON(needs.gate.outputs.build-linux-aarch64-force) }} build-windows: name: Build win-64, CUDA ${{ needs.gate.outputs.cuda-build-ver }} @@ -426,7 +512,6 @@ jobs: if: >- ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.gate.outputs.skip) && - !fromJSON(needs.gate.outputs.doc-only) && fromJSON(needs.gate.outputs.build-windows) }} permissions: actions: read @@ -438,7 +523,8 @@ jobs: prev-cuda-version: ${{ needs.gate.outputs.cuda-prev-build-ver }} baseline-run-id: ${{ needs.gate.outputs.moon-base-run-id }} moon-base: ${{ needs.gate.outputs.moon-base }} - force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} + python-matrix: ${{ needs.gate.outputs.build-python-matrix }} + force-all: ${{ fromJSON(needs.gate.outputs.build-windows-force) }} # GitHub allocates each platform lane; Moon selects the affected sdist tasks # and their declared package dependencies inside the reusable workflow. @@ -449,7 +535,6 @@ jobs: if: >- ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.gate.outputs.skip) && - !fromJSON(needs.gate.outputs.doc-only) && fromJSON(needs.gate.outputs.sdist-linux) }} permissions: actions: read @@ -460,7 +545,7 @@ jobs: cuda-version: ${{ needs.gate.outputs.cuda-build-ver }} baseline-run-id: ${{ needs.gate.outputs.moon-base-run-id }} moon-base: ${{ needs.gate.outputs.moon-base }} - force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} + force-all: ${{ fromJSON(needs.gate.outputs.sdist-linux-force) }} test-sdist-windows: name: Test sdist win-64 @@ -469,7 +554,6 @@ jobs: if: >- ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.gate.outputs.skip) && - !fromJSON(needs.gate.outputs.doc-only) && fromJSON(needs.gate.outputs.sdist-windows) }} permissions: actions: read @@ -480,7 +564,7 @@ jobs: cuda-version: ${{ needs.gate.outputs.cuda-build-ver }} baseline-run-id: ${{ needs.gate.outputs.moon-base-run-id }} moon-base: ${{ needs.gate.outputs.moon-base }} - force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} + force-all: ${{ fromJSON(needs.gate.outputs.sdist-windows-force) }} test-linux-64: name: Test linux-64 @@ -494,20 +578,17 @@ jobs: (needs.build-linux-64.result == 'success' || needs.build-linux-64.result == 'skipped') && !fromJSON(needs.gate.outputs.skip) && - !fromJSON(needs.gate.outputs.doc-only) && fromJSON(needs.gate.outputs.test-linux) }} permissions: actions: read contents: read uses: ./.github/workflows/test-wheel-linux.yml with: - build-type: pull-request host-platform: linux-64 build-ctk-ver: ${{ needs.gate.outputs.cuda-build-ver }} matrix: ${{ needs.gate.outputs.test-linux-64-matrix }} nruns: ${{ (github.event_name == 'schedule' && 5) || 1 }} run-id: ${{ needs.build-linux-64.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} - sha: ${{ needs.build-linux-64.result == 'success' && github.sha || needs.gate.outputs.moon-base-sha }} moon-base: ${{ needs.gate.outputs.moon-base }} force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} @@ -523,20 +604,17 @@ jobs: (needs.build-linux-aarch64.result == 'success' || needs.build-linux-aarch64.result == 'skipped') && !fromJSON(needs.gate.outputs.skip) && - !fromJSON(needs.gate.outputs.doc-only) && fromJSON(needs.gate.outputs.test-linux) }} permissions: actions: read contents: read uses: ./.github/workflows/test-wheel-linux.yml with: - build-type: pull-request host-platform: linux-aarch64 build-ctk-ver: ${{ needs.gate.outputs.cuda-build-ver }} matrix: ${{ needs.gate.outputs.test-linux-aarch64-matrix }} nruns: ${{ (github.event_name == 'schedule' && 5) || 1 }} run-id: ${{ needs.build-linux-aarch64.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} - sha: ${{ needs.build-linux-aarch64.result == 'success' && github.sha || needs.gate.outputs.moon-base-sha }} moon-base: ${{ needs.gate.outputs.moon-base }} force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} @@ -552,20 +630,17 @@ jobs: (needs.build-windows.result == 'success' || needs.build-windows.result == 'skipped') && !fromJSON(needs.gate.outputs.skip) && - !fromJSON(needs.gate.outputs.doc-only) && fromJSON(needs.gate.outputs.test-windows) }} permissions: actions: read contents: read uses: ./.github/workflows/test-wheel-windows.yml with: - build-type: pull-request host-platform: win-64 build-ctk-ver: ${{ needs.gate.outputs.cuda-build-ver }} matrix: ${{ needs.gate.outputs.test-windows-matrix }} nruns: ${{ (github.event_name == 'schedule' && 5) || 1 }} run-id: ${{ needs.build-windows.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} - sha: ${{ needs.build-windows.result == 'success' && github.sha || needs.gate.outputs.moon-base-sha }} moon-base: ${{ needs.gate.outputs.moon-base }} force-all: ${{ fromJSON(needs.gate.outputs.moon-force-all) }} @@ -591,7 +666,6 @@ jobs: with: is-release: ${{ github.ref_type == 'tag' }} run-id: ${{ needs.build-linux-64.result == 'success' && github.run_id || needs.gate.outputs.moon-base-run-id }} - sha: ${{ github.sha }} precommit-windows: name: Pre-commit on Windows @@ -671,39 +745,14 @@ jobs: exit fi - doc_only="${{ needs.gate.outputs.doc-only }}" - force_all="${{ needs.gate.outputs.moon-force-all }}" - - linux_64=false - linux_aarch64=false - windows=false - if [[ "${doc_only}" != "true" || "${force_all}" == "true" ]]; then - linux_64="${{ needs.gate.outputs.build-linux-64 }}" - fi - if [[ "${doc_only}" != "true" ]]; then - linux_aarch64="${{ needs.gate.outputs.build-linux-aarch64 }}" - windows="${{ needs.gate.outputs.build-windows }}" - fi - - check_result build-linux-64 "$(expected_for "${linux_64}")" "${{ needs.build-linux-64.result }}" - check_result build-linux-aarch64 "$(expected_for "${linux_aarch64}")" "${{ needs.build-linux-aarch64.result }}" - check_result build-windows "$(expected_for "${windows}")" "${{ needs.build-windows.result }}" - - sdist_linux=false - sdist_windows=false - test_linux=false - test_windows=false - if [[ "${doc_only}" != "true" ]]; then - sdist_linux="${{ needs.gate.outputs.sdist-linux }}" - sdist_windows="${{ needs.gate.outputs.sdist-windows }}" - test_linux="${{ needs.gate.outputs.test-linux }}" - test_windows="${{ needs.gate.outputs.test-windows }}" - fi - check_result test-sdist-linux "$(expected_for "${sdist_linux}")" "${{ needs.test-sdist-linux.result }}" - check_result test-sdist-windows "$(expected_for "${sdist_windows}")" "${{ needs.test-sdist-windows.result }}" - check_result test-linux-64 "$(expected_for "${test_linux}")" "${{ needs.test-linux-64.result }}" - check_result test-linux-aarch64 "$(expected_for "${test_linux}")" "${{ needs.test-linux-aarch64.result }}" - check_result test-windows "$(expected_for "${test_windows}")" "${{ needs.test-windows.result }}" + check_result build-linux-64 "$(expected_for "${{ needs.gate.outputs.build-linux-64 }}")" "${{ needs.build-linux-64.result }}" + check_result build-linux-aarch64 "$(expected_for "${{ needs.gate.outputs.build-linux-aarch64 }}")" "${{ needs.build-linux-aarch64.result }}" + check_result build-windows "$(expected_for "${{ needs.gate.outputs.build-windows }}")" "${{ needs.build-windows.result }}" + check_result test-sdist-linux "$(expected_for "${{ needs.gate.outputs.sdist-linux }}")" "${{ needs.test-sdist-linux.result }}" + check_result test-sdist-windows "$(expected_for "${{ needs.gate.outputs.sdist-windows }}")" "${{ needs.test-sdist-windows.result }}" + check_result test-linux-64 "$(expected_for "${{ needs.gate.outputs.test-linux }}")" "${{ needs.test-linux-64.result }}" + check_result test-linux-aarch64 "$(expected_for "${{ needs.gate.outputs.test-linux }}")" "${{ needs.test-linux-aarch64.result }}" + check_result test-windows "$(expected_for "${{ needs.gate.outputs.test-windows }}")" "${{ needs.test-windows.result }}" check_result quality "$(expected_for "${{ needs.gate.outputs.quality }}")" "${{ needs.quality.result }}" check_result doc "$(expected_for "${{ needs.gate.outputs.docs }}")" "${{ needs.doc.result }}" diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index fc234999fca..6991a973b80 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -93,7 +93,6 @@ jobs: HOST_PLATFORM: ${{ env.HOST_PLATFORM }} LOCAL_CTK: ${{ env.LOCAL_CTK }} PY_VER: ${{ env.PY_VER }} - SHA: ${{ github.sha }} run: | ./ci/tools/env-vars test echo "CUDA_PYTHON_COVERAGE=1" >> $GITHUB_ENV @@ -119,10 +118,15 @@ jobs: python -m venv .venv - name: Install pip with build-constraint support - run: .venv/bin/python -m pip install "pip>=25.3" + run: >- + .venv/bin/python -m pip install + --constraint ci/build-constraints.txt pip - name: Build and install cuda-pathfinder wheel run: | + BUILD_CONSTRAINTS="$(realpath ci/build-constraints.txt)" + export PIP_BUILD_CONSTRAINT="${BUILD_CONSTRAINTS}" + export PIP_CONSTRAINT="${BUILD_CONSTRAINTS}" .venv/bin/pip wheel -v --no-deps ./cuda_pathfinder -w ./wheels/ .venv/bin/pip install -v ./wheels/cuda_pathfinder*.whl --group ./cuda_pathfinder/pyproject.toml:test @@ -133,7 +137,8 @@ jobs: test -f "${pathfinder_wheels[0]}" mkdir -p wheel-constraints pathfinder_uri="file://$(realpath "${pathfinder_wheels[0]}")" - printf 'cuda-pathfinder @ %s\n' "${pathfinder_uri}" | tee wheel-constraints/cuda-bindings.txt + cp ci/build-constraints.txt wheel-constraints/cuda-bindings.txt + printf 'cuda-pathfinder @ %s\n' "${pathfinder_uri}" >> wheel-constraints/cuda-bindings.txt - name: Build and install cuda-bindings wheel run: | @@ -154,10 +159,11 @@ jobs: mkdir -p wheel-constraints pathfinder_uri="file://$(realpath "${pathfinder_wheels[0]}")" bindings_uri="file://$(realpath "${bindings_wheels[0]}")" + cp ci/build-constraints.txt wheel-constraints/cuda-core.txt { printf 'cuda-pathfinder @ %s\n' "${pathfinder_uri}" printf 'cuda-bindings @ %s\n' "${bindings_uri}" - } | tee wheel-constraints/cuda-core.txt + } >> wheel-constraints/cuda-core.txt - name: Build and install cuda-core run: | @@ -168,7 +174,7 @@ jobs: - name: Install coverage tools run: | - .venv/bin/pip install coverage pytest-cov Cython + .venv/bin/pip install --constraint ci/build-constraints.txt coverage pytest-cov Cython - name: Set cuda package install root run: | @@ -260,7 +266,11 @@ jobs: - name: Build cuda.pathfinder wheel run: | - .venv/Scripts/python -m pip install "pip>=25.3" wheel setuptools Cython + .venv/Scripts/python -m pip install \ + --constraint ci/build-constraints.txt pip wheel setuptools Cython + BUILD_CONSTRAINTS="$(cygpath -w "$(pwd)/ci/build-constraints.txt")" + export PIP_BUILD_CONSTRAINT="${BUILD_CONSTRAINTS}" + export PIP_CONSTRAINT="${BUILD_CONSTRAINTS}" .venv/Scripts/pip wheel -v --no-deps ./cuda_pathfinder -w ./wheels/ - name: Constrain builds to the local cuda.pathfinder wheel @@ -270,7 +280,8 @@ jobs: test -f "${pathfinder_wheels[0]}" mkdir -p wheel-constraints pathfinder_uri="file:///$(cygpath -am "${pathfinder_wheels[0]}")" - printf 'cuda-pathfinder @ %s\n' "${pathfinder_uri}" | tee wheel-constraints/cuda-bindings.txt + cp ci/build-constraints.txt wheel-constraints/cuda-bindings.txt + printf 'cuda-pathfinder @ %s\n' "${pathfinder_uri}" >> wheel-constraints/cuda-bindings.txt - name: Build cuda.bindings wheel run: | @@ -291,10 +302,11 @@ jobs: mkdir -p wheel-constraints pathfinder_uri="file:///$(cygpath -am "${pathfinder_wheels[0]}")" bindings_uri="file:///$(cygpath -am "${bindings_wheels[0]}")" + cp ci/build-constraints.txt wheel-constraints/cuda-core.txt { printf 'cuda-pathfinder @ %s\n' "${pathfinder_uri}" printf 'cuda-bindings @ %s\n' "${bindings_uri}" - } | tee wheel-constraints/cuda-core.txt + } >> wheel-constraints/cuda-core.txt - name: Build cuda.core wheel run: | @@ -308,7 +320,7 @@ jobs: # `cuda` is a namespace package. - name: Repair the Windows wheels run: | - .venv/Scripts/pip install delvewheel + .venv/Scripts/pip install --constraint ci/build-constraints.txt delvewheel mkdir -p wheels-repaired for whl in ./wheels/cuda_bindings-*.whl ./wheels/cuda_core-*.whl; do .venv/Scripts/delvewheel repair --namespace-pkg cuda \ @@ -397,23 +409,28 @@ jobs: run: | python -m venv .venv + - name: Install pip with build-constraint support + run: >- + .venv/Scripts/python -m pip install + --constraint ci/build-constraints.txt pip + - name: Install wheels from build job run: | - .venv/Scripts/pip install ./wheels/cuda_pathfinder*.whl + .venv/Scripts/pip install --constraint ci/build-constraints.txt ./wheels/cuda_pathfinder*.whl echo "Installed cuda.pathfinder" - .venv/Scripts/pip install ./wheels/cuda_bindings*.whl + .venv/Scripts/pip install --constraint ci/build-constraints.txt ./wheels/cuda_bindings*.whl echo "Installed cuda.bindings" - .venv/Scripts/pip install ./wheels/cuda_core*.whl + .venv/Scripts/pip install --constraint ci/build-constraints.txt ./wheels/cuda_core*.whl echo "Installed cuda.core" - name: Install test dependencies and coverage tools run: | - .venv/Scripts/pip install coverage pytest-cov Cython - .venv/Scripts/pip install --group ./cuda_pathfinder/pyproject.toml:test - .venv/Scripts/pip install --group ./cuda_bindings/pyproject.toml:test - .venv/Scripts/pip install --group ./cuda_core/pyproject.toml:test + .venv/Scripts/pip install --constraint ci/build-constraints.txt coverage pytest-cov Cython + .venv/Scripts/pip install --constraint ci/build-constraints.txt --group ./cuda_pathfinder/pyproject.toml:test + .venv/Scripts/pip install --constraint ci/build-constraints.txt --group ./cuda_bindings/pyproject.toml:test + .venv/Scripts/pip install --constraint ci/build-constraints.txt --group ./cuda_core/pyproject.toml:test - name: Get install root id: install-root @@ -483,7 +500,7 @@ jobs: - name: Install coverage run: | # .coveragerc enables Cython.Coverage plugin; ensure it's available here too. - pip install coverage[toml] Cython + pip install --constraint ci/build-constraints.txt coverage[toml] Cython - name: Download Linux coverage data uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 diff --git a/.github/workflows/test-sdist-linux.yml b/.github/workflows/test-sdist-linux.yml index 438ee6ef86d..6e7e561338e 100644 --- a/.github/workflows/test-sdist-linux.yml +++ b/.github/workflows/test-sdist-linux.yml @@ -41,7 +41,8 @@ jobs: CUDA_VER: ${{ inputs.cuda-version }} HOST_PLATFORM: ${{ inputs.host-platform }} MOON_BASE: ${{ inputs.moon-base }} - MOON_FORCE_ALL: ${{ inputs.force-all && 'true' || 'false' }} + # Only canonical outputs cross runners; upstream-none also omits hash deps. + MOON_CACHE: "off" MOON_HEAD: ${{ github.sha }} PY_VER: "3.12" steps: @@ -66,7 +67,9 @@ jobs: auto-setup: false - name: Install build tools - run: python -m pip install "pip>=25.3" build + run: >- + python -m pip install --constraint ci/build-constraints.txt + "pip>=25.3" "build==1.5.0" # Cython packages need CTK + sccache. # The env vars ACTIONS_CACHE_SERVICE_V2, ACTIONS_RESULTS_URL, and ACTIONS_RUNTIME_TOKEN @@ -91,8 +94,6 @@ jobs: enable-apt: true - name: Set environment variables - env: - SHA: ${{ github.sha }} run: ./ci/tools/env-vars build - name: Restore trusted exact-base Moon lane @@ -115,45 +116,37 @@ jobs: run: | export CC="sccache cc" export CXX="sccache c++" - args=() - if [[ "${MOON_FORCE_ALL}" == "true" ]]; then + args=(--upstream none --downstream none) + if [[ "${{ inputs.force-all }}" == "true" ]]; then args+=(--force) fi - # Preserve affected granularity while retaining the package build - # order. cuda.core and the metapackage are independent after their - # shared prerequisites and can run in parallel. - moon ci pathfinder:sdist --upstream none --downstream none "${args[@]}" - moon ci bindings:sdist --upstream none --downstream none "${args[@]}" - moon ci core:sdist metapackage:sdist --upstream none --downstream none "${args[@]}" + # Moon 2.5.1 drops dependency ordering with upstream-none, so stage + # consumers explicitly while retaining parallel independent peers. + moon ci pathfinder:sdist "${args[@]}" + moon ci bindings:sdist "${args[@]}" + moon ci core:sdist metapackage:sdist "${args[@]}" - name: Validate sdist outputs run: | - validated=0 for project in cuda_pathfinder cuda_bindings cuda_core cuda_python; do output="${project}/.moon-out/sdist" - if [[ ! -d "${output}" ]]; then - continue - fi + test -d "${output}" test "$(find "${output}" -maxdepth 1 -name '*.tar.gz' -type f | wc -l)" -eq 1 test "$(find "${output}" -maxdepth 1 -name '*.whl' -type f | wc -l)" -eq 1 - validated=$((validated + 1)) done - test "${validated}" -gt 0 - name: Show sccache stats if: ${{ always() }} run: sccache --show-stats - # GitHub transports Moon's portable cache between trusted exact runs; - # Moon remains responsible for hashes, hits, and output hydration. + # GitHub transports canonical outputs between trusted exact-base runs. + # Moon's hash and output caches remain local to each runner. - name: Upload sdist Moon lane if: ${{ always() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: moon-lane-sdist-${{ inputs.host-platform }} path: | - .moon/cache/hashes - .moon/cache/outputs cuda_pathfinder/.moon-out/sdist cuda_bindings/.moon-out/sdist cuda_core/.moon-out/sdist diff --git a/.github/workflows/test-sdist-windows.yml b/.github/workflows/test-sdist-windows.yml index ca73effe125..d9068f1e06f 100644 --- a/.github/workflows/test-sdist-windows.yml +++ b/.github/workflows/test-sdist-windows.yml @@ -47,7 +47,8 @@ jobs: CUDA_VER: ${{ inputs.cuda-version }} HOST_PLATFORM: ${{ inputs.host-platform }} MOON_BASE: ${{ inputs.moon-base }} - MOON_FORCE_ALL: ${{ inputs.force-all && 'true' || 'false' }} + # Only canonical outputs cross runners; upstream-none also omits hash deps. + MOON_CACHE: "off" MOON_HEAD: ${{ github.sha }} PY_VER: "3.12" steps: @@ -75,7 +76,9 @@ jobs: uses: step-security/msvc-dev-cmd@22c98154b708dbd743e6f27a933cf6ceba3305c4 # v1.13.1 - name: Install build tools - run: python -m pip install "pip>=25.3" build + run: >- + python -m pip install --constraint ci/build-constraints.txt + "pip>=25.3" "build==1.5.0" # Cython packages need CTK. No sccache on Windows (this is a correctness # smoke test, not a production build; see build-wheel.yml which also @@ -88,8 +91,6 @@ jobs: cuda-version: ${{ inputs.cuda-version }} - name: Set environment variables - env: - SHA: ${{ github.sha }} run: ./ci/tools/env-vars build - name: Restore trusted exact-base Moon lane @@ -103,41 +104,33 @@ jobs: - name: Build affected sdists and verify wheels with Moon run: | - args=() - if [[ "${MOON_FORCE_ALL}" == "true" ]]; then + args=(--upstream none --downstream none) + if [[ "${{ inputs.force-all }}" == "true" ]]; then args+=(--force) fi - # Preserve affected granularity while retaining the package build - # order. cuda.core and the metapackage are independent after their - # shared prerequisites and can run in parallel. - moon ci pathfinder:sdist --upstream none --downstream none "${args[@]}" - moon ci bindings:sdist --upstream none --downstream none "${args[@]}" - moon ci core:sdist metapackage:sdist --upstream none --downstream none "${args[@]}" + # Moon 2.5.1 drops dependency ordering with upstream-none, so stage + # consumers explicitly while retaining parallel independent peers. + moon ci pathfinder:sdist "${args[@]}" + moon ci bindings:sdist "${args[@]}" + moon ci core:sdist metapackage:sdist "${args[@]}" - name: Validate sdist outputs run: | - validated=0 for project in cuda_pathfinder cuda_bindings cuda_core cuda_python; do output="${project}/.moon-out/sdist" - if [[ ! -d "${output}" ]]; then - continue - fi + test -d "${output}" test "$(find "${output}" -maxdepth 1 -name '*.tar.gz' -type f | wc -l)" -eq 1 test "$(find "${output}" -maxdepth 1 -name '*.whl' -type f | wc -l)" -eq 1 - validated=$((validated + 1)) done - test "${validated}" -gt 0 - # GitHub transports Moon's portable cache between trusted exact runs; - # Moon remains responsible for hashes, hits, and output hydration. + # GitHub transports canonical outputs between trusted exact-base runs. + # Moon's hash and output caches remain local to each runner. - name: Upload sdist Moon lane if: ${{ always() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: moon-lane-sdist-${{ inputs.host-platform }} path: | - .moon/cache/hashes - .moon/cache/outputs cuda_pathfinder/.moon-out/sdist cuda_bindings/.moon-out/sdist cuda_core/.moon-out/sdist diff --git a/.github/workflows/test-wheel-linux.yml b/.github/workflows/test-wheel-linux.yml index 1eeebe6bcf2..ec3b7cf817f 100644 --- a/.github/workflows/test-wheel-linux.yml +++ b/.github/workflows/test-wheel-linux.yml @@ -7,9 +7,6 @@ name: "CI: Test wheels" on: workflow_call: inputs: - build-type: - type: string - required: true host-platform: type: string required: true @@ -29,6 +26,10 @@ on: Defaults to the current run when empty. type: string default: '' + source-ref: + description: "Exact source ref to test; defaults to the caller commit" + type: string + default: '' test-mode: description: > Test mode: 'standard' (default), 'nightly-pytorch', @@ -36,12 +37,6 @@ on: 'nightly-cuda-core'. type: string default: 'standard' - sha: - description: > - Commit SHA used to construct artifact names. - Defaults to github.sha (current run) when empty. - type: string - default: '' moon-base: description: "Base revision used by Moon affected checks" type: string @@ -60,9 +55,8 @@ jobs: name: Python ${{ matrix.PY_VER }}, CUDA ${{ matrix.CUDA_VER }} (${{ (matrix.LOCAL_CTK == '1' && 'local') || 'wheels' }}), GPU ${{ matrix.GPU }}${{ matrix.GPU_COUNT != '1' && format(' (x{0})', matrix.GPU_COUNT) || '' }}${{ matrix.FLAVOR && format(', {0}', matrix.FLAVOR) || '' }}${{ matrix.ENV.TORCH_VER && format(', {0}+{1}', matrix.ENV.TORCH_VER, matrix.ENV.TORCH_CUDA) || '' }}${{ matrix.ENV.MODE == 'nightly-numba-cuda' && ', latest' || '' }} timeout-minutes: 60 env: - CUDA_PYTHON_LANE: test-${{ inputs.host-platform }}-py${{ matrix.PY_VER }}-cuda${{ matrix.CUDA_VER }} MOON_BASE: ${{ inputs.moon-base }} - MOON_HEAD: ${{ github.sha }} + MOON_HEAD: ${{ inputs.source-ref || github.sha }} strategy: fail-fast: false matrix: ${{ fromJSON(inputs.matrix) }} @@ -88,6 +82,7 @@ jobs: with: fetch-depth: 0 filter: blob:none + ref: ${{ inputs.source-ref || github.sha }} - name: Set up Moon if: ${{ inputs.test-mode == 'standard' }} @@ -128,7 +123,6 @@ jobs: HOST_PLATFORM: ${{ inputs.host-platform }} LOCAL_CTK: ${{ matrix.LOCAL_CTK }} PY_VER: ${{ matrix.PY_VER }} - SHA: ${{ inputs.sha || github.sha }} SKIP_BINDINGS_TEST_OVERRIDE: "0" run: ./ci/tools/env-vars test @@ -146,41 +140,6 @@ jobs: run-id: ${{ inputs.run-id || github.run_id }} github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Stage Moon lane outputs for legacy test tooling - run: | - cp cuda_pathfinder/.moon-out/wheel-pure/*.whl cuda_pathfinder/ - echo "CUDA_CORE_ARTIFACTS_DIR=${GITHUB_WORKSPACE}/cuda_core/.moon-out/wheel-merged" >> "$GITHUB_ENV" - if [[ "${BINDINGS_SOURCE}" == "main" ]]; then - echo "CUDA_BINDINGS_ARTIFACTS_DIR=${GITHUB_WORKSPACE}/cuda_bindings/.moon-out/wheel-current" >> "$GITHUB_ENV" - fi - - - name: Download cuda.bindings build artifacts from the prior branch - if: ${{ env.BINDINGS_SOURCE == 'backport' }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # See https://github.com/cli/cli/blob/trunk/docs/install_linux.md#debian-ubuntu-linux-raspberry-pi-os-apt. - # gh is needed for artifact fetching. - mkdir -p -m 755 /etc/apt/keyrings \ - && out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \ - && cat $out | tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \ - && chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \ - && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ - && apt update \ - && apt install gh -y - - OLD_BRANCH=$(yq '.backport_branch' ci/versions.yml) - OLD_BASENAME="cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda*-${{ inputs.host-platform }}*" - LATEST_PRIOR_RUN_ID=$(./ci/tools/lookup-run-id --branch "${OLD_BRANCH}" NVIDIA/cuda-python "CI") - - gh run download $LATEST_PRIOR_RUN_ID -p ${OLD_BASENAME} -R NVIDIA/cuda-python - rm -rf ${OLD_BASENAME}-tests # exclude cython test artifacts - ls -al $OLD_BASENAME - mkdir -p "${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}" - mv $OLD_BASENAME/*.whl "${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}"/ - rmdir $OLD_BASENAME - - - name: Set up Python ${{ matrix.PY_VER }} uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: @@ -226,12 +185,9 @@ jobs: CUDA_PYTHON_USE_STAGED_BINDINGS_VERSION: "1" CUDA_VER: ${{ matrix.CUDA_VER }} LOCAL_CTK: ${{ matrix.LOCAL_CTK }} - run: | - args=() - if [[ "${{ inputs.force-all }}" == "true" || "${{ inputs.build-type }}" != "pull-request" ]]; then - args+=(--force) - fi - moon ci ':#ci-test-linux' --upstream deep --downstream none "${args[@]}" + run: >- + moon ci ':#ci-test-linux' --upstream deep --downstream none + ${{ inputs.force-all && '--force' || '' }} # ── Nightly: install wheels + optional dep together ── - name: Install cuda-python wheels + PyTorch diff --git a/.github/workflows/test-wheel-windows.yml b/.github/workflows/test-wheel-windows.yml index f11eb34c66b..a71247a6a33 100644 --- a/.github/workflows/test-wheel-windows.yml +++ b/.github/workflows/test-wheel-windows.yml @@ -7,9 +7,6 @@ name: "CI: Test wheels" on: workflow_call: inputs: - build-type: - type: string - required: true host-platform: type: string required: true @@ -29,6 +26,10 @@ on: Defaults to the current run when empty. type: string default: '' + source-ref: + description: "Exact source ref to test; defaults to the caller commit" + type: string + default: '' test-mode: description: > Test mode: 'standard' (default), 'nightly-pytorch', @@ -36,12 +37,6 @@ on: 'nightly-cuda-core'. type: string default: 'standard' - sha: - description: > - Commit SHA used to construct artifact names. - Defaults to github.sha (current run) when empty. - type: string - default: '' moon-base: description: "Base revision used by Moon affected checks" type: string @@ -57,9 +52,8 @@ jobs: timeout-minutes: 60 # The build stage could fail but we want the CI to keep moving. env: - CUDA_PYTHON_LANE: test-${{ inputs.host-platform }}-py${{ matrix.PY_VER }}-cuda${{ matrix.CUDA_VER }} MOON_BASE: ${{ inputs.moon-base }} - MOON_HEAD: ${{ github.sha }} + MOON_HEAD: ${{ inputs.source-ref || github.sha }} strategy: fail-fast: false matrix: ${{ fromJSON(inputs.matrix) }} @@ -73,6 +67,7 @@ jobs: with: fetch-depth: 0 filter: blob:none + ref: ${{ inputs.source-ref || github.sha }} - name: Set up Moon if: ${{ inputs.test-mode == 'standard' }} @@ -121,7 +116,6 @@ jobs: HOST_PLATFORM: ${{ inputs.host-platform }} LOCAL_CTK: ${{ matrix.LOCAL_CTK }} PY_VER: ${{ matrix.PY_VER }} - SHA: ${{ inputs.sha || github.sha }} SKIP_BINDINGS_TEST_OVERRIDE: "0" shell: bash --noprofile --norc -xeuo pipefail {0} run: ./ci/tools/env-vars test @@ -141,33 +135,6 @@ jobs: run-id: ${{ inputs.run-id || github.run_id }} github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Stage Moon lane outputs for legacy test tooling - shell: bash --noprofile --norc -xeuo pipefail {0} - run: | - cp cuda_pathfinder/.moon-out/wheel-pure/*.whl cuda_pathfinder/ - echo "CUDA_CORE_ARTIFACTS_DIR=${GITHUB_WORKSPACE}/cuda_core/.moon-out/wheel-merged" >> "$GITHUB_ENV" - if [[ "${BINDINGS_SOURCE}" == "main" ]]; then - echo "CUDA_BINDINGS_ARTIFACTS_DIR=${GITHUB_WORKSPACE}/cuda_bindings/.moon-out/wheel-current" >> "$GITHUB_ENV" - fi - - - name: Download cuda.bindings build artifacts from the prior branch - if: ${{ env.BINDINGS_SOURCE == 'backport' }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - shell: bash --noprofile --norc -xeuo pipefail {0} - run: | - OLD_BRANCH=$(yq '.backport_branch' ci/versions.yml) - OLD_BASENAME="cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda*-${{ inputs.host-platform }}*" - LATEST_PRIOR_RUN_ID=$(./ci/tools/lookup-run-id --branch "${OLD_BRANCH}" NVIDIA/cuda-python "CI") - - gh run download $LATEST_PRIOR_RUN_ID -p ${OLD_BASENAME} -R NVIDIA/cuda-python - rm -rf ${OLD_BASENAME}-tests # exclude cython test artifacts - ls -al $OLD_BASENAME - mkdir -p "${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}" - mv $OLD_BASENAME/*.whl "${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}"/ - rmdir $OLD_BASENAME - - - name: Set up Python ${{ matrix.PY_VER }} uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: @@ -210,12 +177,9 @@ jobs: CUDA_VER: ${{ matrix.CUDA_VER }} LOCAL_CTK: ${{ matrix.LOCAL_CTK }} shell: bash --noprofile --norc -xeuo pipefail {0} - run: | - args=() - if [[ "${{ inputs.force-all }}" == "true" || "${{ inputs.build-type }}" != "pull-request" ]]; then - args+=(--force) - fi - moon ci ':#ci-test-windows' --upstream deep --downstream none "${args[@]}" + run: >- + moon ci ':#ci-test-windows' --upstream deep --downstream none + ${{ inputs.force-all && '--force' || '' }} # ── Nightly: install wheels + optional dep together ── - name: Install Visual C++ Redistributable (required by PyTorch on Windows) diff --git a/.moon/tasks/docs.yml b/.moon/tasks/docs.yml new file mode 100644 index 00000000000..8af61c878b6 --- /dev/null +++ b/.moon/tasks/docs.yml @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +$schema: https://moonrepo.dev/schemas/v2/tasks.json + +inheritedBy: + tag: docs-package + +tasks: + docs-ci: + command: bash + args: ['$projectSource/docs/build_docs.sh', moon-ci] + deps: + - test-helpers:prepare-docs + env: + CUDA_PYTHON_DOCS_LATEST_ONLY: '${CUDA_PYTHON_DOCS_LATEST_ONLY}' + inputs: + - '@group(package)' + - '@group(docs)' + - '/cuda_python/docs/build_component_docs.sh' + - '/cuda_python/docs/environment-docs.yml' + - '/.github/workflows/build-docs.yml' + outputs: + - 'docs/build/html' + tags: [ci-docs] + type: build + options: + os: linux + runInCI: true diff --git a/.moon/tasks/installed-tests.yml b/.moon/tasks/installed-tests.yml new file mode 100644 index 00000000000..e30b6dba784 --- /dev/null +++ b/.moon/tasks/installed-tests.yml @@ -0,0 +1,49 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +$schema: https://moonrepo.dev/schemas/v2/tasks.json + +inheritedBy: + tag: installed-test-package + +tasks: + test-installed-linux: + command: bash + args: [ci/tools/run-tests, '$project'] + inputs: + - '@group(package)' + - '/ci/tools/run-tests' + - '/ci/test-matrix.yml' + - '/ci/tools/env-vars' + - '/tests/**/*' + - '/.github/workflows/build-wheel.yml' + - '/ci/tools/guess_latest.sh' + - '/ci/tools/install_gpu_driver.sh' + - '/.github/workflows/test-wheel-linux.yml' + tags: [ci-test-linux] + type: test + options: + mutex: ci-python-gpu + os: linux + runInCI: true + + test-installed-windows: + command: bash + args: [ci/tools/run-tests, '$project'] + inputs: + - '@group(package)' + - '/ci/tools/run-tests' + - '/ci/test-matrix.yml' + - '/ci/tools/env-vars' + - '/tests/**/*' + - '/.github/workflows/build-wheel.yml' + - '/ci/tools/configure_driver_mode.ps1' + - '/ci/tools/install_gpu_driver.ps1' + - '/.github/workflows/test-wheel-windows.yml' + tags: [ci-test-windows] + type: test + options: + mutex: ci-python-gpu + os: windows + runInCI: true diff --git a/.moon/tasks/native-package.yml b/.moon/tasks/native-package.yml new file mode 100644 index 00000000000..afbe73cf7a5 --- /dev/null +++ b/.moon/tasks/native-package.yml @@ -0,0 +1,58 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +$schema: https://moonrepo.dev/schemas/v2/tasks.json + +inheritedBy: + tag: native-package + +tasks: + sdist: + deps: + - target: test-helpers:fingerprint-python-build + cacheStrategy: hash + - target: test-helpers:fingerprint-native-context + cacheStrategy: hash + + wheel-current: + deps: + - target: ~:fingerprint-package + cacheStrategy: hash + - target: test-helpers:fingerprint-python-build + cacheStrategy: hash + - target: test-helpers:fingerprint-native-context + cacheStrategy: hash + inputs: + - '@group(package)' + - '/ci/build-constraints.txt' + - '/ci/tools/env-vars' + - '/ci/versions.yml' + - '/ci/build-matrix.yml' + - '/.github/workflows/build-wheel.yml' + outputs: + - '.moon-out/wheel-current' + tags: [ci-build-native] + type: build + options: + cache: true + priority: critical + runInCI: true + + cython-test-assets: + deps: + - target: test-helpers:fingerprint-test-assets + cacheStrategy: hash + inputs: + - '@group(package)' + - 'tests/cython/**/*' + - '/ci/build-matrix.yml' + - '/.github/workflows/build-wheel.yml' + outputs: + - '.moon-out/cython-tests' + tags: [ci-build-cython-assets, ci-build-native] + type: build + options: + cache: true + os: [linux, windows] + runInCI: true diff --git a/.moon/tasks/pixi-package.yml b/.moon/tasks/pixi-package.yml new file mode 100644 index 00000000000..2b2c7ded84c --- /dev/null +++ b/.moon/tasks/pixi-package.yml @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +$schema: https://moonrepo.dev/schemas/v2/tasks.json + +inheritedBy: + tag: pixi-package + +fileGroups: + tests: + - 'examples/**/*' + - 'tests/**/*' + - 'pixi.toml' + - 'pixi.lock' + - 'pytest.ini' + docs: + - 'docs/**/*' + - 'pixi.toml' + - 'pixi.lock' + +tasks: + test: + command: bash + args: + - -euo + - pipefail + - -c + - | + PIXI_ENVIRONMENT=$(printenv PIXI_ENVIRONMENT_NAME || true) + if [[ -n "$PIXI_ENVIRONMENT" ]]; then + exec pixi run --manifest-path $projectSource/pixi.toml \ + --environment "$PIXI_ENVIRONMENT" test + fi + exec pixi run --manifest-path $projectSource/pixi.toml test + inputs: + - '@group(package)' + - '@group(tests)' + type: test + + docs: + command: pixi + args: [run, --manifest-path, '$projectSource/pixi.toml', --environment, docs] + inputs: + - '@group(package)' + - '@group(docs)' + - '/.github/workflows/build-docs.yml' diff --git a/.moon/tasks/pure-wheel-package.yml b/.moon/tasks/pure-wheel-package.yml new file mode 100644 index 00000000000..67731d4717a --- /dev/null +++ b/.moon/tasks/pure-wheel-package.yml @@ -0,0 +1,76 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +$schema: https://moonrepo.dev/schemas/v2/tasks.json + +inheritedBy: + tag: pure-wheel-package + +tasks: + sdist: + command: bash + args: + - -euo + - pipefail + - -c + - | + PROJECT_SOURCE=$projectSource + PROJECT_LABEL=$project + OUTPUT_ROOT="$PROJECT_SOURCE/.moon-out" + OUTPUT="$OUTPUT_ROOT/sdist" + if [[ -L "$OUTPUT_ROOT" || ( -e "$OUTPUT_ROOT" && ! -d "$OUTPUT_ROOT" ) ]]; then + echo "refusing to use non-directory output root: $OUTPUT_ROOT" >&2 + exit 1 + fi + if [[ -L "$OUTPUT" || ( -e "$OUTPUT" && ! -d "$OUTPUT" ) ]]; then + echo "refusing to replace non-directory output: $OUTPUT" >&2 + exit 1 + fi + mkdir -p "$OUTPUT_ROOT" + rm -rf -- "$OUTPUT" + mkdir -p "$OUTPUT" + SOURCE_DATE_EPOCH=$(printenv SOURCE_DATE_EPOCH || true) + export SOURCE_DATE_EPOCH + [[ -n "$SOURCE_DATE_EPOCH" ]] || SOURCE_DATE_EPOCH=$(git log -1 --format=%ct HEAD) + BUILD_CONSTRAINTS=$(realpath ci/build-constraints.txt) + case "$(uname -s)" in + CYGWIN*|MINGW*|MSYS*) BUILD_CONSTRAINTS=$(cygpath -w "$BUILD_CONSTRAINTS") ;; + esac + export PIP_BUILD_CONSTRAINT="$BUILD_CONSTRAINTS" + export PIP_CONSTRAINT="$BUILD_CONSTRAINTS" + python -m build --sdist --outdir "$OUTPUT" "$PROJECT_SOURCE" + shopt -s nullglob + set -- "$OUTPUT"/*.tar.gz + [[ $# -eq 1 ]] || { + echo "expected one $PROJECT_LABEL source distribution, found $#" >&2 + exit 1 + } + ARCHIVE=$1 + python -m pip wheel --no-deps --wheel-dir "$OUTPUT" "$ARCHIVE" + set -- "$OUTPUT"/*.whl + [[ $# -eq 1 ]] || { + echo "expected one $PROJECT_LABEL wheel from source distribution, found $#" >&2 + exit 1 + } + deps: + - target: test-helpers:fingerprint-python-build + cacheStrategy: hash + + wheel-pure: + deps: + - target: ~:fingerprint-package + cacheStrategy: hash + - target: test-helpers:fingerprint-python-build + cacheStrategy: hash + inputs: + - '@group(package)' + - '/ci/build-constraints.txt' + - '/.github/workflows/build-wheel.yml' + outputs: + - '.moon-out/wheel-pure' + type: build + options: + cache: true + priority: critical + runInCI: true diff --git a/.moon/tasks/python-package.yml b/.moon/tasks/python-package.yml new file mode 100644 index 00000000000..eb3b9e8d32e --- /dev/null +++ b/.moon/tasks/python-package.yml @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +$schema: https://moonrepo.dev/schemas/v2/tasks.json + +inheritedBy: + tag: python-package + +fileGroups: + # setuptools-scm's file finder includes every tracked project file in an + # sdist, not only the files that affect an installed wheel. Moon applies the + # workspace VCS ignore rules while expanding this glob, so generated build + # and .moon-out directories remain outside the hash. + sdist: + - '**/*' + - '.git_archival.txt' + - '!{.cache,.moon-out,.mypy_cache,.nox,.pixi,.pytest_cache,.ruff_cache,.tox,.venv,build,dist,htmlcov}/**/*' + - '!**/{__pycache__,*.egg-info,cython_debug}/**/*' + - '!**/*.{a,lib,o,pyc,pyo,so}' + - '!**/_version.py' + - '!docs/{build,source/generated}/**/*' + +taskOptions: + cache: false + runFromWorkspaceRoot: true + runInCI: false + shell: false + +tasks: + fingerprint-package: + inputs: [] + checks: + - check: fingerprint + script: >- + python -c "import subprocess; + subprocess.run(['git', 'describe', '--always', '--dirty', '--tags', '--long', + '--match', 'v*[0-9]*'], check=True)" + hash: stdout + type: build + options: + internal: true + runInCI: true + + sdist: + deps: + - target: ~:fingerprint-package + cacheStrategy: hash + inputs: + - '@group(sdist)' + - '/ci/build-constraints.txt' + - '/.github/workflows/test-sdist-linux.yml' + - '/.github/workflows/test-sdist-windows.yml' + outputs: + - '.moon-out/sdist' + tags: [ci-sdist] + type: build + options: + cache: true + runInCI: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d54a89f4a42..fd6061b6112 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -262,6 +262,17 @@ install, select, or configure Python, and it does not create Python environments remain supported and can still be invoked directly; Moon delegates to the environment that the contributor or CI runner has already prepared. +Before running package-build tasks, make sure `python` resolves to the interpreter you intend to use. In an existing +virtual environment, uv environment, or Pixi environment, the CI build frontends can be installed without asking +Moon to manage Python: + +```console +$ python -m pip install --constraint ci/build-constraints.txt pip build cibuildwheel twine wheel +``` + +Native wheel tasks also require the appropriate CUDA toolkit and platform compiler to already be active. Test and +documentation tasks continue to use the existing Pixi and uv environments declared by their projects. + Use Moon to inspect the graph, run one task locally, or execute the affected portion of the graph as CI does: ```console @@ -291,122 +302,66 @@ test interpreter before running `bindings:cython-test-assets` or `core:cython-te bindings, and core wheels are staged inputs rather than executable Moon dependencies. To run either task locally, first build or copy exactly one current wheel for each package into its corresponding `.moon-out` directory. -For ephemeral runners, CI uploads Moon's `.moon/cache/hashes` and `.moon/cache/outputs` directories as -ordinary immutable GitHub workflow artifacts. A later producer restores the lane-qualified artifact from the -successful trusted `main` run at the exact merge-base commit, then runs `moon ci`; GitHub transports the local cache -while Moon alone interprets its hashes and hydrates task outputs. Lane bundles also carry Moon's canonical task -outputs between heterogeneous build and test runners, while conventional named wheel artifacts remain available for -release tooling. Native build lanes include the inexpensive cuda-pathfinder wheel directly; the Linux/Python 3.12 -lane also carries the cuda-python metapackage used by docs and releases. Context-sensitive documentation is rebuilt as four parallel Moon tasks whenever its runner is -selected. Missing or incomplete cache artifacts conservatively allocate the producer runners and start with an empty -cache. Generated `.moon/cache` and `.moon-out` directories are ignored by Git. +Moon's `.moon/cache` remains local to each runner. For ephemeral-runner reuse, GitHub Actions instead transports the +canonical `.moon-out` directories in lane-qualified, immutable workflow artifacts. The gate accepts a lane only from +a successful trusted push run at the exact merge-base commit and only when every required artifact in that lane is +present. A producer restores those outputs before running `moon ci`, so affected tasks rebuild while unchanged staged +outputs remain available to their consumers. Tests and documentation may consume the same exact-base lane directly +when no producer is affected. Missing, expired, or incomplete lanes conservatively allocate and force the required +producer runner. + +Conventional named wheel artifacts remain available to release tooling. Native lanes include the inexpensive +cuda-pathfinder wheel directly; the Linux/Python 3.12 lane also includes the cuda-python metapackage used by +non-release documentation. Context-sensitive documentation is rebuilt as parallel Moon tasks whenever its runner is +selected. Generated `.moon/cache` and `.moon-out` directories are ignored by Git. CI sets `CUDA_PYTHON_USE_STAGED_BINDINGS_VERSION=1` when the metapackage must match a trusted staged `cuda.bindings` development wheel. Leave this variable unset for normal local builds; `root:pure-wheel` then derives the metapackage version from the current checkout and ignores any stale staged bindings output. -The draft reuses only immutable cache artifacts from a successful trusted `main` run at the exact merge-base. It does -not yet treat wheel or sdist tasks as hermetic enough for a general cross-revision remote cache: their isolated build -environments still resolve ranged build dependencies and rely on runner/container compiler and repair-tool images. -Before enabling that broader cache, pin the isolated build constraints and immutable toolchain images (and include -their identities in task fingerprints), or leave those producer tasks out of the persistent cache. +All isolated package builds set both `PIP_BUILD_CONSTRAINT` and `PIP_CONSTRAINT` from +`ci/build-constraints.txt`; CI also installs its build frontends from that file. Public `pyproject.toml` compatibility +ranges remain unchanged. Moon fingerprints the active interpreter, installed tools, selected environment, and native +compiler identities for its local cache. The builds still depend on provisioned runner, container, CUDA, and repair +tool environments, so they are not treated as a hermetic cross-revision remote cache. The production contract remains +the narrower exact-base `.moon-out` transport described above. ### CI Pipeline Flow -![CUDA Python CI Pipeline Flow](ci/ci-pipeline.svg) - -Alternative Mermaid diagram representation: - ```mermaid flowchart TD - %% Trigger Events - subgraph TRIGGER["🔄 TRIGGER EVENTS"] - T1["• Push to main branch"] - T2["• Pull request
• Manual workflow dispatch"] - T1 --- T2 - end - - %% Build Stage - subgraph BUILD["🔨 BUILD STAGE"] - subgraph BUILD_PLATFORMS["Parallel Platform Builds"] - B1["linux-64
(Self-hosted)"] - B2["linux-aarch64
(Self-hosted)"] - B3["win-64
(GitHub-hosted)"] - end - BUILD_DETAILS["• Python versions: 3.10, 3.11, 3.12, 3.13, 3.14
• CUDA version: 13.0.0 (build-time)
• Components: cuda-core, cuda-bindings,
cuda-pathfinder, cuda-python"] - end - - %% Artifact Storage - subgraph ARTIFACTS["📦 ARTIFACT STORAGE"] - subgraph GITHUB_ARTIFACTS["GitHub Artifacts"] - GA1["• Wheel files (.whl)
• Test artifacts
• Documentation
(30-day retention)"] - end - subgraph GITHUB_CACHE["GitHub Cache"] - GC1["• Mini CTK cache"] - end - end - - %% Test Stage - subgraph TEST["🧪 TEST STAGE"] - subgraph TEST_PLATFORMS["Parallel Platform Tests"] - TS1["linux-64
(Self-hosted)"] - TS2["linux-aarch64
(Self-hosted)"] - TS3["win-64
(GitHub-hosted)"] - end - TEST_DETAILS["• Download wheels from artifacts
• Test against multiple CUDA runtime versions
• Run Python unit tests, Cython tests, examples"] - ARTIFACT_FLOWS["Artifact Flows:
• cuda-pathfinder: main → backport
• cuda-bindings: backport → main"] - end - - %% Release Pipeline - subgraph RELEASE["🚀 RELEASE PIPELINE"] - subgraph RELEASE_STAGES["Sequential Release Steps"] - R1["Validation
• Artifact integrity
• Git tag verification"] - R2["Publishing
• PyPI/TestPyPI
• Component or all releases"] - R3["Documentation
• GitHub Pages
• Release notes"] - R1 --> R2 --> R3 - end - RELEASE_DETAILS["• Manual workflow dispatch with run ID
• Supports individual component or full releases"] - end - - %% Main Flow - TRIGGER --> BUILD - BUILD -.->|"wheel upload"| ARTIFACTS - ARTIFACTS -.-> TEST - TEST --> RELEASE - - %% Artifact Flow Arrows (Cache Reuse) - GITHUB_CACHE -.->|"mini CTK reuse"| BUILD - GITHUB_CACHE -.->|"mini CTK reuse"| TEST - - %% Artifact Flow Arrows (Wheel Fetch) - GITHUB_ARTIFACTS -.->|"wheel fetch"| TEST - GITHUB_ARTIFACTS -.->|"wheel fetch"| RELEASE - - %% Styling - classDef triggerStyle fill:#e8f4fd,stroke:#2196F3,stroke-width:2px,color:#1976D2 - classDef buildStyle fill:#f3e5f5,stroke:#9C27B0,stroke-width:2px,color:#7B1FA2 - classDef artifactStyle fill:#fff3e0,stroke:#FF9800,stroke-width:2px,color:#F57C00 - classDef testStyle fill:#e8f5e8,stroke:#4CAF50,stroke-width:2px,color:#388E3C - classDef releaseStyle fill:#ffebee,stroke:#f44336,stroke-width:2px,color:#D32F2F - - class TRIGGER,T1,T2 triggerStyle - class BUILD,BUILD_PLATFORMS,B1,B2,B3,BUILD_DETAILS buildStyle - class ARTIFACTS,GITHUB_ARTIFACTS,GITHUB_CACHE,GA1,GC1 artifactStyle - class TEST,TEST_PLATFORMS,TS1,TS2,TS3,TEST_DETAILS,ARTIFACT_FLOWS testStyle - class RELEASE,RELEASE_STAGES,R1,R2,R3,RELEASE_DETAILS releaseStyle + trigger["PR, push, tag, schedule, or manual run"] --> gate["Gate runner
moon query tasks --affected"] + base[("Trusted exact-base
.moon-out lane bundles")] -. "inspect completeness" .-> gate + + gate -->|"affected native lane"| native["Native wheel runners
platform x ci/build-matrix.yml"] + gate -->|"affected source distributions"| sdist["Linux and Windows sdist runners"] + gate -->|"affected tests"| tests["GPU test runners"] + gate -->|"affected docs"| docs["Parallel component docs"] + gate -->|"affected quality"| quality["Contracts and API checks"] + + base -. "restore unchanged outputs" .-> native + base -. "feed consumers when producers are skipped" .-> tests + base -. "feed consumers when producers are skipped" .-> docs + native --> lanes[("Canonical .moon-out
lane artifacts")] + lanes --> tests + lanes --> docs + + native --> named[("Named wheel artifacts")] + sdist --> sdist_lanes[("Canonical .moon-out
sdist lane artifacts")] + named --> release["Tag/manual release validation and publishing"] ``` ### Pipeline Execution Details -**Parallel Execution**: The CI pipeline leverages parallel execution to optimize build and test times: -- **Build Stage**: Different architectures/operating systems (linux-64, linux-aarch64, win-64) are built in parallel across their respective runners -- **Test Stage**: Different architectures/operating systems/CUDA versions are tested in parallel; documentation preview is also built in parallel with testing +**Parallel Execution**: GitHub Actions allocates the required runner classes in parallel. Within each provisioned +environment, Moon schedules independent tasks concurrently while preserving package and staged-output dependencies. +The native Python ABI rows come from `ci/build-matrix.yml`, and CUDA versions come from `ci/versions.yml`. ### Branch-specific Artifact Flow #### Main Branch - **Build** → **Test** → **Documentation** → **Potential Release** -- Artifacts stored as `{component}-python{version}-{platform}-{sha}` +- Canonical `.moon-out` lane artifacts feed affected CI; named wheel artifacts feed release tooling - Full test coverage across all platforms and CUDA versions - **Artifact flow out**: `cuda-pathfinder` artifacts → backport branches @@ -421,11 +376,11 @@ flowchart TD - **Self-hosted runners**: Used for Linux builds and GPU testing (more resources, faster builds) - **GitHub-hosted runners**: Used for Windows builds and general tasks -- **Artifact retention**: 30 days for GitHub Artifacts (wheels, docs, tests) -- **Cache retention**: GitHub Cache for build dependencies and environments +- **Artifact retention**: 30 days for reusable Moon lanes; specialized artifacts declare their own retention +- **Cache ownership**: Moon caches remain runner-local; GitHub caches CTK and compiler downloads separately - **Security**: All commits must be signed, untrusted code blocked - **Parallel execution**: Matrix builds across Python versions and platforms -- **Component isolation**: Each component (core, bindings, pathfinder, python) can be built/released independently +- **Component isolation**: Core, bindings, pathfinder, and the metapackage can be built or released independently ## Code coverage diff --git a/ci/.ci-pipeline-regen.md b/ci/.ci-pipeline-regen.md deleted file mode 100644 index 7ddf9b970d5..00000000000 --- a/ci/.ci-pipeline-regen.md +++ /dev/null @@ -1,106 +0,0 @@ -# CUDA Python CI Pipeline SVG Regeneration Instructions - -This file contains the prompt and requirements for regenerating `ci-pipeline.svg` with the same styling and content. - -## Styling Requirements - -- Hand-drawn Excalidraw-style design with rough, sketchy borders -- Comic Sans MS font family for all text -- Imperfect lines and curves that mimic hand-drawn aesthetics -- Canvas size: 900x800 pixels -- Color scheme: - - Trigger Events: #e8f4fd background, #2196F3 border, #1976D2 text - - Build Stage: #f3e5f5 background, #9C27B0 border, #7B1FA2 text - - Artifact Storage: #fff3e0 background, #FF9800 border, #F57C00 text - - Test Stage: #e8f5e8 background, #4CAF50 border, #388E3C text - - Release Pipeline: #ffebee background, #f44336 border, #D32F2F text - -## Content Structure - -1. **Title**: "CUDA Python CI Pipeline Flow" - -2. **Trigger Events** (top blue box): - - Push to main branch - - Pull request - - Manual workflow dispatch - -3. **Build Stage** (purple box): - - Three platform boxes: linux-64 (Self-hosted), linux-aarch64 (Self-hosted), win-64 (GitHub-hosted) - - Details: Python versions 3.9-3.13, CUDA 13.0.0 (build-time) - - Components: cuda-core, cuda-bindings, cuda-pathfinder, cuda-python - -4. **Artifact Storage** (orange box): - - GitHub Artifacts box: Wheel files (.whl), Test artifacts, Documentation (30-day retention) - - GitHub Cache box: Mini CTK cache - -5. **Test Stage** (green box): - - Three platform boxes: linux-64 (Self-hosted), linux-aarch64 (Self-hosted), win-64 (GitHub-hosted) - - Details: Download wheels from artifacts, Test against multiple CUDA runtime versions, Run Python unit tests, Cython tests, examples - - Artifact Flows (in red text): - • cuda-pathfinder: main → backport - • cuda-bindings: backport → main - -6. **Release Pipeline** (red box): - - Three sequential boxes: Validation → Publishing → Documentation - - Validation: Artifact integrity, Git tag verification - - Publishing: PyPI/TestPyPI, Component or all releases - - Documentation: GitHub Pages, Release notes - - Details: Manual workflow dispatch with run ID, Supports individual component or full releases - -## Arrow Requirements - -- Main flow arrows: Trigger → Build → Artifact → Test → Release -- Additional artifact flow arrows (dashed, orange #FF9800): - - From GitHub Cache (mini CTK) back to Build Stage with "mini CTK reuse" label - - From GitHub Artifacts (wheels) to Release Pipeline with "wheel fetch" label - - **NEW**: From GitHub Cache (mini CTK) to Test Stage with "mini CTK reuse" label - - **NEW**: From GitHub Artifacts (wheels) to Test Stage with "wheel fetch" label -- Arrow marker definition with hand-drawn style (orange arrow heads, not black) -- Use stroke-dasharray="5,3" for artifact flow arrows - -## Critical Arrow Positioning Requirements (UPDATED) - -**IMPORTANT**: Arrows must NOT overlap with stage boxes. Ensure proper clearance: - -1. **Mini CTK reuse arrow** (GitHub Cache → Build Stage): - - Arrow endpoint Y coordinate must be BELOW the Build Stage box edge (y=292) - - Use y=295 or greater for the endpoint to ensure no overlap - - Position "mini CTK reuse" text to the RIGHT of the arrow (not left) for less visual clutter - - Text color should be orange (#FF9800) to match arrow - -2. **Wheel fetch arrow** (GitHub Artifacts → Release Pipeline): - - Arrow endpoint Y coordinate must be ABOVE the Release Pipeline box edge (y=652) - - Use y=645 or smaller for the endpoint to provide proper margin - - Position "wheel fetch" text between Test Stage and Release Pipeline boxes - - Text should be to the LEFT of the arrow for better spacing - -## Font Size Requirements (UPDATED) - -- ALL text labels must use consistent 12pt font size for readability -- No 9pt text - this is too small and hard to read -- Title: 16pt, Stage headers: 14pt, All other text: 12pt - -## Key Features - -- All boxes use rough, hand-drawn paths (not perfect rectangles) -- Text should be properly sized and positioned within boxes -- Platform boxes within each stage should be clearly separated -- Maintain consistent spacing and alignment -- Orange arrow heads must match the orange arrow color - -## Text Positioning - -- Use text-anchor="middle" for centered headers -- Use text-anchor="start" for left-aligned bullet points -- Ensure all text fits within their enclosing boxes -- Use transforms for angled text labels on artifact flow arrows -- Artifact flow arrow text positioning is critical - follow positioning requirements above - -## Recent Manual Adjustments Applied - -- Fixed arrow endpoint positioning to prevent overlap with stage boxes -- Moved mini CTK reuse arrow endpoint from y=285 to y=295 -- Moved wheel fetch arrow endpoint from y=650 to y=645 -- Repositioned text labels for better visual separation -- Standardized all text to 12pt font size for consistency -- Changed arrow heads from black to orange to match arrow color diff --git a/ci/build-constraints.txt b/ci/build-constraints.txt new file mode 100644 index 00000000000..413f042c71e --- /dev/null +++ b/ci/build-constraints.txt @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +# Keep CI and Moon's isolated package builds reproducible. Public +# build-system requirements intentionally retain their compatibility ranges. +build==1.5.0 +cibuildwheel==4.1.1 +Cython==3.2.9 +delvewheel==1.13.0 +packaging==26.3 +pefile==2024.8.26 +pip==26.1.2 +setuptools==83.0.0 +setuptools-scm==10.2.1 +tomli==2.4.1; python_version < "3.11" +twine==7.0.0 +typing-extensions==4.16.0; python_version < "3.11" +vcs-versioning==2.2.3 +wheel==0.46.3 diff --git a/ci/build-matrix.yml b/ci/build-matrix.yml new file mode 100644 index 00000000000..255d337086c --- /dev/null +++ b/ci/build-matrix.yml @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +# The JSON-compatible body is consumed directly as a GitHub Actions matrix. +{ + "include": [ + {"python-version": "3.10", "python-version-formatted": "310"}, + {"python-version": "3.11", "python-version-formatted": "311"}, + {"python-version": "3.12", "python-version-formatted": "312"}, + {"python-version": "3.13", "python-version-formatted": "313"}, + {"python-version": "3.14", "python-version-formatted": "314"}, + {"python-version": "3.14t", "python-version-formatted": "314t"}, + {"python-version": "3.15", "python-version-formatted": "315"}, + {"python-version": "3.15t", "python-version-formatted": "315t"} + ] +} diff --git a/ci/ci-pipeline.svg b/ci/ci-pipeline.svg deleted file mode 100644 index eeff4c69fd1..00000000000 --- a/ci/ci-pipeline.svg +++ /dev/null @@ -1,172 +0,0 @@ - - - - - - - - - - - - - - - - - - CUDA Python CI Pipeline Flow - - - - TRIGGER EVENTS - • Push to main branch - • Pull request - • Manual workflow dispatch - - - - - - BUILD STAGE - - - - linux-64 - Self-hosted - - - linux-aarch64 - Self-hosted - - - win-64 - GitHub-hosted - - - • Python versions: 3.9, 3.10, 3.11, 3.12, 3.13 - • CUDA version: 13.0.0 (build-time) - • Components: cuda-core, cuda-bindings, - cuda-pathfinder, cuda-python - - - - wheel upload - - - - ARTIFACT STORAGE - - - - GitHub Artifacts - • Wheel files (.whl) - • Test artifacts - • Documentation - (30-day retention) - - - GitHub Cache - • Mini CTK cache - - - - - - - - mini CTK reuse - - - - wheel fetch - - - - - mini CTK reuse - - - - wheel fetch - - - - TEST STAGE - - - - linux-64 - Self-hosted - - - linux-aarch64 - Self-hosted - - - win-64 - GitHub-hosted - - - • Download wheels from artifacts - • Test against multiple CUDA runtime versions - • Run Python unit tests, Cython tests, examples - Artifact Flows: - • cuda-pathfinder: main → backport - • cuda-bindings: backport → main - - - - - - - RELEASE PIPELINE - - - - Validation - • Artifact integrity - • Git tag verification - - - - - - Publishing - • PyPI/TestPyPI - • Component or all releases - - - - - - Documentation - • GitHub Pages - • Release notes - - - • Manual workflow dispatch with run ID - • Supports individual component or full releases - diff --git a/ci/tools/env-vars b/ci/tools/env-vars index 8ffbfa13472..de9f1564020 100755 --- a/ci/tools/env-vars +++ b/ci/tools/env-vars @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 @@ -28,15 +28,7 @@ fi echo "${TOOLS_PATH}" >> $GITHUB_PATH echo "CUDA_PYTHON_PARALLEL_LEVEL=$(nproc)" >> $GITHUB_ENV -CUDA_CORE_ARTIFACT_BASENAME="cuda-core-python${PYTHON_VERSION_FORMATTED}-${HOST_PLATFORM}" -{ - echo "CUDA_CORE_ARTIFACT_BASENAME=${CUDA_CORE_ARTIFACT_BASENAME}" - echo "CUDA_CORE_ARTIFACT_NAME=${CUDA_CORE_ARTIFACT_BASENAME}-${SHA}" - echo "CUDA_CORE_ARTIFACTS_DIR=$(realpath "${REPO_DIR}/cuda_core/dist")" - echo "CUDA_CORE_CYTHON_TESTS_DIR=$(realpath "${REPO_DIR}/cuda_core/tests/cython")" - echo "CUDA_CORE_TEST_BINARIES_DIR=$(realpath "${REPO_DIR}/cuda_core/tests/test_binaries")" - echo "PYTHON_VERSION_FORMATTED=${PYTHON_VERSION_FORMATTED}" -} >> $GITHUB_ENV +echo "PYTHON_VERSION_FORMATTED=${PYTHON_VERSION_FORMATTED}" >> $GITHUB_ENV if [[ "${1}" == "build" ]]; then # platform is handled by the default value of platform (`auto`) in cibuildwheel @@ -45,14 +37,20 @@ if [[ "${1}" == "build" ]]; then BUILD_CUDA_MAJOR="$(cut -d '.' -f 1 <<< ${CUDA_VER})" echo "BUILD_CUDA_MAJOR=${BUILD_CUDA_MAJOR}" >> $GITHUB_ENV echo "BUILD_PREV_CUDA_MAJOR=$((${BUILD_CUDA_MAJOR} - 1))" >> $GITHUB_ENV - CUDA_BINDINGS_ARTIFACT_BASENAME="cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda${CUDA_VER}-${HOST_PLATFORM}" + if [[ -n "${SHA:-}" ]]; then + CUDA_BINDINGS_ARTIFACT_BASENAME="cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda${CUDA_VER}-${HOST_PLATFORM}" + CUDA_CORE_ARTIFACT_BASENAME="cuda-core-python${PYTHON_VERSION_FORMATTED}-${HOST_PLATFORM}" + { + echo "CUDA_BINDINGS_ARTIFACT_NAME=${CUDA_BINDINGS_ARTIFACT_BASENAME}-${SHA}" + echo "CUDA_CORE_ARTIFACT_NAME=${CUDA_CORE_ARTIFACT_BASENAME}-${SHA}" + } >> $GITHUB_ENV + fi # Enforce an explicit cache dir so that we can reuse this path later echo "SCCACHE_DIR=${HOME}/.cache/sccache" >> $GITHUB_ENV echo "SCCACHE_CACHE_SIZE=1G" >> $GITHUB_ENV elif [[ "${1}" == "test" ]]; then BUILD_CUDA_MAJOR="$(cut -d '.' -f 1 <<< ${BUILD_CUDA_VER})" TEST_CUDA_MAJOR="$(cut -d '.' -f 1 <<< ${CUDA_VER})" - CUDA_BINDINGS_ARTIFACT_BASENAME="cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda${BUILD_CUDA_VER}-${HOST_PLATFORM}" # BINDINGS_SOURCE controls which cuda-bindings to install at test time: # main — use the just-built bindings wheel from this CI run @@ -105,10 +103,3 @@ elif [[ "${1}" == "test" ]]; then echo "TEST_CUDA_MINOR=${TEST_CUDA_MINOR}" } >> $GITHUB_ENV fi - -{ - echo "CUDA_BINDINGS_ARTIFACT_BASENAME=${CUDA_BINDINGS_ARTIFACT_BASENAME}" - echo "CUDA_BINDINGS_ARTIFACT_NAME=${CUDA_BINDINGS_ARTIFACT_BASENAME}-${SHA}" - echo "CUDA_BINDINGS_ARTIFACTS_DIR=$(realpath "${REPO_DIR}/cuda_bindings/dist")" - echo "CUDA_BINDINGS_CYTHON_TESTS_DIR=$(realpath "${REPO_DIR}/cuda_bindings/tests/cython")" -} >> $GITHUB_ENV diff --git a/ci/tools/merge_cuda_core_wheels.py b/ci/tools/merge_cuda_core_wheels.py index 6f95c97ec30..16538c8aed8 100644 --- a/ci/tools/merge_cuda_core_wheels.py +++ b/ci/tools/merge_cuda_core_wheels.py @@ -26,6 +26,8 @@ import sys import tempfile import zipfile +from collections.abc import Mapping +from datetime import datetime, timezone from pathlib import Path @@ -38,8 +40,10 @@ def _wheel_from_directory(directory: Path) -> Path: def _clean_output_wheels(output_dir: Path) -> None: """Remove wheel files without recursively deleting the caller's directory.""" - if output_dir.is_symlink(): - raise ValueError(f"output path must not be a symlink: {output_dir}") + absolute_output = output_dir.absolute() + for component in (absolute_output, *absolute_output.parents): + if component.is_symlink(): + raise ValueError(f"output path must not contain symlinks: {component}") if output_dir.exists() and not output_dir.is_dir(): raise ValueError(f"output path is not a directory: {output_dir}") output_dir.mkdir(parents=True, exist_ok=True) @@ -48,7 +52,22 @@ def _clean_output_wheels(output_dir: Path) -> None: wheel.unlink() -def run_command(cmd: list[str], cwd: Path | None = None, env: dict = os.environ) -> subprocess.CompletedProcess: +def _wheel_source_date_epoch(wheels: list[Path]) -> str: + """Derive a stable wheel-pack timestamp from the input archives.""" + timestamps: list[int] = [] + for wheel in wheels: + with zipfile.ZipFile(wheel) as archive: + timestamps.extend( + int(datetime(*info.date_time, tzinfo=timezone.utc).timestamp()) for info in archive.infolist() + ) + if not timestamps: + raise ValueError("input wheels contain no archive entries") + return str(max(timestamps)) + + +def run_command( + cmd: list[str], cwd: Path | None = None, env: Mapping[str, str] | None = None +) -> subprocess.CompletedProcess: """Run a command with error handling.""" print(f"Running: {' '.join(cmd)}") if cwd: @@ -199,6 +218,8 @@ def merge_wheels(wheels: list[Path], output_dir: Path, show_wheel_contents: bool base_wheel_name = wheels[0].with_suffix(".whl").name print(f"Repacking merged wheel as: {base_wheel_name}", file=sys.stderr) + pack_environment = os.environ.copy() + pack_environment.setdefault("SOURCE_DATE_EPOCH", _wheel_source_date_epoch(wheels)) run_command( [ sys.executable, @@ -208,7 +229,8 @@ def merge_wheels(wheels: list[Path], output_dir: Path, show_wheel_contents: bool str(base_wheel), "--dest-dir", str(output_dir), - ] + ], + env=pack_environment, ) # Find the output wheel diff --git a/ci/tools/run-tests b/ci/tools/run-tests index 9c0a1e30af1..f9476524620 100755 --- a/ci/tools/run-tests +++ b/ci/tools/run-tests @@ -22,52 +22,38 @@ test_module=${1} repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" select_one() { + if [[ ${#} -ne 3 ]]; then + echo "Error: select_one requires exactly one artifact directory" >&2 + exit 1 + fi local description=${1} local result_name=${2} - shift 2 - local directory files - for directory in "$@"; do - [[ -z "${directory}" ]] && continue - shopt -s nullglob - files=("${directory}"/*.whl) - shopt -u nullglob - if [[ ${#files[@]} -eq 1 ]]; then - printf -v "${result_name}" '%s' "${files[0]}" - return - fi - if [[ ${#files[@]} -gt 1 ]]; then - echo "Error: Expected one ${description} in ${directory}, found ${#files[@]}" >&2 - exit 1 - fi - done - echo "Error: Expected one ${description}; searched $*" >&2 - exit 1 + local directory=${3} + local files + shopt -s nullglob + files=("${directory}"/*.whl) + shopt -u nullglob + if [[ ${#files[@]} -ne 1 ]]; then + echo "Error: Expected one ${description} in ${directory}, found ${#files[@]}" >&2 + exit 1 + fi + printf -v "${result_name}" '%s' "${files[0]}" +} + +native_path() { + python -c 'import os, sys; print(os.path.abspath(sys.argv[1]))' "${1}" } -stage_generated() { - local source=${1} - local destination=${2} - shift 2 - local pattern selected=() stale=() path - for pattern in "$@"; do - shopt -s nullglob - for path in "${source}"/${pattern}; do - [[ -f "${path}" ]] && selected+=("${path}") - done - for path in "${destination}"/${pattern}; do - [[ -f "${path}" ]] && stale+=("${path}") - done - shopt -u nullglob - done - if [[ ${#selected[@]} -eq 0 ]]; then - echo "Error: No generated test files found in ${source}" >&2 +prepend_python_path() { + local directory=${1} + local native_directory separator + if [[ ! -d "${directory}" ]]; then + echo "Error: Generated Cython test directory does not exist: ${directory}" >&2 exit 1 fi - mkdir -p "${destination}" - if [[ ${#stale[@]} -gt 0 ]]; then - rm -f -- "${stale[@]}" - fi - cp -- "${selected[@]}" "${destination}/" + native_directory=$(native_path "${directory}") + separator=$(python -c 'import os; print(os.pathsep)') + export PYTHONPATH="${native_directory}${PYTHONPATH:+${separator}${PYTHONPATH}}" } if [[ "${test_module}" == "bindings" && "${SKIP_CUDA_BINDINGS_TEST:-0}" == 1 ]]; then @@ -81,13 +67,13 @@ if [[ "${test_module}" == "metapackage" ]]; then exit 0 fi select_one "pathfinder wheel" PATHFINDER_WHL \ - "${repo_dir}/cuda_pathfinder/.moon-out/wheel-pure" "${repo_dir}/cuda_pathfinder" + "${repo_dir}/cuda_pathfinder/.moon-out/wheel-pure" select_one "bindings wheel" BINDINGS_WHL \ - "${repo_dir}/cuda_bindings/.moon-out/wheel-current" "${CUDA_BINDINGS_ARTIFACTS_DIR:-}" "${repo_dir}/cuda_bindings/dist" + "${repo_dir}/cuda_bindings/.moon-out/wheel-current" select_one "merged core wheel" CORE_WHL \ - "${repo_dir}/cuda_core/.moon-out/wheel-merged" "${CUDA_CORE_ARTIFACTS_DIR:-}" "${repo_dir}/cuda_core/dist" + "${repo_dir}/cuda_core/.moon-out/wheel-merged" select_one "metapackage wheel" METAPACKAGE_WHL \ - "${repo_dir}/cuda_python/.moon-out/wheel-pure" "${repo_dir}" "${repo_dir}/cuda_python" + "${repo_dir}/cuda_python/.moon-out/wheel-pure" if [[ "${LOCAL_CTK:-1}" != 1 ]]; then METAPACKAGE_WHL="${METAPACKAGE_WHL}[all]" fi @@ -101,17 +87,31 @@ fi # all wheels together in a single pip call further below. if [[ "${test_module}" != nightly-* ]]; then select_one "pathfinder wheel" PATHFINDER_WHL \ - "${repo_dir}/cuda_pathfinder/.moon-out/wheel-pure" "${repo_dir}/cuda_pathfinder" + "${repo_dir}/cuda_pathfinder/.moon-out/wheel-pure" pushd ./cuda_pathfinder echo "Installing pathfinder wheel" pip install "${PATHFINDER_WHL}" --group test popd fi -if [[ "${test_module}" == "core" ]]; then - : "${CUDA_BINDINGS_ARTIFACTS_DIR:=${repo_dir}/cuda_bindings/.moon-out/wheel-current}" - : "${CUDA_CORE_ARTIFACTS_DIR:=${repo_dir}/cuda_core/.moon-out/wheel-merged}" - export CUDA_BINDINGS_ARTIFACTS_DIR CUDA_CORE_ARTIFACTS_DIR +if [[ "${test_module}" == "core" || "${test_module}" == nightly-* ]]; then + case "${BINDINGS_SOURCE:-main}" in + main) + CUDA_BINDINGS_ARTIFACTS_DIR="${repo_dir}/cuda_bindings/.moon-out/wheel-current" + export CUDA_BINDINGS_ARTIFACTS_DIR + ;; + backport) + CUDA_BINDINGS_ARTIFACTS_DIR="${repo_dir}/cuda_bindings/.moon-out/wheel-previous" + export CUDA_BINDINGS_ARTIFACTS_DIR + ;; + published) ;; + *) + echo "Error: Invalid BINDINGS_SOURCE '${BINDINGS_SOURCE}'" >&2 + exit 1 + ;; + esac + CUDA_CORE_ARTIFACTS_DIR="${repo_dir}/cuda_core/.moon-out/wheel-merged" + export CUDA_CORE_ARTIFACTS_DIR fi if [[ "${test_module}" == "pathfinder" ]]; then @@ -127,12 +127,8 @@ if [[ "${test_module}" == "pathfinder" ]]; then echo "Number of \"INFO test_\" lines: $line_count" popd elif [[ "${test_module}" == "bindings" ]]; then - : "${CUDA_BINDINGS_ARTIFACTS_DIR:=${repo_dir}/cuda_bindings/.moon-out/wheel-current}" + CUDA_BINDINGS_ARTIFACTS_DIR="${repo_dir}/cuda_bindings/.moon-out/wheel-current" export CUDA_BINDINGS_ARTIFACTS_DIR - stage_generated \ - "${repo_dir}/cuda_bindings/.moon-out/cython-tests" \ - "${repo_dir}/cuda_bindings/tests/cython" \ - 'test_*.so' 'test_*.pyd' 'test_*.dylib' echo "Installing bindings wheel" pushd ./cuda_bindings select_one "bindings wheel" BINDINGS_WHL "${CUDA_BINDINGS_ARTIFACTS_DIR}" @@ -144,7 +140,8 @@ elif [[ "${test_module}" == "bindings" ]]; then echo "Running bindings tests" ${SANITIZER_CMD} pytest -rxXs -v --randomly-dont-reorganize tests/ if [[ "${SKIP_CYTHON_TEST}" == 0 ]]; then - ${SANITIZER_CMD} pytest -rxXs -v --randomly-dont-reorganize tests/cython + prepend_python_path "${repo_dir}/cuda_bindings/.moon-out/cython-tests" + ${SANITIZER_CMD} pytest -rxXs -v --randomly-dont-reorganize --import-mode=importlib tests/cython fi popd elif [[ "${test_module}" == "core" || "${test_module}" == nightly-* ]]; then @@ -180,21 +177,14 @@ elif [[ "${test_module}" == "core" || "${test_module}" == nightly-* ]]; then fi if [[ "${test_module}" == nightly-* ]]; then - # Resolve pathfinder wheel to absolute path before pushd. - # CUDA_BINDINGS_ARTIFACTS_DIR and CUDA_CORE_ARTIFACTS_DIR are already - # absolute (set via realpath in env-vars). - PATHFINDER_WHL=($(realpath ./cuda_pathfinder/*.whl)) + select_one "pathfinder wheel" PATHFINDER_WHEEL \ + "${repo_dir}/cuda_pathfinder/.moon-out/wheel-pure" + PATHFINDER_WHL=("${PATHFINDER_WHEEL}") fi if [[ "${test_module}" == "core" ]]; then - stage_generated \ - "${repo_dir}/cuda_core/.moon-out/cython-tests" \ - "${repo_dir}/cuda_core/tests/cython" \ - 'test_*.so' 'test_*.pyd' 'test_*.dylib' - stage_generated \ - "${repo_dir}/cuda_core/.moon-out/test-binaries" \ - "${repo_dir}/cuda_core/tests/test_binaries" \ - '*.o' '*.a' '*.lib' + CUDA_CORE_TEST_BINARIES_DIR=$(native_path "${repo_dir}/cuda_core/.moon-out/test-binaries") + export CUDA_CORE_TEST_BINARIES_DIR # pushd so --group reads test dependency groups from cuda_core/pyproject.toml. pushd ./cuda_core echo "Installing bindings (source: ${BINDINGS_SOURCE})" @@ -210,7 +200,8 @@ elif [[ "${test_module}" == "core" || "${test_module}" == nightly-* ]]; then # Currently our CI always installs the latest bindings (from either major version). # This is not compatible with the test requirements. if [[ "${SKIP_CYTHON_TEST}" == 0 ]]; then - ${SANITIZER_CMD} pytest -rxXs -v --randomly-dont-reorganize tests/cython + prepend_python_path "${repo_dir}/cuda_core/.moon-out/cython-tests" + ${SANITIZER_CMD} pytest -rxXs -v --randomly-dont-reorganize --import-mode=importlib tests/cython fi popd elif [[ "${test_module}" == "nightly-cuda-core" ]]; then diff --git a/ci/tools/tests/test_moon_tasks.py b/ci/tools/tests/test_moon_tasks.py index 97d03e7427c..2413f277578 100644 --- a/ci/tools/tests/test_moon_tasks.py +++ b/ci/tools/tests/test_moon_tasks.py @@ -13,12 +13,28 @@ import subprocess import tempfile import unittest +import zipfile +from datetime import datetime, timezone from pathlib import Path -from ci.tools.merge_cuda_core_wheels import _clean_output_wheels, _wheel_from_directory +from ci.tools.merge_cuda_core_wheels import _clean_output_wheels, _wheel_from_directory, _wheel_source_date_epoch class WheelMergerInputOutputTest(unittest.TestCase): + def test_derives_reproducible_epoch_from_input_wheels(self) -> None: + with tempfile.TemporaryDirectory() as temporary_directory: + root = Path(temporary_directory) + wheels = [] + for index, date_time in enumerate(((2024, 1, 2, 3, 4, 6), (2025, 6, 7, 8, 9, 10))): + wheel = root / f"input-{index}.whl" + with zipfile.ZipFile(wheel, "w") as archive: + info = zipfile.ZipInfo("payload.txt", date_time=date_time) + archive.writestr(info, b"payload") + wheels.append(wheel) + + expected = int(datetime(2025, 6, 7, 8, 9, 10, tzinfo=timezone.utc).timestamp()) + self.assertEqual(_wheel_source_date_epoch(wheels), str(expected)) + def test_selects_exactly_one_wheel_from_a_directory(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: wheel_dir = Path(temporary_directory) @@ -51,11 +67,79 @@ def test_clean_output_rejects_a_symlinked_directory(self) -> None: output = root / "output" output.symlink_to(target, target_is_directory=True) - with self.assertRaisesRegex(ValueError, "must not be a symlink"): + with self.assertRaisesRegex(ValueError, "must not contain symlinks"): _clean_output_wheels(output) + def test_clean_output_rejects_a_symlinked_parent(self) -> None: + with tempfile.TemporaryDirectory() as temporary_directory: + root = Path(temporary_directory) + target = root / "target" + target.mkdir() + (target / "outside.whl").touch() + parent = root / "linked-parent" + parent.symlink_to(target, target_is_directory=True) + + with self.assertRaisesRegex(ValueError, "must not contain symlinks"): + _clean_output_wheels(parent / "wheel-merged") + + self.assertTrue((target / "outside.whl").exists()) + class MoonTaskCommandTest(unittest.TestCase): + def test_main_and_nightly_tests_use_canonical_moon_wheel_directories(self) -> None: + script = (Path(__file__).resolve().parents[1] / "run-tests").read_text(encoding="utf-8") + for path in ( + "cuda_pathfinder/.moon-out/wheel-pure", + "cuda_bindings/.moon-out/wheel-current", + "cuda_bindings/.moon-out/wheel-previous", + "cuda_bindings/.moon-out/cython-tests", + "cuda_core/.moon-out/wheel-merged", + "cuda_core/.moon-out/cython-tests", + "cuda_core/.moon-out/test-binaries", + "cuda_python/.moon-out/wheel-pure", + ): + self.assertIn(path, script) + self.assertNotIn("stage_generated", script) + self.assertNotIn('"${repo_dir}/cuda_pathfinder"', script) + self.assertNotIn('"${repo_dir}/cuda_core/dist"', script) + self.assertNotIn('"${repo_dir}/cuda_python"', script) + self.assertNotIn('"${repo_dir}" "${repo_dir}/cuda_python"', script) + + def test_env_vars_defers_bindings_wheel_directory_selection_to_test_runner(self) -> None: + bash = shutil.which("bash") + self.assertIsNotNone(bash) + assert bash is not None + for cuda_version, source in ( + ("13.3.0", "main"), + ("12.9.1", "backport"), + ): + with self.subTest(source=source), tempfile.TemporaryDirectory() as temporary_directory: + temporary = Path(temporary_directory) + github_env = temporary / "github-env" + github_path = temporary / "github-path" + result = subprocess.run( # noqa: S603 + [bash, "ci/tools/env-vars", "test"], + cwd=Path(__file__).resolve().parents[3], + env={ + **os.environ, + "BUILD_CUDA_VER": "13.3.0", + "CUDA_VER": cuda_version, + "GITHUB_ENV": str(github_env), + "GITHUB_PATH": str(github_path), + "HOST_PLATFORM": "linux-64", + "LOCAL_CTK": "1", + "PY_VER": "3.11", + }, + check=False, + capture_output=True, + text=True, + ) + + self.assertEqual(result.returncode, 0, result.stderr) + values = github_env.read_text(encoding="utf-8").splitlines() + self.assertIn(f"BINDINGS_SOURCE={source}", values) + self.assertFalse(any(value.startswith("CUDA_BINDINGS_ARTIFACTS_DIR=") for value in values)) + def test_declared_unsupported_bindings_lane_skips_before_artifact_lookup(self) -> None: bash = shutil.which("bash") self.assertIsNotNone(bash) diff --git a/ci/tools/tests/test_moon_workspace.py b/ci/tools/tests/test_moon_workspace.py index f66fa0b60c0..c3b10763c99 100644 --- a/ci/tools/tests/test_moon_workspace.py +++ b/ci/tools/tests/test_moon_workspace.py @@ -4,18 +4,23 @@ # These tests intentionally use stdlib unittest so Moon's contract task does # not need a separately managed Python test environment. -# ruff: noqa: PT009 +# ruff: noqa: PT009, PT027 from __future__ import annotations import json import os +import re +import runpy import shutil import subprocess +import sys import tempfile +import types import unittest from pathlib import Path from typing import Any +from unittest import mock REPO_ROOT = Path(__file__).resolve().parents[3] EXPECTED_PROJECTS = { @@ -27,7 +32,16 @@ "test-helpers": "cuda_python_test_helpers", } EXECUTION_TAG_TARGETS = { - "ci-wheel-current": {"bindings:wheel-current", "core:wheel-current"}, + "ci-build-native": { + "pathfinder:wheel-pure", + "bindings:wheel-current", + "core:wheel-current", + "bindings:cython-test-assets", + "core:cython-test-assets", + "core:wheel-previous", + "core:test-binaries", + "core:wheel-merge", + }, "ci-build-cython-assets": { "bindings:cython-test-assets", "core:cython-test-assets", @@ -64,26 +78,14 @@ "bindings:unit-test", }, } -RUNNER_TAG_TARGETS = { - "runner-build-linux-64": { - "pathfinder:wheel-pure", - "bindings:wheel-current", - "core:wheel-current", - "bindings:cython-test-assets", - "core:cython-test-assets", - "core:wheel-previous", - "core:test-binaries", - "core:wheel-merge", - }, - "runner-sdist-linux": EXECUTION_TAG_TARGETS["ci-sdist"], - "runner-sdist-windows": EXECUTION_TAG_TARGETS["ci-sdist"], - "runner-test-linux": EXECUTION_TAG_TARGETS["ci-test-linux"], - "runner-test-windows": EXECUTION_TAG_TARGETS["ci-test-windows"], - "runner-docs": EXECUTION_TAG_TARGETS["ci-docs"], - "runner-quality": EXECUTION_TAG_TARGETS["ci-quality"], + +INTERNAL_FINGERPRINT_TARGETS = { + *(f"{project}:fingerprint-package" for project in ("pathfinder", "bindings", "core", "metapackage")), + "test-helpers:fingerprint-python-context", + "test-helpers:fingerprint-python-build", + "test-helpers:fingerprint-native-context", + "test-helpers:fingerprint-test-assets", } -RUNNER_TAG_TARGETS["runner-build-linux-aarch64"] = RUNNER_TAG_TARGETS["runner-build-linux-64"] -RUNNER_TAG_TARGETS["runner-build-windows"] = RUNNER_TAG_TARGETS["runner-build-linux-64"] CACHED_OUTPUTS = { "pathfinder:wheel-pure": ".moon-out/wheel-pure", @@ -101,6 +103,22 @@ "metapackage:sdist": ".moon-out/sdist", } FINGERPRINTED_TARGETS = set(CACHED_OUTPUTS) +SCM_FINGERPRINTED_TARGETS = FINGERPRINTED_TARGETS - { + "bindings:cython-test-assets", + "core:cython-test-assets", + "core:test-binaries", + "core:wheel-merge", +} +NATIVE_FINGERPRINTED_TARGETS = { + "bindings:wheel-current", + "bindings:sdist", + "bindings:cython-test-assets", + "core:wheel-current", + "core:wheel-previous", + "core:sdist", + "core:cython-test-assets", + "core:test-binaries", +} class MoonWorkspaceContractTest(unittest.TestCase): @@ -110,6 +128,7 @@ def setUpClass(cls) -> None: if not cls.moon: raise unittest.SkipTest("Moon is not installed; set MOON_BIN to test the workspace") cls.tasks = cls.moon_json("tasks", "--json") + cls.tasks.extend(cls.moon_json("task", target, "--json") for target in INTERNAL_FINGERPRINT_TARGETS) cls.by_target = {task["target"]: task for task in cls.tasks} @classmethod @@ -143,19 +162,25 @@ def test_only_force_all_tasks_are_allocation_only(self) -> None: self.assertFalse(task.get("outputs")) def test_precise_inputs_are_owned_without_hiding_new_paths(self) -> None: - def affected(path: str) -> set[str]: + def affected( + path: str, + *, + upstream: str = "none", + downstream: str = "deep", + ) -> set[str]: + arguments = [ + self.moon, + "query", + "tasks", + "--affected", + "stdin", + "--upstream", + upstream, + "--downstream", + downstream, + ] result = subprocess.run( # noqa: S603 - the binary is explicitly selected in setUpClass. - [ - self.moon, - "query", - "tasks", - "--affected", - "stdin", - "--upstream", - "none", - "--downstream", - "deep", - ], + arguments, cwd=REPO_ROOT, check=True, input=path, @@ -175,6 +200,109 @@ def affected(path: str) -> set[str]: quality = affected("ci/tools/tests/test_moon_tasks.py") self.assertIn("root:quality-moon-contracts", quality) self.assertNotIn("root:force-all-unowned", quality) + nightly = affected(".github/workflows/ci-nightly.yml") + self.assertIn("root:quality-moon-contracts", nightly) + self.assertNotIn("root:force-all-unowned", nightly) + for quality_input in ( + ".gitignore", + ".github/workflows/build-docs.yml", + ".github/workflows/build-wheel.yml", + ".github/workflows/ci-nightly.yml", + ".github/workflows/ci.yml", + ".github/workflows/test-sdist-linux.yml", + ".github/workflows/test-sdist-windows.yml", + ".github/workflows/test-wheel-linux.yml", + ".github/workflows/test-wheel-windows.yml", + "ci/tools/env-vars", + "ci/build-matrix.yml", + "ci/test-matrix.yml", + "ci/tools/merge_cuda_core_wheels.py", + "ci/tools/run-tests", + "cuda_bindings/tests/cython/build_tests.py", + "cuda_bindings/docs/build_docs.sh", + "cuda_core/tests/cython/build_tests.py", + "cuda_core/docs/build_docs.sh", + "cuda_pathfinder/docs/build_docs.sh", + "cuda_python/docs/assemble_moon_docs.sh", + "cuda_python/docs/build_component_docs.sh", + "cuda_python/docs/build_docs.sh", + "cuda_python/docs/environment-docs.yml", + "cuda_python_test_helpers/cuda_python_test_helpers/cython_test_builder.py", + ): + self.assertIn("root:quality-moon-contracts", affected(quality_input), quality_input) + + metapackage_only = affected("cuda_python/pyproject.toml") + self.assertTrue( + { + "metapackage:wheel-pure", + "metapackage:sdist", + "metapackage:test-installed-linux", + "metapackage:test-installed-windows", + "metapackage:docs-ci", + } + <= metapackage_only, + ) + self.assertFalse( + any("ci-build-native" in self.by_target[target].get("tags", []) for target in metapackage_only), + ) + + current_wheels = {"bindings:wheel-current", "core:wheel-current"} + pathfinder_direct = affected("cuda_pathfinder/cuda/__init__.py", downstream="none") + bindings_direct = affected("cuda_bindings/cuda/__init__.py", downstream="none") + self.assertTrue( + ( + current_wheels + | { + "bindings:sdist", + "core:sdist", + "core:wheel-previous", + "metapackage:sdist", + "metapackage:wheel-pure", + } + ) + <= pathfinder_direct + ) + self.assertTrue( + (current_wheels | {"core:sdist", "metapackage:sdist", "metapackage:wheel-pure"}) <= bindings_direct + ) + + for path, target in ( + ("cuda_pathfinder/docs/source/index.rst", "pathfinder:sdist"), + ("cuda_bindings/tests/test_basics.py", "bindings:sdist"), + ("cuda_core/README.md", "core:sdist"), + ("cuda_python/docs/source/index.rst", "metapackage:sdist"), + ): + self.assertIn(target, affected(path, downstream="none"), path) + + def test_native_build_matrix_is_direct_and_covers_test_python_versions(self) -> None: + matrix_text = (REPO_ROOT / "ci" / "build-matrix.yml").read_text(encoding="utf-8") + matrix = json.loads("\n".join(line for line in matrix_text.splitlines() if not line.lstrip().startswith("#"))) + self.assertEqual(set(matrix), {"include"}) + rows = matrix["include"] + self.assertIsInstance(rows, list) + self.assertTrue(rows) + + versions: set[str] = set() + formatted_versions: set[str] = set() + for row in rows: + self.assertEqual(set(row), {"python-version", "python-version-formatted"}) + version = row["python-version"] + formatted = row["python-version-formatted"] + self.assertIsInstance(version, str) + self.assertIsInstance(formatted, str) + self.assertRegex(version, r"^3\.(?:0|[1-9][0-9]*)t?$") + self.assertRegex(formatted, r"^3[0-9]+t?$") + self.assertEqual(formatted, version.replace(".", "")) + self.assertNotIn(version, versions) + self.assertNotIn(formatted, formatted_versions) + versions.add(version) + formatted_versions.add(formatted) + + self.assertIn("3.12", versions) + test_matrix = (REPO_ROOT / "ci" / "test-matrix.yml").read_text(encoding="utf-8") + test_versions = set(re.findall(r"\bPY_VER:\s*'([^']+)'", test_matrix)) + self.assertTrue(test_versions) + self.assertLessEqual(test_versions, versions) def test_bindings_benchmark_smoke_uses_materialized_wheels(self) -> None: task = self.by_target["bindings:smoke-linux"] @@ -185,12 +313,13 @@ def test_bindings_benchmark_smoke_uses_materialized_wheels(self) -> None: self.assertIn("benchmarks/cuda_bindings/run_pyperf.py", task["script"]) self.assertNotIn("moon_ci.py", str(task["inputs"])) - def test_execution_and_runner_tags_select_real_tasks(self) -> None: - for tag, expected in {**EXECUTION_TAG_TARGETS, **RUNNER_TAG_TARGETS}.items(): + def test_semantic_execution_tags_select_real_tasks(self) -> None: + for tag, expected in EXECUTION_TAG_TARGETS.items(): selected = {task["target"] for task in self.tasks if tag in task.get("tags", [])} self.assertEqual(selected, expected, tag) for target in selected: self.assertTrue(self.by_target[target]["options"]["runInCI"], target) + self.assertFalse({tag for task in self.tasks for tag in task.get("tags", []) if tag.startswith("runner-")}) def test_cached_producers_have_explicit_non_overlapping_outputs(self) -> None: cached = {task["target"] for task in self.tasks if task["options"]["cache"]} @@ -205,22 +334,92 @@ def test_cached_producers_have_explicit_non_overlapping_outputs(self) -> None: destinations.add((project, output)) for target in FINGERPRINTED_TARGETS: task = self.by_target[target] - self.assertTrue(task.get("checks"), target) - scripts = [check["script"] for check in task["checks"]] - self.assertTrue(any("git describe" in script for script in scripts), target) + self.assertFalse(task.get("checks"), target) + self.assertNotIn("cacheKey", task["options"], target) + closure: set[str] = set() + pending = [dependency["target"] for dependency in task.get("deps", [])] + while pending: + dependency = pending.pop() + if dependency in closure: + continue + closure.add(dependency) + pending.extend(dep["target"] for dep in self.by_target[dependency].get("deps", [])) + fingerprint_tasks = [self.by_target[dependency] for dependency in closure if "fingerprint" in dependency] + self.assertTrue(fingerprint_tasks, target) + scripts = [ + check["script"] + for fingerprint_task in fingerprint_tasks + for check in fingerprint_task.get("checks", []) + ] self.assertTrue(any("SETUPTOOLS_SCM_" in script for script in scripts), target) self.assertTrue(any("python_implementation" in script for script in scripts), target) + if target in SCM_FINGERPRINTED_TARGETS: + self.assertTrue(any("'git', 'describe'" in script for script in scripts), target) self.assertFalse(task.get("inputEnv"), target) self.assertNotIn("moon_fingerprint.py", json.dumps(task), target) self.assertNotIn("ACTIONS_RUNTIME", "\n".join(scripts), target) - for target in ("bindings:wheel-current", "core:wheel-current", "core:wheel-previous"): - scripts = "\n".join(check["script"] for check in self.by_target[target]["checks"]) + for target in NATIVE_FINGERPRINTED_TARGETS: + closure: set[str] = set() + pending = [dep["target"] for dep in self.by_target[target]["deps"]] + while pending: + dependency = pending.pop() + if dependency in closure: + continue + closure.add(dependency) + pending.extend(dep["target"] for dep in self.by_target[dependency].get("deps", [])) + scripts = "\n".join( + check["script"] for dependency in closure for check in self.by_target[dependency].get("checks", []) + ) self.assertIn("CUDA_PYTHON_COVERAGE", scripts, target) + self.assertIn("CUDA_HOME", scripts, target) + self.assertIn("CFLAGS", scripts, target) + self.assertIn("LDFLAGS", scripts, target) self.assertIn("name.startswith('CIBW_')", scripts, target) self.assertIn("ACTIONS_VALUE=", scripts, target) self.assertIn("hashlib.sha256", scripts, target) + fingerprint_tasks = [task for task in self.tasks if "fingerprint" in task["target"]] + self.assertEqual({task["target"] for task in fingerprint_tasks}, INTERNAL_FINGERPRINT_TARGETS) + for task in fingerprint_tasks: + self.assertEqual(task["command"], "noop", task["target"]) + self.assertFalse(task["options"]["cache"], task["target"]) + self.assertTrue(task["options"]["internal"], task["target"]) + self.assertTrue(task["options"]["runInCI"], task["target"]) + self.assertTrue(task.get("checks"), task["target"]) + + expected_patterns = { + "pathfinder": "cuda-pathfinder-v*[0-9]*", + "bindings": "v*[0-9]*", + "core": "cuda-core-v*[0-9]*", + "metapackage": "v*[0-9]*", + } + for project, pattern in expected_patterns.items(): + script = self.by_target[f"{project}:fingerprint-package"]["checks"][0]["script"] + self.assertIn(pattern, script) + self.assertNotIn("CUDA_PYTHON_SCM_TAG_PATTERN", script) + + def test_fingerprint_checks_execute_without_task_environment(self) -> None: + bash = shutil.which("bash") + self.assertIsNotNone(bash) + assert bash is not None + scripts = { + check["script"] + for target in INTERNAL_FINGERPRINT_TARGETS + for check in self.by_target[target].get("checks", []) + } + for script in scripts: + result = subprocess.run( # noqa: S603 - Bash executes checked-in Moon configuration. + [bash, "-c", script], + cwd=REPO_ROOT, + check=False, + env=os.environ, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + self.assertEqual(result.returncode, 0, f"{script}\n{result.stdout}") + def test_artifact_commands_are_encoded_in_moon(self) -> None: artifact_commands = { "pathfinder:wheel-pure": "python -m pip wheel", @@ -239,14 +438,32 @@ def test_artifact_commands_are_encoded_in_moon(self) -> None: self.assertFalse(task["options"]["shell"], target) self.assertEqual(task["args"][:3], ["-euo", "pipefail", "-c"]) self.assertIn(expected_command, task["args"][3]) - self.assertIn(".moon-out/", task["args"][3]) + self.assertIn(".moon-out", task["args"][3]) self.assertIn("[[ $# -eq 1 ]]", task["args"][3]) + self.assertIn({"file": "/ci/build-constraints.txt"}, task["inputs"]) + self.assertIn("ci/build-constraints.txt", task["args"][3]) + self.assertIn("PIP_BUILD_CONSTRAINT", task["args"][3]) + self.assertIn("PIP_CONSTRAINT", task["args"][3]) + self.assertIn("SOURCE_DATE_EPOCH=$(printenv SOURCE_DATE_EPOCH || true)", task["args"][3]) + self.assertIn("git log -1 --format=%ct HEAD", task["args"][3]) + if target.endswith(":sdist"): + self.assertIn("@group(sdist)", task["inputs"]) + self.assertNotIn("@group(package)", task["inputs"]) + + for target in ("bindings:wheel-current", "core:wheel-current", "core:wheel-previous"): + self.assertIn("SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH", self.by_target[target]["args"][3]) metapackage = self.by_target["metapackage:wheel-pure"] self.assertIn({"glob": "/cuda_bindings/.moon-out/wheel-current/*.whl", "cache": True}, metapackage["inputs"]) self.assertIn("CUDA_PYTHON_USE_STAGED_BINDINGS_VERSION", metapackage["args"][3]) self.assertIn("SETUPTOOLS_SCM_PRETEND_VERSION_FOR_CUDA_PYTHON", metapackage["args"][3]) + current = self.by_target["core:wheel-current"] + previous = self.by_target["core:wheel-previous"] + self.assertEqual(current["args"], previous["args"]) + self.assertEqual(current["env"]["CUDA_PYTHON_WHEEL_VARIANT"], "current") + self.assertEqual(previous["env"]["CUDA_PYTHON_WHEEL_VARIANT"], "previous") + def test_metapackage_uses_staged_bindings_version_only_when_requested(self) -> None: bash = shutil.which("bash") self.assertIsNotNone(bash) @@ -255,6 +472,9 @@ def test_metapackage_uses_staged_bindings_version_only_when_requested(self) -> N with tempfile.TemporaryDirectory() as temporary_directory: workspace = Path(temporary_directory) + constraints = workspace / "ci" / "build-constraints.txt" + constraints.parent.mkdir() + constraints.write_text("setuptools==83.0.0\n", encoding="utf-8") bindings = workspace / "cuda_bindings" / ".moon-out" / "wheel-current" bindings.mkdir(parents=True) (bindings / "stale.whl").touch() @@ -282,6 +502,7 @@ def run(mode: str | None) -> subprocess.CompletedProcess[str]: **os.environ, "COMMAND_LOG": str(command_log), "PATH": f"{fake_python.parent}{os.pathsep}{os.environ['PATH']}", + "SOURCE_DATE_EPOCH": "1234567890", } environment.pop("CUDA_PYTHON_USE_STAGED_BINDINGS_VERSION", None) if mode is not None: @@ -314,6 +535,149 @@ def test_explicit_commands_do_not_use_moons_extra_shell_wrapper(self) -> None: if task["command"] not in {"noop", "set"}: self.assertFalse(task["options"]["shell"], task["target"]) + def test_cython_asset_builds_isolate_generated_sources_in_moon_output(self) -> None: + def exercise_builder(project: str) -> None: + with tempfile.TemporaryDirectory() as temporary_directory: + root = Path(temporary_directory) + script_dir = root / project / "tests" / "cython" + script_dir.mkdir(parents=True) + script = script_dir / "build_tests.py" + shutil.copy2(REPO_ROOT / project / "tests" / "cython" / "build_tests.py", script) + (script_dir / "test_probe.pyx").write_text("# cython probe\n", encoding="utf-8") + + bindings_init = root / "bindings-source" / "cuda" / "bindings" / "__init__.py" + bindings_init.parent.mkdir(parents=True) + bindings_init.touch() + cuda_root = root / "cuda-toolkit" + (cuda_root / "include").mkdir(parents=True) + if project == "cuda_core": + (root / project / "cuda" / "core" / "_include").mkdir(parents=True) + + cython_calls: list[dict[str, Any]] = [] + setup_calls: list[list[str]] = [] + + def fake_cythonize(sources: list[str], **options: Any) -> list[str]: + cython_calls.append(dict(options)) + source = Path(sources[0]) + generated_dir = Path(options["build_dir"]) if "build_dir" in options else source.parent + if not source.is_absolute() and "build_dir" not in options: + generated_dir = Path.cwd() + generated_dir.mkdir(parents=True, exist_ok=True) + (generated_dir / "test_probe.cpp").write_text("// generated\n", encoding="utf-8") + return ["fake-extension"] + + def fake_setup(**_: Any) -> None: + setup_calls.append(sys.argv.copy()) + if "--build-temp" in sys.argv: + build_temp = Path(sys.argv[sys.argv.index("--build-temp") + 1]) + build_temp.mkdir(parents=True) + if "--build-lib" in sys.argv: + build_lib = Path(sys.argv[sys.argv.index("--build-lib") + 1]) + build_lib.mkdir(parents=True, exist_ok=True) + (build_lib / "test_probe.fake.so").touch() + + cython_package = types.ModuleType("Cython") + cython_build = types.ModuleType("Cython.Build") + setuptools = types.ModuleType("setuptools") + cuda = types.ModuleType("cuda") + cuda_bindings = types.ModuleType("cuda.bindings") + cython_package.__dict__["__path__"] = [] + cython_build.__dict__["cythonize"] = fake_cythonize + setuptools.__dict__["setup"] = fake_setup + cuda.__dict__["__path__"] = [] + cuda.__dict__["bindings"] = cuda_bindings + cuda_bindings.__dict__["__file__"] = str(bindings_init) + fake_modules = { + "Cython": cython_package, + "Cython.Build": cython_build, + "setuptools": setuptools, + "cuda": cuda, + "cuda.bindings": cuda_bindings, + } + + def run_builder(*arguments: str) -> None: + original_argv = sys.argv + original_cwd = Path.cwd() + helper_source = str(REPO_ROOT / "cuda_python_test_helpers") + try: + sys.argv = [str(script), *arguments] + sys.path.insert(0, helper_source) + with ( + mock.patch.dict(os.environ, {"CUDA_HOME": str(cuda_root)}), + mock.patch.dict(sys.modules, fake_modules), + ): + runpy.run_path(str(script), run_name="__main__") + finally: + sys.argv = original_argv + sys.path.remove(helper_source) + os.chdir(original_cwd) + + output = root / project / ".moon-out" / "cython-tests" + run_builder("--output-dir", str(output)) + self.assertEqual(cython_calls[0]["build_dir"], str(output / ".cython-build")) + self.assertFalse((output / ".cython-build").exists()) + self.assertFalse((output / ".build-temp").exists()) + self.assertFalse((script_dir / "test_probe.cpp").exists()) + self.assertIn("--build-lib", setup_calls[0]) + + run_builder() + self.assertNotIn("build_dir", cython_calls[1]) + self.assertIn("--inplace", setup_calls[1]) + self.assertTrue((script_dir / "test_probe.cpp").is_file()) + + for project in ("cuda_bindings", "cuda_core"): + with self.subTest(project=project): + exercise_builder(project) + + def test_shared_cython_builder_confines_outputs_and_preserves_include_flags(self) -> None: + helper_source = str(REPO_ROOT / "cuda_python_test_helpers") + sys.path.insert(0, helper_source) + try: + from cuda_python_test_helpers.cython_test_builder import ( + _output_directory, + _set_compiler_include_paths, + ) + finally: + sys.path.remove(helper_source) + + with tempfile.TemporaryDirectory() as temporary_directory: + root = Path(temporary_directory) + script_dir = root / "cuda_bindings" / "tests" / "cython" + script_dir.mkdir(parents=True) + output = root / "cuda_bindings" / ".moon-out" / "cython-tests" + output.mkdir(parents=True) + (output / "stale.so").touch() + + self.assertEqual( + _output_directory(script_dir, "cuda_bindings/.moon-out/cython-tests"), + output, + ) + self.assertEqual(list(output.iterdir()), []) + with self.assertRaisesRegex(ValueError, "output must be below"): + _output_directory(script_dir, str(script_dir)) + + posix_environment = {"CPLUS_INCLUDE_PATH": "/existing/include"} + _set_compiler_include_paths( + [Path("/core/include"), Path("/cuda/include")], + environ=posix_environment, + platform_name="posix", + ) + self.assertEqual( + posix_environment, + {"CPLUS_INCLUDE_PATH": "/core/include:/cuda/include:/existing/include"}, + ) + + windows_environment = {"CL": "/D EXISTING"} + _set_compiler_include_paths( + [Path("C:/core include"), Path("C:/CUDA/include")], + environ=windows_environment, + platform_name="nt", + ) + self.assertEqual( + windows_environment, + {"CL": '/I"C:/core include" /I"C:/CUDA/include" /D EXISTING'}, + ) + def test_embedded_bash_preserves_runtime_variables_for_moon(self) -> None: scripts: dict[str, str] = {} for target, task in self.by_target.items(): @@ -357,9 +721,10 @@ def test_embedded_bash_preserves_runtime_variables_for_moon(self) -> None: "metapackage:test-installed-windows": "metapackage", }.items(): task = self.by_target[target] - if task["command"] != "noop": - self.assertEqual(task["command"], "bash") - self.assertEqual(task["args"], ["ci/tools/run-tests", project]) + if target.endswith(("windows", "windows-strict")) and task["command"] == "noop": + continue + self.assertEqual(task["command"], "bash") + self.assertEqual(task["args"], ["ci/tools/run-tests", project]) preparation = self.by_target["test-helpers:prepare-test-assets"] self.assertEqual(preparation["command"], "bash") @@ -367,21 +732,31 @@ def test_embedded_bash_preserves_runtime_variables_for_moon(self) -> None: self.assertIn("python -m pip install", preparation["args"][3]) self.assertIn("--clean-output", self.by_target["core:wheel-merge"]["args"]) self.assertIn("--output-dir", self.by_target["core:test-binaries"]["args"]) - for target in ("bindings:cython-test-assets", "core:cython-test-assets"): - self.assertEqual(self.by_target[target]["command"], "bash") - self.assertIn("--output-dir", self.by_target[target]["args"]) + for target, driver in ( + ("bindings:cython-test-assets", "cuda_bindings/tests/cython/build_tests.py"), + ("core:cython-test-assets", "cuda_core/tests/cython/build_tests.py"), + ): + task = self.by_target[target] + self.assertEqual(task["command"], "python") + self.assertEqual(task["args"][0], driver) + self.assertIn("--output-dir", task["args"]) + self.assertEqual(task["env"]["PYTHONPATH"], "cuda_python_test_helpers") + self.assertIn( + {"file": "/cuda_python_test_helpers/cuda_python_test_helpers/cython_test_builder.py"}, + task["inputs"], + ) def test_same_environment_build_dependencies_use_output_bytes(self) -> None: expected = { "bindings:wheel-current": {"pathfinder:wheel-pure"}, - "core:wheel-current": {"bindings:wheel-current"}, + "core:wheel-current": {"pathfinder:wheel-pure", "bindings:wheel-current"}, "core:wheel-previous": {"pathfinder:wheel-pure"}, + "core:wheel-merge": {"core:wheel-previous"}, "bindings:sdist": {"pathfinder:sdist"}, "core:sdist": {"pathfinder:sdist", "bindings:sdist"}, - "metapackage:sdist": {"bindings:sdist"}, + "metapackage:sdist": set(), "metapackage:test-installed-linux": {"metapackage:wheel-pure"}, "metapackage:test-installed-windows": {"metapackage:wheel-pure"}, - "metapackage:docs-ci": {"metapackage:wheel-pure"}, "root:docs-ci": { "pathfinder:docs-ci", "bindings:docs-ci", @@ -390,16 +765,114 @@ def test_same_environment_build_dependencies_use_output_bytes(self) -> None: }, } for target, dependencies in expected.items(): - configured = {dep["target"] for dep in self.by_target[target]["deps"]} + configured = {dep["target"] for dep in self.by_target[target]["deps"] if dep["cacheStrategy"] == "outputs"} self.assertEqual(configured, dependencies, target) - self.assertTrue(all(dep["cacheStrategy"] == "outputs" for dep in self.by_target[target]["deps"]), target) def test_native_asset_preparation_is_shared(self) -> None: prep = self.by_target["test-helpers:prepare-test-assets"] + fingerprint = self.by_target["test-helpers:fingerprint-test-assets"] + native_context = self.by_target["test-helpers:fingerprint-native-context"] self.assertFalse(prep["options"]["cache"]) + self.assertIn("pip install --force-reinstall --no-deps", prep["args"][3]) + self.assertEqual( + {dep["target"] for dep in fingerprint["deps"]}, + {prep["target"], native_context["target"]}, + ) + self.assertEqual( + {dep["target"] for dep in native_context["deps"]}, + {"test-helpers:fingerprint-python-context"}, + ) for target in ("bindings:cython-test-assets", "core:cython-test-assets"): deps = {dep["target"] for dep in self.by_target[target]["deps"]} - self.assertEqual(deps, {"test-helpers:prepare-test-assets"}) + self.assertEqual(deps, {fingerprint["target"]}) + + def test_cibuildwheel_host_is_restored_after_target_assets(self) -> None: + workflow = (REPO_ROOT / ".github" / "workflows" / "build-wheel.yml").read_text(encoding="utf-8") + target_setup = workflow.index("id: setup-python2") + target_install = workflow.index("- name: Install target-Python build tools", target_setup) + target_assets = workflow.index("- name: Build target-Python test assets with Moon", target_install) + host_restore = workflow.index("- name: Restore cibuildwheel host Python", target_assets) + previous_build = workflow.index("- name: Build previous-CTK outputs and merge wheels with Moon", host_restore) + + self.assertIn("python-version: ${{ matrix.python-version }}", workflow[target_setup:target_install]) + self.assertNotIn("cibuildwheel", workflow[target_install:target_assets]) + self.assertIn('python-version: "3.12"', workflow[host_restore:previous_build]) + + def test_workflows_stage_moon_phases_and_pin_standard_nightly_source(self) -> None: + build = (REPO_ROOT / ".github" / "workflows" / "build-wheel.yml").read_text(encoding="utf-8") + self.assertIn('MOON_CACHE: "off"', build) + current_calls = ( + "moon ci pathfinder:wheel-pure", + "moon ci bindings:wheel-current", + 'moon ci "${targets[@]}"', + ) + current_positions = tuple(build.index(call) for call in current_calls) + self.assertEqual(current_positions, tuple(sorted(current_positions))) + previous = build.index("moon ci core:wheel-previous core:test-binaries") + merge = build.index("moon ci core:wheel-merge", previous) + self.assertLess(previous, merge) + self.assertIn("cuda_bindings/.moon-out/wheel-previous", build) + for obsolete_upload in ( + "Upload cuda.bindings Cython tests", + "Upload cuda.core Cython tests", + "Upload cuda.core test binaries", + ): + self.assertNotIn(obsolete_upload, build) + + for workflow_name in ("test-sdist-linux.yml", "test-sdist-windows.yml"): + workflow = (REPO_ROOT / ".github" / "workflows" / workflow_name).read_text(encoding="utf-8") + self.assertIn('MOON_CACHE: "off"', workflow) + positions = tuple( + workflow.index(call) + for call in ( + "moon ci pathfinder:sdist", + "moon ci bindings:sdist", + "moon ci core:sdist metapackage:sdist", + ) + ) + self.assertEqual(positions, tuple(sorted(positions)), workflow_name) + + for workflow_name in ("test-wheel-linux.yml", "test-wheel-windows.yml"): + workflow = (REPO_ROOT / ".github" / "workflows" / workflow_name).read_text(encoding="utf-8") + self.assertIn("source-ref:", workflow) + self.assertIn("ref: ${{ inputs.source-ref || github.sha }}", workflow) + self.assertIn("MOON_HEAD: ${{ inputs.source-ref || github.sha }}", workflow) + self.assertNotIn("lookup-run-id", workflow) + + nightly = (REPO_ROOT / ".github" / "workflows" / "ci-nightly.yml").read_text(encoding="utf-8") + self.assertIn("HEAD_SHA: ${{ steps.find.outputs.head_sha }}", nightly) + self.assertEqual(nightly.count("source-ref:"), 1) + standard = nightly[nightly.index("test-standard-linux-aarch64:") :] + self.assertIn("source-ref: ${{ needs.find-wheels.outputs.HEAD_SHA }}", standard) + + def test_cross_phase_assets_keep_only_required_context_and_source_proxies(self) -> None: + current = self.by_target["core:wheel-current"] + self.assertNotIn({"file": "/ci/tools/merge_cuda_core_wheels.py"}, current["inputs"]) + + previous = self.by_target["core:wheel-previous"] + for project in ("pathfinder", "bindings"): + self.assertIn({"project": project, "group": "package"}, previous["inputs"]) + + for target in ("metapackage:wheel-pure", "metapackage:sdist"): + for project in ("pathfinder", "bindings"): + self.assertIn({"project": project, "group": "package"}, self.by_target[target]["inputs"]) + + merge = self.by_target["core:wheel-merge"] + self.assertEqual( + {dependency["target"] for dependency in merge["deps"]}, + {"test-helpers:fingerprint-python-build", "core:wheel-previous"}, + ) + for project in ("pathfinder", "bindings"): + self.assertIn({"project": project, "group": "package"}, merge["inputs"]) + self.assertIn("@group(package)", merge["inputs"]) + + binaries = self.by_target["core:test-binaries"] + self.assertEqual( + {dependency["target"] for dependency in binaries["deps"]}, + {"test-helpers:fingerprint-native-context"}, + ) + self.assertFalse(any(isinstance(value, dict) and "project" in value for value in binaries["inputs"])) + self.assertNotIn("@group(package)", binaries["inputs"]) def test_platform_test_tasks_are_serialized_and_os_scoped(self) -> None: for tag, operating_system in (("ci-test-linux", "linux"), ("ci-test-windows", "windows")): @@ -441,26 +914,79 @@ def test_docs_components_run_in_parallel_before_assembly(self) -> None: docs = self.by_target["root:docs-ci"] self.assertFalse(docs["options"]["cache"]) self.assertTrue(docs["options"]["runDepsInParallel"]) - self.assertEqual(docs["command"], "bash") - self.assertEqual(docs["args"], ["cuda_python/docs/assemble_moon_docs.sh"]) - root_inputs = docs["inputs"] - self.assertIn({"project": "core", "group": "package"}, root_inputs) - self.assertIn({"project": "metapackage", "group": "docs"}, root_inputs) - self.assertIn({"file": "/.github/workflows/build-wheel.yml"}, root_inputs) - for target in EXECUTION_TAG_TARGETS["ci-docs"] - {"root:docs-ci"}: + self.assertEqual(docs["outputs"], [{"file": ".moon-out/docs"}]) + + preparation = self.by_target["test-helpers:prepare-docs"] + self.assertFalse(preparation["options"]["cache"]) + self.assertFalse(preparation.get("deps")) + preparation_script = preparation["args"][3] + self.assertIn("python -m pip install --force-reinstall", preparation_script) + metapackage_install = preparation_script.split("python -m pip install --force-reinstall --no-deps", 1)[1] + self.assertIn('"$METAPACKAGE_WHEEL"', metapackage_install) + for component_wheel in ("PATHFINDER_WHEEL", "BINDINGS_WHEEL", "CORE_WHEEL"): + self.assertNotIn(f'"${component_wheel}"', metapackage_install) + for wheel_input in ( + "/cuda_pathfinder/.moon-out/wheel-pure/*.whl", + "/cuda_bindings/.moon-out/wheel-current/*.whl", + "/cuda_core/.moon-out/wheel-merged/*.whl", + "/cuda_python/.moon-out/wheel-pure/*.whl", + ): + self.assertIn({"glob": wheel_input, "cache": True}, preparation["inputs"]) + + component_targets = EXECUTION_TAG_TARGETS["ci-docs"] - {"root:docs-ci"} + for target in component_targets: task = self.by_target[target] self.assertFalse(task["options"]["cache"]) - self.assertEqual(task["command"], "bash") - self.assertEqual(task["args"][-1], "moon-ci") + self.assertEqual(task["outputs"], [{"file": "docs/build/html"}]) + self.assertEqual({dependency["target"] for dependency in task["deps"]}, {preparation["target"]}) self.assertIn({"file": "/cuda_python/docs/environment-docs.yml"}, task["inputs"]) + self.assertIn({"file": "/cuda_python/docs/build_component_docs.sh"}, task["inputs"]) - metapackage_inputs = self.by_target["metapackage:docs-ci"]["inputs"] - for project in ( - "pathfinder", - "bindings", - "core", + graph = self.moon_json("action-graph", "root:docs-ci", "--json") + targets_by_node = { + int(node): action["params"]["target"] + for node, action in graph["data"].items() + if action["action"] == "run-task" + } + self.assertEqual( + set(targets_by_node.values()), + {"root:docs-ci", preparation["target"], *component_targets}, + ) + edges = { + (targets_by_node[parent], targets_by_node[dependency]) for parent, dependency, _ in graph["graph"]["edges"] + } + self.assertEqual( + edges, + { + *(("root:docs-ci", target) for target in component_targets), + *((target, preparation["target"]) for target in component_targets), + }, + ) + + workflow = (REPO_ROOT / ".github" / "workflows" / "build-docs.yml").read_text(encoding="utf-8") + assembler = (REPO_ROOT / "cuda_python" / "docs" / "assemble_moon_docs.sh").read_text(encoding="utf-8") + component_builder = (REPO_ROOT / "cuda_python" / "docs" / "build_component_docs.sh").read_text(encoding="utf-8") + self.assertIn("moon run metapackage:wheel-pure", workflow) + self.assertIn("moon ci root:docs-ci", workflow) + self.assertIn('moon ci "${MOON_PROJECT}:docs-ci"', workflow) + self.assertIn('cp -aL "${COMPONENT}/docs/build/html/."', workflow) + self.assertNotIn(".moon-out/docs-ci", workflow) + for project in ("cuda_pathfinder", "cuda_bindings", "cuda_core", "cuda_python"): + self.assertIn(f"/{project}/docs/build/html", assembler) + self.assertNotIn(".moon-out/docs-ci", assembler) + self.assertNotIn("pip install cuda_pathfinder/.moon-out", workflow) + for project, component in ( + ("cuda_pathfinder", "cuda-pathfinder"), + ("cuda_bindings", "cuda-bindings"), + ("cuda_core", "cuda-core"), + ("cuda_python", "cuda-python"), ): - self.assertIn({"project": project, "group": "package"}, metapackage_inputs) + wrapper = (REPO_ROOT / project / "docs" / "build_docs.sh").read_text(encoding="utf-8") + self.assertIn("build_component_docs.sh", wrapper) + self.assertIn(component, wrapper) + self.assertIn(f"{component})", component_builder) + docs_environment = (REPO_ROOT / "cuda_python" / "docs" / "environment-docs.yml").read_text(encoding="utf-8") + self.assertIn("- python =3.12", docs_environment) def test_quality_tasks_use_external_refs_and_one_selector(self) -> None: release = self.by_target["core:quality-api-release"] @@ -509,17 +1035,12 @@ def test_moon_task_helpers_are_removed(self) -> None: for filename in removed: self.assertNotIn(filename, serialized, task["target"]) - def test_universal_wheels_share_existing_runner_lanes(self) -> None: + def test_universal_wheels_share_native_build_lanes(self) -> None: self.assertFalse((REPO_ROOT / ".github" / "workflows" / "build-pure-wheel.yml").exists()) self.assertFalse({task["target"] for task in self.tasks if "ci-wheel-pure" in task.get("tags", [])}) - self.assertFalse({task["target"] for task in self.tasks if "runner-build-portable" in task.get("tags", [])}) pathfinder = self.by_target["pathfinder:wheel-pure"] - self.assertTrue( - {"runner-build-linux-64", "runner-build-linux-aarch64", "runner-build-windows"} <= set(pathfinder["tags"]) - ) - self.assertFalse( - {tag for tag in self.by_target["metapackage:wheel-pure"].get("tags", []) if tag.startswith("runner-")} - ) + self.assertIn("ci-build-native", pathfinder["tags"]) + self.assertNotIn("ci-build-native", self.by_target["metapackage:wheel-pure"].get("tags", [])) self.assertNotIn( "build-pure-wheel.yml", "\n".join(path.read_text(encoding="utf-8") for path in REPO_ROOT.rglob("moon.yml")), @@ -552,6 +1073,17 @@ def test_generated_cache_and_output_roots_are_ignored(self) -> None: self.assertIn(".moon/cache/", ignore) self.assertIn(".moon-out/", ignore) + def test_cross_run_artifacts_transport_canonical_outputs_only(self) -> None: + for relative_path in ( + ".github/workflows/build-wheel.yml", + ".github/workflows/test-sdist-linux.yml", + ".github/workflows/test-sdist-windows.yml", + ): + workflow = (REPO_ROOT / relative_path).read_text(encoding="utf-8") + self.assertIn(".moon-out/", workflow, relative_path) + self.assertNotIn(".moon/cache/hashes", workflow, relative_path) + self.assertNotIn(".moon/cache/outputs", workflow, relative_path) + if __name__ == "__main__": unittest.main() diff --git a/cuda_bindings/AGENTS.md b/cuda_bindings/AGENTS.md index 8c544f8872a..cde0096a74a 100644 --- a/cuda_bindings/AGENTS.md +++ b/cuda_bindings/AGENTS.md @@ -37,7 +37,7 @@ subpackage in the `cuda-python` monorepo. - **Primary tests**: `pytest tests/` - **Cython tests**: - - build: `tests/cython/build_tests.sh` (or platform equivalent) + - build: `pixi run build-cython-tests` - run: `pytest tests/cython/` - **Examples**: example coverage is pytest-based under `examples/`. - **Benchmarks**: run with `pytest --benchmark-only benchmarks/` when needed. diff --git a/cuda_bindings/docs/build_docs.sh b/cuda_bindings/docs/build_docs.sh index 72530155929..0873ee548b2 100755 --- a/cuda_bindings/docs/build_docs.sh +++ b/cuda_bindings/docs/build_docs.sh @@ -1,105 +1,9 @@ -#!/bin/bash +#!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -set -ex +set -euo pipefail SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) -cd "${SCRIPT_DIR}" - -MOON_CI="0" -if [[ "$#" == "0" ]]; then - LATEST_ONLY="0" -elif [[ "$#" == "1" && "$1" == "latest-only" ]]; then - LATEST_ONLY="1" -elif [[ "$#" == "1" && "$1" == "moon-ci" ]]; then - MOON_CI="1" - DOCS_LATEST_ONLY="${CUDA_PYTHON_DOCS_LATEST_ONLY:-true}" - case "${DOCS_LATEST_ONLY,,}" in - 1|true) LATEST_ONLY="1" ;; - 0|false) LATEST_ONLY="0" ;; - *) - echo "CUDA_PYTHON_DOCS_LATEST_ONLY must be true, false, 1, or 0" >&2 - exit 1 - ;; - esac -else - echo "usage: ./build_docs.sh [latest-only|moon-ci]" - exit 1 -fi - -if [[ "${MOON_CI}" == "1" ]]; then - if [[ -L build || ( -e build && ! -d build ) ]]; then - echo "refusing to replace non-directory docs build output: ${SCRIPT_DIR}/build" >&2 - exit 1 - fi - rm -rf build -fi - -# SPHINX_CUDA_BINDINGS_VER is used to create a subdir under build/html -# (the Makefile file for sphinx-build also honors it if defined). -# If there's a post release (ex: .post1) we don't want it to show up in the -# version selector or directory structure. -if [[ -z "${SPHINX_CUDA_BINDINGS_VER}" ]]; then - export SPHINX_CUDA_BINDINGS_VER=$(python -c "from importlib.metadata import version; \ - ver = '.'.join(str(version('cuda-bindings')).split('.')[:3]); \ - print(ver)" \ - | awk -F'+' '{print $1}') -fi - -if [[ "${LATEST_ONLY}" == "1" && -z "${BUILD_PREVIEW:-}" && -z "${BUILD_LATEST:-}" ]]; then - export BUILD_LATEST=1 -fi - -# build the docs (in parallel) -if [[ -z "${SPHINXOPTS:-}" ]]; then - HTML_SPHINXOPTS="-j 4 -d build/.doctrees" -else - HTML_SPHINXOPTS="${SPHINXOPTS}" -fi -SPHINXOPTS="${HTML_SPHINXOPTS}" make html - -# for debugging/developing (conf.py), please comment out the above line and -# use the line below instead, as we must build in serial to avoid getting -# obsecure Sphinx errors -#SPHINXOPTS="-v" make html - -# to support version dropdown menu -cp ./versions.json build/html -cp ./nv-versions.json build/html - -# to have a redirection page (to the latest docs) -cp source/_templates/main.html build/html/index.html - -# ensure that the latest docs is the one we built -if [[ $LATEST_ONLY == "0" ]]; then - cp -r build/html/${SPHINX_CUDA_BINDINGS_VER} build/html/latest -else - mv build/html/${SPHINX_CUDA_BINDINGS_VER} build/html/latest -fi - -# ensure that the Sphinx reference uses the latest docs -cp build/html/latest/objects.inv build/html - -if [[ "${MOON_CI}" == "1" ]]; then - SOURCE="${SCRIPT_DIR}/build/html" - OUTPUT_ROOT="${SCRIPT_DIR}/../.moon-out" - OUTPUT="${OUTPUT_ROOT}/docs-ci" - if [[ -L "${SOURCE}" || ! -d "${SOURCE}" ]]; then - echo "documentation output not found: ${SOURCE}" >&2 - exit 1 - fi - if [[ -L "${OUTPUT_ROOT}" || ( -e "${OUTPUT_ROOT}" && ! -d "${OUTPUT_ROOT}" ) ]]; then - echo "refusing to use non-directory Moon output root: ${OUTPUT_ROOT}" >&2 - exit 1 - fi - if [[ -L "${OUTPUT}" || ( -e "${OUTPUT}" && ! -d "${OUTPUT}" ) ]]; then - echo "refusing to replace non-directory Moon docs output: ${OUTPUT}" >&2 - exit 1 - fi - mkdir -p "${OUTPUT_ROOT}" - rm -rf "${OUTPUT}" - mkdir -p "${OUTPUT}" - cp -aL "${SOURCE}/." "${OUTPUT}/" -fi +exec "${SCRIPT_DIR}/../../cuda_python/docs/build_component_docs.sh" cuda-bindings "$@" diff --git a/cuda_bindings/moon.yml b/cuda_bindings/moon.yml index ed2c51bfe5a..b44ea921a7e 100644 --- a/cuda_bindings/moon.yml +++ b/cuda_bindings/moon.yml @@ -6,20 +6,12 @@ $schema: https://moonrepo.dev/schemas/v2/project.json language: unknown layer: library +tags: [docs-package, installed-test-package, native-package, pixi-package, python-package] dependsOn: - id: pathfinder scope: production - id: test-helpers scope: development -toolchains: - default: system - -taskOptions: - cache: false - runFromWorkspaceRoot: true - runInCI: false - shell: false - fileGroups: package: - '.git_archival.txt' @@ -30,15 +22,6 @@ fileGroups: - 'MANIFEST.in' - 'pyproject.toml' - 'setup.py' - tests: - - 'examples/**/*' - - 'tests/**/*' - - 'pixi.toml' - - 'pixi.lock' - docs: - - 'docs/**/*' - - 'pixi.toml' - - 'pixi.lock' benchmarks: - '/benchmarks/cuda_bindings/benchmarks/**/*' - '/benchmarks/cuda_bindings/runner/**/*' @@ -83,6 +66,9 @@ tasks: CONSTRAINTS=cuda_bindings/.moon-out/constraints-current reset_directory "$OUTPUT" reset_directory "$CONSTRAINTS" + SOURCE_DATE_EPOCH=$(printenv SOURCE_DATE_EPOCH || true) + export SOURCE_DATE_EPOCH + [[ -n "$SOURCE_DATE_EPOCH" ]] || SOURCE_DATE_EPOCH=$(git log -1 --format=%ct HEAD) shopt -s nullglob set -- cuda_pathfinder/.moon-out/wheel-pure/*.whl @@ -93,6 +79,7 @@ tasks: PATHFINDER_WHEEL=$1 CONSTRAINT_FILE="$CONSTRAINTS/build.txt" + cp ci/build-constraints.txt "$CONSTRAINT_FILE" HOST_PLATFORM_RESOLVED=$(printenv HOST_PLATFORM || true) if [[ -z "$HOST_PLATFORM_RESOLVED" ]]; then HOST_PLATFORM_RESOLVED=$(python -c "import platform; print('win-64' if platform.system() == 'Windows' else 'linux-' + platform.machine())") @@ -100,16 +87,16 @@ tasks: if [[ "$HOST_PLATFORM_RESOLVED" == win* ]]; then PATHFINDER_URI=$(python -c "from pathlib import Path; import sys; print(Path(sys.argv[1]).resolve().as_uri())" "$PATHFINDER_WHEEL") CONSTRAINT_HOST=$(cygpath -w "$(pwd)/$CONSTRAINT_FILE") - printf 'cuda-pathfinder @ %s\n' "$PATHFINDER_URI" > "$CONSTRAINT_FILE" + printf 'cuda-pathfinder @ %s\n' "$PATHFINDER_URI" >> "$CONSTRAINT_FILE" CIBW_ENVIRONMENT_BASE=$(printenv CIBW_ENVIRONMENT_WINDOWS || true) - export CIBW_ENVIRONMENT_WINDOWS="$CIBW_ENVIRONMENT_BASE PIP_BUILD_CONSTRAINT=\"$CONSTRAINT_HOST\" PIP_CONSTRAINT=\"$CONSTRAINT_HOST\"" + export CIBW_ENVIRONMENT_WINDOWS="$CIBW_ENVIRONMENT_BASE PIP_BUILD_CONSTRAINT=\"$CONSTRAINT_HOST\" PIP_CONSTRAINT=\"$CONSTRAINT_HOST\" SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH" else PATHFINDER_URI=$(python -c "from pathlib import Path; import sys; print(Path('/host' + Path(sys.argv[1]).resolve().as_posix()).as_uri())" "$PATHFINDER_WHEEL") CONSTRAINT_HOST=$(realpath "$CONSTRAINT_FILE") CONSTRAINT_CONTAINER="/host$CONSTRAINT_HOST" - printf 'cuda-pathfinder @ %s\n' "$PATHFINDER_URI" > "$CONSTRAINT_FILE" + printf 'cuda-pathfinder @ %s\n' "$PATHFINDER_URI" >> "$CONSTRAINT_FILE" CIBW_ENVIRONMENT_BASE=$(printenv CIBW_ENVIRONMENT_LINUX || true) - export CIBW_ENVIRONMENT_LINUX="$CIBW_ENVIRONMENT_BASE PIP_BUILD_CONSTRAINT=\"$CONSTRAINT_CONTAINER\" PIP_CONSTRAINT=\"$CONSTRAINT_CONTAINER\"" + export CIBW_ENVIRONMENT_LINUX="$CIBW_ENVIRONMENT_BASE PIP_BUILD_CONSTRAINT=\"$CONSTRAINT_CONTAINER\" PIP_CONSTRAINT=\"$CONSTRAINT_CONTAINER\" SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH" fi export PIP_BUILD_CONSTRAINT="$CONSTRAINT_HOST" export PIP_CONSTRAINT="$CONSTRAINT_HOST" @@ -136,77 +123,10 @@ tasks: CUDA_PATH: '${CUDA_PATH}' HOST_PLATFORM: '${HOST_PLATFORM}' PY_VER: '${PY_VER}' + # Moon 2.5.1 filters each explicitly selected phase before expanding task + # relations, so keep the upstream source proxy for granular CI calls. inputs: - - '@group(package)' - {project: pathfinder, group: package} - - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' - - '/ci/tools/env-vars' - - '/ci/versions.yml' - - '/.github/workflows/build-wheel.yml' - outputs: - - '.moon-out/wheel-current' - checks: - - &bindings_scm_fingerprint - check: fingerprint - script: git describe --always --dirty --tags --long --match 'v*[0-9]*' - hash: stdout - - &scm_environment_fingerprint - check: fingerprint - script: >- - python -c "import hashlib, os; - names = sorted(name for name in os.environ - if name == 'SOURCE_DATE_EPOCH' or name.startswith(('SETUPTOOLS_SCM_', 'VCS_VERSIONING_'))); - payload = '\0'.join(name + '=' + os.environ[name] for name in names); - print(hashlib.sha256(payload.encode()).hexdigest())" - hash: stdout - - &native_environment_fingerprint - check: fingerprint - script: >- - python -c "import hashlib, os, re; - names = {'BUILD_CUDA_MAJOR', 'BUILD_CUDA_VER', 'BUILD_PREV_CUDA_MAJOR', - 'CC', - 'CL', 'CPLUS_INCLUDE_PATH', 'CXX', 'CUDA_CORE_BUILD_MAJOR', 'CUDA_PATH', - 'CUDA_PYTHON_COVERAGE', 'CUDA_PYTHON_LANE', 'CUDA_PYTHON_PARALLEL_LEVEL', 'CUDA_VER', - 'HOST_PLATFORM', 'PY_EXT_SUFFIX', 'PY_VER', 'SCCACHE_CACHE_SIZE', - 'SCCACHE_DIR', 'SCCACHE_PATH'}; - names.update(name for name in os.environ if name.startswith('CIBW_')); - redact = lambda value: re.sub(r'(?i)\\bACTIONS_[A-Z0-9_]+=(?:\"[^\"]*\"|\\S+)', 'ACTIONS_VALUE=', value); - payload = '\0'.join(name + '=' + redact(os.environ.get(name, '')) for name in sorted(names)); - print(hashlib.sha256(payload.encode()).hexdigest())" - hash: stdout - - &python_runtime_fingerprint - check: fingerprint - script: >- - python -c "import platform, sysconfig; - print(platform.python_implementation(), platform.python_version(), - sysconfig.get_config_var('SOABI') or '', sep='\n')" - hash: stdout - - &python_platform_fingerprint - check: fingerprint - script: >- - python -c "import platform; - print(platform.system(), platform.machine(), sep='\n')" - hash: stdout - - &python_build_tools_fingerprint - check: fingerprint - script: >- - python -c "import importlib.metadata as metadata; - names = ('build', 'cibuildwheel', 'packaging', 'pip', 'setuptools', 'setuptools-scm', 'wheel'); - normalize = lambda value: value.lower().replace('_', '-'); - versions = {normalize(dist.metadata['Name']): dist.version for dist in metadata.distributions() if dist.metadata['Name']}; - print(*(name + '=' + versions.get(name, '') for name in names), sep='\n')" - hash: stdout - tags: - - ci-wheel-current - - runner-build-linux-64 - - runner-build-linux-aarch64 - - runner-build-windows - type: build - options: - cache: true - cacheKey: wheel-current-v2 - priority: critical - runInCI: true sdist: command: bash @@ -226,6 +146,9 @@ tasks: mkdir -p cuda_bindings/.moon-out rm -rf -- cuda_bindings/.moon-out/sdist mkdir -p cuda_bindings/.moon-out/sdist + SOURCE_DATE_EPOCH=$(printenv SOURCE_DATE_EPOCH || true) + export SOURCE_DATE_EPOCH + [[ -n "$SOURCE_DATE_EPOCH" ]] || SOURCE_DATE_EPOCH=$(git log -1 --format=%ct HEAD) shopt -s nullglob set -- cuda_pathfinder/.moon-out/sdist/*.whl @@ -236,6 +159,7 @@ tasks: PATHFINDER_WHEEL=$1 CONSTRAINT_FILE=$(mktemp) trap 'rm -f "$CONSTRAINT_FILE"' EXIT + cp ci/build-constraints.txt "$CONSTRAINT_FILE" HOST_PLATFORM_RESOLVED=$(printenv HOST_PLATFORM || true) if [[ -z "$HOST_PLATFORM_RESOLVED" ]]; then HOST_PLATFORM_RESOLVED=$(python -c "import platform; print('win-64' if platform.system() == 'Windows' else 'linux-' + platform.machine())") @@ -247,7 +171,7 @@ tasks: PATHFINDER_URI=$(python -c "from pathlib import Path; import sys; print(Path(sys.argv[1]).resolve().as_uri())" "$PATHFINDER_WHEEL") CONSTRAINT_HOST=$(realpath "$CONSTRAINT_FILE") fi - printf 'cuda-pathfinder @ %s\n' "$PATHFINDER_URI" > "$CONSTRAINT_FILE" + printf 'cuda-pathfinder @ %s\n' "$PATHFINDER_URI" >> "$CONSTRAINT_FILE" export PIP_BUILD_CONSTRAINT="$CONSTRAINT_HOST" export PIP_CONSTRAINT="$CONSTRAINT_HOST" @@ -274,105 +198,30 @@ tasks: HOST_PLATFORM: '${HOST_PLATFORM}' PY_VER: '${PY_VER}' inputs: - - '@group(package)' - {project: pathfinder, group: package} - - '/.github/workflows/test-sdist-linux.yml' - - '/.github/workflows/test-sdist-windows.yml' - outputs: - - '.moon-out/sdist' - checks: - - *bindings_scm_fingerprint - - *scm_environment_fingerprint - - *native_environment_fingerprint - - *python_runtime_fingerprint - - *python_platform_fingerprint - - *python_build_tools_fingerprint - tags: [ci-sdist, runner-sdist-linux, runner-sdist-windows] - type: build - options: - cache: true - cacheKey: sdist-v2 - runInCI: true cython-test-assets: - command: bash + command: python args: - - cuda_bindings/tests/cython/build_tests.sh + - cuda_bindings/tests/cython/build_tests.py - --output-dir - cuda_bindings/.moon-out/cython-tests - deps: - - test-helpers:prepare-test-assets + env: + PYTHONPATH: cuda_python_test_helpers inputs: - - '@group(package)' - - 'tests/cython/**/*' - {project: pathfinder, group: package} - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' - '/cuda_bindings/.moon-out/wheel-current/*.whl' - - '/.github/workflows/build-wheel.yml' - outputs: - - '.moon-out/cython-tests' - checks: - - *bindings_scm_fingerprint - - *scm_environment_fingerprint - - *native_environment_fingerprint - - *python_runtime_fingerprint - - *python_platform_fingerprint - - *python_build_tools_fingerprint - - check: fingerprint - script: >- - python -c "import importlib.metadata as metadata; - names = ('cython', 'numpy'); - normalize = lambda value: value.lower().replace('_', '-'); - versions = {normalize(dist.metadata['Name']): dist.version for dist in metadata.distributions() if dist.metadata['Name']}; - print(*(name + '=' + versions.get(name, '') for name in names), sep='\n')" - hash: stdout - - check: fingerprint - script: >- - python -c "import os, shlex, shutil, subprocess, sysconfig; - commands = {'cc', 'c++', 'cl', 'nvcc'}; - configured = (os.environ.get('CC') or sysconfig.get_config_var('CC') or '', - os.environ.get('CXX') or sysconfig.get_config_var('CXX') or ''); - commands.update(token for value in configured for token in shlex.split(value, posix=os.name != 'nt') if token and not token.startswith('-')); - print(*(command + '=' + str(result.returncode) + '\n' + result.stdout.strip() - for command in sorted(commands) if (path := shutil.which(command)) - for result in (subprocess.run([path, '--version'], check=False, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, text=True, timeout=10),)), sep='\n')" - hash: stdout - tags: - - ci-build-cython-assets - - runner-build-linux-64 - - runner-build-linux-aarch64 - - runner-build-windows - type: build - options: - cache: true - cacheKey: cython-test-assets-v2 - os: [linux, windows] - runInCI: true + - '/cuda_python_test_helpers/cuda_python_test_helpers/cython_test_builder.py' test: - command: bash - args: - - -euo - - pipefail - - -c - - | - PIXI_ENVIRONMENT=$(printenv PIXI_ENVIRONMENT_NAME || true) - if [[ -n "$PIXI_ENVIRONMENT" ]]; then - exec pixi run --manifest-path cuda_bindings/pixi.toml \ - --environment "$PIXI_ENVIRONMENT" test - fi - exec pixi run --manifest-path cuda_bindings/pixi.toml test inputs: - - '@group(package)' - - '@group(tests)' - '/cuda_pathfinder/**/*' - '/cuda_python_test_helpers/**/*' - '/benchmarks/cuda_bindings/**/*' - '/ci/test-matrix.yml' - '/ci/tools/env-vars' - '/tests/**/*' - type: test bench: command: pixi @@ -432,7 +281,7 @@ tasks: - '/ci/tools/install_gpu_driver.sh' - '/tests/**/*' - '/.github/workflows/test-wheel-linux.yml' - tags: [ci-test-linux, runner-test-linux] + tags: [ci-test-linux] type: test options: mutex: ci-python-gpu @@ -452,84 +301,31 @@ tasks: - benchmarks/cuda_bindings/tests inputs: - '@group(benchmark-tests)' - tags: [ci-quality, runner-quality] + tags: [ci-quality] type: test options: os: linux runInCI: true test-installed-linux: - command: bash - args: [ci/tools/run-tests, bindings] inputs: - - '@group(package)' - '@group(tests)' - {project: pathfinder, group: package} - '/cuda_python_test_helpers/**/*' - - '/ci/tools/run-tests' - - '/ci/test-matrix.yml' - - '/ci/tools/env-vars' - - '/tests/**/*' - - '/.github/workflows/build-wheel.yml' - - '/ci/tools/guess_latest.sh' - - '/ci/tools/install_gpu_driver.sh' - '/ci/tools/setup-sanitizer' - - '/.github/workflows/test-wheel-linux.yml' - tags: [ci-test-linux, runner-test-linux] - type: test - options: - mutex: ci-python-gpu - os: linux - runInCI: true test-installed-windows: - command: bash - args: [ci/tools/run-tests, bindings] inputs: - - '@group(package)' - '@group(tests)' - {project: pathfinder, group: package} - '/cuda_python_test_helpers/**/*' - - '/ci/tools/run-tests' - - '/ci/test-matrix.yml' - - '/ci/tools/env-vars' - - '/tests/**/*' - - '/.github/workflows/build-wheel.yml' - - '/ci/tools/configure_driver_mode.ps1' - - '/ci/tools/install_gpu_driver.ps1' - - '/.github/workflows/test-wheel-windows.yml' - tags: [ci-test-windows, runner-test-windows] - type: test - options: - mutex: ci-python-gpu - os: windows - runInCI: true docs: - command: pixi - args: [run, --manifest-path, cuda_bindings/pixi.toml, --environment, docs, build-docs] + args: [build-docs] inputs: - - '@group(package)' - - '@group(docs)' - {project: pathfinder, group: package} - {project: pathfinder, group: docs} - - '/.github/workflows/build-docs.yml' docs-ci: - command: bash - args: [cuda_bindings/docs/build_docs.sh, moon-ci] - env: - CUDA_PYTHON_DOCS_LATEST_ONLY: '${CUDA_PYTHON_DOCS_LATEST_ONLY}' inputs: - - '@group(package)' - - '@group(docs)' - {project: pathfinder, group: package} - - '/cuda_python/docs/environment-docs.yml' - - '/.github/workflows/build-docs.yml' - outputs: - - '.moon-out/docs-ci' - tags: [ci-docs, runner-docs] - type: build - options: - os: linux - runInCI: true diff --git a/cuda_bindings/pixi.toml b/cuda_bindings/pixi.toml index 9d122d17413..f626d586cc2 100644 --- a/cuda_bindings/pixi.toml +++ b/cuda_bindings/pixi.toml @@ -150,10 +150,10 @@ libnvfatbin = "*" libcufile = "*" [target.linux.tasks.build-cython-tests] -cmd = ["$PIXI_PROJECT_ROOT/tests/cython/build_tests.sh"] +cmd = ["python", "$PIXI_PROJECT_ROOT/tests/cython/build_tests.py"] [target.win-64.tasks.build-cython-tests] -cmd = ["$PIXI_PROJECT_ROOT/tests/cython/build_tests.bat"] +cmd = ["python", "$PIXI_PROJECT_ROOT/tests/cython/build_tests.py"] [target.linux.tasks.test] cmd = [ diff --git a/cuda_bindings/pyproject.toml b/cuda_bindings/pyproject.toml index 15ee1782eed..67b124a4cbc 100644 --- a/cuda_bindings/pyproject.toml +++ b/cuda_bindings/pyproject.toml @@ -89,6 +89,7 @@ repair-wheel-command = "delvewheel repair --namespace-pkg cuda -w {dest_dir} {wh [tool.pytest.ini_options] required_plugins = "pytest-benchmark" addopts = "--benchmark-disable --showlocals --durations=20" +pythonpath = ["../cuda_python_test_helpers"] norecursedirs = ["tests/cython", "examples"] xfail_strict = true # Keep this authorship marker registry in sync across all pytest config roots. diff --git a/cuda_bindings/tests/conftest.py b/cuda_bindings/tests/conftest.py index fada7d95601..699adc15150 100644 --- a/cuda_bindings/tests/conftest.py +++ b/cuda_bindings/tests/conftest.py @@ -2,31 +2,13 @@ # SPDX-License-Identifier: Apache-2.0 import functools -import importlib import inspect -import pathlib -import sys from contextlib import contextmanager import pytest import cuda.bindings.driver as cuda -# Keep in sync with cuda_core/tests/conftest.py. -try: - import cuda_python_test_helpers._pytest_plugin # noqa: F401 -except ImportError as e: - # Don't call .resolve(): resolving symlinks can make parents[2] point - # somewhere other than the monorepo root if a sub-directory is symlinked. - _test_helpers_root = pathlib.Path(__file__).parents[2] / "cuda_python_test_helpers" - if not _test_helpers_root.is_dir(): - raise RuntimeError(f"cuda-python-test-helpers not installed and not found at {_test_helpers_root}") from e - for _k in list(sys.modules): - if _k == "cuda_python_test_helpers" or _k.startswith("cuda_python_test_helpers."): - del sys.modules[_k] - sys.path.insert(0, str(_test_helpers_root)) - importlib.invalidate_caches() - pytest_plugins = ["cuda_python_test_helpers._pytest_plugin"] diff --git a/cuda_bindings/tests/cython/build_tests.bat b/cuda_bindings/tests/cython/build_tests.bat deleted file mode 100644 index 0ef6abb06f3..00000000000 --- a/cuda_bindings/tests/cython/build_tests.bat +++ /dev/null @@ -1,12 +0,0 @@ -@echo off - -REM SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -REM SPDX-License-Identifier: Apache-2.0 - -setlocal -set CL=%CL% /I"%CUDA_HOME%\include" -REM The Python driver provides Cython's .pxd include path and builds in this -REM directory so Windows does not duplicate the checkout path in link outputs. -python "%~dp0build_tests.py" -set "BUILD_RESULT=%ERRORLEVEL%" -endlocal & exit /b %BUILD_RESULT% diff --git a/cuda_bindings/tests/cython/build_tests.py b/cuda_bindings/tests/cython/build_tests.py index 44b2460f765..b6db2054ff6 100644 --- a/cuda_bindings/tests/cython/build_tests.py +++ b/cuda_bindings/tests/cython/build_tests.py @@ -1,102 +1,17 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Build cuda_bindings Cython test extensions in-place. +"""Build cuda_bindings Cython test extensions.""" -pixi-build's editable install exposes the `cuda` namespace package via a -PEP 660 finder hook. Python's import machinery honors the hook, but -Cython's filesystem .pxd resolver only walks real directories on sys.path, -so `cimport cuda.bindings.*` fails to locate the .pxd files. We resolve -the namespace package's source root from `cuda.bindings.__file__` and pass -it via `include_path=` so cythonize finds the .pxd tree on every platform. -""" - -from __future__ import annotations - -import argparse -import os -import shutil -import sys -from pathlib import Path - -from Cython.Build import cythonize -from setuptools import setup - -import cuda.bindings - - -def _bindings_source_root() -> Path: - # cuda.bindings.__file__ -> ...//cuda/bindings/__init__.py - root = Path(cuda.bindings.__file__).resolve().parents[2] - if not (root / "cuda" / "bindings").is_dir(): - raise RuntimeError( - f"cuda.bindings source tree not found at {root}; pixi-build editable install layout may have changed." - ) - return root - - -def _output_directory(script_dir: Path, value: str) -> Path: - project_root = script_dir.parents[1] - output_root = project_root / ".moon-out" - requested = Path(value) - output = Path(os.path.abspath(requested if requested.is_absolute() else project_root.parent / requested)) - if output_root not in output.parents: - raise ValueError(f"output must be below {output_root}: {output}") - current = output - while current != project_root: - if current.is_symlink(): - raise ValueError(f"output path must not traverse a symlink: {current}") - current = current.parent - if output.exists(): - if not output.is_dir(): - raise ValueError(f"refusing to replace non-directory output: {output}") - shutil.rmtree(output) - output.mkdir(parents=True) - return output +from cuda_python_test_helpers.cython_test_builder import build_cython_tests def main() -> None: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--output-dir") - args = parser.parse_args() - script_dir = Path(__file__).resolve().parent - output = _output_directory(script_dir, args.output_dir) if args.output_dir else None - # Avoid appending the absolute checkout path under build/temp: the - # concatenated path can exceed Windows' path limit. These files are siblings. - os.chdir(script_dir) - pyx_files = sorted(p.name for p in script_dir.glob("test_*.pyx")) - if not pyx_files: - raise SystemExit(f"no test_*.pyx files under {script_dir}") - - ext_modules = cythonize( - pyx_files, - language_level=3, + build_cython_tests( + script_file=__file__, + distribution_name="cuda_bindings_cython_tests", nthreads=1, - include_path=[str(_bindings_source_root())], - compiler_directives={"freethreading_compatible": True}, ) - # pytest imports each extension by bare module name (see test_cython.py), - # so build in-place next to its .pyx regardless of the invoking cwd. - sys.argv = [sys.argv[0], "build_ext"] - if output is None: - sys.argv.append("--inplace") - else: - build_temp = output / ".build-temp" - sys.argv.extend(["--build-lib", str(output), "--build-temp", str(build_temp)]) - setup(name="cuda_bindings_cython_tests", ext_modules=ext_modules) - if output is not None: - if build_temp.exists(): - shutil.rmtree(build_temp) - for source in pyx_files: - matches = [ - path - for pattern in (f"{Path(source).stem}*.so", f"{Path(source).stem}*.pyd", f"{Path(source).stem}*.dylib") - for path in output.glob(pattern) - if path.is_file() - ] - if len(matches) != 1: - raise RuntimeError(f"expected one extension for {source} in {output}, found {len(matches)}") - if __name__ == "__main__": main() diff --git a/cuda_bindings/tests/cython/build_tests.sh b/cuda_bindings/tests/cython/build_tests.sh deleted file mode 100755 index d6e9d2433ab..00000000000 --- a/cuda_bindings/tests/cython/build_tests.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -set -eo pipefail - -# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -UNAME=$(uname) -if [ "$UNAME" == "Linux" ] ; then - SCRIPTPATH=$(dirname $(realpath "$0")) - export CPLUS_INCLUDE_PATH=$CUDA_HOME/include:${CPLUS_INCLUDE_PATH:-} -elif [[ "$UNAME" == CYGWIN* || "$UNAME" == MINGW* || "$UNAME" == MSYS* ]] ; then - SCRIPTPATH="$(dirname $(cygpath -w $(realpath "$0")))" - export CL="/I\"${CUDA_HOME}\\include\" ${CL:-}" -else - exit 1 -fi - -# Use a Python driver so the cuda.bindings source root is resolved at -# runtime and passed via Cython's include_path -- avoids platform-specific -# PYTHONPATH separator handling and surfaces import errors as exceptions. -# nthreads=1 inside the driver mirrors the previous `-j 1` to side-step -# any process-pool issues and keep builds deterministic. -python "${SCRIPTPATH}/build_tests.py" "$@" diff --git a/cuda_core/AGENTS.md b/cuda_core/AGENTS.md index 9d80ab74aaa..79052224622 100644 --- a/cuda_core/AGENTS.md +++ b/cuda_core/AGENTS.md @@ -36,7 +36,7 @@ This file describes `cuda_core`, the high-level Pythonic CUDA subpackage in the - **Primary tests**: `pytest tests/` - **Cython tests**: - - build: `tests/cython/build_tests.sh` (or platform equivalent) + - build: `pixi run build-cython-tests` - run: `pytest tests/cython/` - **Examples**: validate affected examples in `examples/` when changing user workflows or public APIs. diff --git a/cuda_core/docs/build_docs.sh b/cuda_core/docs/build_docs.sh index 33939942e51..bc0f7ce6bbc 100755 --- a/cuda_core/docs/build_docs.sh +++ b/cuda_core/docs/build_docs.sh @@ -1,100 +1,9 @@ -#!/bin/bash +#!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -set -ex +set -euo pipefail SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) -cd "${SCRIPT_DIR}" - -MOON_CI="0" -if [[ "$#" == "0" ]]; then - LATEST_ONLY="0" -elif [[ "$#" == "1" && "$1" == "latest-only" ]]; then - LATEST_ONLY="1" -elif [[ "$#" == "1" && "$1" == "moon-ci" ]]; then - MOON_CI="1" - DOCS_LATEST_ONLY="${CUDA_PYTHON_DOCS_LATEST_ONLY:-true}" - case "${DOCS_LATEST_ONLY,,}" in - 1|true) LATEST_ONLY="1" ;; - 0|false) LATEST_ONLY="0" ;; - *) - echo "CUDA_PYTHON_DOCS_LATEST_ONLY must be true, false, 1, or 0" >&2 - exit 1 - ;; - esac -else - echo "usage: ./build_docs.sh [latest-only|moon-ci]" - exit 1 -fi - -if [[ "${MOON_CI}" == "1" ]]; then - if [[ -L build || ( -e build && ! -d build ) ]]; then - echo "refusing to replace non-directory docs build output: ${SCRIPT_DIR}/build" >&2 - exit 1 - fi - rm -rf build -fi - -# SPHINX_CUDA_CORE_VER is used to create a subdir under build/html -# (the Makefile file for sphinx-build also honors it if defined) -if [[ -z "${SPHINX_CUDA_CORE_VER}" ]]; then - export SPHINX_CUDA_CORE_VER=$(python -c "from importlib.metadata import version; print(version('cuda-core'))" \ - | awk -F'+' '{print $1}') -fi - -if [[ "${LATEST_ONLY}" == "1" && -z "${BUILD_PREVIEW:-}" && -z "${BUILD_LATEST:-}" ]]; then - export BUILD_LATEST=1 -fi - -# build the docs. Allow callers to override SPHINXOPTS for serial/debug runs. -if [[ -z "${SPHINXOPTS:-}" ]]; then - HTML_SPHINXOPTS="-W --keep-going -j 4 -d build/.doctrees" -else - HTML_SPHINXOPTS="${SPHINXOPTS}" -fi -SPHINXOPTS="${HTML_SPHINXOPTS}" -make html - -# to support version dropdown menu -cp ./versions.json build/html -cp ./nv-versions.json build/html - -# to have a redirection page (to the latest docs) -cp source/_templates/main.html build/html/index.html - -# ensure that the latest docs is the one we built -if [[ $LATEST_ONLY == "0" ]]; then - cp -r build/html/${SPHINX_CUDA_CORE_VER} build/html/latest -else - mv build/html/${SPHINX_CUDA_CORE_VER} build/html/latest -fi - -# ensure that the Sphinx reference uses the latest docs -cp build/html/latest/objects.inv build/html - -# clean up previously auto-generated files -rm -rf source/generated/ - -if [[ "${MOON_CI}" == "1" ]]; then - SOURCE="${SCRIPT_DIR}/build/html" - OUTPUT_ROOT="${SCRIPT_DIR}/../.moon-out" - OUTPUT="${OUTPUT_ROOT}/docs-ci" - if [[ -L "${SOURCE}" || ! -d "${SOURCE}" ]]; then - echo "documentation output not found: ${SOURCE}" >&2 - exit 1 - fi - if [[ -L "${OUTPUT_ROOT}" || ( -e "${OUTPUT_ROOT}" && ! -d "${OUTPUT_ROOT}" ) ]]; then - echo "refusing to use non-directory Moon output root: ${OUTPUT_ROOT}" >&2 - exit 1 - fi - if [[ -L "${OUTPUT}" || ( -e "${OUTPUT}" && ! -d "${OUTPUT}" ) ]]; then - echo "refusing to replace non-directory Moon docs output: ${OUTPUT}" >&2 - exit 1 - fi - mkdir -p "${OUTPUT_ROOT}" - rm -rf "${OUTPUT}" - mkdir -p "${OUTPUT}" - cp -aL "${SOURCE}/." "${OUTPUT}/" -fi +exec "${SCRIPT_DIR}/../../cuda_python/docs/build_component_docs.sh" cuda-core "$@" diff --git a/cuda_core/moon.yml b/cuda_core/moon.yml index ab3ff4389b4..ac05433807d 100644 --- a/cuda_core/moon.yml +++ b/cuda_core/moon.yml @@ -6,6 +6,7 @@ $schema: https://moonrepo.dev/schemas/v2/project.json language: unknown layer: library +tags: [docs-package, installed-test-package, native-package, pixi-package, python-package] dependsOn: - id: pathfinder scope: production @@ -13,15 +14,6 @@ dependsOn: scope: production - id: test-helpers scope: development -toolchains: - default: system - -taskOptions: - cache: false - runFromWorkspaceRoot: true - runInCI: false - shell: false - fileGroups: package: - '.git_archival.txt' @@ -33,18 +25,18 @@ fileGroups: - 'NOTICE' - 'pyproject.toml' - 'setup.py' - tests: - - 'examples/**/*' - - 'tests/**/*' - - 'pixi.toml' - - 'pixi.lock' - - 'pytest.ini' - docs: - - 'docs/**/*' - - 'pixi.toml' - - 'pixi.lock' - tasks: + fingerprint-package: + checks: + - check: fingerprint + script: >- + python -c "import subprocess; + subprocess.run(['git', 'describe', '--always', '--dirty', '--tags', '--long', + '--match', 'cuda-core-v*[0-9]*'], check=True)" + hash: stdout + options: + mergeChecks: replace + wheel-current: command: bash args: @@ -69,25 +61,42 @@ tasks: mkdir -p "$DIRECTORY" } - CUDA_MAJOR=$(printenv BUILD_CUDA_MAJOR || true) - if [[ -z "$CUDA_MAJOR" ]]; then - CUDA_VERSION=$(printenv BUILD_CUDA_VER || true) - if [[ -z "$CUDA_VERSION" ]]; then - CUDA_VERSION=$(printenv CUDA_VER || true) - fi - CUDA_MAJOR=$(cut -d . -f 1 <<< "$CUDA_VERSION") - fi - if [[ -z "$CUDA_MAJOR" ]] && command -v nvcc >/dev/null; then - CUDA_MAJOR=$(nvcc --version | sed -n 's/.*release \([0-9][0-9]*\).*/\1/p' | head -n 1) - fi + WHEEL_VARIANT=$(printenv CUDA_PYTHON_WHEEL_VARIANT || true) + case "$WHEEL_VARIANT" in + current) + CUDA_MAJOR=$(printenv BUILD_CUDA_MAJOR || true) + if [[ -z "$CUDA_MAJOR" ]]; then + CUDA_VERSION=$(printenv BUILD_CUDA_VER || true) + if [[ -z "$CUDA_VERSION" ]]; then + CUDA_VERSION=$(printenv CUDA_VER || true) + fi + CUDA_MAJOR=$(cut -d . -f 1 <<< "$CUDA_VERSION") + fi + if [[ -z "$CUDA_MAJOR" ]] && command -v nvcc >/dev/null; then + CUDA_MAJOR=$(nvcc --version | sed -n 's/.*release \([0-9][0-9]*\).*/\1/p' | head -n 1) + fi + ERROR_MESSAGE="set BUILD_CUDA_MAJOR/BUILD_CUDA_VER or activate a toolkit with nvcc" + ;; + previous) + CUDA_MAJOR=$(printenv BUILD_PREV_CUDA_MAJOR || true) + ERROR_MESSAGE="BUILD_PREV_CUDA_MAJOR must be a numeric CUDA major version" + ;; + *) + echo "CUDA_PYTHON_WHEEL_VARIANT must be current or previous" >&2 + exit 1 + ;; + esac [[ "$CUDA_MAJOR" =~ ^[0-9]+$ ]] || { - echo "set BUILD_CUDA_MAJOR/BUILD_CUDA_VER or activate a toolkit with nvcc" >&2 + echo "$ERROR_MESSAGE" >&2 exit 1 } - OUTPUT=cuda_core/.moon-out/wheel-current - CONSTRAINTS=cuda_core/.moon-out/constraints-current + OUTPUT="cuda_core/.moon-out/wheel-$WHEEL_VARIANT" + CONSTRAINTS="cuda_core/.moon-out/constraints-$WHEEL_VARIANT" reset_directory "$OUTPUT" reset_directory "$CONSTRAINTS" + SOURCE_DATE_EPOCH=$(printenv SOURCE_DATE_EPOCH || true) + export SOURCE_DATE_EPOCH + [[ -n "$SOURCE_DATE_EPOCH" ]] || SOURCE_DATE_EPOCH=$(git log -1 --format=%ct HEAD) shopt -s nullglob set -- cuda_pathfinder/.moon-out/wheel-pure/*.whl @@ -96,14 +105,16 @@ tasks: exit 1 } PATHFINDER_WHEEL=$1 - set -- cuda_bindings/.moon-out/wheel-current/*.whl + BINDINGS_OUTPUT="cuda_bindings/.moon-out/wheel-$WHEEL_VARIANT" + set -- "$BINDINGS_OUTPUT"/*.whl [[ $# -eq 1 ]] || { - echo "expected one current cuda.bindings wheel, found $#" >&2 + echo "expected one $WHEEL_VARIANT cuda.bindings wheel, found $#" >&2 exit 1 } BINDINGS_WHEEL=$1 CONSTRAINT_FILE="$CONSTRAINTS/build.txt" + cp ci/build-constraints.txt "$CONSTRAINT_FILE" HOST_PLATFORM_RESOLVED=$(printenv HOST_PLATFORM || true) if [[ -z "$HOST_PLATFORM_RESOLVED" ]]; then HOST_PLATFORM_RESOLVED=$(python -c "import platform; print('win-64' if platform.system() == 'Windows' else 'linux-' + platform.machine())") @@ -115,9 +126,9 @@ tasks: { printf 'cuda-pathfinder @ %s\n' "$PATHFINDER_URI" printf 'cuda-bindings @ %s\n' "$BINDINGS_URI" - } > "$CONSTRAINT_FILE" + } >> "$CONSTRAINT_FILE" CIBW_ENVIRONMENT_BASE=$(printenv CIBW_ENVIRONMENT_WINDOWS || true) - export CIBW_ENVIRONMENT_WINDOWS="$CIBW_ENVIRONMENT_BASE PIP_BUILD_CONSTRAINT=\"$CONSTRAINT_HOST\" PIP_CONSTRAINT=\"$CONSTRAINT_HOST\" CUDA_CORE_BUILD_MAJOR=$CUDA_MAJOR" + export CIBW_ENVIRONMENT_WINDOWS="$CIBW_ENVIRONMENT_BASE PIP_BUILD_CONSTRAINT=\"$CONSTRAINT_HOST\" PIP_CONSTRAINT=\"$CONSTRAINT_HOST\" CUDA_CORE_BUILD_MAJOR=$CUDA_MAJOR SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH" else PATHFINDER_URI=$(python -c "from pathlib import Path; import sys; print(Path('/host' + Path(sys.argv[1]).resolve().as_posix()).as_uri())" "$PATHFINDER_WHEEL") BINDINGS_URI=$(python -c "from pathlib import Path; import sys; print(Path('/host' + Path(sys.argv[1]).resolve().as_posix()).as_uri())" "$BINDINGS_WHEEL") @@ -126,9 +137,9 @@ tasks: { printf 'cuda-pathfinder @ %s\n' "$PATHFINDER_URI" printf 'cuda-bindings @ %s\n' "$BINDINGS_URI" - } > "$CONSTRAINT_FILE" + } >> "$CONSTRAINT_FILE" CIBW_ENVIRONMENT_BASE=$(printenv CIBW_ENVIRONMENT_LINUX || true) - export CIBW_ENVIRONMENT_LINUX="$CIBW_ENVIRONMENT_BASE PIP_BUILD_CONSTRAINT=\"$CONSTRAINT_CONTAINER\" PIP_CONSTRAINT=\"$CONSTRAINT_CONTAINER\" CUDA_CORE_BUILD_MAJOR=$CUDA_MAJOR" + export CIBW_ENVIRONMENT_LINUX="$CIBW_ENVIRONMENT_BASE PIP_BUILD_CONSTRAINT=\"$CONSTRAINT_CONTAINER\" PIP_CONSTRAINT=\"$CONSTRAINT_CONTAINER\" CUDA_CORE_BUILD_MAJOR=$CUDA_MAJOR SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH" fi export PIP_BUILD_CONSTRAINT="$CONSTRAINT_HOST" export PIP_CONSTRAINT="$CONSTRAINT_HOST" @@ -144,13 +155,15 @@ tasks: fi set -- "$OUTPUT"/*.whl [[ $# -eq 1 ]] || { - echo "expected one current cuda.core wheel, found $#" >&2 + echo "expected one $WHEEL_VARIANT cuda.core wheel, found $#" >&2 exit 1 } WHEEL=$1 WHEEL_WITHOUT_SUFFIX=$(printf '%s\n' "$WHEEL" | sed 's/\.whl$//') mv -- "$WHEEL" "$WHEEL_WITHOUT_SUFFIX.cu$CUDA_MAJOR.whl" deps: + - target: pathfinder:wheel-pure + cacheStrategy: outputs - target: bindings:wheel-current cacheStrategy: outputs env: @@ -158,215 +171,52 @@ tasks: BUILD_CUDA_VER: '${BUILD_CUDA_VER}' CIBW_BUILD: '${CIBW_BUILD}' CUDA_PATH: '${CUDA_PATH}' + CUDA_PYTHON_WHEEL_VARIANT: current HOST_PLATFORM: '${HOST_PLATFORM}' PY_VER: '${PY_VER}' inputs: - - '@group(package)' + # Keep same-phase source proxies so affected selection sees upstream + # changes before Moon expands executable dependencies. - {project: pathfinder, group: package} - {project: bindings, group: package} - - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' - - '/cuda_bindings/.moon-out/wheel-current/*.whl' - - '/ci/tools/env-vars' - - '/ci/tools/merge_cuda_core_wheels.py' - - '/ci/versions.yml' - - '/.github/workflows/build-wheel.yml' - outputs: - - '.moon-out/wheel-current' - checks: - - &core_scm_fingerprint - check: fingerprint - script: git describe --always --dirty --tags --long --match 'cuda-core-v*[0-9]*' - hash: stdout - - &scm_environment_fingerprint - check: fingerprint - script: >- - python -c "import hashlib, os; - names = sorted(name for name in os.environ - if name == 'SOURCE_DATE_EPOCH' or name.startswith(('SETUPTOOLS_SCM_', 'VCS_VERSIONING_'))); - payload = '\0'.join(name + '=' + os.environ[name] for name in names); - print(hashlib.sha256(payload.encode()).hexdigest())" - hash: stdout - - &native_environment_fingerprint - check: fingerprint - script: >- - python -c "import hashlib, os, re; - names = {'BUILD_CUDA_MAJOR', 'BUILD_CUDA_VER', 'BUILD_PREV_CUDA_MAJOR', - 'CC', - 'CL', 'CPLUS_INCLUDE_PATH', 'CXX', 'CUDA_CORE_BUILD_MAJOR', 'CUDA_PATH', - 'CUDA_PYTHON_COVERAGE', 'CUDA_PYTHON_LANE', 'CUDA_PYTHON_PARALLEL_LEVEL', 'CUDA_VER', - 'HOST_PLATFORM', 'PY_EXT_SUFFIX', 'PY_VER', 'SCCACHE_CACHE_SIZE', - 'SCCACHE_DIR', 'SCCACHE_PATH'}; - names.update(name for name in os.environ if name.startswith('CIBW_')); - redact = lambda value: re.sub(r'(?i)\\bACTIONS_[A-Z0-9_]+=(?:\"[^\"]*\"|\\S+)', 'ACTIONS_VALUE=', value); - payload = '\0'.join(name + '=' + redact(os.environ.get(name, '')) for name in sorted(names)); - print(hashlib.sha256(payload.encode()).hexdigest())" - hash: stdout - - &python_runtime_fingerprint - check: fingerprint - script: >- - python -c "import platform, sysconfig; - print(platform.python_implementation(), platform.python_version(), - sysconfig.get_config_var('SOABI') or '', sep='\n')" - hash: stdout - - &python_platform_fingerprint - check: fingerprint - script: >- - python -c "import platform; - print(platform.system(), platform.machine(), sep='\n')" - hash: stdout - - &python_build_tools_fingerprint - check: fingerprint - script: >- - python -c "import importlib.metadata as metadata; - names = ('build', 'cibuildwheel', 'packaging', 'pip', 'setuptools', 'setuptools-scm', 'wheel'); - normalize = lambda value: value.lower().replace('_', '-'); - versions = {normalize(dist.metadata['Name']): dist.version for dist in metadata.distributions() if dist.metadata['Name']}; - print(*(name + '=' + versions.get(name, '') for name in names), sep='\n')" - hash: stdout - tags: - - ci-wheel-current - - runner-build-linux-64 - - runner-build-linux-aarch64 - - runner-build-windows - type: build - options: - cache: true - cacheKey: wheel-current-v2 - priority: critical - runInCI: true wheel-previous: - command: bash - args: - - -euo - - pipefail - - -c - - | - reset_directory() { - local DIRECTORY="$1" - local ROOT - ROOT=$(dirname -- "$DIRECTORY") - if [[ -L "$ROOT" || ( -e "$ROOT" && ! -d "$ROOT" ) ]]; then - echo "refusing to use non-directory output root: $ROOT" >&2 - exit 1 - fi - if [[ -L "$DIRECTORY" || ( -e "$DIRECTORY" && ! -d "$DIRECTORY" ) ]]; then - echo "refusing to replace non-directory output: $DIRECTORY" >&2 - exit 1 - fi - mkdir -p "$ROOT" - rm -rf -- "$DIRECTORY" - mkdir -p "$DIRECTORY" - } - - CUDA_MAJOR=$(printenv BUILD_PREV_CUDA_MAJOR || true) - [[ "$CUDA_MAJOR" =~ ^[0-9]+$ ]] || { - echo "BUILD_PREV_CUDA_MAJOR must be a numeric CUDA major version" >&2 - exit 1 - } - OUTPUT=cuda_core/.moon-out/wheel-previous - CONSTRAINTS=cuda_core/.moon-out/constraints-previous - reset_directory "$OUTPUT" - reset_directory "$CONSTRAINTS" - - shopt -s nullglob - set -- cuda_pathfinder/.moon-out/wheel-pure/*.whl - [[ $# -eq 1 ]] || { - echo "expected one cuda.pathfinder wheel, found $#" >&2 - exit 1 - } - PATHFINDER_WHEEL=$1 - set -- cuda_bindings/.moon-out/wheel-previous/*.whl - [[ $# -eq 1 ]] || { - echo "expected one previous cuda.bindings wheel, found $#" >&2 - exit 1 - } - BINDINGS_WHEEL=$1 - - CONSTRAINT_FILE="$CONSTRAINTS/build.txt" - HOST_PLATFORM_RESOLVED=$(printenv HOST_PLATFORM || true) - if [[ -z "$HOST_PLATFORM_RESOLVED" ]]; then - HOST_PLATFORM_RESOLVED=$(python -c "import platform; print('win-64' if platform.system() == 'Windows' else 'linux-' + platform.machine())") - fi - if [[ "$HOST_PLATFORM_RESOLVED" == win* ]]; then - PATHFINDER_URI=$(python -c "from pathlib import Path; import sys; print(Path(sys.argv[1]).resolve().as_uri())" "$PATHFINDER_WHEEL") - BINDINGS_URI=$(python -c "from pathlib import Path; import sys; print(Path(sys.argv[1]).resolve().as_uri())" "$BINDINGS_WHEEL") - CONSTRAINT_HOST=$(cygpath -w "$(pwd)/$CONSTRAINT_FILE") - { - printf 'cuda-pathfinder @ %s\n' "$PATHFINDER_URI" - printf 'cuda-bindings @ %s\n' "$BINDINGS_URI" - } > "$CONSTRAINT_FILE" - CIBW_ENVIRONMENT_BASE=$(printenv CIBW_ENVIRONMENT_WINDOWS || true) - export CIBW_ENVIRONMENT_WINDOWS="$CIBW_ENVIRONMENT_BASE PIP_BUILD_CONSTRAINT=\"$CONSTRAINT_HOST\" PIP_CONSTRAINT=\"$CONSTRAINT_HOST\" CUDA_CORE_BUILD_MAJOR=$CUDA_MAJOR" - else - PATHFINDER_URI=$(python -c "from pathlib import Path; import sys; print(Path('/host' + Path(sys.argv[1]).resolve().as_posix()).as_uri())" "$PATHFINDER_WHEEL") - BINDINGS_URI=$(python -c "from pathlib import Path; import sys; print(Path('/host' + Path(sys.argv[1]).resolve().as_posix()).as_uri())" "$BINDINGS_WHEEL") - CONSTRAINT_HOST=$(realpath "$CONSTRAINT_FILE") - CONSTRAINT_CONTAINER="/host$CONSTRAINT_HOST" - { - printf 'cuda-pathfinder @ %s\n' "$PATHFINDER_URI" - printf 'cuda-bindings @ %s\n' "$BINDINGS_URI" - } > "$CONSTRAINT_FILE" - CIBW_ENVIRONMENT_BASE=$(printenv CIBW_ENVIRONMENT_LINUX || true) - export CIBW_ENVIRONMENT_LINUX="$CIBW_ENVIRONMENT_BASE PIP_BUILD_CONSTRAINT=\"$CONSTRAINT_CONTAINER\" PIP_CONSTRAINT=\"$CONSTRAINT_CONTAINER\" CUDA_CORE_BUILD_MAJOR=$CUDA_MAJOR" - fi - export PIP_BUILD_CONSTRAINT="$CONSTRAINT_HOST" - export PIP_CONSTRAINT="$CONSTRAINT_HOST" - export CUDA_CORE_BUILD_MAJOR="$CUDA_MAJOR" - - python -m cibuildwheel --output-dir "$OUTPUT" cuda_core - if [[ "$HOST_PLATFORM_RESOLVED" != win* ]] && find "$OUTPUT" ! -user "$(id -u)" -print -quit | grep -q .; then - command -v sudo >/dev/null || { - echo "cibuildwheel output is not owned by this user and sudo was not found: $OUTPUT" >&2 - exit 1 - } - sudo chown -R "$(id -u):$(id -g)" "$OUTPUT" - fi - set -- "$OUTPUT"/*.whl - [[ $# -eq 1 ]] || { - echo "expected one previous cuda.core wheel, found $#" >&2 - exit 1 - } - WHEEL=$1 - WHEEL_WITHOUT_SUFFIX=$(printf '%s\n' "$WHEEL" | sed 's/\.whl$//') - mv -- "$WHEEL" "$WHEEL_WITHOUT_SUFFIX.cu$CUDA_MAJOR.whl" + extends: wheel-current deps: - target: pathfinder:wheel-pure cacheStrategy: outputs + - target: ~:fingerprint-package + cacheStrategy: hash + - target: test-helpers:fingerprint-python-build + cacheStrategy: hash + - target: test-helpers:fingerprint-native-context + cacheStrategy: hash env: BUILD_PREV_CUDA_MAJOR: '${BUILD_PREV_CUDA_MAJOR}' CIBW_BUILD: '${CIBW_BUILD}' CUDA_PATH: '${CUDA_PATH}' + CUDA_PYTHON_WHEEL_VARIANT: previous HOST_PLATFORM: '${HOST_PLATFORM}' PY_VER: '${PY_VER}' inputs: - '@group(package)' + # Dependency wheels are staged by different toolkit phases, so source + # groups remain the affected-selection edges. - {project: pathfinder, group: package} - {project: bindings, group: package} - - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' - '/cuda_bindings/.moon-out/wheel-previous/*.whl' + - '/ci/build-constraints.txt' - '/ci/tools/env-vars' - '/ci/versions.yml' + - '/ci/build-matrix.yml' - '/.github/workflows/build-wheel.yml' outputs: - '.moon-out/wheel-previous' - checks: - - *core_scm_fingerprint - - *scm_environment_fingerprint - - *native_environment_fingerprint - - *python_runtime_fingerprint - - *python_platform_fingerprint - - *python_build_tools_fingerprint - tags: - - runner-build-linux-64 - - runner-build-linux-aarch64 - - runner-build-windows - type: build options: - cache: true - cacheKey: wheel-previous-v2 - priority: critical - runInCI: true + mergeDeps: replace + mergeEnv: replace + mergeInputs: replace + mergeOutputs: replace wheel-merge: command: python @@ -379,34 +229,26 @@ tasks: - --output-dir - cuda_core/.moon-out/wheel-merged - --clean-output - # Current and previous CUDA toolkits are provisioned outside Moon, so the - # two input wheels are deliberately staged in separate invocations before - # this task merges their declared outputs. + # The current CUDA wheel is staged by an earlier toolkit phase. The + # previous wheel is an executable dependency in this stable toolkit phase. + deps: + - target: test-helpers:fingerprint-python-build + cacheStrategy: hash + - target: ~:wheel-previous + cacheStrategy: outputs inputs: - '@group(package)' - {project: pathfinder, group: package} - {project: bindings, group: package} - '/cuda_core/.moon-out/wheel-current/*.whl' - - '/cuda_core/.moon-out/wheel-previous/*.whl' - '/ci/tools/merge_cuda_core_wheels.py' - '/.github/workflows/build-wheel.yml' outputs: - '.moon-out/wheel-merged' - checks: - - *core_scm_fingerprint - - *scm_environment_fingerprint - - *native_environment_fingerprint - - *python_runtime_fingerprint - - *python_platform_fingerprint - - *python_build_tools_fingerprint - tags: - - runner-build-linux-64 - - runner-build-linux-aarch64 - - runner-build-windows + tags: [ci-build-native] type: build options: cache: true - cacheKey: wheel-merge-v2 runInCI: true sdist: @@ -456,8 +298,12 @@ tasks: exit 1 } BINDINGS_WHEEL=$1 + SOURCE_DATE_EPOCH=$(printenv SOURCE_DATE_EPOCH || true) + export SOURCE_DATE_EPOCH + [[ -n "$SOURCE_DATE_EPOCH" ]] || SOURCE_DATE_EPOCH=$(git log -1 --format=%ct HEAD) CONSTRAINT_FILE=$(mktemp) trap 'rm -f "$CONSTRAINT_FILE"' EXIT + cp ci/build-constraints.txt "$CONSTRAINT_FILE" HOST_PLATFORM_RESOLVED=$(printenv HOST_PLATFORM || true) if [[ -z "$HOST_PLATFORM_RESOLVED" ]]; then HOST_PLATFORM_RESOLVED=$(python -c "import platform; print('win-64' if platform.system() == 'Windows' else 'linux-' + platform.machine())") @@ -474,7 +320,7 @@ tasks: { printf 'cuda-pathfinder @ %s\n' "$PATHFINDER_URI" printf 'cuda-bindings @ %s\n' "$BINDINGS_URI" - } > "$CONSTRAINT_FILE" + } >> "$CONSTRAINT_FILE" export PIP_BUILD_CONSTRAINT="$CONSTRAINT_HOST" export PIP_CONSTRAINT="$CONSTRAINT_HOST" export CUDA_CORE_BUILD_MAJOR="$CUDA_MAJOR" @@ -505,86 +351,24 @@ tasks: HOST_PLATFORM: '${HOST_PLATFORM}' PY_VER: '${PY_VER}' inputs: - - '@group(package)' - {project: pathfinder, group: package} - {project: bindings, group: package} - - '/.github/workflows/test-sdist-linux.yml' - - '/.github/workflows/test-sdist-windows.yml' - outputs: - - '.moon-out/sdist' - checks: - - *core_scm_fingerprint - - *scm_environment_fingerprint - - *native_environment_fingerprint - - *python_runtime_fingerprint - - *python_platform_fingerprint - - *python_build_tools_fingerprint - tags: [ci-sdist, runner-sdist-linux, runner-sdist-windows] - type: build - options: - cache: true - cacheKey: sdist-v2 - runInCI: true cython-test-assets: - command: bash + command: python args: - - cuda_core/tests/cython/build_tests.sh + - cuda_core/tests/cython/build_tests.py - --output-dir - cuda_core/.moon-out/cython-tests - deps: - - test-helpers:prepare-test-assets + env: + PYTHONPATH: cuda_python_test_helpers inputs: - - '@group(package)' - - 'tests/cython/**/*' - {project: pathfinder, group: package} - {project: bindings, group: package} - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' - '/cuda_bindings/.moon-out/wheel-current/*.whl' - '/cuda_core/.moon-out/wheel-current/*.whl' - - '/.github/workflows/build-wheel.yml' - outputs: - - '.moon-out/cython-tests' - checks: - - *core_scm_fingerprint - - *scm_environment_fingerprint - - *native_environment_fingerprint - - *python_runtime_fingerprint - - *python_platform_fingerprint - - *python_build_tools_fingerprint - - &python_test_tools_fingerprint - check: fingerprint - script: >- - python -c "import importlib.metadata as metadata; - names = ('cython', 'numpy'); - normalize = lambda value: value.lower().replace('_', '-'); - versions = {normalize(dist.metadata['Name']): dist.version for dist in metadata.distributions() if dist.metadata['Name']}; - print(*(name + '=' + versions.get(name, '') for name in names), sep='\n')" - hash: stdout - - &native_tools_fingerprint - check: fingerprint - script: >- - python -c "import os, shlex, shutil, subprocess, sysconfig; - commands = {'cc', 'c++', 'cl', 'nvcc'}; - configured = (os.environ.get('CC') or sysconfig.get_config_var('CC') or '', - os.environ.get('CXX') or sysconfig.get_config_var('CXX') or ''); - commands.update(token for value in configured for token in shlex.split(value, posix=os.name != 'nt') if token and not token.startswith('-')); - print(*(command + '=' + str(result.returncode) + '\n' + result.stdout.strip() - for command in sorted(commands) if (path := shutil.which(command)) - for result in (subprocess.run([path, '--version'], check=False, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, text=True, timeout=10),)), sep='\n')" - hash: stdout - tags: - - ci-build-cython-assets - - runner-build-linux-64 - - runner-build-linux-aarch64 - - runner-build-windows - type: build - options: - cache: true - cacheKey: cython-test-assets-v2 - os: [linux, windows] - runInCI: true + - '/cuda_python_test_helpers/cuda_python_test_helpers/cython_test_builder.py' test-binaries: command: python @@ -595,141 +379,60 @@ tasks: env: CUDA_PATH: '${CUDA_PATH}' HOST_PLATFORM: '${HOST_PLATFORM}' + deps: + - target: test-helpers:fingerprint-native-context + cacheStrategy: hash inputs: - - '@group(package)' - - {project: pathfinder, group: package} - - {project: bindings, group: package} - 'tests/test_binaries/build_test_binaries.py' - 'tests/test_binaries/saxpy.cu' - '/.github/workflows/build-wheel.yml' outputs: - '.moon-out/test-binaries' - checks: - - *core_scm_fingerprint - - *scm_environment_fingerprint - - *native_environment_fingerprint - - *python_runtime_fingerprint - - *python_platform_fingerprint - - *python_build_tools_fingerprint - - *python_test_tools_fingerprint - - *native_tools_fingerprint - tags: - - runner-build-linux-64 - - runner-build-linux-aarch64 - - runner-build-windows + tags: [ci-build-native] type: build options: cache: true - cacheKey: test-binaries-v2 os: [linux, windows] runInCI: true test: - command: bash - args: - - -euo - - pipefail - - -c - - | - PIXI_ENVIRONMENT=$(printenv PIXI_ENVIRONMENT_NAME || true) - if [[ -n "$PIXI_ENVIRONMENT" ]]; then - exec pixi run --manifest-path cuda_core/pixi.toml \ - --environment "$PIXI_ENVIRONMENT" test - fi - exec pixi run --manifest-path cuda_core/pixi.toml test inputs: - - '@group(package)' - - '@group(tests)' - '/cuda_pathfinder/**/*' - '/cuda_bindings/**/*' - '/cuda_python_test_helpers/**/*' - '/ci/test-matrix.yml' - '/ci/tools/env-vars' - '/tests/**/*' - type: test test-installed-linux: - command: bash - args: [ci/tools/run-tests, core] inputs: - - '@group(package)' - '@group(tests)' - {project: pathfinder, group: package} - {project: bindings, group: package} - '/cuda_python_test_helpers/**/*' - - '/ci/tools/run-tests' - - '/ci/test-matrix.yml' - - '/ci/tools/env-vars' - - '/tests/**/*' - '/ci/tools/merge_cuda_core_wheels.py' - - '/.github/workflows/build-wheel.yml' - - '/ci/tools/guess_latest.sh' - - '/ci/tools/install_gpu_driver.sh' - '/ci/tools/setup-sanitizer' - - '/.github/workflows/test-wheel-linux.yml' - tags: [ci-test-linux, runner-test-linux] - type: test - options: - mutex: ci-python-gpu - os: linux - runInCI: true test-installed-windows: - command: bash - args: [ci/tools/run-tests, core] inputs: - - '@group(package)' - '@group(tests)' - {project: pathfinder, group: package} - {project: bindings, group: package} - '/cuda_python_test_helpers/**/*' - - '/ci/tools/run-tests' - - '/ci/test-matrix.yml' - - '/ci/tools/env-vars' - - '/tests/**/*' - '/ci/tools/merge_cuda_core_wheels.py' - - '/.github/workflows/build-wheel.yml' - - '/ci/tools/configure_driver_mode.ps1' - - '/ci/tools/install_gpu_driver.ps1' - - '/.github/workflows/test-wheel-windows.yml' - tags: [ci-test-windows, runner-test-windows] - type: test - options: - mutex: ci-python-gpu - os: windows - runInCI: true docs: - command: pixi - args: [run, --manifest-path, cuda_core/pixi.toml, --environment, docs, docs-build] + args: [docs-build] inputs: - - '@group(package)' - - '@group(docs)' - {project: pathfinder, group: package} - {project: pathfinder, group: docs} - {project: bindings, group: package} - {project: bindings, group: docs} - - '/.github/workflows/build-docs.yml' docs-ci: - command: bash - args: [cuda_core/docs/build_docs.sh, moon-ci] - env: - CUDA_PYTHON_DOCS_LATEST_ONLY: '${CUDA_PYTHON_DOCS_LATEST_ONLY}' inputs: - - '@group(package)' - - '@group(docs)' - {project: pathfinder, group: package} - {project: bindings, group: package} - - '/cuda_python/docs/environment-docs.yml' - - '/.github/workflows/build-docs.yml' - outputs: - - '.moon-out/docs-ci' - tags: [ci-docs, runner-docs] - type: build - options: - os: linux - runInCI: true quality-api-release: command: uvx @@ -751,7 +454,7 @@ tasks: inputs: - 'cuda/core/**/*' - '/.github/actions/griffe-api-check/action.yml' - tags: [ci-quality, runner-quality] + tags: [ci-quality] type: test options: os: linux @@ -777,7 +480,7 @@ tasks: inputs: - 'cuda/core/**/*' - '/.github/actions/griffe-api-check/action.yml' - tags: [ci-quality, runner-quality] + tags: [ci-quality] type: test options: os: linux diff --git a/cuda_core/pixi.toml b/cuda_core/pixi.toml index b2c6a3389c3..cf51c60cdbc 100644 --- a/cuda_core/pixi.toml +++ b/cuda_core/pixi.toml @@ -247,7 +247,7 @@ cuda-pathfinder = "*" "backports.strenum" = "*" [target.linux.tasks.build-cython-tests] -cmd = ["$PIXI_PROJECT_ROOT/tests/cython/build_tests.sh"] +cmd = ["python", "$PIXI_PROJECT_ROOT/tests/cython/build_tests.py"] [target.linux.tasks.docs-build] cmd = ["$PIXI_PROJECT_ROOT/docs/build_docs.sh"] @@ -263,7 +263,7 @@ env = { SPHINXOPTS = "-v -j 1 -d build/.doctrees" } default-environment = "docs" [target.win-64.tasks.build-cython-tests] -cmd = ["$PIXI_PROJECT_ROOT/tests/cython/build_tests.bat"] +cmd = ["python", "$PIXI_PROJECT_ROOT/tests/cython/build_tests.py"] [target.linux.tasks.test] cmd = [ diff --git a/cuda_core/pytest.ini b/cuda_core/pytest.ini index 64fcf312a79..ab71e1acd52 100644 --- a/cuda_core/pytest.ini +++ b/cuda_core/pytest.ini @@ -4,6 +4,7 @@ [pytest] addopts = --showlocals --durations=20 +pythonpath = ../cuda_python_test_helpers norecursedirs = cython markers = # Keep this authorship marker registry in sync across all pytest config roots. diff --git a/cuda_core/tests/conftest.py b/cuda_core/tests/conftest.py index b212633ebcf..803b15e8e9c 100644 --- a/cuda_core/tests/conftest.py +++ b/cuda_core/tests/conftest.py @@ -3,30 +3,12 @@ import functools import gc -import importlib import multiprocessing import os -import pathlib -import sys from contextlib import contextmanager import pytest -# Keep in sync with cuda_bindings/tests/conftest.py. -try: - import cuda_python_test_helpers._pytest_plugin # noqa: F401 -except ImportError as e: - # Don't call .resolve(): resolving symlinks can make parents[2] point - # somewhere other than the monorepo root if a sub-directory is symlinked. - _test_helpers_root = pathlib.Path(__file__).parents[2] / "cuda_python_test_helpers" - if not _test_helpers_root.is_dir(): - raise RuntimeError(f"cuda-python-test-helpers not installed and not found at {_test_helpers_root}") from e - for _k in list(sys.modules): - if _k == "cuda_python_test_helpers" or _k.startswith("cuda_python_test_helpers."): - del sys.modules[_k] - sys.path.insert(0, str(_test_helpers_root)) - importlib.invalidate_caches() - pytest_plugins = ["cuda_python_test_helpers._pytest_plugin"] from cuda_python_test_helpers.marks import skipif_need_cuda_headers # noqa: F401 (re-exported for tests) diff --git a/cuda_core/tests/cython/build_tests.py b/cuda_core/tests/cython/build_tests.py index e5108872a76..022c9cbdec9 100644 --- a/cuda_core/tests/cython/build_tests.py +++ b/cuda_core/tests/cython/build_tests.py @@ -1,103 +1,17 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Build cuda_core Cython test extensions in-place. +"""Build cuda_core Cython test extensions.""" -pixi-build's editable install exposes the `cuda` namespace package via a -PEP 660 finder hook. Python's import machinery honors the hook, but -Cython's filesystem .pxd resolver only walks real directories on sys.path, -so `cimport cuda.bindings.*` fails to locate the .pxd files. We resolve -the namespace package's source root from `cuda.bindings.__file__` and pass -it via `include_path=` so cythonize finds the .pxd tree on every platform. -""" - -from __future__ import annotations - -import argparse -import os -import shutil -import sys -from pathlib import Path - -from Cython.Build import cythonize -from setuptools import setup - -import cuda.bindings - - -def _bindings_source_root() -> Path: - # cuda.bindings.__file__ -> ...//cuda/bindings/__init__.py - root = Path(cuda.bindings.__file__).resolve().parents[2] - if not (root / "cuda" / "bindings").is_dir(): - raise RuntimeError( - f"cuda.bindings source tree not found at {root}; pixi-build editable install layout may have changed." - ) - return root - - -def _output_directory(script_dir: Path, value: str) -> Path: - project_root = script_dir.parents[1] - output_root = project_root / ".moon-out" - requested = Path(value) - output = Path(os.path.abspath(requested if requested.is_absolute() else project_root.parent / requested)) - if output_root not in output.parents: - raise ValueError(f"output must be below {output_root}: {output}") - current = output - while current != project_root: - if current.is_symlink(): - raise ValueError(f"output path must not traverse a symlink: {current}") - current = current.parent - if output.exists(): - if not output.is_dir(): - raise ValueError(f"refusing to replace non-directory output: {output}") - shutil.rmtree(output) - output.mkdir(parents=True) - return output +from cuda_python_test_helpers.cython_test_builder import build_cython_tests def main() -> None: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--output-dir") - args = parser.parse_args() - script_dir = Path(__file__).resolve().parent - output = _output_directory(script_dir, args.output_dir) if args.output_dir else None - pyx_files = sorted(str(p) for p in script_dir.glob("test_*.pyx")) - if not pyx_files: - raise SystemExit(f"no test_*.pyx files under {script_dir}") - - ext_modules = cythonize( - pyx_files, - language_level=3, - include_path=[str(_bindings_source_root())], - compiler_directives={"freethreading_compatible": True}, + build_cython_tests( + script_file=__file__, + distribution_name="cuda_core_cython_tests", + include_core_headers=True, ) - # `build_ext --inplace` places the compiled .so relative to the current - # working directory, but pixi runs this task from the project root. pytest - # imports each extension by bare module name (see test_cython.py), which - # only resolves when the .so sits in tests/cython (the dir pytest puts on - # sys.path). chdir here so the .so lands next to its .pyx regardless of the - # invoking cwd. - os.chdir(script_dir) - sys.argv = [sys.argv[0], "build_ext"] - if output is None: - sys.argv.append("--inplace") - else: - build_temp = output / ".build-temp" - sys.argv.extend(["--build-lib", str(output), "--build-temp", str(build_temp)]) - setup(name="cuda_core_cython_tests", ext_modules=ext_modules) - if output is not None: - if build_temp.exists(): - shutil.rmtree(build_temp) - for source in pyx_files: - matches = [ - path - for pattern in (f"{Path(source).stem}*.so", f"{Path(source).stem}*.pyd", f"{Path(source).stem}*.dylib") - for path in output.glob(pattern) - if path.is_file() - ] - if len(matches) != 1: - raise RuntimeError(f"expected one extension for {source} in {output}, found {len(matches)}") - if __name__ == "__main__": main() diff --git a/cuda_core/tests/cython/build_tests.sh b/cuda_core/tests/cython/build_tests.sh deleted file mode 100755 index 26acb0c6c2f..00000000000 --- a/cuda_core/tests/cython/build_tests.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -set -eo pipefail - -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -UNAME=$(uname) -if [ "$UNAME" == "Linux" ] ; then - SCRIPTPATH=$(dirname $(realpath "$0")) - export CPLUS_INCLUDE_PATH=${SCRIPTPATH}/../../cuda/core/_include:$CUDA_HOME/include:${CPLUS_INCLUDE_PATH:-} -elif [[ "$UNAME" == CYGWIN* || "$UNAME" == MINGW* || "$UNAME" == MSYS* ]] ; then - SCRIPTPATH="$(dirname $(cygpath -w $(realpath "$0")))" - CUDA_CORE_INCLUDE_PATH=$(echo "${SCRIPTPATH}\..\..\cuda\core\_include" | sed 's/\\/\\\\/g') - export CL="/I\"${CUDA_CORE_INCLUDE_PATH}\" /I\"${CUDA_HOME}\\include\" ${CL:-}" -else - exit 1 -fi - -# Use a Python driver so the cuda.bindings source root is resolved at -# runtime and passed via Cython's include_path -- avoids platform-specific -# PYTHONPATH separator handling and surfaces import errors as exceptions. -python "${SCRIPTPATH}/build_tests.py" "$@" diff --git a/cuda_core/tests/test_module.py b/cuda_core/tests/test_module.py index 25cf0e24de4..8eb8332ffbc 100644 --- a/cuda_core/tests/test_module.py +++ b/cuda_core/tests/test_module.py @@ -180,11 +180,14 @@ def get_saxpy_fatbin(init_cuda): def _read_saxpy_rdc(kind: str) -> bytes: """Read a pre-built saxpy RDC object or library. - In CI: produced by the build stage. + In CI: read from CUDA_CORE_TEST_BINARIES_DIR after the build stage. In local dev: auto-built on demand if nvcc is available; if you edit saxpy.cu, remove stale RDC files (i.e. saxpy.o, saxpy.a, or saxpy.lib) to force a rebuild. """ - binaries_dir = Path(__file__).parent / "test_binaries" + configured_dir = os.environ.get("CUDA_CORE_TEST_BINARIES_DIR") + if configured_dir == "": + raise ValueError("CUDA_CORE_TEST_BINARIES_DIR must not be empty") + binaries_dir = Path(configured_dir) if configured_dir is not None else Path(__file__).parent / "test_binaries" if kind == "object": rdc_path = binaries_dir / "saxpy.o" elif kind == "library": @@ -193,10 +196,29 @@ def _read_saxpy_rdc(kind: str) -> bytes: raise ValueError(f"unknown saxpy RDC kind: {kind!r}") if not rdc_path.is_file(): + if configured_dir is not None: + raise FileNotFoundError(f"RDC fixture configured by CUDA_CORE_TEST_BINARIES_DIR does not exist: {rdc_path}") _build_saxpy_rdc(binaries_dir) return rdc_path.read_bytes() +@pytest.mark.agent_authored(model="gpt-5.6-sol") +def test_read_saxpy_rdc_uses_configured_directory(tmp_path, monkeypatch): + expected = b"Moon-owned RDC fixture" + (tmp_path / "saxpy.o").write_bytes(expected) + monkeypatch.setenv("CUDA_CORE_TEST_BINARIES_DIR", str(tmp_path)) + + assert _read_saxpy_rdc("object") == expected + + +@pytest.mark.agent_authored(model="gpt-5.6-sol") +def test_read_saxpy_rdc_rejects_missing_configured_fixture(tmp_path, monkeypatch): + monkeypatch.setenv("CUDA_CORE_TEST_BINARIES_DIR", str(tmp_path)) + + with pytest.raises(FileNotFoundError, match="CUDA_CORE_TEST_BINARIES_DIR"): + _read_saxpy_rdc("object") + + def _subprocess_output(result: subprocess.CompletedProcess[str]) -> str: sections = [] if result.stdout: diff --git a/cuda_pathfinder/docs/build_docs.sh b/cuda_pathfinder/docs/build_docs.sh index baa1ac91259..047c8369167 100755 --- a/cuda_pathfinder/docs/build_docs.sh +++ b/cuda_pathfinder/docs/build_docs.sh @@ -1,104 +1,9 @@ -#!/bin/bash +#!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -set -ex +set -euo pipefail SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) -cd "${SCRIPT_DIR}" - -MOON_CI="0" -if [[ "$#" == "0" ]]; then - LATEST_ONLY="0" -elif [[ "$#" == "1" && "$1" == "latest-only" ]]; then - LATEST_ONLY="1" -elif [[ "$#" == "1" && "$1" == "moon-ci" ]]; then - MOON_CI="1" - DOCS_LATEST_ONLY="${CUDA_PYTHON_DOCS_LATEST_ONLY:-true}" - case "${DOCS_LATEST_ONLY,,}" in - 1|true) LATEST_ONLY="1" ;; - 0|false) LATEST_ONLY="0" ;; - *) - echo "CUDA_PYTHON_DOCS_LATEST_ONLY must be true, false, 1, or 0" >&2 - exit 1 - ;; - esac -else - echo "usage: ./build_docs.sh [latest-only|moon-ci]" - exit 1 -fi - -if [[ "${MOON_CI}" == "1" ]]; then - if [[ -L build || ( -e build && ! -d build ) ]]; then - echo "refusing to replace non-directory docs build output: ${SCRIPT_DIR}/build" >&2 - exit 1 - fi - rm -rf build -fi - -# SPHINX_CUDA_PATHFINDER_VER is used to create a subdir under build/html -# (the Makefile file for sphinx-build also honors it if defined). -# If there's a post release (ex: .post1) we don't want it to show up in the -# version selector or directory structure. -if [[ -z "${SPHINX_CUDA_PATHFINDER_VER}" ]]; then - export SPHINX_CUDA_PATHFINDER_VER=$(python -c "from importlib.metadata import version; \ - ver = '.'.join(str(version('cuda-pathfinder')).split('.')[:3]); \ - print(ver)" \ - | awk -F'+' '{print $1}') -fi - -if [[ "${LATEST_ONLY}" == "1" && -z "${BUILD_PREVIEW:-}" && -z "${BUILD_LATEST:-}" ]]; then - export BUILD_LATEST=1 -fi - -# build the docs (in parallel) -if [[ -z "${SPHINXOPTS:-}" ]]; then - HTML_SPHINXOPTS="-W --keep-going -j 4 -d build/.doctrees" -else - HTML_SPHINXOPTS="${SPHINXOPTS}" -fi -SPHINXOPTS="${HTML_SPHINXOPTS}" make html - -# for debugging/developing (conf.py), please comment out the above line and -# use the line below instead, as we must build in serial to avoid getting -# obsecure Sphinx errors -#SPHINXOPTS="-v" make html - -# to support version dropdown menu -cp ./nv-versions.json build/html - -# to have a redirection page (to the latest docs) -cp source/_templates/main.html build/html/index.html - -# ensure that the latest docs is the one we built -if [[ $LATEST_ONLY == "0" ]]; then - cp -r build/html/${SPHINX_CUDA_PATHFINDER_VER} build/html/latest -else - mv build/html/${SPHINX_CUDA_PATHFINDER_VER} build/html/latest -fi - -# ensure that the Sphinx reference uses the latest docs -cp build/html/latest/objects.inv build/html - -if [[ "${MOON_CI}" == "1" ]]; then - SOURCE="${SCRIPT_DIR}/build/html" - OUTPUT_ROOT="${SCRIPT_DIR}/../.moon-out" - OUTPUT="${OUTPUT_ROOT}/docs-ci" - if [[ -L "${SOURCE}" || ! -d "${SOURCE}" ]]; then - echo "documentation output not found: ${SOURCE}" >&2 - exit 1 - fi - if [[ -L "${OUTPUT_ROOT}" || ( -e "${OUTPUT_ROOT}" && ! -d "${OUTPUT_ROOT}" ) ]]; then - echo "refusing to use non-directory Moon output root: ${OUTPUT_ROOT}" >&2 - exit 1 - fi - if [[ -L "${OUTPUT}" || ( -e "${OUTPUT}" && ! -d "${OUTPUT}" ) ]]; then - echo "refusing to replace non-directory Moon docs output: ${OUTPUT}" >&2 - exit 1 - fi - mkdir -p "${OUTPUT_ROOT}" - rm -rf "${OUTPUT}" - mkdir -p "${OUTPUT}" - cp -aL "${SOURCE}/." "${OUTPUT}/" -fi +exec "${SCRIPT_DIR}/../../cuda_python/docs/build_component_docs.sh" cuda-pathfinder "$@" diff --git a/cuda_pathfinder/moon.yml b/cuda_pathfinder/moon.yml index 4b45d38969e..390b0771cd3 100644 --- a/cuda_pathfinder/moon.yml +++ b/cuda_pathfinder/moon.yml @@ -6,15 +6,10 @@ $schema: https://moonrepo.dev/schemas/v2/project.json language: unknown layer: library -toolchains: - default: system - -taskOptions: - cache: false - runFromWorkspaceRoot: true - runInCI: false - shell: false - +tags: [docs-package, installed-test-package, pixi-package, pure-wheel-package, python-package] +dependsOn: + - id: test-helpers + scope: development fileGroups: package: - '.git_archival.txt' @@ -22,59 +17,23 @@ fileGroups: - 'DESCRIPTION.rst' - 'LICENSE' - 'pyproject.toml' - tests: - - 'examples/**/*' - - 'tests/**/*' - - 'pixi.toml' - - 'pixi.lock' - docs: - - 'docs/**/*' - - 'pixi.toml' - - 'pixi.lock' - tasks: - test: - command: bash - args: - - -euo - - pipefail - - -c - - | - PIXI_ENVIRONMENT=$(printenv PIXI_ENVIRONMENT_NAME || true) - if [[ -n "$PIXI_ENVIRONMENT" ]]; then - exec pixi run --manifest-path cuda_pathfinder/pixi.toml \ - --environment "$PIXI_ENVIRONMENT" test - fi - exec pixi run --manifest-path cuda_pathfinder/pixi.toml test - inputs: - - '@group(package)' - - '@group(tests)' - type: test + fingerprint-package: + checks: + - check: fingerprint + script: >- + python -c "import subprocess; + subprocess.run(['git', 'describe', '--always', '--dirty', '--tags', '--long', + '--match', 'cuda-pathfinder-v*[0-9]*'], check=True)" + hash: stdout + options: + mergeChecks: replace test-installed-linux: - command: bash - args: [ci/tools/run-tests, pathfinder] - inputs: - - '@group(package)' - - '@group(tests)' - - '/ci/tools/run-tests' - - '/ci/test-matrix.yml' - - '/ci/tools/env-vars' - - '/tests/**/*' - - '/.github/workflows/build-wheel.yml' - - '/ci/tools/guess_latest.sh' - - '/ci/tools/install_gpu_driver.sh' - - '/.github/workflows/test-wheel-linux.yml' env: CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS: see_what_works CUDA_PATHFINDER_TEST_FIND_NVIDIA_HEADERS_STRICTNESS: see_what_works CUDA_PATHFINDER_TEST_FIND_NVIDIA_BITCODE_LIB_STRICTNESS: see_what_works - tags: [ci-test-linux, runner-test-linux] - type: test - options: - mutex: ci-python-gpu - os: linux - runInCI: true prepare-strict-linux: command: bash @@ -105,151 +64,54 @@ tasks: - '@group(tests)' - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' - '/.github/workflows/test-wheel-linux.yml' - tags: [ci-test-linux, runner-test-linux] + tags: [ci-test-linux] options: mutex: ci-python-gpu os: linux runInCI: true test-installed-linux-strict: - command: bash - args: [ci/tools/run-tests, pathfinder] - inputs: - - '@group(package)' - - '@group(tests)' - - '/ci/tools/run-tests' - - '/ci/test-matrix.yml' - - '/ci/tools/env-vars' - - '/tests/**/*' - - '/.github/workflows/build-wheel.yml' - - '/ci/tools/guess_latest.sh' - - '/ci/tools/install_gpu_driver.sh' - - '/.github/workflows/test-wheel-linux.yml' + extends: test-installed-linux deps: - pathfinder:prepare-strict-linux env: CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS: all_must_work CUDA_PATHFINDER_TEST_FIND_NVIDIA_HEADERS_STRICTNESS: all_must_work CUDA_PATHFINDER_TEST_FIND_NVIDIA_BITCODE_LIB_STRICTNESS: all_must_work - tags: [ci-test-linux, runner-test-linux] - type: test - options: - mutex: ci-python-gpu - os: linux - runInCI: true test-installed-windows: - command: bash - args: [ci/tools/run-tests, pathfinder] - inputs: - - '@group(package)' - - '@group(tests)' - - '/ci/tools/run-tests' - - '/ci/test-matrix.yml' - - '/ci/tools/env-vars' - - '/tests/**/*' - - '/.github/workflows/build-wheel.yml' - - '/ci/tools/configure_driver_mode.ps1' - - '/ci/tools/install_gpu_driver.ps1' - - '/.github/workflows/test-wheel-windows.yml' env: CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS: see_what_works CUDA_PATHFINDER_TEST_FIND_NVIDIA_HEADERS_STRICTNESS: see_what_works CUDA_PATHFINDER_TEST_FIND_NVIDIA_BITCODE_LIB_STRICTNESS: see_what_works - tags: [ci-test-windows, runner-test-windows] - type: test - options: - mutex: ci-python-gpu - os: windows - runInCI: true prepare-strict-windows: - command: bash - args: - - -euo - - pipefail - - -c - - | - [[ "${TEST_CUDA_MAJOR}" =~ ^[0-9]+$ ]] || { - echo "TEST_CUDA_MAJOR must be a numeric CUDA major version" >&2 - exit 1 - } - shopt -s nullglob - set -- cuda_pathfinder/.moon-out/wheel-pure/*.whl - [[ $# -eq 1 ]] || { - echo "expected one pathfinder wheel, found $#" >&2 - exit 1 - } - python -m pip install --only-binary=:all: --verbose "$1" \ - --group "cuda_pathfinder/pyproject.toml:test-cu${TEST_CUDA_MAJOR}" - python -m pip list + extends: prepare-strict-linux deps: - pathfinder:test-installed-windows - env: - TEST_CUDA_MAJOR: '${TEST_CUDA_MAJOR}' inputs: - '@group(package)' - '@group(tests)' - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' - '/.github/workflows/test-wheel-windows.yml' - tags: [ci-test-windows, runner-test-windows] + tags: [ci-test-windows] options: - mutex: ci-python-gpu + mergeDeps: replace + mergeInputs: replace + mergeTags: replace os: windows - runInCI: true test-installed-windows-strict: - command: bash - args: [ci/tools/run-tests, pathfinder] - inputs: - - '@group(package)' - - '@group(tests)' - - '/ci/tools/run-tests' - - '/ci/test-matrix.yml' - - '/ci/tools/env-vars' - - '/tests/**/*' - - '/.github/workflows/build-wheel.yml' - - '/ci/tools/configure_driver_mode.ps1' - - '/ci/tools/install_gpu_driver.ps1' - - '/.github/workflows/test-wheel-windows.yml' + extends: test-installed-windows deps: - pathfinder:prepare-strict-windows env: CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS: all_must_work CUDA_PATHFINDER_TEST_FIND_NVIDIA_HEADERS_STRICTNESS: all_must_work CUDA_PATHFINDER_TEST_FIND_NVIDIA_BITCODE_LIB_STRICTNESS: all_must_work - tags: [ci-test-windows, runner-test-windows] - type: test - options: - mutex: ci-python-gpu - os: windows - runInCI: true docs: - command: pixi - args: [run, --manifest-path, cuda_pathfinder/pixi.toml, --environment, docs, build-docs] - inputs: - - '@group(package)' - - '@group(docs)' - - '/.github/workflows/build-docs.yml' - - docs-ci: - command: bash - args: [cuda_pathfinder/docs/build_docs.sh, moon-ci] - env: - CUDA_PYTHON_DOCS_LATEST_ONLY: '${CUDA_PYTHON_DOCS_LATEST_ONLY}' - inputs: - - '@group(package)' - - '@group(docs)' - - '/cuda_python/docs/environment-docs.yml' - - '/.github/workflows/build-docs.yml' - outputs: - - '.moon-out/docs-ci' - tags: [ci-docs, runner-docs] - type: build - options: - os: linux - runInCI: true + args: [build-docs] wheel-pure: command: bash @@ -269,6 +131,15 @@ tasks: mkdir -p cuda_pathfinder/.moon-out rm -rf -- cuda_pathfinder/.moon-out/wheel-pure mkdir -p cuda_pathfinder/.moon-out/wheel-pure + SOURCE_DATE_EPOCH=$(printenv SOURCE_DATE_EPOCH || true) + export SOURCE_DATE_EPOCH + [[ -n "$SOURCE_DATE_EPOCH" ]] || SOURCE_DATE_EPOCH=$(git log -1 --format=%ct HEAD) + BUILD_CONSTRAINTS=$(realpath ci/build-constraints.txt) + case "$(uname -s)" in + CYGWIN*|MINGW*|MSYS*) BUILD_CONSTRAINTS=$(cygpath -w "$BUILD_CONSTRAINTS") ;; + esac + export PIP_BUILD_CONSTRAINT="$BUILD_CONSTRAINTS" + export PIP_CONSTRAINT="$BUILD_CONSTRAINTS" python -m pip wheel --verbose --no-deps \ --wheel-dir cuda_pathfinder/.moon-out/wheel-pure ./cuda_pathfinder shopt -s nullglob @@ -277,114 +148,4 @@ tasks: echo "expected one cuda.pathfinder wheel, found $#" >&2 exit 1 } - inputs: - - '@group(package)' - - '/.github/workflows/build-wheel.yml' - outputs: - - '.moon-out/wheel-pure' - checks: - - &pathfinder_scm_fingerprint - check: fingerprint - script: git describe --always --dirty --tags --long --match 'cuda-pathfinder-v*[0-9]*' - hash: stdout - - &scm_environment_fingerprint - check: fingerprint - script: >- - python -c "import hashlib, os; - names = sorted(name for name in os.environ - if name == 'SOURCE_DATE_EPOCH' or name.startswith(('SETUPTOOLS_SCM_', 'VCS_VERSIONING_'))); - payload = '\0'.join(name + '=' + os.environ[name] for name in names); - print(hashlib.sha256(payload.encode()).hexdigest())" - hash: stdout - - &python_runtime_fingerprint - check: fingerprint - script: >- - python -c "import platform, sysconfig; - print(platform.python_implementation(), platform.python_version(), - sysconfig.get_config_var('SOABI') or '', sep='\n')" - hash: stdout - - &python_build_tools_fingerprint - check: fingerprint - script: >- - python -c "import importlib.metadata as metadata; - names = ('build', 'cibuildwheel', 'packaging', 'pip', 'setuptools', 'setuptools-scm', 'wheel'); - normalize = lambda value: value.lower().replace('_', '-'); - versions = {normalize(dist.metadata['Name']): dist.version for dist in metadata.distributions() if dist.metadata['Name']}; - print(*(name + '=' + versions.get(name, '') for name in names), sep='\n')" - hash: stdout - tags: [runner-build-linux-64, runner-build-linux-aarch64, runner-build-windows] - type: build - options: - cache: true - cacheKey: wheel-pure-v2 - priority: critical - runInCI: true - - sdist: - command: bash - args: - - -euo - - pipefail - - -c - - | - if [[ -L cuda_pathfinder/.moon-out || ( -e cuda_pathfinder/.moon-out && ! -d cuda_pathfinder/.moon-out ) ]]; then - echo "refusing to use non-directory output root: cuda_pathfinder/.moon-out" >&2 - exit 1 - fi - if [[ -L cuda_pathfinder/.moon-out/sdist || ( -e cuda_pathfinder/.moon-out/sdist && ! -d cuda_pathfinder/.moon-out/sdist ) ]]; then - echo "refusing to replace non-directory output: cuda_pathfinder/.moon-out/sdist" >&2 - exit 1 - fi - mkdir -p cuda_pathfinder/.moon-out - rm -rf -- cuda_pathfinder/.moon-out/sdist - mkdir -p cuda_pathfinder/.moon-out/sdist - python -m build --sdist --outdir cuda_pathfinder/.moon-out/sdist cuda_pathfinder - shopt -s nullglob - set -- cuda_pathfinder/.moon-out/sdist/*.tar.gz - [[ $# -eq 1 ]] || { - echo "expected one cuda.pathfinder source distribution, found $#" >&2 - exit 1 - } - ARCHIVE=$1 - python -m pip wheel --no-deps \ - --wheel-dir cuda_pathfinder/.moon-out/sdist "$ARCHIVE" - set -- cuda_pathfinder/.moon-out/sdist/*.whl - [[ $# -eq 1 ]] || { - echo "expected one cuda.pathfinder wheel from source distribution, found $#" >&2 - exit 1 - } - inputs: - - '@group(package)' - - '/.github/workflows/test-sdist-linux.yml' - - '/.github/workflows/test-sdist-windows.yml' - outputs: - - '.moon-out/sdist' - checks: - - *pathfinder_scm_fingerprint - - *scm_environment_fingerprint - - *python_runtime_fingerprint - - *python_build_tools_fingerprint - - check: fingerprint - script: >- - python -c "import platform; - print(platform.system(), platform.machine(), sep='\n')" - hash: stdout - - check: fingerprint - script: >- - python -c "import hashlib, os; - names = ('BUILD_CUDA_MAJOR', 'BUILD_CUDA_VER', 'BUILD_PREV_CUDA_MAJOR', - 'CC', 'CIBW_BEFORE_BUILD_LINUX', 'CIBW_BEFORE_BUILD_WINDOWS', - 'CIBW_BEFORE_TEST_LINUX', 'CIBW_BUILD', 'CIBW_ENABLE', 'CIBW_TEST_COMMAND', - 'CL', 'CPLUS_INCLUDE_PATH', 'CXX', 'CUDA_CORE_BUILD_MAJOR', 'CUDA_PATH', - 'CUDA_PYTHON_COVERAGE', 'CUDA_PYTHON_LANE', 'CUDA_PYTHON_PARALLEL_LEVEL', 'CUDA_VER', - 'HOST_PLATFORM', 'PY_EXT_SUFFIX', 'PY_VER', 'SCCACHE_CACHE_SIZE', - 'SCCACHE_DIR', 'SCCACHE_PATH'); - payload = '\0'.join(name + '=' + os.environ.get(name, '') for name in names); - print(hashlib.sha256(payload.encode()).hexdigest())" - hash: stdout - tags: [ci-sdist, runner-sdist-linux, runner-sdist-windows] - type: build - options: - cache: true - cacheKey: sdist-v2 - runInCI: true + tags: [ci-build-native] diff --git a/cuda_python/docs/assemble_moon_docs.sh b/cuda_python/docs/assemble_moon_docs.sh index 4a8b9aad81e..51fc6e628f3 100755 --- a/cuda_python/docs/assemble_moon_docs.sh +++ b/cuda_python/docs/assemble_moon_docs.sh @@ -36,7 +36,7 @@ copy_component() { cp -aL "${source}/." "${destination}/" } -copy_component "${REPO_ROOT}/cuda_python/.moon-out/docs-ci" "${OUTPUT}" -copy_component "${REPO_ROOT}/cuda_bindings/.moon-out/docs-ci" "${OUTPUT}/cuda-bindings" -copy_component "${REPO_ROOT}/cuda_core/.moon-out/docs-ci" "${OUTPUT}/cuda-core" -copy_component "${REPO_ROOT}/cuda_pathfinder/.moon-out/docs-ci" "${OUTPUT}/cuda-pathfinder" +copy_component "${REPO_ROOT}/cuda_python/docs/build/html" "${OUTPUT}" +copy_component "${REPO_ROOT}/cuda_bindings/docs/build/html" "${OUTPUT}/cuda-bindings" +copy_component "${REPO_ROOT}/cuda_core/docs/build/html" "${OUTPUT}/cuda-core" +copy_component "${REPO_ROOT}/cuda_pathfinder/docs/build/html" "${OUTPUT}/cuda-pathfinder" diff --git a/cuda_python/docs/build_component_docs.sh b/cuda_python/docs/build_component_docs.sh new file mode 100755 index 00000000000..91c874068cf --- /dev/null +++ b/cuda_python/docs/build_component_docs.sh @@ -0,0 +1,171 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euxo pipefail + +if [[ $# -lt 1 ]]; then + echo "usage: build_component_docs.sh COMPONENT [latest-only|moon-ci]" >&2 + exit 1 +fi + +COMPONENT=$1 +shift + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +REPO_ROOT=$(cd -- "${SCRIPT_DIR}/../.." && pwd) + +case "${COMPONENT}" in + cuda-pathfinder) + PACKAGE_DIR=cuda_pathfinder + DISTRIBUTION=cuda-pathfinder + VERSION_ENV=SPHINX_CUDA_PATHFINDER_VER + VERSION_COMPONENTS=3 + DEFAULT_SPHINXOPTS="-W --keep-going -j 4 -d build/.doctrees" + HONOR_SPHINXOPTS=1 + METADATA_FILES=(nv-versions.json) + CLEAN_GENERATED=0 + ;; + cuda-bindings) + PACKAGE_DIR=cuda_bindings + DISTRIBUTION=cuda-bindings + VERSION_ENV=SPHINX_CUDA_BINDINGS_VER + VERSION_COMPONENTS=3 + DEFAULT_SPHINXOPTS="-j 4 -d build/.doctrees" + HONOR_SPHINXOPTS=1 + METADATA_FILES=(versions.json nv-versions.json) + CLEAN_GENERATED=0 + ;; + cuda-core) + PACKAGE_DIR=cuda_core + DISTRIBUTION=cuda-core + VERSION_ENV=SPHINX_CUDA_CORE_VER + VERSION_COMPONENTS=0 + DEFAULT_SPHINXOPTS="-W --keep-going -j 4 -d build/.doctrees" + HONOR_SPHINXOPTS=1 + METADATA_FILES=(versions.json nv-versions.json) + CLEAN_GENERATED=1 + ;; + cuda-python) + PACKAGE_DIR=cuda_python + DISTRIBUTION=cuda-python + VERSION_ENV=SPHINX_CUDA_PYTHON_VER + VERSION_COMPONENTS=3 + DEFAULT_SPHINXOPTS="-j 4 -d build/.doctrees" + # Preserve the metapackage builder's historical fixed Sphinx options. + HONOR_SPHINXOPTS=0 + METADATA_FILES=(versions.json nv-versions.json) + CLEAN_GENERATED=1 + ;; + *) + echo "unsupported documentation component: ${COMPONENT}" >&2 + exit 1 + ;; +esac + +DOCS_DIR="${REPO_ROOT}/${PACKAGE_DIR}/docs" +if [[ -L "${DOCS_DIR}" || ! -d "${DOCS_DIR}" ]]; then + echo "documentation source directory not found: ${DOCS_DIR}" >&2 + exit 1 +fi +SOURCE_DIR="${DOCS_DIR}/source" +if [[ -L "${SOURCE_DIR}" || ! -d "${SOURCE_DIR}" ]]; then + echo "documentation source directory not found: ${SOURCE_DIR}" >&2 + exit 1 +fi +cd "${DOCS_DIR}" + +MOON_CI=0 +if [[ $# == 0 ]]; then + LATEST_ONLY=0 +elif [[ $# == 1 && $1 == latest-only ]]; then + LATEST_ONLY=1 +elif [[ $# == 1 && $1 == moon-ci ]]; then + MOON_CI=1 + DOCS_LATEST_ONLY=${CUDA_PYTHON_DOCS_LATEST_ONLY:-true} + case "${DOCS_LATEST_ONLY,,}" in + 1|true) LATEST_ONLY=1 ;; + 0|false) LATEST_ONLY=0 ;; + *) + echo "CUDA_PYTHON_DOCS_LATEST_ONLY must be true, false, 1, or 0" >&2 + exit 1 + ;; + esac +else + echo "usage: ./build_docs.sh [latest-only|moon-ci]" >&2 + exit 1 +fi + +if [[ "${LATEST_ONLY}" == 1 && -z "${BUILD_PREVIEW:-}" && -z "${BUILD_LATEST:-}" ]]; then + export BUILD_LATEST=1 +fi + +VERSION_VALUE=${!VERSION_ENV-} +if [[ -z "${VERSION_VALUE}" ]]; then + VERSION_VALUE=$(python -c \ + "from importlib.metadata import version; import sys; value = version(sys.argv[1]); count = int(sys.argv[2]); print('.'.join(value.split('.')[:count]) if count else value)" \ + "${DISTRIBUTION}" "${VERSION_COMPONENTS}") + VERSION_VALUE=${VERSION_VALUE%%+*} +fi +case "${VERSION_VALUE}" in + ""|.|..|latest|*/*) + echo "${VERSION_ENV} must name a safe version directory other than latest" >&2 + exit 1 + ;; +esac +export "${VERSION_ENV}=${VERSION_VALUE}" + +BUILD_DIR="${DOCS_DIR}/build" +if [[ -L "${BUILD_DIR}" || ( -e "${BUILD_DIR}" && ! -d "${BUILD_DIR}" ) ]]; then + echo "refusing to use non-directory docs build output: ${BUILD_DIR}" >&2 + exit 1 +fi +if [[ "${MOON_CI}" == 1 ]]; then + rm -rf -- "${BUILD_DIR}" +fi + +EFFECTIVE_SPHINXOPTS=${DEFAULT_SPHINXOPTS} +if [[ "${HONOR_SPHINXOPTS}" == 1 && -n "${SPHINXOPTS:-}" ]]; then + EFFECTIVE_SPHINXOPTS=${SPHINXOPTS} +fi +SPHINXOPTS="${EFFECTIVE_SPHINXOPTS}" make html + +BUILD_HTML="${BUILD_DIR}/html" +VERSION_OUTPUT="${BUILD_HTML}/${VERSION_VALUE}" +if [[ -L "${BUILD_HTML}" || ! -d "${BUILD_HTML}" ]]; then + echo "documentation output not found: ${BUILD_HTML}" >&2 + exit 1 +fi +if [[ -L "${VERSION_OUTPUT}" || ! -d "${VERSION_OUTPUT}" ]]; then + echo "versioned documentation output not found: ${VERSION_OUTPUT}" >&2 + exit 1 +fi + +for metadata_file in "${METADATA_FILES[@]}"; do + cp -- "${DOCS_DIR}/${metadata_file}" "${BUILD_HTML}/" +done +cp -- "${SOURCE_DIR}/_templates/main.html" "${BUILD_HTML}/index.html" + +LATEST_OUTPUT="${BUILD_HTML}/latest" +if [[ -L "${LATEST_OUTPUT}" || ( -e "${LATEST_OUTPUT}" && ! -d "${LATEST_OUTPUT}" ) ]]; then + echo "refusing to replace non-directory latest docs output: ${LATEST_OUTPUT}" >&2 + exit 1 +fi +rm -rf -- "${LATEST_OUTPUT}" +if [[ "${LATEST_ONLY}" == 0 ]]; then + cp -a -- "${VERSION_OUTPUT}" "${LATEST_OUTPUT}" +else + mv -- "${VERSION_OUTPUT}" "${LATEST_OUTPUT}" +fi + +cp -- "${LATEST_OUTPUT}/objects.inv" "${BUILD_HTML}/" + +if [[ "${CLEAN_GENERATED}" == 1 ]]; then + GENERATED_DIR="${SOURCE_DIR}/generated" + if [[ -L "${GENERATED_DIR}" ]]; then + echo "refusing to remove symlinked generated docs directory: ${GENERATED_DIR}" >&2 + exit 1 + fi + rm -rf -- "${GENERATED_DIR}" +fi diff --git a/cuda_python/docs/build_docs.sh b/cuda_python/docs/build_docs.sh index 237cd176e73..e5dae4768e0 100755 --- a/cuda_python/docs/build_docs.sh +++ b/cuda_python/docs/build_docs.sh @@ -1,103 +1,9 @@ -#!/bin/bash +#!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -set -ex +set -euo pipefail SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) -cd "${SCRIPT_DIR}" - -MOON_CI="0" -if [[ "$#" == "0" ]]; then - LATEST_ONLY="0" -elif [[ "$#" == "1" && "$1" == "latest-only" ]]; then - LATEST_ONLY="1" -elif [[ "$#" == "1" && "$1" == "moon-ci" ]]; then - MOON_CI="1" - DOCS_LATEST_ONLY="${CUDA_PYTHON_DOCS_LATEST_ONLY:-true}" - case "${DOCS_LATEST_ONLY,,}" in - 1|true) LATEST_ONLY="1" ;; - 0|false) LATEST_ONLY="0" ;; - *) - echo "CUDA_PYTHON_DOCS_LATEST_ONLY must be true, false, 1, or 0" >&2 - exit 1 - ;; - esac -else - echo "usage: ./build_docs.sh [latest-only|moon-ci]" - exit 1 -fi - -if [[ "${MOON_CI}" == "1" ]]; then - if [[ -L build || ( -e build && ! -d build ) ]]; then - echo "refusing to replace non-directory docs build output: ${SCRIPT_DIR}/build" >&2 - exit 1 - fi - rm -rf build -fi - -# SPHINX_CUDA_PYTHON_VER is used to create a subdir under build/html -# (the Makefile file for sphinx-build also honors it if defined). -# If there's a post release (ex: .post1) we don't want it to show up in the -# version selector or directory structure. -if [[ -z "${SPHINX_CUDA_PYTHON_VER}" ]]; then - export SPHINX_CUDA_PYTHON_VER=$(python -c "from importlib.metadata import version; \ - ver = '.'.join(str(version('cuda-python')).split('.')[:3]); \ - print(ver)" \ - | awk -F'+' '{print $1}') -fi - -if [[ "${LATEST_ONLY}" == "1" && -z "${BUILD_PREVIEW:-}" && -z "${BUILD_LATEST:-}" ]]; then - export BUILD_LATEST=1 -fi - -# build the docs (in parallel) -SPHINXOPTS="-j 4 -d build/.doctrees" make html - -# for debugging/developing (conf.py), please comment out the above line and -# use the line below instead, as we must build in serial to avoid getting -# obsecure Sphinx errors -#SPHINXOPTS="-v" make html - -# to support version dropdown menu -cp ./versions.json build/html -cp ./nv-versions.json build/html - -# to have a redirection page (to the latest docs) -cp source/_templates/main.html build/html/index.html - -# ensure that the latest docs is the one we built -if [[ $LATEST_ONLY == "0" ]]; then - cp -r build/html/${SPHINX_CUDA_PYTHON_VER} build/html/latest -else - mv build/html/${SPHINX_CUDA_PYTHON_VER} build/html/latest -fi - -# ensure that the Sphinx reference uses the latest docs -cp build/html/latest/objects.inv build/html - -# clean up previously auto-generated files -rm -rf source/generated/ - -if [[ "${MOON_CI}" == "1" ]]; then - SOURCE="${SCRIPT_DIR}/build/html" - OUTPUT_ROOT="${SCRIPT_DIR}/../.moon-out" - OUTPUT="${OUTPUT_ROOT}/docs-ci" - if [[ -L "${SOURCE}" || ! -d "${SOURCE}" ]]; then - echo "documentation output not found: ${SOURCE}" >&2 - exit 1 - fi - if [[ -L "${OUTPUT_ROOT}" || ( -e "${OUTPUT_ROOT}" && ! -d "${OUTPUT_ROOT}" ) ]]; then - echo "refusing to use non-directory Moon output root: ${OUTPUT_ROOT}" >&2 - exit 1 - fi - if [[ -L "${OUTPUT}" || ( -e "${OUTPUT}" && ! -d "${OUTPUT}" ) ]]; then - echo "refusing to replace non-directory Moon docs output: ${OUTPUT}" >&2 - exit 1 - fi - mkdir -p "${OUTPUT_ROOT}" - rm -rf "${OUTPUT}" - mkdir -p "${OUTPUT}" - cp -aL "${SOURCE}/." "${OUTPUT}/" -fi +exec "${SCRIPT_DIR}/build_component_docs.sh" cuda-python "$@" diff --git a/cuda_python/docs/environment-docs.yml b/cuda_python/docs/environment-docs.yml index 3152f0a3a93..7db7da07002 100644 --- a/cuda_python/docs/environment-docs.yml +++ b/cuda_python/docs/environment-docs.yml @@ -5,8 +5,7 @@ name: cuda-python-docs channels: - conda-forge dependencies: - # ATTENTION: This dependency list is duplicated in - # toolshed/setup-docs-env.sh. Please KEEP THEM IN SYNC! + - python =3.12 - cython >=3.2.5,<3.3 - myst-parser - numpy diff --git a/cuda_python/moon.yml b/cuda_python/moon.yml index 4d46c43d4e3..f034f33c0a5 100644 --- a/cuda_python/moon.yml +++ b/cuda_python/moon.yml @@ -6,19 +6,13 @@ $schema: https://moonrepo.dev/schemas/v2/project.json language: unknown layer: library +tags: [docs-package, installed-test-package, pure-wheel-package, python-package] dependsOn: - pathfinder - bindings - core -toolchains: - default: system - -taskOptions: - cache: false - runFromWorkspaceRoot: true - runInCI: false - shell: false - + - id: test-helpers + scope: development fileGroups: package: - 'DESCRIPTION.rst' @@ -67,6 +61,15 @@ tasks: export SETUPTOOLS_SCM_PRETEND_VERSION="$BINDINGS_VERSION" export SETUPTOOLS_SCM_PRETEND_VERSION_FOR_CUDA_PYTHON="$BINDINGS_VERSION" fi + SOURCE_DATE_EPOCH=$(printenv SOURCE_DATE_EPOCH || true) + export SOURCE_DATE_EPOCH + [[ -n "$SOURCE_DATE_EPOCH" ]] || SOURCE_DATE_EPOCH=$(git log -1 --format=%ct HEAD) + BUILD_CONSTRAINTS=$(realpath ci/build-constraints.txt) + case "$(uname -s)" in + CYGWIN*|MINGW*|MSYS*) BUILD_CONSTRAINTS=$(cygpath -w "$BUILD_CONSTRAINTS") ;; + esac + export PIP_BUILD_CONSTRAINT="$BUILD_CONSTRAINTS" + export PIP_CONSTRAINT="$BUILD_CONSTRAINTS" python -m pip wheel --verbose --no-deps \ --wheel-dir cuda_python/.moon-out/wheel-pure ./cuda_python shopt -s nullglob @@ -75,203 +78,46 @@ tasks: echo "expected one cuda-python metapackage wheel, found $#" >&2 exit 1 } + env: + CUDA_PYTHON_USE_STAGED_BINDINGS_VERSION: '${CUDA_PYTHON_USE_STAGED_BINDINGS_VERSION}' inputs: - - '@group(package)' + # CI opts into the staged bindings version. Source proxies select this + # task before the staged dependency wheels are materialized. + - {project: pathfinder, group: package} - {project: bindings, group: package} - '/cuda_bindings/.moon-out/wheel-current/*.whl' - - '/.github/workflows/build-wheel.yml' - outputs: - - '.moon-out/wheel-pure' - checks: - - &metapackage_scm_fingerprint - check: fingerprint - script: git describe --always --dirty --tags --long --match 'v*[0-9]*' - hash: stdout - - &scm_environment_fingerprint - check: fingerprint - script: >- - python -c "import hashlib, os; - names = sorted(name for name in os.environ - if name in ('CUDA_PYTHON_USE_STAGED_BINDINGS_VERSION', 'SOURCE_DATE_EPOCH') - or name.startswith(('SETUPTOOLS_SCM_', 'VCS_VERSIONING_'))); - payload = '\0'.join(name + '=' + os.environ[name] for name in names); - print(hashlib.sha256(payload.encode()).hexdigest())" - hash: stdout - - &python_runtime_fingerprint - check: fingerprint - script: >- - python -c "import platform, sysconfig; - print(platform.python_implementation(), platform.python_version(), - sysconfig.get_config_var('SOABI') or '', sep='\n')" - hash: stdout - - &python_build_tools_fingerprint - check: fingerprint - script: >- - python -c "import importlib.metadata as metadata; - names = ('build', 'cibuildwheel', 'packaging', 'pip', 'setuptools', 'setuptools-scm', 'wheel'); - normalize = lambda value: value.lower().replace('_', '-'); - versions = {normalize(dist.metadata['Name']): dist.version for dist in metadata.distributions() if dist.metadata['Name']}; - print(*(name + '=' + versions.get(name, '') for name in names), sep='\n')" - hash: stdout - type: build - options: - cache: true - cacheKey: wheel-pure-v3 - priority: critical - runInCI: true sdist: - command: bash - args: - - -euo - - pipefail - - -c - - | - if [[ -L cuda_python/.moon-out || ( -e cuda_python/.moon-out && ! -d cuda_python/.moon-out ) ]]; then - echo "refusing to use non-directory output root: cuda_python/.moon-out" >&2 - exit 1 - fi - if [[ -L cuda_python/.moon-out/sdist || ( -e cuda_python/.moon-out/sdist && ! -d cuda_python/.moon-out/sdist ) ]]; then - echo "refusing to replace non-directory output: cuda_python/.moon-out/sdist" >&2 - exit 1 - fi - mkdir -p cuda_python/.moon-out - rm -rf -- cuda_python/.moon-out/sdist - mkdir -p cuda_python/.moon-out/sdist - python -m build --sdist --outdir cuda_python/.moon-out/sdist cuda_python - shopt -s nullglob - set -- cuda_python/.moon-out/sdist/*.tar.gz - [[ $# -eq 1 ]] || { - echo "expected one cuda-python source distribution, found $#" >&2 - exit 1 - } - ARCHIVE=$1 - python -m pip wheel --no-deps \ - --wheel-dir cuda_python/.moon-out/sdist "$ARCHIVE" - set -- cuda_python/.moon-out/sdist/*.whl - [[ $# -eq 1 ]] || { - echo "expected one cuda-python wheel from source distribution, found $#" >&2 - exit 1 - } - deps: - - target: bindings:sdist - cacheStrategy: outputs inputs: - - '@group(package)' + - {project: pathfinder, group: package} - {project: bindings, group: package} - - '/.github/workflows/test-sdist-linux.yml' - - '/.github/workflows/test-sdist-windows.yml' - outputs: - - '.moon-out/sdist' - checks: - - *metapackage_scm_fingerprint - - *scm_environment_fingerprint - - *python_runtime_fingerprint - - *python_build_tools_fingerprint - - check: fingerprint - script: >- - python -c "import platform; - print(platform.system(), platform.machine(), sep='\n')" - hash: stdout - - check: fingerprint - script: >- - python -c "import hashlib, os; - names = ('BUILD_CUDA_MAJOR', 'BUILD_CUDA_VER', 'BUILD_PREV_CUDA_MAJOR', - 'CC', 'CIBW_BEFORE_BUILD_LINUX', 'CIBW_BEFORE_BUILD_WINDOWS', - 'CIBW_BEFORE_TEST_LINUX', 'CIBW_BUILD', 'CIBW_ENABLE', 'CIBW_TEST_COMMAND', - 'CL', 'CPLUS_INCLUDE_PATH', 'CXX', 'CUDA_CORE_BUILD_MAJOR', 'CUDA_PATH', - 'CUDA_PYTHON_COVERAGE', 'CUDA_PYTHON_LANE', 'CUDA_PYTHON_PARALLEL_LEVEL', 'CUDA_VER', - 'HOST_PLATFORM', 'PY_EXT_SUFFIX', 'PY_VER', 'SCCACHE_CACHE_SIZE', - 'SCCACHE_DIR', 'SCCACHE_PATH'); - payload = '\0'.join(name + '=' + os.environ.get(name, '') for name in names); - print(hashlib.sha256(payload.encode()).hexdigest())" - hash: stdout - tags: [ci-sdist, runner-sdist-linux, runner-sdist-windows] - type: build - options: - cache: true - cacheKey: sdist-v2 - runInCI: true test-installed-linux: - command: bash - args: [ci/tools/run-tests, metapackage] deps: - target: metapackage:wheel-pure cacheStrategy: outputs inputs: - - '@group(package)' - {project: pathfinder, group: package} - {project: bindings, group: package} - {project: core, group: package} - '/cuda_core/.moon-out/wheel-merged/*.whl' - '/cuda_python/.moon-out/wheel-pure/*.whl' - - '/ci/tools/run-tests' - - '/ci/test-matrix.yml' - - '/ci/tools/env-vars' - - '/tests/**/*' - '/ci/tools/merge_cuda_core_wheels.py' - - '/.github/workflows/build-wheel.yml' - - '/ci/tools/guess_latest.sh' - - '/ci/tools/install_gpu_driver.sh' - - '/.github/workflows/test-wheel-linux.yml' - tags: [ci-test-linux, runner-test-linux] - type: test - options: - mutex: ci-python-gpu - os: linux - runInCI: true test-installed-windows: - command: bash - args: [ci/tools/run-tests, metapackage] deps: - target: metapackage:wheel-pure cacheStrategy: outputs inputs: - - '@group(package)' - {project: pathfinder, group: package} - {project: bindings, group: package} - {project: core, group: package} - '/cuda_core/.moon-out/wheel-merged/*.whl' - '/cuda_python/.moon-out/wheel-pure/*.whl' - - '/ci/tools/run-tests' - - '/ci/test-matrix.yml' - - '/ci/tools/env-vars' - - '/tests/**/*' - '/ci/tools/merge_cuda_core_wheels.py' - - '/.github/workflows/build-wheel.yml' - - '/ci/tools/configure_driver_mode.ps1' - - '/ci/tools/install_gpu_driver.ps1' - - '/.github/workflows/test-wheel-windows.yml' - tags: [ci-test-windows, runner-test-windows] - type: test - options: - mutex: ci-python-gpu - os: windows - runInCI: true docs-ci: - command: bash - args: [cuda_python/docs/build_docs.sh, moon-ci] - deps: - - target: metapackage:wheel-pure - cacheStrategy: outputs - env: - CUDA_PYTHON_DOCS_LATEST_ONLY: '${CUDA_PYTHON_DOCS_LATEST_ONLY}' inputs: - - '@group(package)' - - '@group(docs)' - {project: pathfinder, group: package} - {project: bindings, group: package} - {project: core, group: package} - - '/cuda_python/.moon-out/wheel-pure/*.whl' - - '/cuda_python/docs/environment-docs.yml' - - '/.github/workflows/build-docs.yml' - outputs: - - '.moon-out/docs-ci' - tags: [ci-docs, runner-docs] - type: build - options: - os: linux - runInCI: true diff --git a/cuda_python_test_helpers/cuda_python_test_helpers/cython_test_builder.py b/cuda_python_test_helpers/cuda_python_test_helpers/cython_test_builder.py new file mode 100644 index 00000000000..8b7369fe64f --- /dev/null +++ b/cuda_python_test_helpers/cuda_python_test_helpers/cython_test_builder.py @@ -0,0 +1,147 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Shared builder for the cuda.bindings and cuda.core Cython test extensions.""" + +from __future__ import annotations + +import argparse +import os +import shutil +import sys +from collections.abc import MutableMapping, Sequence +from pathlib import Path + + +def _bindings_source_root() -> Path: + import cuda.bindings + + # cuda.bindings.__file__ -> ...//cuda/bindings/__init__.py + root = Path(cuda.bindings.__file__).resolve().parents[2] + if not (root / "cuda" / "bindings").is_dir(): + raise RuntimeError( + f"cuda.bindings source tree not found at {root}; pixi-build editable install layout may have changed." + ) + return root + + +def _output_directory(script_dir: Path, value: str) -> Path: + project_root = script_dir.parents[1] + output_root = project_root / ".moon-out" + requested = Path(value) + output = Path(os.path.abspath(requested if requested.is_absolute() else project_root.parent / requested)) + if output_root not in output.parents: + raise ValueError(f"output must be below {output_root}: {output}") + + current = output + while current != project_root: + if current.is_symlink(): + raise ValueError(f"output path must not traverse a symlink: {current}") + current = current.parent + + if output.exists(): + if not output.is_dir(): + raise ValueError(f"refusing to replace non-directory output: {output}") + shutil.rmtree(output) + output.mkdir(parents=True) + return output + + +def _set_compiler_include_paths( + include_dirs: Sequence[Path], + *, + environ: MutableMapping[str, str] | None = None, + platform_name: str | None = None, +) -> None: + environment = os.environ if environ is None else environ + platform_name = os.name if platform_name is None else platform_name + if platform_name == "nt": + flags = " ".join(f'/I"{path}"' for path in include_dirs) + environment["CL"] = " ".join(part for part in (flags, environment.get("CL", "")) if part) + else: + paths = [str(path) for path in include_dirs] + if existing := environment.get("CPLUS_INCLUDE_PATH"): + paths.append(existing) + environment["CPLUS_INCLUDE_PATH"] = ":".join(paths) + + +def _configure_compiler_includes(script_dir: Path, *, include_core_headers: bool) -> None: + cuda_root = os.environ.get("CUDA_HOME") or os.environ.get("CUDA_PATH") + if not cuda_root: + raise RuntimeError("CUDA_HOME or CUDA_PATH must identify the CUDA Toolkit") + + include_dirs = [] + if include_core_headers: + include_dirs.append(script_dir.parents[1] / "cuda" / "core" / "_include") + include_dirs.append(Path(cuda_root) / "include") + + missing = [path for path in include_dirs if not path.is_dir()] + if missing: + raise RuntimeError(f"required include directory does not exist: {missing[0]}") + _set_compiler_include_paths(include_dirs) + + +def build_cython_tests( + *, + script_file: str, + distribution_name: str, + include_core_headers: bool = False, + nthreads: int | None = None, +) -> None: + """Build all ``test_*.pyx`` siblings of *script_file*.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output-dir") + args = parser.parse_args() + + script_dir = Path(script_file).resolve().parent + output = _output_directory(script_dir, args.output_dir) if args.output_dir else None + _configure_compiler_includes(script_dir, include_core_headers=include_core_headers) + + # Use short sibling names. Appending an absolute checkout path under + # build/temp can exceed Windows' path limit. + os.chdir(script_dir) + pyx_files = sorted(path.name for path in script_dir.glob("test_*.pyx")) + if not pyx_files: + raise SystemExit(f"no test_*.pyx files under {script_dir}") + + from Cython.Build import cythonize + from setuptools import setup + + cython_options: dict[str, object] = { + "language_level": 3, + "include_path": [str(_bindings_source_root())], + "compiler_directives": {"freethreading_compatible": True}, + } + if nthreads is not None: + cython_options["nthreads"] = nthreads + + # Cython otherwise writes generated C/C++ beside the .pyx inputs. Moon + # builds keep all generated sources and compiler intermediates in outputs. + if output is None: + ext_modules = cythonize(pyx_files, **cython_options) + else: + cython_build = output / ".cython-build" + ext_modules = cythonize(pyx_files, build_dir=str(cython_build), **cython_options) + + sys.argv = [sys.argv[0], "build_ext"] + if output is None: + sys.argv.append("--inplace") + else: + build_temp = output / ".build-temp" + sys.argv.extend(["--build-lib", str(output), "--build-temp", str(build_temp)]) + setup(name=distribution_name, ext_modules=ext_modules) + + if output is None: + return + + for intermediate in (build_temp, cython_build): + if intermediate.exists(): + shutil.rmtree(intermediate) + for source in pyx_files: + matches = [ + path + for pattern in (f"{Path(source).stem}*.so", f"{Path(source).stem}*.pyd", f"{Path(source).stem}*.dylib") + for path in output.glob(pattern) + if path.is_file() + ] + if len(matches) != 1: + raise RuntimeError(f"expected one extension for {source} in {output}, found {len(matches)}") diff --git a/cuda_python_test_helpers/moon.yml b/cuda_python_test_helpers/moon.yml index 13a85531daf..a8621c8bf48 100644 --- a/cuda_python_test_helpers/moon.yml +++ b/cuda_python_test_helpers/moon.yml @@ -6,9 +6,6 @@ $schema: https://moonrepo.dev/schemas/v2/project.json language: unknown layer: library -toolchains: - default: system - taskOptions: cache: false runFromWorkspaceRoot: true @@ -16,6 +13,95 @@ taskOptions: shell: false tasks: + # These context checks are safe to run beside dependency installation: they + # do not enumerate the environment that pip is mutating. + fingerprint-python-context: + inputs: [] + checks: + - check: fingerprint + script: >- + python -c "import hashlib, os; + names = sorted(name for name in os.environ + if name == 'SOURCE_DATE_EPOCH' or name.startswith(('SETUPTOOLS_SCM_', 'VCS_VERSIONING_'))); + payload = '\0'.join(name + '=' + os.environ[name] for name in names); + print(hashlib.sha256(payload.encode()).hexdigest())" + hash: stdout + - check: fingerprint + script: >- + python -c "import platform, sysconfig; + print(platform.python_implementation(), platform.python_version(), + sysconfig.get_config_var('SOABI') or '', sep='\n')" + hash: stdout + type: build + options: + internal: true + runInCI: true + + # Cached package builds also hash the complete installed distribution set. + # Keep this separate from python-context because test setup mutates it. + fingerprint-python-build: + deps: + - target: test-helpers:fingerprint-python-context + cacheStrategy: hash + inputs: [] + checks: + - check: fingerprint + script: >- + python -c "import importlib.metadata as metadata; + normalize = lambda value: value.lower().replace('_', '-'); + versions = sorted((normalize(dist.metadata['Name']), dist.version) + for dist in metadata.distributions() if dist.metadata['Name']); + print(*(name + '=' + version for name, version in versions), sep='\n')" + hash: stdout + type: build + options: + internal: true + runInCI: true + + fingerprint-native-context: + deps: + - target: test-helpers:fingerprint-python-context + cacheStrategy: hash + inputs: [] + checks: + - check: fingerprint + script: >- + python -c "import hashlib, os, re; + names = {'AR', 'ARFLAGS', 'BUILD_CUDA_MAJOR', 'BUILD_CUDA_VER', 'BUILD_PREV_CUDA_MAJOR', + 'CC', 'CFLAGS', 'CL', 'CPPFLAGS', 'CPLUS_INCLUDE_PATH', 'CXX', 'CXXFLAGS', + 'CUDA_CORE_BUILD_MAJOR', 'CUDA_HOME', 'CUDA_PATH', + 'CUDA_PYTHON_COVERAGE', 'CUDA_PYTHON_PARALLEL_LEVEL', 'CUDA_VER', + 'HOST_PLATFORM', 'INCLUDE', 'LD', 'LDFLAGS', 'LDSHARED', 'LIB', 'LIBPATH', + 'NM', 'NVCCFLAGS', 'NVCC_APPEND_FLAGS', 'NVCC_PREPEND_FLAGS', + 'PY_EXT_SUFFIX', 'PY_VER', 'RANLIB', + 'SCCACHE_CACHE_SIZE', 'SCCACHE_DIR', 'SCCACHE_PATH', 'STRIP', '_CL_'}; + names.update(name for name in os.environ if name.startswith('CIBW_')); + redact = lambda value: re.sub(r'(?i)\\bACTIONS_[A-Z0-9_]+=(?:\"[^\"]*\"|\\S+)', 'ACTIONS_VALUE=', value); + payload = '\0'.join(name + '=' + redact(os.environ.get(name, '')) for name in sorted(names)); + print(hashlib.sha256(payload.encode()).hexdigest())" + hash: stdout + - check: fingerprint + script: >- + python -c "import platform; + print(platform.system(), platform.machine(), sep='\n')" + hash: stdout + - check: fingerprint + script: >- + python -c "import os, shlex, shutil, subprocess, sysconfig; + commands = {'cc', 'c++', 'cl', 'nvcc'}; + configured = (os.environ.get('CC') or sysconfig.get_config_var('CC') or '', + os.environ.get('CXX') or sysconfig.get_config_var('CXX') or ''); + commands.update(token for value in configured for token in shlex.split(value, posix=os.name != 'nt') if token and not token.startswith('-')); + print(*(command + '=' + path + '\n' + str(result.returncode) + '\n' + result.stdout.strip() + for command in sorted(commands) if (path := shutil.which(command)) + for result in (subprocess.run([path, '--version'], check=False, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True, timeout=10),)), sep='\n')" + hash: stdout + type: build + options: + internal: true + runInCI: true + # GitHub Actions runs this after changing to the test Python/toolkit phase. # The canonical wheel inputs are therefore staged by the preceding build # phase instead of being executable task dependencies in this project. @@ -45,10 +131,11 @@ tasks: exit 1 } CORE_WHEEL=$1 - python -m pip install \ + python -m pip install --force-reinstall --no-deps \ "$PATHFINDER_WHEEL" \ "$BINDINGS_WHEEL" \ - "$CORE_WHEEL" \ + "$CORE_WHEEL" + python -m pip install \ --group cuda_bindings/pyproject.toml:test \ --group cuda_core/pyproject.toml:test inputs: @@ -62,3 +149,70 @@ tasks: options: os: [linux, windows] runInCI: true + + # Test dependencies are installed by prepare-test-assets. Keep the package + # versions behind that mutation, while sharing the unchanged platform, + # compiler, and environment context with native package builds. + fingerprint-test-assets: + deps: + - target: test-helpers:prepare-test-assets + - target: test-helpers:fingerprint-native-context + cacheStrategy: hash + inputs: [] + checks: + - check: fingerprint + script: >- + python -c "import importlib.metadata as metadata; + names = ('build', 'cibuildwheel', 'cython', 'numpy', 'packaging', 'pip', 'setuptools', 'setuptools-scm', 'wheel'); + normalize = lambda value: value.lower().replace('_', '-'); + versions = {normalize(dist.metadata['Name']): dist.version for dist in metadata.distributions() if dist.metadata['Name']}; + print(*(name + '=' + versions.get(name, '') for name in names), sep='\n')" + hash: stdout + type: build + options: + internal: true + os: [linux, windows] + runInCI: true + + # Documentation runs after the workflow has staged one wheel per package. + # Keep these as raw inputs: depending on the producers would introduce a + # metapackage/docs project cycle and rebuild already-staged artifacts. + prepare-docs: + command: bash + args: + - -euo + - pipefail + - -c + - | + one_wheel() { + local LABEL="$1" + shift + [[ $# -eq 1 ]] || { + echo "expected one $LABEL wheel, found $#" >&2 + exit 1 + } + printf '%s\n' "$1" + } + + shopt -s nullglob + PATHFINDER_WHEEL=$(one_wheel cuda.pathfinder cuda_pathfinder/.moon-out/wheel-pure/*.whl) + BINDINGS_WHEEL=$(one_wheel cuda.bindings cuda_bindings/.moon-out/wheel-current/*.whl) + CORE_WHEEL=$(one_wheel cuda.core cuda_core/.moon-out/wheel-merged/*.whl) + METAPACKAGE_WHEEL=$(one_wheel cuda-python cuda_python/.moon-out/wheel-pure/*.whl) + + python -m pip install --force-reinstall \ + "$PATHFINDER_WHEEL" \ + "$BINDINGS_WHEEL" \ + "$CORE_WHEEL" + python -m pip install --force-reinstall --no-deps \ + "$METAPACKAGE_WHEEL" + inputs: + - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' + - '/cuda_bindings/.moon-out/wheel-current/*.whl' + - '/cuda_core/.moon-out/wheel-merged/*.whl' + - '/cuda_python/.moon-out/wheel-pure/*.whl' + - '/.github/workflows/build-docs.yml' + type: build + options: + os: linux + runInCI: true diff --git a/moon.yml b/moon.yml index 164f6b07883..cb7385c456e 100644 --- a/moon.yml +++ b/moon.yml @@ -15,9 +15,6 @@ dependsOn: scope: development - id: metapackage scope: development -toolchains: - default: system - taskOptions: cache: false runInCI: false @@ -29,6 +26,8 @@ fileGroups: - '/moon.yml' - '/**/moon.yml' - '/ci/tools/env-vars' + - '/ci/build-constraints.txt' + - '/ci/build-matrix.yml' - '/ci/versions.yml' - '/.github/actions/**/*' - '/.github/workflows/ci.yml' @@ -54,13 +53,13 @@ fileGroups: - '!/SECURITY.md' - '!/benchmarks/cuda_bindings/README.md' - '!/benchmarks/cuda_core/README.md' - - '!/ci/ci-pipeline.svg' - '!/cuda_bindings/README.md' - '!/cuda_core/README.md' - '!/cuda_python/README.md' - '!/toolshed/README.md' - '!/.agents/**/*' - '!/.github/actions/**/*' + - '!/.github/workflows/ci-nightly.yml' - '!/.github/workflows/ci.yml' - '!/.github/workflows/build-docs.yml' - '!/.github/workflows/build-wheel.yml' @@ -78,6 +77,8 @@ fileGroups: - '!/benchmarks/cuda_bindings/run_cpp.py' - '!/benchmarks/cuda_bindings/run_pyperf.py' - '!/ci/test-matrix.yml' + - '!/ci/build-constraints.txt' + - '!/ci/build-matrix.yml' - '!/ci/versions.yml' - '!/ci/tools/configure_driver_mode.ps1' - '!/ci/tools/env-vars' @@ -173,10 +174,33 @@ tasks: inputs: - '/.moon/**/*' - '/**/moon.yml' + - '/.gitignore' + - '/.github/workflows/build-docs.yml' + - '/.github/workflows/build-wheel.yml' + - '/.github/workflows/ci-nightly.yml' + - '/.github/workflows/ci.yml' + - '/.github/workflows/test-sdist-linux.yml' + - '/.github/workflows/test-sdist-windows.yml' + - '/.github/workflows/test-wheel-linux.yml' + - '/.github/workflows/test-wheel-windows.yml' + - '/ci/tools/env-vars' + - '/ci/build-matrix.yml' + - '/ci/test-matrix.yml' + - '/ci/tools/merge_cuda_core_wheels.py' - '/ci/tools/run-tests' - '/ci/tools/tests/test_moon_tasks.py' - '/ci/tools/tests/test_moon_workspace.py' - tags: [ci-quality, runner-quality] + - '/cuda_bindings/tests/cython/build_tests.py' + - '/cuda_bindings/docs/build_docs.sh' + - '/cuda_core/tests/cython/build_tests.py' + - '/cuda_core/docs/build_docs.sh' + - '/cuda_pathfinder/docs/build_docs.sh' + - '/cuda_python/docs/assemble_moon_docs.sh' + - '/cuda_python/docs/build_component_docs.sh' + - '/cuda_python/docs/build_docs.sh' + - '/cuda_python/docs/environment-docs.yml' + - '/cuda_python_test_helpers/cuda_python_test_helpers/cython_test_builder.py' + tags: [ci-quality] type: test options: os: linux @@ -209,24 +233,11 @@ tasks: - target: metapackage:docs-ci cacheStrategy: outputs inputs: - - {project: pathfinder, group: package} - - {project: pathfinder, group: docs} - - {project: bindings, group: package} - - {project: bindings, group: docs} - - {project: core, group: package} - - {project: core, group: docs} - - {project: metapackage, group: package} - - {project: metapackage, group: docs} - - '/cuda_pathfinder/.moon-out/docs-ci/**/*' - - '/cuda_bindings/.moon-out/docs-ci/**/*' - - '/cuda_core/.moon-out/docs-ci/**/*' - - '/cuda_python/.moon-out/docs-ci/**/*' - '/cuda_python/docs/assemble_moon_docs.sh' - - '/.github/workflows/build-wheel.yml' - '/.github/workflows/build-docs.yml' outputs: - '.moon-out/docs' - tags: [ci-docs, runner-docs] + tags: [ci-docs] type: build options: os: linux diff --git a/pytest.ini b/pytest.ini index 505b4269490..4038a9e0f86 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,6 +3,7 @@ [pytest] addopts = --showlocals --durations=20 +pythonpath = cuda_python_test_helpers norecursedirs = cuda_bindings/examples cuda_core/examples @@ -11,6 +12,7 @@ testpaths = cuda_pathfinder/tests cuda_bindings/tests cuda_core/tests + cuda_python_test_helpers/tests tests/integration markers = diff --git a/toolshed/setup-docs-env.sh b/toolshed/setup-docs-env.sh deleted file mode 100755 index 9acbaa8e391..00000000000 --- a/toolshed/setup-docs-env.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env bash - -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -# Setup a local conda environment for building the sphinx docs to mirror the CI environment -# (see cuda_python/docs/environment-docs.yml). -# -# Usage: -# ./toolshed/setup-docs-env.sh -# -# Notes: -# - Requires an existing Miniforge/Conda install and `conda` on PATH. -# - Installs the same packages as CI’s environment-docs.yml. - -set -euo pipefail - -ENV_NAME="cuda-python-docs" -PYVER="3.12" - -have_cmd() { command -v "$1" >/dev/null 2>&1; } - -# --- sanity checks ----------------------------------------------------------- -if ! have_cmd conda; then - echo "ERROR: 'conda' not found on PATH. Please ensure Miniforge is installed and initialized." >&2 - exit 1 -fi - -# Load conda's shell integration into this bash process -eval "$(conda shell.bash hook)" - -if conda env list | awk '{print $1}' | grep -qx "${ENV_NAME}"; then - echo "⚠ Environment '${ENV_NAME}' already exists → NO ACTION" - exit 0 -fi - -echo "Creating environment '${ENV_NAME}'…" -# ATTENTION: This dependency list is duplicated in -# cuda_python/docs/environment-docs.yml. Please KEEP THEM IN SYNC! -conda create -y -n "${ENV_NAME}" \ - "python=${PYVER}" \ - "cython>=3.2.5,<3.3" \ - myst-parser \ - numpy \ - numpydoc \ - pip \ - pydata-sphinx-theme \ - pytest \ - scipy \ - "sphinx<8.2.0" \ - sphinx-copybutton \ - myst-nb \ - enum_tools \ - sphinx-toolbox \ - pyclibrary - -conda activate "${ENV_NAME}" -python -m pip install --upgrade pip -python -m pip install nvidia-sphinx-theme - -echo -echo "✅ Environment '${ENV_NAME}' is ready." -echo -echo "Build docs with e.g.:" -echo " conda activate ${ENV_NAME}" -echo " cd cuda_pathfinder/" -echo " pip install -e ." -echo " (cd docs/ && rm -rf build source/generated && ./build_docs.sh)"