diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 7bb70809556..d350b725321 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -57,9 +57,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 }} + 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 @@ -85,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: | @@ -103,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 @@ -112,55 +120,38 @@ 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="${{ github.sha }}" - DOCS_GITHUB_REF="${{ github.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 cuda-python build artifacts + + - name: Download universal release wheels + if: ${{ inputs.is-release }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: cuda-python-wheel + pattern: "cuda-*-wheel" path: . + merge-multiple: false 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 native Moon lane + if: ${{ !inputs.is-release }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: cuda-pathfinder-wheel - path: ./cuda_pathfinder + name: moon-lane-build-linux-64-py312 + path: . 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 - 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: Download cuda.bindings build artifacts if: ${{ inputs.is-release }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -171,18 +162,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 }} - - name: Download cuda.core build artifacts if: ${{ inputs.is-release }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -194,27 +173,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 + - 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 - # 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 + - name: Install release packages + if: ${{ inputs.is-release }} + run: | + 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 @@ -230,39 +207,58 @@ jobs: - name: Build all docs if: ${{ inputs.component == 'all' }} run: | - pushd cuda_python/docs/ if [[ "${{ inputs.is-release }}" == "false" ]]; then - ./build_all_docs.sh latest-only + # 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/ ./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' }} 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 390d6f88ae2..00aa0be494b 100644 --- a/.github/workflows/build-wheel.yml +++ b/.github/workflows/build-wheel.yml @@ -14,38 +14,23 @@ on: prev-cuda-version: required: true type: string - build-pathfinder: - required: false - type: boolean - default: true - build-bindings: - required: false - type: boolean - default: true - build-core: - required: false - type: boolean - default: true - build-python: - 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 + moon-base: + description: "Base revision used by Moon affected checks" + required: true type: string - default: "" + 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 + type: boolean + default: false defaults: run: @@ -59,20 +44,14 @@ jobs: build: strategy: fail-fast: false - matrix: - python-version: - - "3.10" - - "3.11" - - "3.12" - - "3.13" - - "3.14" - - "3.14t" - - "3.15" - - "3.15t" + 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 @@ -83,7 +62,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' @@ -91,7 +70,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: | @@ -117,14 +95,30 @@ 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 lane + if: ${{ inputs.baseline-run-id != '' && !inputs.force-all }} + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + 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 @@ -156,188 +150,23 @@ 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: Download reusable cuda.pathfinder wheel - if: ${{ !inputs.build-pathfinder }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: cuda-pathfinder-wheel - path: cuda_pathfinder - github-token: ${{ github.token }} - run-id: ${{ inputs.baseline-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/*.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 + - name: Install externally managed build tools + 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 - 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 }} - uses: pypa/cibuildwheel@4726cd35bb13f7bde50cf2761f2499ac7b3aa32c # v4.1.1 - with: - package-dir: ./cuda_bindings/ - output-dir: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} - env: - 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 }} - 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 - 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 }} - 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 && - "/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: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} - 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) ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} - ls -lahR ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} - - - 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 - - - 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 - if-no-files-found: error - - - 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 }} + - name: Build current wheels with Moon env: + CUDA_PYTHON_USE_STAGED_BINDINGS_VERSION: "1" + 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' @@ -350,9 +179,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 }} - 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 @@ -366,117 +192,55 @@ 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 }} - 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 && - "/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 cuda.core artifacts directory and rename - 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=(--upstream none --downstream none) + if [[ "${{ inputs.force-all }}" == "true" ]]; then + args+=(--force) 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 }} - - - 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 }} - 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' }} - run: | - pushd cuda_python - pip wheel -v --no-deps . - twine check --strict *.whl - popd - - - name: Download reusable cuda-python wheel - if: ${{ !inputs.build-python && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: cuda-python-wheel - path: cuda_python - 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" + # 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 + targets+=(metapackage:wheel-pure) fi - $CHOWN -R $(whoami) cuda_python/*.whl - ls -lahR cuda_python - - - name: Upload cuda-python build artifacts - if: ${{ strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: cuda-python-wheel - path: cuda_python/*.whl - if-no-files-found: error + moon ci "${targets[@]}" "${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 --constraint ci/build-constraints.txt + "pip>=25.3" 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 @@ -486,63 +250,32 @@ 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/*.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 }} - 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 - - - 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 }} - if-no-files-found: error - - - name: Build cuda.core Cython tests - if: ${{ inputs.test-core }} + - name: Build target-Python test assets with Moon + env: + MOON_BASE: ${{ inputs.moon-base }} + MOON_HEAD: ${{ github.sha }} 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 - - - name: Upload cuda.core Cython tests - if: ${{ inputs.test-core }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + 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: - name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-tests - path: ${{ env.CUDA_CORE_CYTHON_TESTS_DIR }}/test_*${{ env.PY_EXT_SUFFIX }} - if-no-files-found: error + python-version: "3.12" # 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: @@ -550,25 +283,7 @@ jobs: cuda-version: ${{ inputs.prev-cuda-version }} cuda-path: "./cuda_toolkit_prev" - - name: Build cuda.core test binaries - if: ${{ inputs.test-core }} - run: | - nvcc --version - python "${{ env.CUDA_CORE_TEST_BINARIES_DIR }}/build_test_binaries.py" - - - 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: | - ${{ env.CUDA_CORE_TEST_BINARIES_DIR }}/*.o - ${{ env.CUDA_CORE_TEST_BINARIES_DIR }}/*.a - ${{ env.CUDA_CORE_TEST_BINARIES_DIR }}/*.lib - if-no-files-found: error - - name: Download cuda.bindings build artifacts from the prior branch - if: ${{ inputs.build-core }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | @@ -587,44 +302,20 @@ 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 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: 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 }} + - name: Build previous-CTK outputs and merge wheels with Moon env: + 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 +329,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 @@ -653,68 +342,99 @@ 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 }} - 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 && - "/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=(--upstream none --downstream none) + if [[ "${{ inputs.force-all }}" == "true" ]]; then + args+=(--force) + fi + + # 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: List the cuda.core artifacts directory and rename - 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) ${{ 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 }} - - - 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 }}" - - - name: Check cuda.core wheel - if: ${{ inputs.build-core }} - run: | - twine check --strict ${{ env.CUDA_CORE_ARTIFACTS_DIR }}/*.whl + $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_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" && + "${{ 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 + with: + name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }} + path: cuda_bindings/.moon-out/wheel-current/*.whl + if-no-files-found: error + overwrite: true - 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 native Moon lane + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: moon-lane-build-${{ inputs.host-platform }}-py${{ matrix.python-version-formatted }} + path: | + 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 + 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 + retention-days: 30 diff --git a/.github/workflows/ci-nightly.yml b/.github/workflows/ci-nightly.yml index 0188ebf2524..3e70d3370ee 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 @@ -64,17 +78,96 @@ jobs: id: find env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REQUESTED_RUN_ID: ${{ inputs.run-id }} run: | - 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') + expected=() + host_platforms=(linux-64 linux-aarch64 win-64) + for host_platform in "${host_platforms[@]}"; do + while IFS= read -r python_version; do + expected+=("moon-lane-build-${host_platform}-py${python_version}") + done < <(yq -r '.include[]."python-version-formatted"' ci/build-matrix.yml) + 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 "${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 + .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 +190,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: @@ -109,13 +241,11 @@ 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_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)" @@ -127,13 +257,11 @@ 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_filter: 'map(select(.ENV.MODE == "nightly-pytorch"))' + matrix: ${{ needs.find-wheels.outputs.PYTORCH_LINUX_ARM64_MATRIX }} test-pytorch-windows: name: "Nightly PyTorch (win-64)" @@ -145,13 +273,11 @@ 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_filter: 'map(select(.ENV.MODE == "nightly-pytorch"))' + matrix: ${{ needs.find-wheels.outputs.PYTORCH_WINDOWS_MATRIX }} # ── numba-cuda tests ── @@ -165,13 +291,11 @@ 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_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)" @@ -183,13 +307,11 @@ 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_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)" @@ -201,13 +323,11 @@ 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_filter: 'map(select(.ENV.MODE == "nightly-numba-cuda"))' + matrix: ${{ needs.find-wheels.outputs.NUMBA_WINDOWS_MATRIX }} # ── numba-cuda-mlir tests ── @@ -221,13 +341,11 @@ 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_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)" @@ -239,13 +357,11 @@ 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_filter: 'map(select(.ENV.MODE == "nightly-numba-cuda-mlir"))' + matrix: ${{ needs.find-wheels.outputs.MLIR_WINDOWS_MATRIX }} # ── Released cuda-core against main pathfinder/bindings ── @@ -259,13 +375,11 @@ 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_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)" @@ -277,13 +391,11 @@ 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_filter: 'map(select(.ENV.MODE == "nightly-cuda-core"))' + matrix: ${{ needs.find-wheels.outputs.CORE_WINDOWS_MATRIX }} # ── Standard tests on nightly-only runners ── @@ -297,13 +409,13 @@ 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 - matrix_filter: 'map(select(.ENV.MODE == "nightly-standard"))' + force-all: true + matrix: ${{ needs.find-wheels.outputs.STANDARD_LINUX_ARM64_MATRIX }} # ── Status check ── @@ -328,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 2aadf222306..84e583d7f84 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,231 +21,407 @@ 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 identifies runner classes before expensive jobs are 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 }} + build-python-matrix: ${{ steps.build-matrix.outputs.python }} + skip: ${{ steps.directives.outputs.skip }} + moon-base: ${{ steps.baseline.outputs.moon-base }} + moon-base-run-id: ${{ steps.baseline.outputs.moon-base-run-id }} + 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 }} + 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} steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: - fetch-depth: 1 - - name: Get CUDA build versions - id: get-vars + # 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 + + - name: Read CI variables + id: vars run: | - cuda_build_ver=$(yq '.cuda.build.version' ci/versions.yml) - echo "cuda_build_ver=$cuda_build_ver" >> $GITHUB_OUTPUT + 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" - 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 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" - 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 }} + BUILD_PYTHON_MATRIX: ${{ steps.build-matrix.outputs.python }} 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 - fi - base=$(git merge-base HEAD "origin/${BASE_REF}") - changed=$(git diff --name-only "$base"...HEAD) + 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 + 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 + base_run_id="${candidate}" + artifacts="$( + gh api --paginate --slurp \ + "repos/${GITHUB_REPOSITORY}/actions/runs/${candidate}/artifacts?per_page=100" \ + | jq -c '[.[].artifacts[]]' + )" + + 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 - has_match() { - grep -qE "$1" <<< "$changed" && echo true || echo false - } + # 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. + base="$(git rev-parse HEAD^ 2>/dev/null || git rev-parse HEAD)" + fi { - 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-base-run-id=${base_run_id}" + 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: 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: Query affected Moon tasks + 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: ${{ github.sha }} + 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 + args=(query tasks) + if [[ "${FORCE_ALL}" == "true" ]]; then + moon "${args[@]}" > .moon-affected-tasks.json else - bindings="${BINDINGS}" - core="${CORE}" - pathfinder="${PATHFINDER}" - python_meta="${PYTHON_META}" - test_helpers="${TEST_HELPERS}" - shared="${SHARED}" + moon "${args[@]}" \ + --affected \ + --upstream none \ + --downstream deep \ + > .moon-affected-tasks.json fi - or_flag() { - for v in "$@"; do - if [[ "${v}" == "true" ]]; then - echo "true" - return - fi - done - echo "false" + - name: Publish affected runner lanes + 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: | + force_all="${FORCE_ALL:-true}" + has_tag() { + jq -e --arg tag "$1" \ + 'any(.tasks[][]; (.tags // []) | any(. == $tag))' \ + .moon-affected-tasks.json > /dev/null } - # 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}")" + 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" - # 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}")" + selected() { + [[ "${SKIP}" != "true" ]] && + { [[ "${force_all}" == "true" ]] || has_tag "$1"; } + } + force_lane() { + if [[ "${force_all}" == "true" || "$1" != "true" ]]; then + echo true + else + echo false + fi + } + + 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 "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}" + 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" - 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.should-skip.outputs.skip) && - fromJSON(needs.detect-changes.outputs.core) }} + ${{ !fromJSON(needs.gate.outputs.skip) && + fromJSON(needs.gate.outputs.quality) }} runs-on: ubuntu-latest needs: - - should-skip - - detect-changes + - gate permissions: contents: read steps: - 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 @@ -254,9 +429,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')" @@ -266,247 +438,241 @@ 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 }}" - - - 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: ${{ 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.should-skip.outputs.skip) && - fromJSON(needs.detect-changes.outputs.core) }} - runs-on: ubuntu-latest - needs: - - should-skip - - detect-changes - 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 - shell: bash --noprofile --norc -euo pipefail {0} - run: | git fetch --depth=1 --filter=blob:none origin \ - "${{ needs.detect-changes.outputs.pr_merge_base }}" + "${{ 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) + - name: Run affected quality tasks with Moon + shell: bash --noprofile --norc -euo pipefail {0} + 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' || '' }} + + # 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 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: - - 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 + if: >- + ${{ github.repository_owner == 'nvidia' && + !fromJSON(needs.gate.outputs.skip) && + fromJSON(needs.gate.outputs.build-linux-64) }} + 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 }} + baseline-run-id: ${{ needs.gate.outputs.moon-base-run-id }} + moon-base: ${{ needs.gate.outputs.moon-base }} + python-matrix: ${{ needs.gate.outputs.build-python-matrix }} + force-all: ${{ fromJSON(needs.gate.outputs.build-linux-64-force) }} - # 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 + if: >- + ${{ github.repository_owner == 'nvidia' && + !fromJSON(needs.gate.outputs.skip) && + 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 }} + baseline-run-id: ${{ needs.gate.outputs.moon-base-run-id }} + moon-base: ${{ needs.gate.outputs.moon-base }} + python-matrix: ${{ needs.gate.outputs.build-python-matrix }} + force-all: ${{ fromJSON(needs.gate.outputs.build-linux-aarch64-force) }} - # 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 + if: >- + ${{ github.repository_owner == 'nvidia' && + !fromJSON(needs.gate.outputs.skip) && + 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 }} + baseline-run-id: ${{ needs.gate.outputs.moon-base-run-id }} + moon-base: ${{ needs.gate.outputs.moon-base }} + 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. 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.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.sdist-linux-force) }} - # 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.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.sdist-windows-force) }} - # 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-linux-64 - secrets: inherit + if: >- + ${{ always() && + github.repository_owner == 'nvidia' && + needs.gate.result == 'success' && + (needs.build-linux-64.result == 'success' || + needs.build-linux-64.result == 'skipped') && + !fromJSON(needs.gate.outputs.skip) && + 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 }} + 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 }} + 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-linux-aarch64 - secrets: inherit + if: >- + ${{ always() && + github.repository_owner == 'nvidia' && + needs.gate.result == 'success' && + (needs.build-linux-aarch64.result == 'success' || + needs.build-linux-aarch64.result == 'skipped') && + !fromJSON(needs.gate.outputs.skip) && + 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 }} + 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 }} + 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-windows - secrets: inherit + if: >- + ${{ always() && + github.repository_owner == 'nvidia' && + needs.gate.result == 'success' && + (needs.build-windows.result == 'success' || + needs.build-windows.result == 'skipped') && + !fromJSON(needs.gate.outputs.skip) && + 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 }} + 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 }} + 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-linux-64 + if: >- + ${{ always() && + github.repository_owner == 'nvidia' && + needs.gate.result == 'success' && + (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 }} 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 +685,27 @@ 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 + - quality + - build-linux-64 + - build-linux-aarch64 + - build-windows - test-sdist-linux - test-sdist-windows - test-linux-64 @@ -547,46 +713,49 @@ 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 } - # 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" ]] + 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 + + 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 }}" + check_result precommit-windows success "${{ needs.precommit-windows.result }}" + + [[ "${status}" == "success" ]] 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/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..6e7e561338e 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,17 @@ 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 }} + # Only canonical outputs cross runners; upstream-none also omits hash deps. + MOON_CACHE: "off" + MOON_HEAD: ${{ github.sha }} + PY_VER: "3.12" steps: - name: Checkout ${{ github.event.repository.name }} uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -56,51 +59,28 @@ 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 --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 # 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 +88,70 @@ 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 + run: ./ci/tools/env-vars build + + - 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-lane-sdist-${{ inputs.host-platform }} + path: . + 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 - - - 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 }} + args=(--upstream none --downstream none) + if [[ "${{ inputs.force-all }}" == "true" ]]; then + args+=(--force) + fi + # 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: | - 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 + for project in cuda_pathfinder cuda_bindings cuda_core cuda_python; do + output="${project}/.moon-out/sdist" + 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 + done - name: Show sccache stats - if: ${{ always() && (inputs.build-bindings || inputs.build-core) }} + if: ${{ always() }} run: sccache --show-stats + + # 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: | + 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 + retention-days: 30 diff --git a/.github/workflows/test-sdist-windows.yml b/.github/workflows/test-sdist-windows.yml index 5451d20429e..d9068f1e06f 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,17 @@ 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 }} + # Only canonical outputs cross runners; upstream-none also omits hash deps. + MOON_CACHE: "off" + MOON_HEAD: ${{ github.sha }} + PY_VER: "3.12" steps: - name: Checkout ${{ github.event.repository.name }} uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -62,101 +65,77 @@ 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 + 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 # 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 + run: ./ci/tools/env-vars build - - name: Download cuda.bindings wheel - if: ${{ !inputs.build-bindings && inputs.build-core }} + - 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: cuda-bindings-python312-cuda${{ inputs.cuda-version }}-${{ inputs.host-platform }}-${{ github.sha }} - path: cuda_bindings/dist + name: moon-lane-sdist-${{ inputs.host-platform }} + path: . + 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 - - # 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 }} + args=(--upstream none --downstream none) + if [[ "${{ inputs.force-all }}" == "true" ]]; then + args+=(--force) + fi + # 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: | - 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 + for project in cuda_pathfinder cuda_bindings cuda_core cuda_python; do + output="${project}/.moon-out/sdist" + 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 + done + + # 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: | + 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 + retention-days: 30 diff --git a/.github/workflows/test-wheel-linux.yml b/.github/workflows/test-wheel-linux.yml index 8134a6844fd..ec3b7cf817f 100644 --- a/.github/workflows/test-wheel-linux.yml +++ b/.github/workflows/test-wheel-linux.yml @@ -7,39 +7,29 @@ name: "CI: Test wheels" on: workflow_call: inputs: - build-type: - type: string - required: true host-platform: type: string required: true 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. 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', @@ -47,71 +37,29 @@ 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. + 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: 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: + MOON_BASE: ${{ inputs.moon-base }} + MOON_HEAD: ${{ inputs.source-ref || 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') }} @@ -131,6 +79,18 @@ jobs: steps: - name: Checkout ${{ github.event.repository.name }} uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + filter: blob:none + ref: ${{ inputs.source-ref || github.sha }} + + - 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 @@ -163,8 +123,7 @@ 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: ${{ !inputs.test-bindings && '1' || '0' }} + SKIP_BINDINGS_TEST_OVERRIDE: "0" run: ./ci/tools/env-vars test - name: Apply extra matrix environment variables @@ -173,141 +132,14 @@ 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.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 native Moon lane uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: cuda-python-wheel + 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.bindings build artifacts - if: ${{ (inputs.test-bindings || inputs.test-core || inputs.test-python) && - env.BINDINGS_SOURCE == 'main' }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }} - path: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} - 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' }} - 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=${{ needs.compute-matrix.outputs.OLD_BRANCH }} - 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 - - 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: ${{ env.CUDA_BINDINGS_CYTHON_TESTS_DIR }} - 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_CYTHON_TESTS_DIR - - - name: Download cuda.core build artifacts - if: ${{ inputs.test-core }} - 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 }} - run: | - pwd - ls -lahR $CUDA_CORE_ARTIFACTS_DIR - - - name: Download cuda.core Cython tests - if: ${{ inputs.test-core && env.SKIP_CYTHON_TEST == '0' }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-tests - path: ${{ env.CUDA_CORE_CYTHON_TESTS_DIR }} - 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' }} - run: | - pwd - ls -lahR $CUDA_CORE_CYTHON_TESTS_DIR - - - 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 }} - 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_TEST_BINARIES_DIR - - name: Set up Python ${{ matrix.PY_VER }} uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: @@ -319,8 +151,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" @@ -334,7 +165,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: @@ -343,80 +174,20 @@ 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: run-tests pathfinder - - - 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 - - - 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 - - - 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_PYTHON_USE_STAGED_BINDINGS_VERSION: "1" CUDA_VER: ${{ matrix.CUDA_VER }} LOCAL_CTK: ${{ matrix.LOCAL_CTK }} - run: run-tests core - - - 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[@]}" - - - 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: run-tests pathfinder + 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 04b290b1cd0..a71247a6a33 100644 --- a/.github/workflows/test-wheel-windows.yml +++ b/.github/workflows/test-wheel-windows.yml @@ -7,39 +7,29 @@ name: "CI: Test wheels" on: workflow_call: inputs: - build-type: - type: string - required: true host-platform: type: string required: true 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. 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', @@ -47,62 +37,26 @@ 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. + 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: - 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: + MOON_BASE: ${{ inputs.moon-base }} + MOON_HEAD: ${{ inputs.source-ref || 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') }} @@ -110,6 +64,18 @@ jobs: steps: - name: Checkout ${{ github.event.repository.name }} uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + filter: blob:none + ref: ${{ inputs.source-ref || github.sha }} + + - 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 @@ -150,8 +116,7 @@ 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: ${{ !inputs.test-bindings && '1' || '0' }} + SKIP_BINDINGS_TEST_OVERRIDE: "0" shell: bash --noprofile --norc -xeuo pipefail {0} run: ./ci/tools/env-vars test @@ -162,132 +127,14 @@ 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.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 native Moon lane uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: cuda-python-wheel + 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.bindings build artifacts - if: ${{ (inputs.test-bindings || inputs.test-core || inputs.test-python) && - env.BINDINGS_SOURCE == 'main' }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }} - path: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} - 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' }} - 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 - - 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: ${{ env.CUDA_BINDINGS_CYTHON_TESTS_DIR }} - 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 $env:CUDA_BINDINGS_CYTHON_TESTS_DIR | Select-Object Mode, LastWriteTime, Length, FullName - - - name: Download cuda.core build artifacts - if: ${{ inputs.test-core }} - 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 }} - 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' }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-tests - path: ${{ env.CUDA_CORE_CYTHON_TESTS_DIR }} - 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' }} - run: | - Get-Location - Get-ChildItem -Recurse -Force $env:CUDA_CORE_CYTHON_TESTS_DIR | 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 }} - 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 $env:CUDA_CORE_TEST_BINARIES_DIR | Select-Object Mode, LastWriteTime, Length, FullName - - name: Set up Python ${{ matrix.PY_VER }} uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: @@ -296,8 +143,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" @@ -324,70 +170,16 @@ 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: run-tests pathfinder - - - 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: run-tests bindings - - - 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_PYTHON_USE_STAGED_BINDINGS_VERSION: "1" CUDA_VER: ${{ matrix.CUDA_VER }} LOCAL_CTK: ${{ matrix.LOCAL_CTK }} shell: bash --noprofile --norc -xeuo pipefail {0} - run: run-tests core - - - 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 - - - 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: run-tests pathfinder + 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/.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/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/.moon/workspace.yml b/.moon/workspace.yml new file mode 100644 index 00000000000..f0127a6ef18 --- /dev/null +++ b/.moon/workspace.yml @@ -0,0 +1,29 @@ +# 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: '.' + pathfinder: 'cuda_pathfinder' + bindings: 'cuda_bindings' + core: 'cuda_core' + metapackage: 'cuda_python' + test-helpers: 'cuda_python_test_helpers' + +vcs: + defaultBranch: main + +pipeline: + installDependencies: false + syncProjects: false + syncWorkspace: false + +cache: + cas: + verifyIntegrity: true + +telemetry: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7474ac4d840..fd6061b6112 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,102 +254,114 @@ 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. -### CI Pipeline Flow +### 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. + +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 +``` -![CUDA Python CI Pipeline Flow](ci/ci-pipeline.svg) +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. -Alternative Mermaid diagram representation: +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' --upstream deep --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. + +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. + +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. + +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. + +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 ```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 @@ -363,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 23a8a21289f..16538c8aed8 100644 --- a/ci/tools/merge_cuda_core_wheels.py +++ b/ci/tools/merge_cuda_core_wheels.py @@ -26,10 +26,48 @@ import sys import tempfile import zipfile +from collections.abc import Mapping +from datetime import datetime, timezone from pathlib import Path -def run_command(cmd: list[str], cwd: Path | None = None, env: dict = os.environ) -> subprocess.CompletedProcess: +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 _clean_output_wheels(output_dir: Path) -> None: + """Remove wheel files without recursively deleting the caller's directory.""" + 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) + for wheel in output_dir.glob("*.whl"): + if wheel.is_file() or wheel.is_symlink(): + wheel.unlink() + + +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: @@ -180,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, @@ -189,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 @@ -210,8 +251,19 @@ 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 existing wheel files from the output directory before merging", + ) args = parser.parse_args() @@ -230,11 +282,27 @@ def main(): sys.exit(1) wheels.append(wheel) + for directory_value in args.wheel_dir: + directory = Path(directory_value) + try: + wheel = _wheel_from_directory(directory) + except ValueError as error: + print(f"Error: {error}", file=sys.stderr) + sys.exit(1) + wheels.append(wheel) + if not wheels: print("Error: No wheels provided", file=sys.stderr) sys.exit(1) 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) + output_dir.mkdir(parents=True, exist_ok=True) # Check that we have wheel tool available try: @@ -245,6 +313,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/run-tests b/ci/tools/run-tests index f9cc5a9e870..f9476524620 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() { + if [[ ${#} -ne 3 ]]; then + echo "Error: select_one requires exactly one artifact directory" >&2 + exit 1 + fi + local description=${1} + local result_name=${2} + 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}" +} + +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 + 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 + 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" + select_one "bindings wheel" BINDINGS_WHL \ + "${repo_dir}/cuda_bindings/.moon-out/wheel-current" + select_one "merged core wheel" CORE_WHL \ + "${repo_dir}/cuda_core/.moon-out/wheel-merged" + select_one "metapackage wheel" METAPACKAGE_WHL \ + "${repo_dir}/cuda_python/.moon-out/wheel-pure" + 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" pushd ./cuda_pathfinder echo "Installing pathfinder wheel" - pip install ./*.whl --group test + pip install "${PATHFINDER_WHL}" --group test popd fi +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 pushd ./cuda_pathfinder echo "Running pathfinder tests with " \ @@ -43,17 +127,21 @@ 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 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/ 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 @@ -73,7 +161,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,19 +170,21 @@ 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 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 + 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})" @@ -109,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 new file mode 100644 index 00000000000..2413f277578 --- /dev/null +++ b/ci/tools/tests/test_moon_tasks.py @@ -0,0 +1,223 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +# 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 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, _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) + 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) + + def test_clean_output_only_removes_wheel_files(self) -> None: + with tempfile.TemporaryDirectory() as temporary_directory: + output = Path(temporary_directory) + wheel = output / "stale.whl" + unrelated = output / "keep.txt" + wheel.touch() + unrelated.touch() + + _clean_output_wheels(output) + + 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) + + 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) + 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: + 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_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__": + 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..c3b10763c99 --- /dev/null +++ b/ci/tools/tests/test_moon_workspace.py @@ -0,0 +1,1089 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# 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. +# 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 = { + "root": ".", + "pathfinder": "cuda_pathfinder", + "bindings": "cuda_bindings", + "core": "cuda_core", + "metapackage": "cuda_python", + "test-helpers": "cuda_python_test_helpers", +} +EXECUTION_TAG_TARGETS = { + "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", + }, + "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", + "metapackage:test-installed-linux", + "bindings:smoke-linux", + }, + "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": { + "pathfinder:docs-ci", + "bindings:docs-ci", + "core:docs-ci", + "metapackage:docs-ci", + "root:docs-ci", + }, + "ci-quality": { + "root:quality-moon-contracts", + "core:quality-api-base", + "core:quality-api-release", + "bindings:unit-test", + }, +} + +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", +} + +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", +} +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): + @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.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 + 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_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, {"root:force-all", "root:force-all-unowned"}) + for target in forced: + task = self.by_target[target] + self.assertEqual(task["command"], "noop") + self.assertFalse(task["options"]["cache"]) + self.assertTrue(task["options"]["runInCI"]) + self.assertFalse(task.get("outputs")) + + def test_precise_inputs_are_owned_without_hiding_new_paths(self) -> None: + 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. + arguments, + 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()} + + 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) + 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"] + 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.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"])) + + 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"]} + 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")) + 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.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 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", + "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, expected_command in artifact_commands.items(): + task = self.by_target[target] + 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]) + 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) + assert bash is not None + script = self.by_target["metapackage:wheel-pure"]["args"][3] + + 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() + + 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']}", + "SOURCE_DATE_EPOCH": "1234567890", + } + 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_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(): + 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", + "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 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") + 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, 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": {"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": set(), + "metapackage:test-installed-linux": {"metapackage:wheel-pure"}, + "metapackage:test-installed-windows": {"metapackage:wheel-pure"}, + "root:docs-ci": { + "pathfinder:docs-ci", + "bindings:docs-ci", + "core:docs-ci", + "metapackage:docs-ci", + }, + } + for target, dependencies in expected.items(): + configured = {dep["target"] for dep in self.by_target[target]["deps"] if dep["cacheStrategy"] == "outputs"} + self.assertEqual(configured, dependencies, 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, {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")): + 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) + + # 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:") 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: + 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: + for target in EXECUTION_TAG_TARGETS["ci-test-linux"]: + inputs = self.by_target[target]["inputs"] + 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"]: + inputs = 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_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["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["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"]) + + 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"), + ): + 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"] + 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["root:quality-moon-contracts"]["args"][:2], ["-m", "unittest"]) + 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"]) + + 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["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", + "bindings:docs", + "core:docs", + "bindings:bench", + ): + task = self.by_target[target] + self.assertEqual(task["command"], "pixi") + self.assertFalse(task["options"]["runInCI"]) + + 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: + serialized = json.dumps(task) + for filename in removed: + self.assertNotIn(filename, serialized, task["target"]) + + 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", [])}) + pathfinder = self.by_target["pathfinder:wheel-pure"] + 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")), + ) + 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") + 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.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: + ignore = (REPO_ROOT / ".gitignore").read_text(encoding="utf-8").splitlines() + 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 199ababce60..0873ee548b2 100755 --- a/cuda_bindings/docs/build_docs.sh +++ b/cuda_bindings/docs/build_docs.sh @@ -1,60 +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 -if [[ "$#" == "0" ]]; then - LATEST_ONLY="0" -elif [[ "$#" == "1" && "$1" == "latest-only" ]]; then - LATEST_ONLY="1" -else - echo "usage: ./build_docs.sh [latest-only]" - exit 1 -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 +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +exec "${SCRIPT_DIR}/../../cuda_python/docs/build_component_docs.sh" cuda-bindings "$@" diff --git a/cuda_bindings/moon.yml b/cuda_bindings/moon.yml new file mode 100644 index 00000000000..b44ea921a7e --- /dev/null +++ b/cuda_bindings/moon.yml @@ -0,0 +1,331 @@ +# 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 +tags: [docs-package, installed-test-package, native-package, pixi-package, python-package] +dependsOn: + - id: pathfinder + scope: production + - id: test-helpers + scope: development +fileGroups: + package: + - '.git_archival.txt' + - 'cuda/**/*' + - 'build_hooks.py' + - 'DESCRIPTION.rst' + - 'LICENSE' + - 'MANIFEST.in' + - 'pyproject.toml' + - 'setup.py' + 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: 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" + 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 + [[ $# -eq 1 ]] || { + echo "expected one cuda.pathfinder wheel, found $#" >&2 + exit 1 + } + 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())") + 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\" 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" + CIBW_ENVIRONMENT_BASE=$(printenv CIBW_ENVIRONMENT_LINUX || true) + 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" + + 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}' + 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: + - {project: pathfinder, group: package} + + sdist: + 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 + 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 + [[ $# -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 + 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())") + 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 + env: + BUILD_CUDA_VER: '${BUILD_CUDA_VER}' + CUDA_PATH: '${CUDA_PATH}' + HOST_PLATFORM: '${HOST_PLATFORM}' + PY_VER: '${PY_VER}' + inputs: + - {project: pathfinder, group: package} + + cython-test-assets: + command: python + args: + - cuda_bindings/tests/cython/build_tests.py + - --output-dir + - cuda_bindings/.moon-out/cython-tests + env: + PYTHONPATH: cuda_python_test_helpers + inputs: + - {project: pathfinder, group: package} + - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' + - '/cuda_bindings/.moon-out/wheel-current/*.whl' + - '/cuda_python_test_helpers/cuda_python_test_helpers/cython_test_builder.py' + + test: + inputs: + - '/cuda_pathfinder/**/*' + - '/cuda_python_test_helpers/**/*' + - '/benchmarks/cuda_bindings/**/*' + - '/ci/test-matrix.yml' + - '/ci/tools/env-vars' + - '/tests/**/*' + + 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 + 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 + set -- cuda_pathfinder/.moon-out/wheel-pure/*.whl + [[ $# -eq 1 ]] || { + echo "expected one pathfinder wheel, found $#" >&2 + exit 1 + } + 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_WHEEL" \ + "$BINDINGS_WHEEL" \ + 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] + type: test + options: + mutex: ci-python-gpu + os: linux + runInCI: true + shell: 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] + type: test + options: + os: linux + runInCI: true + + test-installed-linux: + inputs: + - '@group(tests)' + - {project: pathfinder, group: package} + - '/cuda_python_test_helpers/**/*' + - '/ci/tools/setup-sanitizer' + + test-installed-windows: + inputs: + - '@group(tests)' + - {project: pathfinder, group: package} + - '/cuda_python_test_helpers/**/*' + + docs: + args: [build-docs] + inputs: + - {project: pathfinder, group: package} + - {project: pathfinder, group: docs} + + docs-ci: + inputs: + - {project: pathfinder, group: package} 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 5bde350e87b..b6db2054ff6 100644 --- a/cuda_bindings/tests/cython/build_tests.py +++ b/cuda_bindings/tests/cython/build_tests.py @@ -1,59 +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 os -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 +from cuda_python_test_helpers.cython_test_builder import build_cython_tests def main() -> None: - script_dir = Path(__file__).resolve().parent - # 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", "--inplace"] - setup(name="cuda_bindings_cython_tests", ext_modules=ext_modules) - 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 0ca0745d2b4..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 28ae6dd07fd..bc0f7ce6bbc 100755 --- a/cuda_core/docs/build_docs.sh +++ b/cuda_core/docs/build_docs.sh @@ -1,58 +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}" - -if [[ "$#" == "0" ]]; then - LATEST_ONLY="0" -elif [[ "$#" == "1" && "$1" == "latest-only" ]]; then - LATEST_ONLY="1" -else - echo "usage: ./build_docs.sh [latest-only]" - exit 1 -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/ +exec "${SCRIPT_DIR}/../../cuda_python/docs/build_component_docs.sh" cuda-core "$@" diff --git a/cuda_core/moon.yml b/cuda_core/moon.yml new file mode 100644 index 00000000000..ac05433807d --- /dev/null +++ b/cuda_core/moon.yml @@ -0,0 +1,487 @@ +# 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 +tags: [docs-package, installed-test-package, native-package, pixi-package, python-package] +dependsOn: + - id: pathfinder + scope: production + - id: bindings + scope: production + - id: test-helpers + scope: development +fileGroups: + package: + - '.git_archival.txt' + - 'cuda/**/*' + - 'build_hooks.py' + - 'DESCRIPTION.rst' + - 'LICENSE' + - 'MANIFEST.in' + - 'NOTICE' + - 'pyproject.toml' + - 'setup.py' +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: + - -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" + } + + 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 "$ERROR_MESSAGE" >&2 + exit 1 + } + 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 + [[ $# -eq 1 ]] || { + echo "expected one cuda.pathfinder wheel, found $#" >&2 + exit 1 + } + PATHFINDER_WHEEL=$1 + BINDINGS_OUTPUT="cuda_bindings/.moon-out/wheel-$WHEEL_VARIANT" + set -- "$BINDINGS_OUTPUT"/*.whl + [[ $# -eq 1 ]] || { + 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())") + 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 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") + 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 SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH" + 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 $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: + BUILD_CUDA_MAJOR: '${BUILD_CUDA_MAJOR}' + 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: + # Keep same-phase source proxies so affected selection sees upstream + # changes before Moon expands executable dependencies. + - {project: pathfinder, group: package} + - {project: bindings, group: package} + + wheel-previous: + 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_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' + options: + mergeDeps: replace + mergeEnv: replace + mergeInputs: replace + mergeOutputs: replace + + wheel-merge: + command: python + 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 + # 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' + - '/ci/tools/merge_cuda_core_wheels.py' + - '/.github/workflows/build-wheel.yml' + outputs: + - '.moon-out/wheel-merged' + tags: [ci-build-native] + type: build + options: + cache: true + runInCI: true + + sdist: + 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 + 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())") + 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 + - target: bindings:sdist + cacheStrategy: outputs + 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: + - {project: pathfinder, group: package} + - {project: bindings, group: package} + + cython-test-assets: + command: python + args: + - cuda_core/tests/cython/build_tests.py + - --output-dir + - cuda_core/.moon-out/cython-tests + env: + PYTHONPATH: cuda_python_test_helpers + inputs: + - {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' + - '/cuda_python_test_helpers/cuda_python_test_helpers/cython_test_builder.py' + + test-binaries: + command: python + 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}' + deps: + - target: test-helpers:fingerprint-native-context + cacheStrategy: hash + inputs: + - 'tests/test_binaries/build_test_binaries.py' + - 'tests/test_binaries/saxpy.cu' + - '/.github/workflows/build-wheel.yml' + outputs: + - '.moon-out/test-binaries' + tags: [ci-build-native] + type: build + options: + cache: true + os: [linux, windows] + runInCI: true + + test: + inputs: + - '/cuda_pathfinder/**/*' + - '/cuda_bindings/**/*' + - '/cuda_python_test_helpers/**/*' + - '/ci/test-matrix.yml' + - '/ci/tools/env-vars' + - '/tests/**/*' + + test-installed-linux: + inputs: + - '@group(tests)' + - {project: pathfinder, group: package} + - {project: bindings, group: package} + - '/cuda_python_test_helpers/**/*' + - '/ci/tools/merge_cuda_core_wheels.py' + - '/ci/tools/setup-sanitizer' + + test-installed-windows: + inputs: + - '@group(tests)' + - {project: pathfinder, group: package} + - {project: bindings, group: package} + - '/cuda_python_test_helpers/**/*' + - '/ci/tools/merge_cuda_core_wheels.py' + + docs: + args: [docs-build] + inputs: + - {project: pathfinder, group: package} + - {project: pathfinder, group: docs} + - {project: bindings, group: package} + - {project: bindings, group: docs} + + docs-ci: + inputs: + - {project: pathfinder, group: package} + - {project: bindings, group: package} + + 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] + 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] + type: test + options: + os: linux + runInCI: true 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 6cebb0c6ff9..022c9cbdec9 100644 --- a/cuda_core/tests/cython/build_tests.py +++ b/cuda_core/tests/cython/build_tests.py @@ -1,60 +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 os -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 +from cuda_python_test_helpers.cython_test_builder import build_cython_tests def main() -> None: - script_dir = Path(__file__).resolve().parent - 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", "--inplace"] - setup(name="cuda_core_cython_tests", ext_modules=ext_modules) - 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 7ee65c50d68..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 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_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_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 4720cd458f6..047c8369167 100755 --- a/cuda_pathfinder/docs/build_docs.sh +++ b/cuda_pathfinder/docs/build_docs.sh @@ -1,59 +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 -if [[ "$#" == "0" ]]; then - LATEST_ONLY="0" -elif [[ "$#" == "1" && "$1" == "latest-only" ]]; then - LATEST_ONLY="1" -else - echo "usage: ./build_docs.sh [latest-only]" - exit 1 -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 +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +exec "${SCRIPT_DIR}/../../cuda_python/docs/build_component_docs.sh" cuda-pathfinder "$@" diff --git a/cuda_pathfinder/moon.yml b/cuda_pathfinder/moon.yml new file mode 100644 index 00000000000..390b0771cd3 --- /dev/null +++ b/cuda_pathfinder/moon.yml @@ -0,0 +1,151 @@ +# 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 +tags: [docs-package, installed-test-package, pixi-package, pure-wheel-package, python-package] +dependsOn: + - id: test-helpers + scope: development +fileGroups: + package: + - '.git_archival.txt' + - 'cuda/**/*' + - 'DESCRIPTION.rst' + - 'LICENSE' + - 'pyproject.toml' +tasks: + 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: + 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 + + prepare-strict-linux: + 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 + deps: + - pathfinder:test-installed-linux + env: + TEST_CUDA_MAJOR: '${TEST_CUDA_MAJOR}' + inputs: + - '@group(package)' + - '@group(tests)' + - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' + - '/.github/workflows/test-wheel-linux.yml' + tags: [ci-test-linux] + options: + mutex: ci-python-gpu + os: linux + runInCI: true + + test-installed-linux-strict: + 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 + + test-installed-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 + + prepare-strict-windows: + extends: prepare-strict-linux + deps: + - pathfinder:test-installed-windows + inputs: + - '@group(package)' + - '@group(tests)' + - '/cuda_pathfinder/.moon-out/wheel-pure/*.whl' + - '/.github/workflows/test-wheel-windows.yml' + tags: [ci-test-windows] + options: + mergeDeps: replace + mergeInputs: replace + mergeTags: replace + os: windows + + test-installed-windows-strict: + 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 + + docs: + args: [build-docs] + + wheel-pure: + 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 + 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 + set -- cuda_pathfinder/.moon-out/wheel-pure/*.whl + [[ $# -eq 1 ]] || { + echo "expected one cuda.pathfinder wheel, found $#" >&2 + exit 1 + } + tags: [ci-build-native] diff --git a/cuda_python/docs/assemble_moon_docs.sh b/cuda_python/docs/assemble_moon_docs.sh new file mode 100755 index 00000000000..51fc6e628f3 --- /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/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 0b2067ed60c..e5dae4768e0 100755 --- a/cuda_python/docs/build_docs.sh +++ b/cuda_python/docs/build_docs.sh @@ -1,58 +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 -if [[ "$#" == "0" ]]; then - LATEST_ONLY="0" -elif [[ "$#" == "1" && "$1" == "latest-only" ]]; then - LATEST_ONLY="1" -else - echo "usage: ./build_docs.sh [latest-only]" - exit 1 -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/ +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +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 new file mode 100644 index 00000000000..f034f33c0a5 --- /dev/null +++ b/cuda_python/moon.yml @@ -0,0 +1,123 @@ +# 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 +tags: [docs-package, installed-test-package, pure-wheel-package, python-package] +dependsOn: + - pathfinder + - bindings + - core + - id: test-helpers + scope: development +fileGroups: + package: + - 'DESCRIPTION.rst' + - 'LICENSE' + - 'pyproject.toml' + - 'setup.py' + docs: + - 'docs/**/*' + +tasks: + wheel-pure: + 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 + 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 + set -- cuda_python/.moon-out/wheel-pure/*.whl + [[ $# -eq 1 ]] || { + 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: + # 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' + + sdist: + inputs: + - {project: pathfinder, group: package} + - {project: bindings, group: package} + + test-installed-linux: + deps: + - target: metapackage:wheel-pure + cacheStrategy: outputs + inputs: + - {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/merge_cuda_core_wheels.py' + + test-installed-windows: + deps: + - target: metapackage:wheel-pure + cacheStrategy: outputs + inputs: + - {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/merge_cuda_core_wheels.py' + + docs-ci: + inputs: + - {project: pathfinder, group: package} + - {project: bindings, group: package} + - {project: core, group: package} 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 new file mode 100644 index 00000000000..a8621c8bf48 --- /dev/null +++ b/cuda_python_test_helpers/moon.yml @@ -0,0 +1,218 @@ +# 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 +taskOptions: + cache: false + runFromWorkspaceRoot: true + runInCI: false + 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. + 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 --force-reinstall --no-deps \ + "$PATHFINDER_WHEEL" \ + "$BINDINGS_WHEEL" \ + "$CORE_WHEEL" + python -m pip install \ + --group cuda_bindings/pyproject.toml:test \ + --group cuda_core/pyproject.toml:test + 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' + - '/.github/workflows/build-wheel.yml' + 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 new file mode 100644 index 00000000000..cb7385c456e --- /dev/null +++ b/moon.yml @@ -0,0 +1,250 @@ +# 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 +taskOptions: + cache: false + runInCI: false + shell: false + +fileGroups: + orchestration: + - '/.moon/**/*' + - '/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' + - '/.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' + - '!/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' + - '!/.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/build-constraints.txt' + - '!/ci/build-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/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/**/*' + - '!/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_tasks, ci.tools.tests.test_moon_workspace] + 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' + - '/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 + runInCI: true + + test: + deps: + - pathfinder:test + - bindings:test + - core:test + inputs: [] + + docs: + deps: + - pathfinder:docs + - bindings:docs + - core:docs + inputs: [] + + docs-ci: + command: bash + args: [cuda_python/docs/assemble_moon_docs.sh] + 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: + - '/cuda_python/docs/assemble_moon_docs.sh' + - '/.github/workflows/build-docs.yml' + outputs: + - '.moon-out/docs' + tags: [ci-docs] + type: build + options: + os: linux + runInCI: true + + pure-wheel: + deps: + - pathfinder:wheel-pure + - metapackage:wheel-pure + inputs: [] 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/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", 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)"