diff --git a/.github/workflows/build-wheel.yml b/.github/workflows/build-wheel.yml
index ea4778df2f4..c3e798175ab 100644
--- a/.github/workflows/build-wheel.yml
+++ b/.github/workflows/build-wheel.yml
@@ -8,17 +8,14 @@ on:
host-platform:
required: true
type: string
- cuda-version:
- required: true
- type: string
- prev-cuda-version:
+ bindings-config:
+ description: Normalized CUDA bindings package-root registry JSON.
required: true
type: string
workplan:
- description: JSON workplan. An empty value builds and tests everything.
- required: false
+ description: JSON workplan.
+ required: true
type: string
- default: ""
single-cuda-major:
description: "Build wheels for only the current CUDA major; skip the prior-major build and wheel merge"
required: false
@@ -40,14 +37,8 @@ permissions:
jobs:
build:
env:
- BUILD_PATHFINDER: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.pathfinder.needs_build }}
- BUILD_BINDINGS: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.bindings.needs_build }}
- BUILD_CORE: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.core.needs_build }}
- BUILD_PYTHON: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.python.needs_build }}
- TEST_BINDINGS: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.bindings.needs_test }}
- TEST_CORE: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.core.needs_test }}
- BASELINE_RUN_ID: ${{ inputs.workplan != '' && fromJSON(inputs.workplan).baseline.run_id || '' }}
- BASELINE_SHA: ${{ inputs.workplan != '' && fromJSON(inputs.workplan).baseline.sha || '' }}
+ BASELINE_RUN_ID: ${{ fromJSON(inputs.workplan).baseline.run_id }}
+ BASELINE_SHA: ${{ fromJSON(inputs.workplan).baseline.sha }}
strategy:
fail-fast: false
matrix:
@@ -79,6 +70,39 @@ jobs:
# the job level instead of linking into the diff.
run: echo "::add-matcher::.github/matchers/compiler.json"
+ - name: Resolve package-specific workplan gates
+ env:
+ BINDINGS_CONFIG: ${{ inputs.bindings-config }}
+ WORKPLAN: ${{ inputs.workplan }}
+ run: |
+ set -euo pipefail
+ jq -er --argjson config "$BINDINGS_CONFIG" '
+ . as $plan
+ | ($config.package_roots | map(select(.release_status == "current"))
+ | if length == 1 then .[0] else error("expected one current bindings package root") end) as $current
+ | ($config.package_roots | map(select(.release_status == "maintenance"))
+ | if length == 1 then .[0] else error("expected one maintenance bindings package root") end) as $maintenance
+ | {
+ PATHFINDER_SOURCE: $plan.sources.pathfinder,
+ BUILD_PATHFINDER: $plan.modules.pathfinder.needs_build,
+ BUILD_BINDINGS: $plan.modules.bindings.needs_build,
+ BUILD_BINDINGS_CURRENT: $plan.modules.bindings.package_roots[$current.package_root].needs_build,
+ BUILD_BINDINGS_MAINTENANCE: $plan.modules.bindings.package_roots[$maintenance.package_root].needs_build,
+ BUILD_CORE: $plan.modules.core.needs_build,
+ BUILD_CUDA_PYTHON: $plan.modules.python.needs_build,
+ BUILD_CUDA_PYTHON_CURRENT: $plan.modules.python.package_roots[$current.package_root].needs_build,
+ BUILD_CUDA_PYTHON_MAINTENANCE: $plan.modules.python.package_roots[$maintenance.package_root].needs_build,
+ TEST_BINDINGS: $plan.modules.bindings.needs_test,
+ TEST_BINDINGS_CURRENT: $plan.modules.bindings.package_roots[$current.package_root].needs_test,
+ TEST_BINDINGS_MAINTENANCE: $plan.modules.bindings.package_roots[$maintenance.package_root].needs_test,
+ TEST_CORE: $plan.modules.core.needs_test,
+ TEST_CORE_CURRENT: $plan.modules.core.cuda_majors[$current.cuda_variant].needs_test,
+ TEST_CORE_MAINTENANCE: $plan.modules.core.cuda_majors[$maintenance.cuda_variant].needs_test
+ }
+ | to_entries[]
+ | if .value == null then error("missing workplan value for \(.key)") else "\(.key)=\(.value)" end
+ ' <<< "$WORKPLAN" >> "$GITHUB_ENV"
+
- name: Install latest rapidsai/sccache
if: ${{ startsWith(inputs.host-platform, 'linux') && (env.BUILD_BINDINGS == 'true' || env.BUILD_CORE == 'true') }}
run: |
@@ -145,14 +169,22 @@ jobs:
$env:Path += ";$((Get-Location).Path)\\$env:YQ_DIR"
yq --version
+ - name: Install CI tool dependencies
+ run: python -m pip install -e ./ci
+
- name: Set environment variables
env:
- CUDA_VER: ${{ inputs.cuda-version }}
HOST_PLATFORM: ${{ inputs.host-platform }}
PY_VER: ${{ matrix.python-version }}
SHA: ${{ github.sha }}
run: ./ci/tools/env-vars build
+ - name: Prepare local maintenance SCM milestone
+ if: ${{ !inputs.single-cuda-major && (env.BUILD_BINDINGS_MAINTENANCE == 'true' || env.BUILD_CUDA_PYTHON_MAINTENANCE == 'true') }}
+ run: |
+ python -m ci.tools.bindings_scm_version \
+ --package-root "${MAINTENANCE_BINDINGS_ROOT}" --sha "${{ github.sha }}"
+
- name: Dump environment
run: |
env
@@ -170,7 +202,7 @@ jobs:
popd
- name: Download reusable cuda.pathfinder wheel
- if: ${{ env.BUILD_PATHFINDER != 'true' }}
+ if: ${{ env.BUILD_PATHFINDER != 'true' && env.BASELINE_RUN_ID != '' }}
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: cuda-pathfinder-wheel
@@ -179,6 +211,7 @@ jobs:
run-id: ${{ env.BASELINE_RUN_ID }}
- name: List the cuda.pathfinder artifacts directory
+ if: ${{ env.BUILD_PATHFINDER == 'true' || env.BASELINE_RUN_ID != '' }}
run: |
if [[ "${{ inputs.host-platform }}" == win* ]]; then
export CHOWN=chown
@@ -199,38 +232,48 @@ jobs:
if: ${{ env.BUILD_BINDINGS == 'true' }}
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]}")"
+ if [[ "$PATHFINDER_SOURCE" == "artifact" ]]; then
+ if [[ "${#pathfinder_wheels[@]}" -ne 1 || ! -f "${pathfinder_wheels[0]}" ]]; then
+ echo "Expected exactly one cuda.pathfinder wheel" >&2
+ exit 1
+ fi
+ 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}" > wheel-constraints/cuda-bindings.txt
+ elif [[ "$PATHFINDER_SOURCE" == "published" ]]; then
+ : > wheel-constraints/cuda-bindings.txt
else
- pathfinder_uri="file:///host$(realpath "${pathfinder_wheels[0]}")"
+ echo "Unknown cuda.pathfinder source: $PATHFINDER_SOURCE" >&2
+ exit 1
fi
- printf 'cuda-pathfinder @ %s\n' "${pathfinder_uri}" | tee wheel-constraints/cuda-bindings.txt
+ cat wheel-constraints/cuda-bindings.txt
- name: Upload cuda.pathfinder build artifacts
- if: ${{ strategy.job-index == 0 && inputs.host-platform == 'linux-64' }}
+ if: ${{ (env.BUILD_PATHFINDER == 'true' || env.BASELINE_RUN_ID != '') && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: cuda-pathfinder-wheel
path: cuda_pathfinder/*.whl
if-no-files-found: error
- - name: Set up mini CTK
- if: ${{ env.BUILD_BINDINGS == 'true' || env.BUILD_CORE == 'true' || env.TEST_BINDINGS == 'true' || env.TEST_CORE == 'true' }}
+ - name: Set up current mini CTK
+ if: ${{ env.BUILD_BINDINGS_CURRENT == 'true' || env.BUILD_CORE == 'true' || env.TEST_BINDINGS_CURRENT == 'true' || env.TEST_CORE_CURRENT == 'true' }}
uses: ./.github/actions/fetch_ctk
continue-on-error: false
with:
host-platform: ${{ inputs.host-platform }}
- cuda-version: ${{ inputs.cuda-version }}
+ cuda-version: ${{ env.CURRENT_CUDA_VERSION }}
- - name: Build cuda.bindings wheel
- if: ${{ env.BUILD_BINDINGS == 'true' }}
+ - name: Build cuda.bindings ${{ env.CURRENT_CUDA_MAJOR }} wheel
+ if: ${{ env.BUILD_BINDINGS_CURRENT == 'true' }}
uses: pypa/cibuildwheel@b3395e271a35f73dad77f21802abe9b986dfe299 # v4.3.1
with:
- package-dir: ./cuda_bindings_13/
- output-dir: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}
+ package-dir: ./${{ env.CURRENT_BINDINGS_ROOT }}/
+ output-dir: ${{ env.CURRENT_BINDINGS_ARTIFACTS_DIR }}
extras: uv
env:
CIBW_BUILD: ${{ env.CIBW_BUILD }}
@@ -261,43 +304,44 @@ jobs:
"/host/${{ env.SCCACHE_PATH }}" --show-stats --stats-format=json > /host/${{ github.workspace }}/sccache_bindings.json &&
auditwheel repair -w {dest_dir} {wheel}
- - name: Report sccache stats (cuda.bindings)
- if: ${{ env.BUILD_BINDINGS == 'true' && !startsWith(inputs.host-platform, 'win') }}
+ - name: Report sccache stats (cuda.bindings ${{ env.CURRENT_CUDA_MAJOR }})
+ if: ${{ env.BUILD_BINDINGS_CURRENT == 'true' && !startsWith(inputs.host-platform, 'win') }}
uses: ./.github/actions/sccache-summary
with:
json-file: sccache_bindings.json
- label: "cuda.bindings"
- build-step: "Build cuda.bindings wheel"
+ label: "cuda.bindings ${{ env.CURRENT_CUDA_MAJOR }}"
+ build-step: "Build cuda.bindings ${{ env.CURRENT_CUDA_MAJOR }} wheel"
- - name: Download reusable cuda.bindings wheel
- if: ${{ env.BUILD_BINDINGS != 'true' }}
+ - name: Download reusable cuda.bindings ${{ env.CURRENT_CUDA_MAJOR }} wheel
+ if: ${{ env.BUILD_BINDINGS_CURRENT != 'true' && env.BASELINE_RUN_ID != '' }}
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
- name: ${{ env.CUDA_BINDINGS_ARTIFACT_BASENAME }}-${{ env.BASELINE_SHA }}
- path: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}
+ name: ${{ env.CURRENT_BINDINGS_ARTIFACT_BASENAME }}-${{ env.BASELINE_SHA }}
+ path: ${{ env.CURRENT_BINDINGS_ARTIFACTS_DIR }}
github-token: ${{ github.token }}
run-id: ${{ env.BASELINE_RUN_ID }}
- - name: List the cuda.bindings artifacts directory
+ - name: List the cuda.bindings ${{ env.CURRENT_CUDA_MAJOR }} artifacts directory
+ if: ${{ env.BUILD_BINDINGS_CURRENT == 'true' || env.BASELINE_RUN_ID != '' }}
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 }}
+ $CHOWN -R $(whoami) ${{ env.CURRENT_BINDINGS_ARTIFACTS_DIR }}
+ ls -lahR ${{ env.CURRENT_BINDINGS_ARTIFACTS_DIR }}
- - name: Check cuda.bindings wheel
- if: ${{ env.BUILD_BINDINGS == 'true' }}
+ - name: Check cuda.bindings ${{ env.CURRENT_CUDA_MAJOR }} wheel
+ if: ${{ env.BUILD_BINDINGS_CURRENT == 'true' }}
run: |
- twine check --strict ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}/*.whl
+ twine check --strict ${{ env.CURRENT_BINDINGS_ARTIFACTS_DIR }}/*.whl
- name: Constrain cuda.core to the local cuda.bindings wheel
if: ${{ env.BUILD_CORE == 'true' }}
run: |
pathfinder_wheels=(cuda_pathfinder/cuda_pathfinder-*.whl)
- bindings_wheels=("${CUDA_BINDINGS_ARTIFACTS_DIR}"/cuda_bindings-"${BUILD_CUDA_MAJOR}".*.whl)
+ bindings_wheels=("${CURRENT_BINDINGS_ARTIFACTS_DIR}"/cuda_bindings-"${CURRENT_CUDA_MAJOR}".*.whl)
test "${#pathfinder_wheels[@]}" -eq 1
test "${#bindings_wheels[@]}" -eq 1
test -f "${pathfinder_wheels[0]}"
@@ -315,14 +359,15 @@ jobs:
printf 'cuda-bindings @ %s\n' "${bindings_uri}"
} | tee wheel-constraints/cuda-core.txt
- - name: Upload cuda.bindings build artifacts
+ - name: Upload cuda.bindings ${{ env.CURRENT_CUDA_MAJOR }} build artifacts
+ if: ${{ env.BUILD_BINDINGS_CURRENT == 'true' || env.BASELINE_RUN_ID != '' }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
- name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }}
- path: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}/*.whl
+ name: ${{ env.CURRENT_BINDINGS_ARTIFACT_NAME }}
+ path: ${{ env.CURRENT_BINDINGS_ARTIFACTS_DIR }}/*.whl
if-no-files-found: error
- - name: Build cuda.core wheel
+ - name: Build cuda.core ${{ env.CURRENT_CUDA_VARIANT }} wheel
if: ${{ env.BUILD_CORE == 'true' }}
uses: pypa/cibuildwheel@b3395e271a35f73dad77f21802abe9b986dfe299 # v4.3.1
with:
@@ -337,7 +382,7 @@ 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 }}
+ CUDA_CORE_BUILD_MAJOR=${{ env.CURRENT_CUDA_MAJOR }}
CUDA_PYTHON_WERROR=1
UV_BUILD_CONSTRAINT=/host/${{ github.workspace }}/wheel-constraints/cuda-core.txt
UV_CONSTRAINT=/host/${{ github.workspace }}/wheel-constraints/cuda-core.txt
@@ -351,7 +396,7 @@ 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 }}
+ CUDA_CORE_BUILD_MAJOR=${{ env.CURRENT_CUDA_MAJOR }}
CUDA_PYTHON_WERROR=1
UV_BUILD_CONSTRAINT="$(cygpath -w ./wheel-constraints/cuda-core.txt)"
UV_CONSTRAINT="$(cygpath -w ./wheel-constraints/cuda-core.txt)"
@@ -367,7 +412,7 @@ jobs:
with:
json-file: sccache_core.json
label: "cuda.core"
- build-step: "Build cuda.core wheel"
+ build-step: "Build cuda.core ${{ env.CURRENT_CUDA_VARIANT }} wheel"
- name: List the cuda.core artifacts directory and rename
if: ${{ env.BUILD_CORE == 'true' }}
@@ -380,12 +425,12 @@ jobs:
$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}"
+ mkdir -p "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${CURRENT_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}"
+ new_name="${base_name}.cu${CURRENT_CUDA_MAJOR}.whl"
+ mv "${wheel}" "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${CURRENT_CUDA_MAJOR}/${new_name}"
echo "Renamed wheel to: ${new_name}"
fi
done
@@ -393,7 +438,7 @@ jobs:
ls -lahR ${{ env.CUDA_CORE_ARTIFACTS_DIR }}
- name: Download reusable cuda.core wheel
- if: ${{ env.BUILD_CORE != 'true' }}
+ if: ${{ env.BUILD_CORE != 'true' && env.BASELINE_RUN_ID != '' }}
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: ${{ env.CUDA_CORE_ARTIFACT_BASENAME }}-${{ env.BASELINE_SHA }}
@@ -401,42 +446,71 @@ jobs:
github-token: ${{ github.token }}
run-id: ${{ env.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: ${{ env.BUILD_PYTHON == 'true' && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }}
+ # We only need/want one copy of each pure Python wheel, so use linux-64 index 0.
+ - name: Build and check cuda-python ${{ env.MAINTENANCE_CUDA_MAJOR }} wheel
+ if: ${{ !inputs.single-cuda-major && env.BUILD_CUDA_PYTHON_MAINTENANCE == 'true' && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }}
run: |
+ mkdir -p "cuda_python/dist/${MAINTENANCE_CUDA_VARIANT}"
pushd cuda_python
- pip wheel -v --no-deps .
- twine check --strict *.whl
+ CUDA_PYTHON_BUILD_MAJOR="${MAINTENANCE_CUDA_MAJOR}" \
+ pip wheel -v --no-deps --wheel-dir "dist/${MAINTENANCE_CUDA_VARIANT}" .
+ twine check --strict "dist/${MAINTENANCE_CUDA_VARIANT}"/*.whl
+ popd
+
+ - name: Download reusable cuda-python ${{ env.MAINTENANCE_CUDA_MAJOR }} wheel
+ if: ${{ !inputs.single-cuda-major && env.BUILD_CUDA_PYTHON_MAINTENANCE != 'true' && env.BASELINE_RUN_ID != '' && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }}
+ uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
+ with:
+ name: ${{ env.MAINTENANCE_PYTHON_ARTIFACT_NAME }}
+ path: cuda_python/dist/${{ env.MAINTENANCE_CUDA_VARIANT }}
+ github-token: ${{ github.token }}
+ run-id: ${{ env.BASELINE_RUN_ID }}
+
+ - name: Upload cuda-python ${{ env.MAINTENANCE_CUDA_MAJOR }} build artifacts
+ if: ${{ !inputs.single-cuda-major && (env.BUILD_CUDA_PYTHON_MAINTENANCE == 'true' || env.BASELINE_RUN_ID != '') && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }}
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
+ with:
+ name: ${{ env.MAINTENANCE_PYTHON_ARTIFACT_NAME }}
+ path: cuda_python/dist/${{ env.MAINTENANCE_CUDA_VARIANT }}/*.whl
+ if-no-files-found: error
+
+ - name: Build and check cuda-python ${{ env.CURRENT_CUDA_MAJOR }} wheel
+ if: ${{ env.BUILD_CUDA_PYTHON_CURRENT == 'true' && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }}
+ run: |
+ mkdir -p "cuda_python/dist/${CURRENT_CUDA_VARIANT}"
+ pushd cuda_python
+ CUDA_PYTHON_BUILD_MAJOR="${CURRENT_CUDA_MAJOR}" \
+ pip wheel -v --no-deps --wheel-dir "dist/${CURRENT_CUDA_VARIANT}" .
+ twine check --strict "dist/${CURRENT_CUDA_VARIANT}"/*.whl
popd
- - name: Download reusable cuda-python wheel
- if: ${{ env.BUILD_PYTHON != 'true' && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }}
+ - name: Download reusable cuda-python ${{ env.CURRENT_CUDA_MAJOR }} wheel
+ if: ${{ env.BUILD_CUDA_PYTHON_CURRENT != 'true' && env.BASELINE_RUN_ID != '' && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }}
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
- name: cuda-python-wheel
- path: cuda_python
+ name: ${{ env.CURRENT_PYTHON_ARTIFACT_NAME }}
+ path: cuda_python/dist/${{ env.CURRENT_CUDA_VARIANT }}
github-token: ${{ github.token }}
run-id: ${{ env.BASELINE_RUN_ID }}
- - name: List the cuda-python artifacts directory
- if: ${{ strategy.job-index == 0 && inputs.host-platform == 'linux-64' }}
+ - name: Upload cuda-python ${{ env.CURRENT_CUDA_MAJOR }} build artifacts
+ if: ${{ (env.BUILD_CUDA_PYTHON_CURRENT == 'true' || env.BASELINE_RUN_ID != '') && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }}
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
+ with:
+ name: ${{ env.CURRENT_PYTHON_ARTIFACT_NAME }}
+ path: cuda_python/dist/${{ env.CURRENT_CUDA_VARIANT }}/*.whl
+ if-no-files-found: error
+
+ - name: List the cuda-python artifacts directories
+ if: ${{ (env.BUILD_CUDA_PYTHON == 'true' || env.BASELINE_RUN_ID != '') && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }}
run: |
if [[ "${{ inputs.host-platform }}" == win* ]]; then
export CHOWN=chown
else
export CHOWN="sudo chown"
fi
- $CHOWN -R $(whoami) cuda_python/*.whl
- ls -lahR cuda_python
-
- - name: Upload cuda-python build artifacts
- if: ${{ strategy.job-index == 0 && inputs.host-platform == 'linux-64' }}
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
- with:
- name: cuda-python-wheel
- path: cuda_python/*.whl
- if-no-files-found: error
+ $CHOWN -R $(whoami) cuda_python/dist
+ ls -lahR cuda_python/dist
- name: Set up Python
id: setup-python2
@@ -464,7 +538,19 @@ jobs:
- name: Install cuda.pathfinder (required for next step)
if: ${{ env.TEST_BINDINGS == 'true' || env.TEST_CORE == 'true' }}
run: |
- pip install cuda_pathfinder/*.whl
+ pathfinder_wheels=(cuda_pathfinder/*.whl)
+ if [[ "$PATHFINDER_SOURCE" == "artifact" ]]; then
+ if [[ "${#pathfinder_wheels[@]}" -ne 1 || ! -f "${pathfinder_wheels[0]}" ]]; then
+ echo "Expected exactly one cuda.pathfinder wheel" >&2
+ exit 1
+ fi
+ pip install "${pathfinder_wheels[0]}"
+ elif [[ "$PATHFINDER_SOURCE" == "published" ]]; then
+ pip install cuda-pathfinder
+ else
+ echo "Unknown cuda.pathfinder source: $PATHFINDER_SOURCE" >&2
+ exit 1
+ fi
- name: Hide GNU link.exe so Meson finds MSVC link.exe
if: ${{ startsWith(inputs.host-platform, 'win') && (env.TEST_BINDINGS == 'true' || env.TEST_CORE == 'true') }}
@@ -473,28 +559,36 @@ jobs:
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: ${{ env.TEST_BINDINGS == 'true' }}
+ - name: Build cuda.bindings ${{ env.CURRENT_CUDA_MAJOR }} Cython tests
+ if: ${{ env.TEST_BINDINGS_CURRENT == 'true' }}
run: |
- pip install ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}/*.whl --group ./cuda_bindings_13/pyproject.toml:test
- pushd ${{ env.CUDA_BINDINGS_CYTHON_TESTS_DIR }}
+ bindings_wheels=(${{ env.CURRENT_BINDINGS_ARTIFACTS_DIR }}/*.whl)
+ test "${#bindings_wheels[@]}" -eq 1
+ test -f "${bindings_wheels[0]}"
+ bindings_pyproject="${CURRENT_BINDINGS_ROOT}/pyproject.toml"
+ if grep -q '^\[dependency-groups\][[:space:]]*$' "${bindings_pyproject}"; then
+ python -m pip install "${bindings_wheels[0]}" --group "${bindings_pyproject}:test"
+ else
+ python -m pip install "${bindings_wheels[0]}[test]"
+ fi
+ pushd ${{ env.CURRENT_BINDINGS_CYTHON_TESTS_DIR }}
bash build_tests.sh
popd
- - name: Upload cuda.bindings Cython tests
- if: ${{ env.TEST_BINDINGS == 'true' }}
+ - name: Upload cuda.bindings ${{ env.CURRENT_CUDA_MAJOR }} Cython tests
+ if: ${{ env.TEST_BINDINGS_CURRENT == 'true' }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
- name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }}-tests
- path: ${{ env.CUDA_BINDINGS_CYTHON_TESTS_DIR }}/test_*${{ env.PY_EXT_SUFFIX }}
+ name: ${{ env.CURRENT_BINDINGS_ARTIFACT_NAME }}-tests
+ path: ${{ env.CURRENT_BINDINGS_CYTHON_TESTS_DIR }}/test_*${{ env.PY_EXT_SUFFIX }}
if-no-files-found: error
- - name: Build cuda.core Cython tests
- if: ${{ env.TEST_CORE == 'true' }}
+ - name: Build cuda.core ${{ env.CURRENT_CUDA_VARIANT }} Cython tests
+ if: ${{ env.TEST_CORE_CURRENT == 'true' }}
run: |
- pip install ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}/*.whl
+ pip install ${{ env.CURRENT_BINDINGS_ARTIFACTS_DIR }}/*.whl
if ${{ env.BUILD_CORE == 'true' }}; then
- core_wheel=$(find "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${BUILD_CUDA_MAJOR}" -maxdepth 1 -type f -name '*.whl' -print -quit)
+ core_wheel=$(find "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${CURRENT_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
@@ -507,22 +601,22 @@ jobs:
bash build_tests.sh
popd
- - name: Upload cuda.core Cython tests
- if: ${{ env.TEST_CORE == 'true' }}
+ - name: Upload current cuda.core Cython tests
+ if: ${{ env.TEST_CORE_CURRENT == 'true' }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
- name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-tests
+ name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-${{ env.CURRENT_CUDA_VARIANT }}-tests
path: ${{ env.CUDA_CORE_CYTHON_TESTS_DIR }}/test_*${{ env.PY_EXT_SUFFIX }}
if-no-files-found: error
- # Note: This overwrites CUDA_PATH etc
- - name: Set up mini CTK
- if: ${{ !inputs.single-cuda-major && (env.BUILD_CORE == 'true' || env.TEST_CORE == 'true') }}
+ # Note: This overwrites CUDA_PATH etc.
+ - name: Set up maintenance mini CTK
+ if: ${{ !inputs.single-cuda-major && (env.BUILD_BINDINGS_MAINTENANCE == 'true' || env.BUILD_CORE == 'true' || env.TEST_BINDINGS_MAINTENANCE == 'true' || env.TEST_CORE_MAINTENANCE == 'true') }}
uses: ./.github/actions/fetch_ctk
continue-on-error: false
with:
host-platform: ${{ inputs.host-platform }}
- cuda-version: ${{ inputs.prev-cuda-version }}
+ cuda-version: ${{ env.MAINTENANCE_CUDA_VERSION }}
cuda-path: "./cuda_toolkit_prev"
- name: Build cuda.core test binaries
@@ -542,47 +636,111 @@ jobs:
${{ env.CUDA_CORE_TEST_BINARIES_DIR }}/*.lib
if-no-files-found: error
- - name: Download cuda.bindings build artifacts from the prior branch
- if: ${{ !inputs.single-cuda-major && env.BUILD_CORE == 'true' }}
+ - name: Build cuda.bindings ${{ env.MAINTENANCE_CUDA_MAJOR }} wheel
+ if: ${{ !inputs.single-cuda-major && env.BUILD_BINDINGS_MAINTENANCE == 'true' }}
+ uses: pypa/cibuildwheel@b3395e271a35f73dad77f21802abe9b986dfe299 # v4.3.1
+ with:
+ package-dir: ./${{ env.MAINTENANCE_BINDINGS_ROOT }}/
+ output-dir: ${{ env.MAINTENANCE_BINDINGS_ARTIFACTS_DIR }}
+ extras: uv
env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ CIBW_BUILD: ${{ env.CIBW_BUILD }}
+ # Use the same frontend and constraints as the current package.
+ CIBW_BUILD_FRONTEND: "build[uv]"
+ CIBW_ENVIRONMENT_LINUX: >
+ CUDA_PATH=/host/${{ env.CUDA_PATH }}
+ CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }}
+ UV_BUILD_CONSTRAINT=/host/${{ github.workspace }}/wheel-constraints/cuda-bindings.txt
+ UV_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 }}
+ CIBW_ENVIRONMENT_WINDOWS: >
+ CUDA_PATH="$(cygpath -w ${{ env.CUDA_PATH }})"
+ CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }}
+ UV_BUILD_CONSTRAINT="$(cygpath -w ./wheel-constraints/cuda-bindings.txt)"
+ UV_CONSTRAINT="$(cygpath -w ./wheel-constraints/cuda-bindings.txt)"
+ # Dump sccache stats during repair; no dummy test stage.
+ CIBW_REPAIR_WHEEL_COMMAND_LINUX: >
+ "/host/${{ env.SCCACHE_PATH }}" --show-adv-stats &&
+ "/host/${{ env.SCCACHE_PATH }}" --show-stats --stats-format=json > /host/${{ github.workspace }}/sccache_bindings_maintenance.json &&
+ auditwheel repair -w {dest_dir} {wheel}
+
+ - name: Report sccache stats (cuda.bindings ${{ env.MAINTENANCE_CUDA_MAJOR }})
+ if: ${{ !inputs.single-cuda-major && env.BUILD_BINDINGS_MAINTENANCE == 'true' && !startsWith(inputs.host-platform, 'win') }}
+ uses: ./.github/actions/sccache-summary
+ with:
+ json-file: sccache_bindings_maintenance.json
+ label: "cuda.bindings ${{ env.MAINTENANCE_CUDA_MAJOR }}"
+ build-step: "Build cuda.bindings ${{ env.MAINTENANCE_CUDA_MAJOR }} wheel"
+
+ - name: Download reusable cuda.bindings ${{ env.MAINTENANCE_CUDA_MAJOR }} wheel
+ if: ${{ !inputs.single-cuda-major && env.BUILD_BINDINGS_MAINTENANCE != 'true' && env.BASELINE_RUN_ID != '' }}
+ uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
+ with:
+ name: ${{ env.MAINTENANCE_BINDINGS_ARTIFACT_BASENAME }}-${{ env.BASELINE_SHA }}
+ path: ${{ env.MAINTENANCE_BINDINGS_ARTIFACTS_DIR }}
+ github-token: ${{ github.token }}
+ run-id: ${{ env.BASELINE_RUN_ID }}
+
+ - name: List the cuda.bindings ${{ env.MAINTENANCE_CUDA_MAJOR }} artifacts directory
+ if: ${{ !inputs.single-cuda-major && (env.BUILD_BINDINGS_MAINTENANCE == 'true' || env.BASELINE_RUN_ID != '') }}
run: |
- if ! (command -v gh 2>&1 >/dev/null); then
- # 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
+ if [[ "${{ inputs.host-platform }}" == win* ]]; then
+ export CHOWN=chown
+ else
+ export CHOWN="sudo chown"
fi
+ $CHOWN -R $(whoami) ${{ env.MAINTENANCE_BINDINGS_ARTIFACTS_DIR }}
+ ls -lahR ${{ env.MAINTENANCE_BINDINGS_ARTIFACTS_DIR }}
+
+ - name: Check cuda.bindings ${{ env.MAINTENANCE_CUDA_MAJOR }} wheel
+ if: ${{ !inputs.single-cuda-major && env.BUILD_BINDINGS_MAINTENANCE == 'true' }}
+ run: twine check --strict ${{ env.MAINTENANCE_BINDINGS_ARTIFACTS_DIR }}/*.whl
+
+ - name: Upload cuda.bindings ${{ env.MAINTENANCE_CUDA_MAJOR }} build artifacts
+ if: ${{ !inputs.single-cuda-major && (env.BUILD_BINDINGS_MAINTENANCE == 'true' || env.BASELINE_RUN_ID != '') }}
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
+ with:
+ name: ${{ env.MAINTENANCE_BINDINGS_ARTIFACT_NAME }}
+ path: ${{ env.MAINTENANCE_BINDINGS_ARTIFACTS_DIR }}/*.whl
+ if-no-files-found: error
- OLD_BRANCH=$(yq '.backport_branch' ci/versions.yml)
- OLD_ARTIFACT_PATTERN="cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda*-${{ inputs.host-platform }}-*[0-9a-f]"
- LATEST_PRIOR_RUN_ID=$(./ci/tools/lookup-run-id \
- --branch "${OLD_BRANCH}" \
- --artifact "${OLD_ARTIFACT_PATTERN}" \
- NVIDIA/cuda-python "CI")
- PREV_BINDINGS_DIR="cuda_bindings_13/dist-prev"
-
- gh run download \
- "${LATEST_PRIOR_RUN_ID}" \
- -p "${OLD_ARTIFACT_PATTERN}" \
- -R NVIDIA/cuda-python
- OLD_ARTIFACT_DIR=$(compgen -G "${OLD_ARTIFACT_PATTERN}")
- test -d "${OLD_ARTIFACT_DIR}"
- ls -al "${OLD_ARTIFACT_DIR}"
- mkdir -p "${PREV_BINDINGS_DIR}"
- mv "${OLD_ARTIFACT_DIR}"/*.whl "${PREV_BINDINGS_DIR}"
- rmdir "${OLD_ARTIFACT_DIR}"
-
- - name: Constrain previous cuda.core to the downloaded cuda.bindings wheel
+ - name: Build cuda.bindings ${{ env.MAINTENANCE_CUDA_MAJOR }} Cython tests
+ if: ${{ !inputs.single-cuda-major && env.TEST_BINDINGS_MAINTENANCE == 'true' }}
+ run: |
+ pip uninstall -y cuda-bindings
+ bindings_wheels=(${{ env.MAINTENANCE_BINDINGS_ARTIFACTS_DIR }}/*.whl)
+ test "${#bindings_wheels[@]}" -eq 1
+ test -f "${bindings_wheels[0]}"
+ bindings_pyproject="${MAINTENANCE_BINDINGS_ROOT}/pyproject.toml"
+ if grep -q '^\[dependency-groups\][[:space:]]*$' "${bindings_pyproject}"; then
+ python -m pip install "${bindings_wheels[0]}" --group "${bindings_pyproject}:test"
+ else
+ python -m pip install "${bindings_wheels[0]}[test]"
+ fi
+ pushd ${{ env.MAINTENANCE_BINDINGS_CYTHON_TESTS_DIR }}
+ bash build_tests.sh
+ popd
+
+ - name: Upload cuda.bindings ${{ env.MAINTENANCE_CUDA_MAJOR }} Cython tests
+ if: ${{ !inputs.single-cuda-major && env.TEST_BINDINGS_MAINTENANCE == 'true' }}
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
+ with:
+ name: ${{ env.MAINTENANCE_BINDINGS_ARTIFACT_NAME }}-tests
+ path: ${{ env.MAINTENANCE_BINDINGS_CYTHON_TESTS_DIR }}/test_*${{ env.PY_EXT_SUFFIX }}
+ if-no-files-found: error
+
+ - name: Constrain maintenance cuda.core to the local cuda.bindings wheel
if: ${{ !inputs.single-cuda-major && env.BUILD_CORE == 'true' }}
run: |
pathfinder_wheels=(cuda_pathfinder/cuda_pathfinder-*.whl)
- bindings_wheels=(cuda_bindings_13/dist-prev/cuda_bindings-"${BUILD_PREV_CUDA_MAJOR}".*.whl)
+ bindings_wheels=("${MAINTENANCE_BINDINGS_ARTIFACTS_DIR}"/cuda_bindings-"${MAINTENANCE_CUDA_MAJOR}".*.whl)
test "${#pathfinder_wheels[@]}" -eq 1
test "${#bindings_wheels[@]}" -eq 1
test -f "${pathfinder_wheels[0]}"
@@ -598,9 +756,9 @@ jobs:
{
printf 'cuda-pathfinder @ %s\n' "${pathfinder_uri}"
printf 'cuda-bindings @ %s\n' "${bindings_uri}"
- } | tee wheel-constraints/cuda-core-prev.txt
+ } | tee wheel-constraints/cuda-core-maintenance.txt
- - name: Build cuda.core wheel
+ - name: Build cuda.core ${{ env.MAINTENANCE_CUDA_VARIANT }} wheel
if: ${{ !inputs.single-cuda-major && env.BUILD_CORE == 'true' }}
uses: pypa/cibuildwheel@b3395e271a35f73dad77f21802abe9b986dfe299 # v4.3.1
with:
@@ -615,10 +773,10 @@ 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_PREV_CUDA_MAJOR }}
+ CUDA_CORE_BUILD_MAJOR=${{ env.MAINTENANCE_CUDA_MAJOR }}
CUDA_PYTHON_WERROR=1
- UV_BUILD_CONSTRAINT=/host/${{ github.workspace }}/wheel-constraints/cuda-core-prev.txt
- UV_CONSTRAINT=/host/${{ github.workspace }}/wheel-constraints/cuda-core-prev.txt
+ UV_BUILD_CONSTRAINT=/host/${{ github.workspace }}/wheel-constraints/cuda-core-maintenance.txt
+ UV_CONSTRAINT=/host/${{ github.workspace }}/wheel-constraints/cuda-core-maintenance.txt
CC="/host/${{ env.SCCACHE_PATH }} cc"
CXX="/host/${{ env.SCCACHE_PATH }} c++"
SCCACHE_GHA_ENABLED=true
@@ -629,23 +787,23 @@ 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 }}
+ CUDA_CORE_BUILD_MAJOR=${{ env.MAINTENANCE_CUDA_MAJOR }}
CUDA_PYTHON_WERROR=1
- UV_BUILD_CONSTRAINT="$(cygpath -w ./wheel-constraints/cuda-core-prev.txt)"
- UV_CONSTRAINT="$(cygpath -w ./wheel-constraints/cuda-core-prev.txt)"
+ UV_BUILD_CONSTRAINT="$(cygpath -w ./wheel-constraints/cuda-core-maintenance.txt)"
+ UV_CONSTRAINT="$(cygpath -w ./wheel-constraints/cuda-core-maintenance.txt)"
# Dump sccache stats during repair; no dummy test stage.
CIBW_REPAIR_WHEEL_COMMAND_LINUX: >
"/host/${{ env.SCCACHE_PATH }}" --show-adv-stats &&
- "/host/${{ env.SCCACHE_PATH }}" --show-stats --stats-format=json > /host/${{ github.workspace }}/sccache_core_prev.json &&
+ "/host/${{ env.SCCACHE_PATH }}" --show-stats --stats-format=json > /host/${{ github.workspace }}/sccache_core_maintenance.json &&
auditwheel repair -w {dest_dir} {wheel}
- - name: Report sccache stats (cuda.core prev)
+ - name: Report sccache stats (maintenance cuda.core)
if: ${{ !inputs.single-cuda-major && env.BUILD_CORE == 'true' && !startsWith(inputs.host-platform, 'win') }}
uses: ./.github/actions/sccache-summary
with:
- json-file: sccache_core_prev.json
- label: "cuda.core (prev CTK)"
- build-step: "Build cuda.core wheel"
+ json-file: sccache_core_maintenance.json
+ label: "cuda.core (maintenance CTK)"
+ build-step: "Build cuda.core ${{ env.MAINTENANCE_CUDA_VARIANT }} wheel"
- name: List the cuda.core artifacts directory and rename
if: ${{ !inputs.single-cuda-major && env.BUILD_CORE == 'true' }}
@@ -659,32 +817,66 @@ jobs:
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}"
+ mkdir -p "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${MAINTENANCE_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}"
+ new_name="${base_name}.cu${MAINTENANCE_CUDA_MAJOR}.whl"
+ mv "${wheel}" "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${MAINTENANCE_CUDA_MAJOR}/${new_name}"
echo "Renamed wheel to: ${new_name}"
fi
done
ls -lahR ${{ env.CUDA_CORE_ARTIFACTS_DIR }}
+ - name: Build cuda.core ${{ env.MAINTENANCE_CUDA_VARIANT }} Cython tests
+ if: ${{ !inputs.single-cuda-major && env.TEST_CORE_MAINTENANCE == 'true' }}
+ run: |
+ # The current package's test extensions were built in this same source tree.
+ # Remove generated outputs so Cython and the compiler cannot reuse them.
+ find "${{ env.CUDA_CORE_CYTHON_TESTS_DIR }}" -maxdepth 1 -type f \
+ \( -name 'test_*.cpp' -o -name 'test_*.so' -o -name 'test_*.pyd' \) -delete
+ if [[ -d "${{ env.CUDA_CORE_CYTHON_TESTS_DIR }}/build" ]]; then
+ find "${{ env.CUDA_CORE_CYTHON_TESTS_DIR }}/build" -type f -delete
+ fi
+ pip uninstall -y cuda-bindings cuda-core
+ pip install ${{ env.MAINTENANCE_BINDINGS_ARTIFACTS_DIR }}/*.whl
+ if ${{ env.BUILD_CORE == 'true' }}; then
+ core_wheel=$(find "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${MAINTENANCE_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 maintenance cuda.core Cython tests
+ if: ${{ !inputs.single-cuda-major && env.TEST_CORE_MAINTENANCE == 'true' }}
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
+ with:
+ name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-${{ env.MAINTENANCE_CUDA_VARIANT }}-tests
+ path: ${{ env.CUDA_CORE_CYTHON_TESTS_DIR }}/test_*${{ env.PY_EXT_SUFFIX }}
+ if-no-files-found: error
+
- name: Merge cuda.core wheels
if: ${{ !inputs.single-cuda-major && env.BUILD_CORE == 'true' }}
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 \
+ "${{ env.CUDA_CORE_ARTIFACTS_DIR }}"/cu"${CURRENT_CUDA_MAJOR}"/cuda_core*.whl \
+ "${{ env.CUDA_CORE_ARTIFACTS_DIR }}"/cu"${MAINTENANCE_CUDA_MAJOR}"/cuda_core*.whl \
--output-dir "${{ env.CUDA_CORE_ARTIFACTS_DIR }}"
- name: Finalize single-major cuda.core wheel
if: ${{ inputs.single-cuda-major && env.BUILD_CORE == 'true' }}
run: |
- for wheel in "${{ env.CUDA_CORE_ARTIFACTS_DIR }}"/cu"${BUILD_CUDA_MAJOR}"/*.cu"${BUILD_CUDA_MAJOR}".whl; do
- base_name=$(basename "${wheel}" ".cu${BUILD_CUDA_MAJOR}.whl")
+ for wheel in "${{ env.CUDA_CORE_ARTIFACTS_DIR }}"/cu"${CURRENT_CUDA_MAJOR}"/*.cu"${CURRENT_CUDA_MAJOR}".whl; do
+ base_name=$(basename "${wheel}" ".cu${CURRENT_CUDA_MAJOR}.whl")
mv "${wheel}" "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/${base_name}.whl"
done
ls -lahR "${{ env.CUDA_CORE_ARTIFACTS_DIR }}"
@@ -695,6 +887,7 @@ jobs:
twine check --strict ${{ env.CUDA_CORE_ARTIFACTS_DIR }}/*.whl
- name: Upload cuda.core build artifacts
+ if: ${{ env.BUILD_CORE == 'true' || env.BASELINE_RUN_ID != '' }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 174f560e245..0921ba497b5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -3,7 +3,6 @@
# 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.
name: "CI"
concurrency:
@@ -27,61 +26,6 @@ on:
workflow_dispatch: {}
jobs:
- ci-vars:
- if: ${{ github.repository_owner == 'nvidia' }}
- runs-on: ubuntu-latest
- outputs:
- CUDA_BUILD_VER: ${{ steps.get-vars.outputs.cuda_build_ver }}
- CUDA_PREV_BUILD_VER: ${{ steps.get-vars.outputs.cuda_prev_build_ver }}
- WINDOWS_ARM64_SUPPORTED: ${{ steps.get-vars.outputs.windows_arm64_supported }}
- WINDOWS_ARM64_SINGLE_CUDA_MAJOR: ${{ steps.get-vars.outputs.windows_arm64_single_cuda_major }}
- PYTHON_SUPPORTED: ${{ steps.get-vars.outputs.python_supported }}
- steps:
- - name: Checkout repository
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- with:
- fetch-depth: 1
- - name: Get CUDA build versions
- id: get-vars
- run: |
- cuda_build_ver=$(yq '.cuda.build.version' ci/versions.yml)
- echo "cuda_build_ver=$cuda_build_ver" >> $GITHUB_OUTPUT
-
- cuda_prev_build_ver=$(yq '.cuda.prev_build.version' ci/versions.yml)
- echo "cuda_prev_build_ver=$cuda_prev_build_ver" >> $GITHUB_OUTPUT
-
- # Flattened one-line JSON for pass-through to build-wheel.yml via
- # fromJSON on the receiving side.
- python_supported=$(yq -o json -I 0 '.python.supported' ci/versions.yml)
- echo "python_supported=$python_supported" >> $GITHUB_OUTPUT
-
- # Windows ARM64 is available starting with CUDA 13.4.
- if [[ "$cuda_build_ver" =~ ^([0-9]+)\.([0-9]+)(\.|$) ]]; then
- cuda_build_major="${BASH_REMATCH[1]}"
- cuda_build_minor="${BASH_REMATCH[2]}"
- else
- echo "Invalid CUDA build version: $cuda_build_ver" >&2
- exit 1
- fi
- if (( cuda_build_major > 13 || (cuda_build_major == 13 && cuda_build_minor >= 4) )); then
- windows_arm64_supported=true
- else
- windows_arm64_supported=false
- fi
- echo "windows_arm64_supported=$windows_arm64_supported" >> $GITHUB_OUTPUT
-
- # No CUDA 13 windows-arm64 toolkit exists for a major other than the
- # current one (windows-arm64 support started mid-way through the 13.x
- # series), so cuda.core can only be built against a single CUDA major
- # while the build major is still 13. Once the build major advances to
- # 14, a CUDA 13 windows-arm64 toolkit will exist as the prior major.
- if [[ "$cuda_build_major" == "13" ]]; then
- windows_arm64_single_cuda_major=true
- else
- windows_arm64_single_cuda_major=false
- fi
- echo "windows_arm64_single_cuda_major=$windows_arm64_single_cuda_major" >> $GITHUB_OUTPUT
-
should-skip:
if: ${{ github.repository_owner == 'nvidia' }}
runs-on: ubuntu-latest
@@ -122,19 +66,21 @@ jobs:
#
# Dependency graph (verified in pyproject.toml files):
# cuda_pathfinder -> (no internal deps)
- # cuda_bindings -> cuda_pathfinder
+ # each registered cuda.bindings package -> cuda_pathfinder
# cuda_core -> cuda_pathfinder, cuda_bindings
# cuda_python -> cuda_pathfinder, cuda_bindings, cuda_core (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.
+ # downstream module. A change to either bindings package rebuilds cuda_core
+ # against both majors, but rebuilds only the matching bindings package.
# A change to cuda_core alone skips rebuilding/retesting cuda_bindings and
# cuda_pathfinder, but still retests the downstream cuda-python metapackage.
# Shared build/orchestration changes run the full pipeline; test-only CI
# infrastructure runs every test suite without rebuilding package wheels.
- # 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.
+ # On pushes to main, schedule, workflow_dispatch, or non-bindings release
+ # tags we unconditionally run everything because there is no meaningful
+ # "changed paths" baseline for those events. Bare bindings tags select the
+ # corresponding registered cuda.bindings package root and cuda-python build.
detect-changes:
if: ${{ github.repository_owner == 'nvidia' }}
runs-on: ubuntu-latest
@@ -143,6 +89,11 @@ jobs:
actions: read
contents: read
outputs:
+ bindings-config: ${{ steps.registry.outputs.bindings-config }}
+ current-cuda-version: ${{ steps.registry.outputs.current-cuda-version }}
+ python-supported: ${{ steps.registry.outputs.python-supported }}
+ windows-arm64-supported: ${{ steps.registry.outputs.windows-arm64-supported }}
+ windows-arm64-single-cuda-major: ${{ steps.registry.outputs.windows-arm64-single-cuda-major }}
workplan: ${{ steps.workplan.outputs.workplan }}
steps:
- name: Checkout repository
@@ -152,6 +103,50 @@ jobs:
fetch-depth: ${{ startsWith(github.ref_name, 'pull-request/') && '0' || '1' }}
filter: blob:none
+ - name: Install CI tool test dependencies
+ run: python3 -m pip install -e './ci[test]'
+
+ - name: Resolve registered build platforms
+ id: registry
+ run: |
+ set -euo pipefail
+ bindings_config=$(python3 -m ci.tools.bindings_config)
+ current_cuda_version=$(jq -er \
+ '.package_roots[] | select(.release_status == "current") | .toolkit_version' \
+ <<< "$bindings_config")
+
+ if [[ "$current_cuda_version" =~ ^([0-9]+)\.([0-9]+)(\.|$) ]]; then
+ current_cuda_major="${BASH_REMATCH[1]}"
+ current_cuda_minor="${BASH_REMATCH[2]}"
+ else
+ echo "Invalid current CUDA version: $current_cuda_version" >&2
+ exit 1
+ fi
+
+ # Windows ARM64 is available starting with CUDA 13.4.
+ if (( current_cuda_major > 13 || (current_cuda_major == 13 && current_cuda_minor >= 4) )); then
+ windows_arm64_supported=true
+ else
+ windows_arm64_supported=false
+ fi
+
+ # CUDA 13 introduced Windows ARM64 mid-release-line, so there is no
+ # CUDA 12 Windows ARM64 toolkit. Once current advances to CUDA 14,
+ # CUDA 13 can be built as the maintenance major on this platform.
+ if [[ "$current_cuda_major" == "13" ]]; then
+ windows_arm64_single_cuda_major=true
+ else
+ windows_arm64_single_cuda_major=false
+ fi
+
+ {
+ echo "python-supported=$(yq -o json -I 0 '.python.supported' ci/versions.yml)"
+ echo "bindings-config=$bindings_config"
+ echo "current-cuda-version=$current_cuda_version"
+ echo "windows-arm64-supported=$windows_arm64_supported"
+ echo "windows-arm64-single-cuda-major=$windows_arm64_single_cuda_major"
+ } >> "$GITHUB_OUTPUT"
+
- name: Resolve PR merge base
id: merge-base
if: ${{ startsWith(github.ref_name, 'pull-request/') }}
@@ -172,8 +167,12 @@ jobs:
if: ${{ startsWith(github.ref_name, 'pull-request/') }}
env:
BASE_REF: ${{ needs.should-skip.outputs.base-ref }}
+ BINDINGS_CONFIG: ${{ steps.registry.outputs.bindings-config }}
+ CURRENT_CUDA_VERSION: ${{ steps.registry.outputs.current-cuda-version }}
MERGE_BASE: ${{ steps.merge-base.outputs.sha }}
GH_TOKEN: ${{ github.token }}
+ WINDOWS_ARM64_SUPPORTED: ${{ steps.registry.outputs.windows-arm64-supported }}
+ WINDOWS_ARM64_SINGLE_CUDA_MAJOR: ${{ steps.registry.outputs.windows-arm64-single-cuda-major }}
run: |
set -uo pipefail
@@ -223,26 +222,46 @@ jobs:
}
missing=()
- for name in cuda-pathfinder-wheel cuda-python-wheel; do
+ has_artifact cuda-pathfinder-wheel || missing+=(cuda-pathfinder-wheel)
+
+ if ! cuda_versions_json=$(jq -ce '[.package_roots[].toolkit_version]' <<< "$BINDINGS_CONFIG"); then
+ unavailable
+ fi
+ mapfile -t cuda_versions < <(jq -r '.[]' <<< "$cuda_versions_json")
+ for version in "${cuda_versions[@]}"; do
+ name="cuda-python-wheel-cuda${version}"
has_artifact "$name" || missing+=("$name")
done
- cuda_version=$(yq '.cuda.build.version' ci/versions.yml)
if ! python_versions=$(yq -r '.python.supported[]' ci/versions.yml); then
unavailable
fi
if ! platforms=$(yq -r '.platforms[]' ci/test-matrix.yml); then
unavailable
fi
+ if [[ "$WINDOWS_ARM64_SUPPORTED" == "true" ]]; then
+ platforms+=$'\nwin-arm64'
+ fi
if [[ -z "${python_versions}" || -z "${platforms}" ]]; then
unavailable
fi
while IFS= read -r python_version; do
python=${python_version//./}
while IFS= read -r platform; do
- binding="cuda-bindings-python${python}-cuda${cuda_version}-${platform}-${merge_base}"
+ # CPython 3.10 has no official Windows ARM64 build.
+ if [[ "$platform" == "win-arm64" && "$python_version" == "3.10" ]]; then
+ continue
+ fi
+ for version in "${cuda_versions[@]}"; do
+ if [[ "$platform" == "win-arm64" &&
+ "$WINDOWS_ARM64_SINGLE_CUDA_MAJOR" == "true" &&
+ "$version" != "$CURRENT_CUDA_VERSION" ]]; then
+ continue
+ fi
+ binding="cuda-bindings-python${python}-cuda${version}-${platform}-${merge_base}"
+ has_artifact "$binding" || missing+=("$binding")
+ done
core="cuda-core-python${python}-${platform}-${merge_base}"
- has_artifact "$binding" || missing+=("$binding")
has_artifact "$core" || missing+=("$core")
done <<< "${platforms}"
done <<< "${python_versions}"
@@ -258,27 +277,29 @@ jobs:
echo "Reusable artifacts: run \`${run_id}\` at \`${merge_base}\` on \`${BASE_REF}\`."
} >> "$GITHUB_STEP_SUMMARY"
- - name: Test CI workplan planner
- run: python3 -m unittest ci/tools/tests/test_compute_ci_plan.py
+ - name: Validate bindings package metadata and test CI tools
+ run: |
+ python3 -m ci.tools.bindings_config --check-package-metadata
+ python3 -m pytest -q --noconftest ci/tools/tests
- name: Compute CI workplan
id: workplan
env:
MERGE_BASE: ${{ steps.merge-base.outputs.sha }}
BASELINE_RUN_ID: ${{ steps.baseline.outputs.run_id }}
+ RELEASE_TAG: ${{ github.ref_type == 'tag' && github.ref_name || '' }}
run: |
set -euo pipefail
- workplan=$(python3 ci/tools/compute_ci_plan.py \
- --merge-base "$MERGE_BASE" \
- --baseline-run-id "$BASELINE_RUN_ID")
- echo "workplan=$workplan" >> "$GITHUB_OUTPUT"
- {
- echo
- echo "### CI workplan"
- echo '```json'
- jq . <<< "$workplan"
- echo '```'
- } >> "$GITHUB_STEP_SUMMARY"
+ planner_args=(
+ --merge-base "$MERGE_BASE"
+ --baseline-run-id "$BASELINE_RUN_ID"
+ )
+ if [[ -n "$RELEASE_TAG" ]]; then
+ planner_args+=(--release-tag "$RELEASE_TAG")
+ fi
+ python3 -m ci.tools.compute_ci_plan "${planner_args[@]}" \
+ --github-output "$GITHUB_OUTPUT" \
+ --github-step-summary "$GITHUB_STEP_SUMMARY"
api-check-core-vs-release:
name: API check (cuda_core vs. latest release)
@@ -372,7 +393,6 @@ jobs:
# - if: condition (build-linux-64 omits doc-only check since it's needed for docs)
build-linux-64:
needs:
- - ci-vars
- should-skip
- detect-changes
strategy:
@@ -380,7 +400,7 @@ jobs:
matrix:
host-platform:
- linux-64
- name: Build ${{ matrix.host-platform }}, CUDA ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }}
+ name: Build ${{ fromJSON(needs.detect-changes.outputs.workplan).labels.build }} (${{ matrix.host-platform }})
if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.skip) }}
permissions:
actions: read
@@ -389,15 +409,13 @@ jobs:
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 }}
+ bindings-config: ${{ needs.detect-changes.outputs.bindings-config }}
workplan: ${{ needs.detect-changes.outputs.workplan }}
- python-versions: ${{ needs.ci-vars.outputs.PYTHON_SUPPORTED }}
+ python-versions: ${{ needs.detect-changes.outputs.python-supported }}
# See build-linux-64 for why build jobs are split by platform.
build-linux-aarch64:
needs:
- - ci-vars
- should-skip
- detect-changes
strategy:
@@ -405,7 +423,7 @@ jobs:
matrix:
host-platform:
- linux-aarch64
- name: Build ${{ matrix.host-platform }}, CUDA ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }}
+ name: Build ${{ fromJSON(needs.detect-changes.outputs.workplan).labels.build_platform }} (${{ matrix.host-platform }})
if: ${{ github.repository_owner == 'nvidia' &&
!fromJSON(needs.should-skip.outputs.skip) &&
!fromJSON(needs.should-skip.outputs.doc-only) &&
@@ -417,15 +435,13 @@ jobs:
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 }}
+ bindings-config: ${{ needs.detect-changes.outputs.bindings-config }}
workplan: ${{ needs.detect-changes.outputs.workplan }}
- python-versions: ${{ needs.ci-vars.outputs.PYTHON_SUPPORTED }}
+ python-versions: ${{ needs.detect-changes.outputs.python-supported }}
# See build-linux-64 for why build jobs are split by platform.
build-windows:
needs:
- - ci-vars
- should-skip
- detect-changes
strategy:
@@ -433,7 +449,7 @@ jobs:
matrix:
host-platform:
- win-64
- name: Build ${{ matrix.host-platform }}, CUDA ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }}
+ name: Build ${{ fromJSON(needs.detect-changes.outputs.workplan).labels.build_platform }} (${{ matrix.host-platform }})
if: ${{ github.repository_owner == 'nvidia' &&
!fromJSON(needs.should-skip.outputs.skip) &&
!fromJSON(needs.should-skip.outputs.doc-only) &&
@@ -445,23 +461,22 @@ jobs:
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 }}
+ bindings-config: ${{ needs.detect-changes.outputs.bindings-config }}
workplan: ${{ needs.detect-changes.outputs.workplan }}
- python-versions: ${{ needs.ci-vars.outputs.PYTHON_SUPPORTED }}
+ python-versions: ${{ needs.detect-changes.outputs.python-supported }}
# Windows ARM64 is available starting with CUDA 13.4. Build only the current
# CUDA major because no prior-major toolkit or wheel artifacts exist.
build-windows-arm64:
needs:
- - ci-vars
- should-skip
- detect-changes
- name: Build win-arm64, CUDA ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }}
+ name: Build ${{ fromJSON(needs.detect-changes.outputs.workplan).labels.build_current }} (win-arm64)
if: ${{ github.repository_owner == 'nvidia' &&
!fromJSON(needs.should-skip.outputs.skip) &&
!fromJSON(needs.should-skip.outputs.doc-only) &&
- needs.ci-vars.outputs.WINDOWS_ARM64_SUPPORTED == 'true' &&
+ needs.detect-changes.outputs.windows-arm64-supported == 'true' &&
+ fromJSON(needs.detect-changes.outputs.workplan).jobs.windows_arm64_build &&
fromJSON(needs.detect-changes.outputs.workplan).jobs.platforms.windows }}
permissions:
actions: read
@@ -470,11 +485,10 @@ jobs:
uses: ./.github/workflows/build-wheel.yml
with:
host-platform: win-arm64
- cuda-version: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }}
- prev-cuda-version: ${{ needs.ci-vars.outputs.CUDA_PREV_BUILD_VER }}
+ bindings-config: ${{ needs.detect-changes.outputs.bindings-config }}
workplan: ${{ needs.detect-changes.outputs.workplan }}
- single-cuda-major: ${{ needs.ci-vars.outputs.WINDOWS_ARM64_SINGLE_CUDA_MAJOR == 'true' }}
- python-versions: ${{ needs.ci-vars.outputs.PYTHON_SUPPORTED }}
+ single-cuda-major: ${{ needs.detect-changes.outputs.windows-arm64-single-cuda-major == 'true' }}
+ python-versions: ${{ needs.detect-changes.outputs.python-supported }}
# NOTE: test-sdist jobs are split by platform (mirroring build-* and test-wheel-*)
# so platform-specific sources (e.g. cuda_bindings_13/*_windows.pyx selected by
@@ -483,12 +497,15 @@ jobs:
# - host-platform value
# - uses: (test-sdist-linux.yml vs test-sdist-windows.yml)
test-sdist-linux:
+ strategy:
+ fail-fast: false
+ matrix:
+ package: ${{ fromJSON(needs.detect-changes.outputs.bindings-config).package_roots }}
needs:
- - ci-vars
- should-skip
- detect-changes
- build-linux-64
- name: Test sdist linux-64
+ name: Sdist ${{ fromJSON(needs.detect-changes.outputs.workplan).labels.sdist[matrix.package.package_root] }} (linux-64)
if: ${{ github.repository_owner == 'nvidia' &&
!fromJSON(needs.should-skip.outputs.skip) &&
!fromJSON(needs.should-skip.outputs.doc-only) &&
@@ -500,18 +517,21 @@ jobs:
uses: ./.github/workflows/test-sdist-linux.yml
with:
host-platform: linux-64
- cuda-version: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }}
+ bindings-package: ${{ toJSON(matrix.package) }}
workplan: ${{ needs.detect-changes.outputs.workplan }}
# See test-sdist-linux for why sdist test jobs are split by platform.
test-sdist-windows:
+ strategy:
+ fail-fast: false
+ matrix:
+ package: ${{ fromJSON(needs.detect-changes.outputs.bindings-config).package_roots }}
needs:
- - ci-vars
- should-skip
- detect-changes
- build-linux-64
- build-windows
- name: Test sdist win-64
+ name: Sdist ${{ fromJSON(needs.detect-changes.outputs.workplan).labels.sdist[matrix.package.package_root] }} (win-64)
if: ${{ github.repository_owner == 'nvidia' &&
!fromJSON(needs.should-skip.outputs.skip) &&
!fromJSON(needs.should-skip.outputs.doc-only) &&
@@ -523,7 +543,7 @@ jobs:
uses: ./.github/workflows/test-sdist-windows.yml
with:
host-platform: win-64
- cuda-version: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }}
+ bindings-package: ${{ toJSON(matrix.package) }}
workplan: ${{ needs.detect-changes.outputs.workplan }}
# NOTE: Test jobs are split by platform for the same reason as build jobs (see
@@ -545,7 +565,6 @@ jobs:
actions: read
contents: read # This is required for actions/checkout
needs:
- - ci-vars
- should-skip
- detect-changes
- build-linux-64
@@ -554,7 +573,7 @@ jobs:
with:
build-type: pull-request
host-platform: ${{ matrix.host-platform }}
- build-ctk-ver: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }}
+ bindings-config: ${{ needs.detect-changes.outputs.bindings-config }}
nruns: ${{ (github.event_name == 'schedule' && 5) || 1}}
workplan: ${{ needs.detect-changes.outputs.workplan }}
@@ -573,7 +592,6 @@ jobs:
actions: read
contents: read # This is required for actions/checkout
needs:
- - ci-vars
- should-skip
- detect-changes
- build-linux-64
@@ -583,7 +601,7 @@ jobs:
with:
build-type: pull-request
host-platform: ${{ matrix.host-platform }}
- build-ctk-ver: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }}
+ bindings-config: ${{ needs.detect-changes.outputs.bindings-config }}
nruns: ${{ (github.event_name == 'schedule' && 5) || 1}}
workplan: ${{ needs.detect-changes.outputs.workplan }}
@@ -602,7 +620,6 @@ jobs:
actions: read
contents: read # This is required for actions/checkout
needs:
- - ci-vars
- should-skip
- detect-changes
- build-linux-64
@@ -612,7 +629,7 @@ jobs:
with:
build-type: pull-request
host-platform: ${{ matrix.host-platform }}
- build-ctk-ver: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }}
+ bindings-config: ${{ needs.detect-changes.outputs.bindings-config }}
nruns: ${{ (github.event_name == 'schedule' && 5) || 1}}
workplan: ${{ needs.detect-changes.outputs.workplan }}
@@ -625,11 +642,16 @@ jobs:
contents: write
pull-requests: write
needs:
- - ci-vars
- build-linux-64
secrets: inherit
uses: ./.github/workflows/build-docs.yml
with:
+ # Bare cuda.bindings tags intentionally produce only their matching
+ # cuda.bindings and cuda-python artifacts. Keep tag docs on that focused
+ # artifact set; component-specific release runs build the other docs.
+ component: ${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'v') && 'cuda-bindings' || 'all' }}
+ git-tag: ${{ github.ref_type == 'tag' && github.ref_name || '' }}
+ run-id: ${{ github.run_id }}
is-release: ${{ github.ref_type == 'tag' }}
checks:
@@ -637,7 +659,6 @@ jobs:
if: ${{ always() && github.repository_owner == 'nvidia' }}
runs-on: ubuntu-latest
needs:
- - ci-vars
- should-skip
- detect-changes
- build-linux-64
@@ -674,7 +695,8 @@ jobs:
doc_only="${{ needs.should-skip.outputs.doc-only }}"
linux_selected="${{ needs.detect-changes.outputs.workplan && fromJSON(needs.detect-changes.outputs.workplan).jobs.platforms.linux || false }}"
windows_selected="${{ needs.detect-changes.outputs.workplan && fromJSON(needs.detect-changes.outputs.workplan).jobs.platforms.windows || false }}"
- windows_arm64_supported="${{ needs.ci-vars.outputs.WINDOWS_ARM64_SUPPORTED }}"
+ windows_arm64_supported="${{ needs.detect-changes.outputs.windows-arm64-supported }}"
+ windows_arm64_selected="${{ needs.detect-changes.outputs.workplan && fromJSON(needs.detect-changes.outputs.workplan).jobs.windows_arm64_build || false }}"
build_selected="${{ needs.detect-changes.outputs.workplan && fromJSON(needs.detect-changes.outputs.workplan).jobs.sdist_tests || false }}"
run_core_api_check="${{ needs.detect-changes.outputs.workplan && fromJSON(needs.detect-changes.outputs.workplan).jobs.core_api_checks || false }}"
is_pr="${{ startsWith(github.ref_name, 'pull-request/') }}"
@@ -690,7 +712,6 @@ jobs:
}
# Control jobs, the universal Linux build, and docs always run.
- check_result "ci-vars" "success"
check_result "should-skip" "success"
check_result "detect-changes" "success"
check_result "build-linux-64" "success"
@@ -709,7 +730,7 @@ jobs:
check_result "build-windows" "$windows_expected"
windows_arm64_expected="skipped"
- if [[ "$windows_arm64_supported" == "true" ]]; then
+ if [[ "$windows_arm64_supported" == "true" && "$windows_arm64_selected" == "true" ]]; then
windows_arm64_expected="$windows_expected"
fi
check_result "build-windows-arm64" "$windows_arm64_expected"
diff --git a/.github/workflows/test-sdist-linux.yml b/.github/workflows/test-sdist-linux.yml
index f061246d501..b5609d06187 100644
--- a/.github/workflows/test-sdist-linux.yml
+++ b/.github/workflows/test-sdist-linux.yml
@@ -8,13 +8,13 @@ on:
host-platform:
required: true
type: string
- cuda-version:
+ bindings-package:
+ description: Normalized CUDA bindings package JSON.
required: true
type: string
workplan:
- description: JSON workplan. An empty value builds everything.
- required: false
- default: ""
+ description: JSON workplan.
+ required: true
type: string
defaults:
@@ -27,13 +27,17 @@ permissions:
jobs:
test-sdist:
- name: Test sdist builds
- if: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).jobs.sdist_tests }}
+ name: Sdist ${{ fromJSON(inputs.workplan).labels.sdist[fromJSON(inputs.bindings-package).package_root] }}
+ if: ${{ fromJSON(inputs.workplan).jobs.sdist_package_roots[fromJSON(inputs.bindings-package).package_root] }}
env:
- BUILD_PATHFINDER: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.pathfinder.needs_build }}
- BUILD_BINDINGS: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.bindings.needs_build }}
- BUILD_CORE: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.core.needs_build }}
- BUILD_PYTHON: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.python.needs_build }}
+ BINDINGS_ROOT: ${{ fromJSON(inputs.bindings-package).package_root }}
+ CUDA_BUILD_VERSION: ${{ fromJSON(inputs.bindings-package).toolkit_version }}
+ CUDA_PYTHON_BUILD_MAJOR: ${{ fromJSON(inputs.bindings-package).cuda_major }}
+ BUILD_PATHFINDER: ${{ fromJSON(inputs.workplan).modules.pathfinder.needs_build }}
+ BUILD_BINDINGS: ${{ fromJSON(inputs.workplan).modules.bindings.package_roots[fromJSON(inputs.bindings-package).package_root].needs_build }}
+ BUILD_CORE: ${{ fromJSON(inputs.workplan).modules.core.cuda_majors[fromJSON(inputs.bindings-package).cuda_variant].needs_build }}
+ BUILD_CUDA_PYTHON: ${{ fromJSON(inputs.workplan).modules.python.package_roots[fromJSON(inputs.bindings-package).package_root].needs_build }}
+ PATHFINDER_SOURCE: ${{ fromJSON(inputs.workplan).sources.pathfinder }}
timeout-minutes: 60
runs-on: linux-amd64-cpu8
steps:
@@ -51,8 +55,14 @@ jobs:
with:
python-version: "3.12"
- - name: Install build tools
- run: python -m pip install "pip>=25.3" build
+ - name: Install build and CI tools
+ run: python -m pip install "pip>=25.3" build -e ./ci
+
+ - name: Prepare local maintenance SCM milestone
+ if: ${{ fromJSON(inputs.bindings-package).release_status == 'maintenance' }}
+ run: |
+ python -m ci.tools.bindings_scm_version \
+ --package-root "${BINDINGS_ROOT}" --sha "${{ github.sha }}"
# Pure Python packages -- no CTK needed.
- name: Build cuda.pathfinder sdist and wheel-from-sdist
@@ -62,13 +72,14 @@ jobs:
pip wheel --no-deps --wheel-dir cuda_pathfinder/dist cuda_pathfinder/dist/*.tar.gz
- name: Build cuda-python sdist and wheel-from-sdist
- if: ${{ env.BUILD_PYTHON == 'true' }}
+ if: ${{ env.BUILD_CUDA_PYTHON == 'true' }}
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: ${{ env.BUILD_PATHFINDER != 'true' && (env.BUILD_BINDINGS == 'true' || env.BUILD_CORE == 'true') }}
+ if: ${{ env.BUILD_PATHFINDER != 'true' && (env.BUILD_BINDINGS == 'true' || env.BUILD_CORE == 'true') &&
+ env.PATHFINDER_SOURCE == 'artifact' }}
uses: ./.github/actions/download-artifact-retry
with:
name: cuda-pathfinder-wheel
@@ -78,11 +89,21 @@ jobs:
if: ${{ env.BUILD_BINDINGS == 'true' }}
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
+ if [[ "${PATHFINDER_SOURCE}" == "artifact" ]]; then
+ if [[ "${#pathfinder_wheels[@]}" -ne 1 || ! -f "${pathfinder_wheels[0]}" ]]; then
+ echo "Expected exactly one cuda-pathfinder wheel in cuda_pathfinder/dist" >&2
+ exit 1
+ fi
+ pathfinder_uri="file://$(realpath "${pathfinder_wheels[0]}")"
+ printf 'cuda-pathfinder @ %s\n' "${pathfinder_uri}" > wheel-constraints/cuda-bindings.txt
+ elif [[ "${PATHFINDER_SOURCE}" == "published" ]]; then
+ : > wheel-constraints/cuda-bindings.txt
+ else
+ echo "Error: PATHFINDER_SOURCE must be artifact or published, got ${PATHFINDER_SOURCE}" >&2
+ exit 1
+ fi
+ cat wheel-constraints/cuda-bindings.txt
# Cython packages need CTK + sccache.
# The env vars ACTIONS_CACHE_SERVICE_V2, ACTIONS_RESULTS_URL, and ACTIONS_RUNTIME_TOKEN
@@ -107,7 +128,7 @@ jobs:
continue-on-error: false
with:
host-platform: ${{ inputs.host-platform }}
- cuda-version: ${{ inputs.cuda-version }}
+ cuda-version: ${{ fromJSON(inputs.bindings-package).toolkit_version }}
# cuda_bindings_13/setup.py parses CUDA headers at import time, so CUDA_PATH
# (set by fetch_ctk) must be available for both sdist and wheel builds.
@@ -119,22 +140,22 @@ jobs:
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_13/
- pip wheel --no-deps --wheel-dir cuda_bindings_13/dist cuda_bindings_13/dist/*.tar.gz
+ python -m build --sdist "${BINDINGS_ROOT}/"
+ pip wheel --no-deps --wheel-dir "${BINDINGS_ROOT}/dist" "${BINDINGS_ROOT}"/dist/*.tar.gz
- name: Download cuda.bindings wheel
if: ${{ env.BUILD_BINDINGS != 'true' && env.BUILD_CORE == 'true' }}
uses: ./.github/actions/download-artifact-retry
with:
- name: cuda-bindings-python312-cuda${{ inputs.cuda-version }}-${{ inputs.host-platform }}-${{ github.sha }}
- path: cuda_bindings_13/dist
+ name: cuda-bindings-python312-cuda${{ fromJSON(inputs.bindings-package).toolkit_version }}-${{ inputs.host-platform }}-${{ github.sha }}
+ path: ${{ env.BINDINGS_ROOT }}/dist
- name: Constrain cuda.core to the local cuda.bindings wheel
if: ${{ env.BUILD_CORE == 'true' }}
run: |
- CUDA_MAJOR="$(echo '${{ inputs.cuda-version }}' | cut -d. -f1)"
+ CUDA_MAJOR='${{ fromJSON(inputs.bindings-package).cuda_major }}'
pathfinder_wheels=(cuda_pathfinder/dist/cuda_pathfinder-*.whl)
- bindings_wheels=(cuda_bindings_13/dist/cuda_bindings-"${CUDA_MAJOR}".*.whl)
+ bindings_wheels=("${BINDINGS_ROOT}"/dist/cuda_bindings-"${CUDA_MAJOR}".*.whl)
test "${#pathfinder_wheels[@]}" -eq 1
test "${#bindings_wheels[@]}" -eq 1
test -f "${pathfinder_wheels[0]}"
@@ -154,7 +175,7 @@ jobs:
if: ${{ env.BUILD_CORE == 'true' }}
run: |
export CUDA_PYTHON_PARALLEL_LEVEL=$(nproc)
- export CUDA_CORE_BUILD_MAJOR="$(echo '${{ inputs.cuda-version }}' | cut -d. -f1)"
+ export CUDA_CORE_BUILD_MAJOR='${{ fromJSON(inputs.bindings-package).cuda_major }}'
export CC="sccache cc"
export CXX="sccache c++"
export PIP_BUILD_CONSTRAINT="$(pwd)/wheel-constraints/cuda-core.txt"
diff --git a/.github/workflows/test-sdist-windows.yml b/.github/workflows/test-sdist-windows.yml
index a24d806b38b..c0dea17003a 100644
--- a/.github/workflows/test-sdist-windows.yml
+++ b/.github/workflows/test-sdist-windows.yml
@@ -14,13 +14,13 @@ on:
host-platform:
required: true
type: string
- cuda-version:
+ bindings-package:
+ description: Normalized CUDA bindings package JSON.
required: true
type: string
workplan:
- description: JSON workplan. An empty value builds everything.
- required: false
- default: ""
+ description: JSON workplan.
+ required: true
type: string
defaults:
@@ -33,13 +33,17 @@ permissions:
jobs:
test-sdist:
- name: Test sdist builds
- if: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).jobs.sdist_tests }}
+ name: Sdist ${{ fromJSON(inputs.workplan).labels.sdist[fromJSON(inputs.bindings-package).package_root] }}
+ if: ${{ fromJSON(inputs.workplan).jobs.sdist_package_roots[fromJSON(inputs.bindings-package).package_root] }}
env:
- BUILD_PATHFINDER: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.pathfinder.needs_build }}
- BUILD_BINDINGS: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.bindings.needs_build }}
- BUILD_CORE: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.core.needs_build }}
- BUILD_PYTHON: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.python.needs_build }}
+ BINDINGS_ROOT: ${{ fromJSON(inputs.bindings-package).package_root }}
+ CUDA_BUILD_VERSION: ${{ fromJSON(inputs.bindings-package).toolkit_version }}
+ CUDA_PYTHON_BUILD_MAJOR: ${{ fromJSON(inputs.bindings-package).cuda_major }}
+ BUILD_PATHFINDER: ${{ fromJSON(inputs.workplan).modules.pathfinder.needs_build }}
+ BUILD_BINDINGS: ${{ fromJSON(inputs.workplan).modules.bindings.package_roots[fromJSON(inputs.bindings-package).package_root].needs_build }}
+ BUILD_CORE: ${{ fromJSON(inputs.workplan).modules.core.cuda_majors[fromJSON(inputs.bindings-package).cuda_variant].needs_build }}
+ BUILD_CUDA_PYTHON: ${{ fromJSON(inputs.workplan).modules.python.package_roots[fromJSON(inputs.bindings-package).package_root].needs_build }}
+ PATHFINDER_SOURCE: ${{ fromJSON(inputs.workplan).sources.pathfinder }}
timeout-minutes: 60
runs-on: windows-2022
steps:
@@ -64,8 +68,15 @@ jobs:
if: ${{ env.BUILD_BINDINGS == 'true' || env.BUILD_CORE == 'true' }}
uses: step-security/msvc-dev-cmd@22c98154b708dbd743e6f27a933cf6ceba3305c4 # v1.13.1
- - name: Install build tools
- run: python -m pip install "pip>=25.3" build
+ - name: Install build and CI tools
+ run: python -m pip install "pip>=25.3" build -e ./ci
+
+ - name: Prepare local maintenance SCM milestone
+ if: ${{ fromJSON(inputs.bindings-package).release_status == 'maintenance' }}
+ shell: bash --noprofile --norc -xeuo pipefail {0}
+ run: |
+ python -m ci.tools.bindings_scm_version \
+ --package-root "${BINDINGS_ROOT}" --sha "${{ github.sha }}"
# Pure Python packages -- no CTK needed.
- name: Build cuda.pathfinder sdist and wheel-from-sdist
@@ -75,13 +86,14 @@ jobs:
pip wheel --no-deps --wheel-dir cuda_pathfinder/dist cuda_pathfinder/dist/*.tar.gz
- name: Build cuda-python sdist and wheel-from-sdist
- if: ${{ env.BUILD_PYTHON == 'true' }}
+ if: ${{ env.BUILD_CUDA_PYTHON == 'true' }}
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: ${{ env.BUILD_PATHFINDER != 'true' && (env.BUILD_BINDINGS == 'true' || env.BUILD_CORE == 'true') }}
+ if: ${{ env.BUILD_PATHFINDER != 'true' && (env.BUILD_BINDINGS == 'true' || env.BUILD_CORE == 'true') &&
+ env.PATHFINDER_SOURCE == 'artifact' }}
uses: ./.github/actions/download-artifact-retry
with:
name: cuda-pathfinder-wheel
@@ -91,11 +103,21 @@ jobs:
if: ${{ env.BUILD_BINDINGS == 'true' }}
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
+ if [[ "${PATHFINDER_SOURCE}" == "artifact" ]]; then
+ if [[ "${#pathfinder_wheels[@]}" -ne 1 || ! -f "${pathfinder_wheels[0]}" ]]; then
+ echo "Expected exactly one cuda-pathfinder wheel in cuda_pathfinder/dist" >&2
+ exit 1
+ fi
+ pathfinder_uri="file:///$(cygpath -am "${pathfinder_wheels[0]}")"
+ printf 'cuda-pathfinder @ %s\n' "${pathfinder_uri}" > wheel-constraints/cuda-bindings.txt
+ elif [[ "${PATHFINDER_SOURCE}" == "published" ]]; then
+ : > wheel-constraints/cuda-bindings.txt
+ else
+ echo "Error: PATHFINDER_SOURCE must be artifact or published, got ${PATHFINDER_SOURCE}" >&2
+ exit 1
+ fi
+ cat wheel-constraints/cuda-bindings.txt
# Cython packages need CTK. No sccache on Windows (this is a correctness
# smoke test, not a production build; see build-wheel.yml which also
@@ -106,7 +128,7 @@ jobs:
continue-on-error: false
with:
host-platform: ${{ inputs.host-platform }}
- cuda-version: ${{ inputs.cuda-version }}
+ cuda-version: ${{ fromJSON(inputs.bindings-package).toolkit_version }}
# cuda_bindings_13/setup.py parses CUDA headers at import time, so CUDA_PATH
# (set by fetch_ctk) must be available for both sdist and wheel builds.
@@ -118,22 +140,22 @@ jobs:
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_13/
- pip wheel --no-deps --wheel-dir cuda_bindings_13/dist cuda_bindings_13/dist/*.tar.gz
+ python -m build --sdist "${BINDINGS_ROOT}/"
+ pip wheel --no-deps --wheel-dir "${BINDINGS_ROOT}/dist" "${BINDINGS_ROOT}"/dist/*.tar.gz
- name: Download cuda.bindings wheel
if: ${{ env.BUILD_BINDINGS != 'true' && env.BUILD_CORE == 'true' }}
uses: ./.github/actions/download-artifact-retry
with:
- name: cuda-bindings-python312-cuda${{ inputs.cuda-version }}-${{ inputs.host-platform }}-${{ github.sha }}
- path: cuda_bindings_13/dist
+ name: cuda-bindings-python312-cuda${{ fromJSON(inputs.bindings-package).toolkit_version }}-${{ inputs.host-platform }}-${{ github.sha }}
+ path: ${{ env.BINDINGS_ROOT }}/dist
- name: Constrain cuda.core to the local cuda.bindings wheel
if: ${{ env.BUILD_CORE == 'true' }}
run: |
- CUDA_MAJOR="$(echo '${{ inputs.cuda-version }}' | cut -d. -f1)"
+ CUDA_MAJOR='${{ fromJSON(inputs.bindings-package).cuda_major }}'
pathfinder_wheels=(cuda_pathfinder/dist/cuda_pathfinder-*.whl)
- bindings_wheels=(cuda_bindings_13/dist/cuda_bindings-"${CUDA_MAJOR}".*.whl)
+ bindings_wheels=("${BINDINGS_ROOT}"/dist/cuda_bindings-"${CUDA_MAJOR}".*.whl)
test "${#pathfinder_wheels[@]}" -eq 1
test "${#bindings_wheels[@]}" -eq 1
test -f "${pathfinder_wheels[0]}"
@@ -153,7 +175,7 @@ jobs:
if: ${{ env.BUILD_CORE == 'true' }}
run: |
export CUDA_PYTHON_PARALLEL_LEVEL=$(nproc)
- export CUDA_CORE_BUILD_MAJOR="$(echo '${{ inputs.cuda-version }}' | cut -d. -f1)"
+ export CUDA_CORE_BUILD_MAJOR='${{ fromJSON(inputs.bindings-package).cuda_major }}'
export PIP_BUILD_CONSTRAINT="$(cygpath -w "$(pwd)/wheel-constraints/cuda-core.txt")"
export PIP_CONSTRAINT="${PIP_BUILD_CONSTRAINT}"
python -m build --sdist cuda_core/
diff --git a/ci/ci-pipeline.mmd b/ci/ci-pipeline.mmd
index b085fa869bc..6dcce24e4f9 100644
--- a/ci/ci-pipeline.mmd
+++ b/ci/ci-pipeline.mmd
@@ -25,16 +25,18 @@ config:
flowchart TB
subgraph TRIGGER["TRIGGER EVENTS"]
direction LR
- T1["Push to main branch"] ~~~ T2["Pull request"] ~~~ T3["Manual workflow dispatch"]
+ T1["Main or approved pull request"] ~~~ T2["Immutable package tag"] ~~~ T3["Manual workflow dispatch"]
end
+ PLAN["Select affected packages and dependencies
A package tag selects its release line
Registry: ci/versions.yml"]
+
subgraph BUILD["BUILD STAGE"]
direction TB
subgraph BUILD_PLATFORMS["Parallel Platform Builds"]
direction LR
- B1["linux-64
(Self-hosted)"] ~~~ B2["linux-aarch64
(Self-hosted)"] ~~~ B3["win-64
(GitHub-hosted)"]
+ B1["Linux x86_64
(Self-hosted)"] ~~~ B2["Linux ARM64
(Self-hosted)"] ~~~ B3["Windows x86_64 / ARM64
(GitHub-hosted)"]
end
- BUILD_DETAILS["Python versions: 3.11, 3.12, 3.13, 3.14, 3.15
CUDA version: 13.0.0 (build-time)
Components: cuda-core, cuda-bindings,
cuda-pathfinder, cuda-python"]
+ BUILD_DETAILS["Python and platform matrices: ci.yml
Bindings toolkits and roots: registry
Build selected bindings, Core,
Pathfinder and metapackage artifacts"]
end
subgraph ARTIFACTS["ARTIFACT STORAGE"]
@@ -51,22 +53,22 @@ flowchart TB
direction TB
subgraph TEST_PLATFORMS["Parallel Platform Tests"]
direction LR
- TS1["linux-64
(Self-hosted)"] ~~~ TS2["linux-aarch64
(Self-hosted)"] ~~~ TS3["win-64
(GitHub-hosted)"]
+ TS1["linux-64
(Self-hosted)"] ~~~ TS2["linux-aarch64
(Self-hosted)"] ~~~ TS3["win-64
(Self-hosted GPU)"]
end
- TEST_DETAILS["Download wheels from artifacts
Test against multiple CUDA runtime versions
Run Python unit tests, Cython tests, and examples"]
- ARTIFACT_FLOWS["Artifact Flows:
cuda-pathfinder: main to backport
cuda-bindings: backport to main"]
+ TEST_DETAILS["Test sources match wheel build revision
Test against multiple CUDA runtime versions
Run Python unit tests, Cython tests, and examples"]
+ ARTIFACT_FLOWS["Both bindings roots build from this source tree
Reuse validated baseline for unaffected packages
Bindings tag: selected line and published Pathfinder
Core older-toolkit tests: published bindings floor"]
end
subgraph RELEASE["RELEASE PIPELINE"]
direction TB
subgraph RELEASE_STAGES["Sequential Release Steps"]
direction LR
- R1["Validation
Artifact integrity
Git tag verification"] --> R2["Publishing
PyPI/TestPyPI
Component or all releases"] --> R3["Documentation
GitHub Pages
Release notes"]
+ R1["Validation
Artifact integrity
Git tag verification"] --> R2["Publishing
PyPI/TestPyPI
Selected component"] --> R3["Documentation
GitHub Pages
Release notes"]
end
- RELEASE_DETAILS["Manual workflow dispatch with run ID
Supports individual component or full releases"]
+ RELEASE_DETAILS["Manual dry run for exact tag and successful CI run
Explicit publication for selected component
Bindings release does not require a Core release"]
end
- TRIGGER --> BUILD
+ TRIGGER --> PLAN --> BUILD
BUILD -. "wheel upload" .-> ARTIFACTS
ARTIFACTS --> TEST
TEST --> RELEASE
@@ -90,7 +92,7 @@ flowchart TB
class TRIGGER triggerStyle
class T1,T2,T3 triggerNodeStyle
class BUILD,BUILD_PLATFORMS buildStyle
- class B1,B2,B3,BUILD_DETAILS buildNodeStyle
+ class PLAN,B1,B2,B3,BUILD_DETAILS buildNodeStyle
class ARTIFACTS,GITHUB_ARTIFACTS,GITHUB_CACHE artifactStyle
class GA1,GC1 artifactNodeStyle
class TEST,TEST_PLATFORMS testStyle
diff --git a/ci/tools/compute_ci_plan.py b/ci/tools/compute_ci_plan.py
index ea03abc31a9..4fc5b9057de 100644
--- a/ci/tools/compute_ci_plan.py
+++ b/ci/tools/compute_ci_plan.py
@@ -12,25 +12,19 @@
import json
import subprocess
from pathlib import Path, PurePosixPath
+from typing import TYPE_CHECKING, Any
+
+from .bindings_config import BindingsConfigError, load_config
+
+if TYPE_CHECKING:
+ from .bindings_config import BindingsConfig
REPO_ROOT = Path(__file__).resolve().parents[2]
-MODULES = ("pathfinder", "bindings", "core", "python")
PLATFORMS = ("linux", "windows")
-PACKAGE_MODULES = {
- "cuda_pathfinder": "pathfinder",
- "cuda_bindings_13": "bindings",
- "cuda_core": "core",
- "cuda_python": "python",
-}
-
-# Source changes have different build and test consumers. In particular,
-# cuda-python source needs a same-version bindings wheel, while a core-only
-# change can reuse the baseline cuda-python wheel.
-SOURCE_IMPACT = {
- "pathfinder": (set(MODULES), set(MODULES)),
- "bindings": ({"bindings", "core", "python"}, {"bindings", "core", "python"}),
- "core": ({"core"}, {"core", "python"}),
- "python": ({"bindings", "python"}, {"python"}),
+PACKAGE_TARGETS: dict[str, tuple[str, None]] = {
+ "cuda_pathfinder": ("pathfinder", None),
+ "cuda_core": ("core", None),
+ "cuda_python": ("python", None),
}
IGNORED_BASENAMES = {"AGENTS.md", "CLAUDE.md", "pixi.lock", "pixi.toml"}
@@ -58,22 +52,72 @@
"ci/tools/setup-sanitizer": "linux",
}
+Target = tuple[str, str | None]
+
def compute_workplan(
- paths: list[str],
*,
+ bindings_config: BindingsConfig,
+ paths: list[str],
+ linked_paths: set[str],
merge_base: str,
baseline_run_id: str,
- linked_paths: set[str] | None = None,
+ release_tag: str,
) -> dict[str, object]:
"""Return the final CI decisions for the supplied changed paths."""
- linked_paths = linked_paths or set()
- source_changes: set[str] = set()
- test_changes: set[str] = set()
+ packages = bindings_config.package_roots
+ package_roots = tuple(package.package_root for package in packages)
+ cuda_variants = tuple(package.cuda_variant for package in packages)
+
+ bindings_targets = frozenset(("bindings", package_root) for package_root in package_roots)
+ core_targets = frozenset(("core", variant) for variant in cuda_variants)
+ python_targets = frozenset(("python", package_root) for package_root in package_roots)
+ all_targets = frozenset({("pathfinder", None), *bindings_targets, *core_targets, *python_targets})
+
+ package_targets = {PurePosixPath(package.package_root): ("bindings", package.package_root) for package in packages}
+ package_targets.update((PurePosixPath(source_dir), target) for source_dir, target in PACKAGE_TARGETS.items())
+
+ # Source changes have different build/test consumers. Core-only changes can
+ # reuse baseline cuda-python wheels; cuda-python changes cannot reuse bindings.
+ source_impacts: dict[Target, tuple[frozenset[Target], frozenset[Target], frozenset[str]]] = {
+ ("pathfinder", None): (all_targets, all_targets, frozenset(package_roots)),
+ ("core", None): (core_targets, frozenset({*core_targets, *python_targets}), frozenset(package_roots)),
+ ("python", None): (
+ frozenset({*bindings_targets, *python_targets}),
+ python_targets,
+ frozenset(package_roots),
+ ),
+ **{
+ ("bindings", package.package_root): (
+ frozenset(
+ {
+ ("bindings", package.package_root),
+ ("python", package.package_root),
+ *core_targets,
+ }
+ ),
+ frozenset(
+ {
+ ("bindings", package.package_root),
+ ("core", package.cuda_variant),
+ ("python", package.package_root),
+ }
+ ),
+ frozenset({package.package_root}),
+ )
+ for package in packages
+ },
+ }
+
+ source_changes: set[tuple[str, str | None]] = set()
+ test_changes: set[tuple[str, str | None]] = set()
test_platforms: set[str] = set()
- force_all = not merge_base or not baseline_run_id
+ release_package = bindings_config.match_tag(release_tag) if release_tag else None
+ if release_tag.startswith("v") and release_package is None:
+ raise BindingsConfigError(f"no configured CUDA bindings package root matches release tag: {release_tag!r}")
+ force_all = release_package is None and (bool(release_tag) or not merge_base or not baseline_run_id)
- if not force_all:
+ if release_package is None and not force_all:
for path in paths:
path_parts = PurePosixPath(path).parts
if not path_parts:
@@ -92,9 +136,20 @@ def compute_workplan(
if path_parts[0] == ".github" or path_parts[-1] in IGNORED_BASENAMES:
continue
- module = PACKAGE_MODULES.get(path_parts[0])
- if module is not None and len(path_parts) > 1:
- relative = path_parts[1:]
+ target_match = next(
+ (
+ (root, target)
+ for root, target in sorted(
+ package_targets.items(), key=lambda item: len(item[0].parts), reverse=True
+ )
+ if len(path_parts) > len(root.parts) and path_parts[: len(root.parts)] == root.parts
+ ),
+ None,
+ )
+ if target_match is not None:
+ root, target = target_match
+ module, _ = target
+ relative = path_parts[len(root.parts) :]
if relative[0] == "docs":
continue
if (
@@ -102,16 +157,21 @@ def compute_workplan(
or relative[0] == "examples"
or (module == "core" and relative == ("pytest.ini",))
):
- test_changes.add(module)
+ if module == "core":
+ test_changes.update(core_targets)
+ elif module == "python":
+ test_changes.update(python_targets)
+ else:
+ test_changes.add(target)
elif PurePosixPath(path).suffix in IGNORED_SUFFIXES and path not in linked_paths:
continue
else:
- source_changes.add(module)
+ source_changes.add(target)
continue
is_test_path = any(part in {"test", "tests"} for part in path_parts[:-1])
if is_test_path:
- test_changes.update(MODULES)
+ test_changes.update(all_targets)
elif (
path in IGNORED_PATHS
or PurePosixPath(path).suffix in IGNORED_SUFFIXES
@@ -119,48 +179,159 @@ def compute_workplan(
):
continue
elif path_parts[0] in {"benchmarks", "cuda_python_test_helpers"}:
- test_changes.update(MODULES)
+ test_changes.update(all_targets)
else:
force_all = True
break
- if force_all:
- builds = set(MODULES)
- tests = set(MODULES)
+ if release_package is not None:
+ builds = {
+ ("bindings", release_package.package_root),
+ ("python", release_package.package_root),
+ }
+ tests = set(builds)
+ test_platforms = set(PLATFORMS)
+ sdist_package_roots = {release_package.package_root}
+ elif force_all:
+ builds = set(all_targets)
+ tests = set(all_targets)
test_platforms = set(PLATFORMS)
+ sdist_package_roots = set(package_roots)
else:
- builds: set[str] = set()
- tests = set(MODULES) if test_platforms else set(test_changes)
- for module in source_changes:
- build_impact, test_impact = SOURCE_IMPACT[module]
+ builds: set[tuple[str, str | None]] = set()
+ tests = set(all_targets) if test_platforms else set(test_changes)
+ sdist_package_roots: set[str] = set()
+ for target in source_changes:
+ build_impact, test_impact, sdist_impact = source_impacts[target]
builds.update(build_impact)
tests.update(test_impact)
+ sdist_package_roots.update(sdist_impact)
if source_changes or test_changes:
test_platforms.update(PLATFORMS)
- modules = {
- module: {
- "needs_build": module in builds,
- "needs_test": module in tests,
+ def flags(module: str, selectors: tuple[str | None, ...]) -> dict[str, bool]:
+ return {
+ "needs_build": any((module, selector) in builds for selector in selectors),
+ "needs_test": any((module, selector) in tests for selector in selectors),
+ }
+
+ modules: dict[str, dict[str, object]] = {"pathfinder": flags("pathfinder", (None,))}
+ for module in ("bindings", "python"):
+ package_decisions = {package.package_root: flags(module, (package.package_root,)) for package in packages}
+ modules[module] = {
+ **flags(module, package_roots),
+ "package_roots": package_decisions,
}
- for module in MODULES
+
+ cuda_major_decisions = {variant: flags("core", (variant,)) for variant in cuda_variants}
+ modules["core"] = {
+ **flags("core", cuda_variants),
+ "cuda_majors": cuda_major_decisions,
+ }
+
+ pathfinder_test = ("pathfinder", None) in tests
+ test_cuda_variants = {
+ package.cuda_variant
+ for package in packages
+ if pathfinder_test
+ or ("core", package.cuda_variant) in tests
+ or any((module, package.package_root) in tests for module in ("bindings", "python"))
}
+
+ def build_label(*, universal: bool, current_only: bool = False) -> str:
+ labels = []
+ if universal and ("pathfinder", None) in builds:
+ labels.append("pathfinder")
+ for module in ("bindings", "core", "python"):
+ component_label = "meta" if module == "python" else module
+ if module == "python" and not universal:
+ continue
+ for package in packages:
+ if current_only and package.release_status != "current":
+ continue
+ selector = package.cuda_variant if module == "core" else package.package_root
+ if (module, selector) in builds:
+ labels.append(f"{component_label}{package.cuda_major}")
+ return "+".join(labels) or "baseline wheels and test extensions"
+
return {
+ "labels": {
+ "build": build_label(universal=True),
+ "build_platform": build_label(universal=False),
+ "build_current": build_label(universal=False, current_only=True),
+ "sdist": {
+ package.package_root: "+".join(
+ [
+ name
+ for selected, name in (
+ (("pathfinder", None) in builds, "pathfinder"),
+ (("bindings", package.package_root) in builds, f"bindings{package.cuda_major}"),
+ (("core", package.cuda_variant) in builds, f"core{package.cuda_major}"),
+ (("python", package.package_root) in builds, f"meta{package.cuda_major}"),
+ )
+ if selected
+ ]
+ )
+ or "excluded"
+ for package in packages
+ },
+ },
"modules": modules,
+ "sources": {
+ # Focused bindings releases intentionally omit unrelated artifacts.
+ "pathfinder": "published" if release_package is not None else "artifact",
+ },
"jobs": {
# These gates cover both optional artifact builds and wheel tests.
"platforms": {platform: platform in test_platforms for platform in PLATFORMS},
- "sdist_tests": bool(builds),
- "core_api_checks": force_all or "core" in source_changes,
+ "sdist_tests": bool(sdist_package_roots),
+ "windows_arm64_build": (bool(baseline_run_id) and release_package is None)
+ or any(
+ int(package.cuda_major) >= 13
+ and (("bindings", package.package_root) in builds or ("core", package.cuda_variant) in builds)
+ for package in packages
+ ),
+ "core_api_checks": force_all or ("core", None) in source_changes,
+ "test_cuda_majors": {variant: variant in test_cuda_variants for variant in cuda_variants},
+ "sdist_package_roots": {
+ package_root: package_root in sdist_package_roots for package_root in package_roots
+ },
},
"merge_base": merge_base,
"baseline": {
- "run_id": baseline_run_id if not force_all else "",
- "sha": merge_base if not force_all else "",
+ "run_id": baseline_run_id if release_package is None and not force_all else "",
+ "sha": merge_base if release_package is None and not force_all else "",
},
}
+def format_workplan_summary(plan: dict[str, Any], config: BindingsConfig) -> str:
+ """Describe selected work and exact artifact reuse without hiding exclusions."""
+ modules = plan["modules"]
+ baseline = plan["baseline"]
+ rows = [("cuda-pathfinder", "shared", modules["pathfinder"])]
+ for package in config.package_roots:
+ for module, name in (("bindings", "cuda-bindings"), ("python", "cuda-python")):
+ rows.append((name, package.cuda_major, modules[module]["package_roots"][package.package_root]))
+ rows.append(("cuda-core", package.cuda_major, modules["core"]["cuda_majors"][package.cuda_variant]))
+ lines = [
+ "### CI workplan",
+ "",
+ "| Component | CUDA major | Wheel | Tests |",
+ "| --- | --- | --- | --- |",
+ ]
+ for component, major, flags in rows:
+ wheel = "build" if flags["needs_build"] else "reuse exact baseline" if baseline["run_id"] else "excluded"
+ tests = "required" if flags["needs_test"] else "excluded by plan"
+ lines.append(f"| {component} | {major} | {wheel} | {tests} |")
+ if baseline["run_id"]:
+ lines.extend(("", f"Reusable artifacts: run `{baseline['run_id']}` at `{baseline['sha']}`."))
+ if plan["sources"]["pathfinder"] == "published":
+ lines.extend(("", "Focused bindings release: tests install published cuda-pathfinder; cuda-core is excluded."))
+ lines.extend(("", "Windows ARM64 builds only supported toolkit majors; CUDA 12 has no Windows ARM64 toolkit.", ""))
+ return "\n".join(lines)
+
+
def _git_output(*args: str) -> bytes:
return subprocess.check_output( # noqa: S603 - argv is passed directly without a shell.
["git", *args], # noqa: S607
@@ -197,21 +368,58 @@ def _expand_linked_paths(paths: list[str], symlink_paths: list[str], *, root: Pa
return expanded
-def main() -> None:
+def main(argv: list[str] | None = None) -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--merge-base", default="")
parser.add_argument("--baseline-run-id", default="")
- args = parser.parse_args()
+ parser.add_argument("--release-tag", default="")
+ parser.add_argument(
+ "--github-output",
+ type=Path,
+ help="append normalized CI records to this GitHub output file",
+ )
+ parser.add_argument(
+ "--github-step-summary",
+ type=Path,
+ help="append a formatted workplan to this GitHub step summary file",
+ )
+ args = parser.parse_args(argv)
+ if args.github_step_summary is not None and args.github_output is None:
+ parser.error("--github-step-summary requires --github-output")
- reusable_baseline = bool(args.merge_base and args.baseline_run_id)
+ config = load_config()
+ reusable_baseline = bool(args.merge_base and args.baseline_run_id and not args.release_tag)
paths, linked_paths = _changed_paths(args.merge_base) if reusable_baseline else ([], set())
- plan = compute_workplan(
- paths,
- merge_base=args.merge_base,
- baseline_run_id=args.baseline_run_id,
- linked_paths=linked_paths,
- )
- print(json.dumps(plan, separators=(",", ":"), sort_keys=True))
+ try:
+ plan = compute_workplan(
+ bindings_config=config,
+ paths=paths,
+ linked_paths=linked_paths,
+ merge_base=args.merge_base,
+ baseline_run_id=args.baseline_run_id,
+ release_tag=args.release_tag,
+ )
+ except BindingsConfigError as error:
+ parser.error(str(error))
+ workplan = json.dumps(plan, separators=(",", ":"), sort_keys=True)
+ if args.github_output is None:
+ print(workplan)
+ return
+
+ records = {
+ "bindings-config": config.to_json(),
+ "workplan": workplan,
+ }
+ with args.github_output.open("a", encoding="utf-8") as output:
+ for name, value in records.items():
+ output.write(f"{name}={value}\n")
+ if args.github_step_summary is not None:
+ formatted = json.dumps(plan, indent=2, sort_keys=True)
+ with args.github_step_summary.open("a", encoding="utf-8") as summary:
+ summary.write(format_workplan_summary(plan, config))
+ summary.write(
+ f"\nFull workplan JSON
\n\n```json\n{formatted}\n```\n \n"
+ )
if __name__ == "__main__":
diff --git a/ci/tools/download-wheels b/ci/tools/download-wheels
index 58e8a213226..c945f0dd6b8 100755
--- a/ci/tools/download-wheels
+++ b/ci/tools/download-wheels
@@ -11,11 +11,13 @@ set -euo pipefail
# Check required arguments
if [[ $# -lt 3 ]]; then
- echo "Usage: $0 [output-dir]" >&2
+ echo "Usage: $0 [output-dir] [git-tag] [bindings-package-json]" >&2
echo " run-id: The GitHub Actions run ID containing the artifacts" >&2
echo " component: The component name pattern to download (e.g., cuda-core, cuda-bindings)" >&2
echo " repository: The GitHub repository (e.g., NVIDIA/cuda-python)" >&2
echo " output-dir: Optional output directory (default: ./dist)" >&2
+ echo " git-tag: Optional release tag used to select its configured bindings package" >&2
+ echo " bindings-package-json: Resolved package JSON; required for bindings/python release tags" >&2
exit 1
fi
@@ -23,6 +25,8 @@ RUN_ID="$1"
COMPONENT="$2"
REPOSITORY="$3"
OUTPUT_DIR="${4:-./dist}"
+GIT_TAG="${5:-}"
+RESOLVED_BINDINGS_PACKAGE_JSON="${6:-}"
# Ensure we have a GitHub token
if [[ -z "${GH_TOKEN:-}" ]]; then
@@ -32,44 +36,106 @@ fi
echo "Downloading wheels for component: $COMPONENT from run: $RUN_ID"
-# Download component wheels using the same logic as release.yml
+ARTIFACTS_DIR="$(mktemp -d)"
+cleanup() {
+ rm -rf -- "$ARTIFACTS_DIR"
+}
+trap cleanup EXIT
+
+# Select artifacts before downloading so non-wheel test artifacts cannot enter
+# the release staging directory.
if [[ "$COMPONENT" == "all" ]]; then
- # Download all component patterns
- gh run download "$RUN_ID" -p "cuda-*" -R "$REPOSITORY"
+ ARTIFACT_PATTERN="cuda-*"
else
- gh run download "$RUN_ID" -p "${COMPONENT}*" -R "$REPOSITORY"
+ ARTIFACT_PATTERN="${COMPONENT}*"
fi
-# Create output directory
-mkdir -p "$OUTPUT_DIR"
+IS_BINDINGS_RELEASE=false
+if [[ "$COMPONENT" != "all" \
+ && "$COMPONENT" =~ ^cuda-(bindings|python)$ \
+ && "$GIT_TAG" == v* ]]; then
+ IS_BINDINGS_RELEASE=true
+fi
-# Process downloaded artifacts
-for p in cuda-*
-do
- if [[ ! -d "$p" ]]; then
- continue
+if [[ "$IS_BINDINGS_RELEASE" == true ]]; then
+ if [[ -z "$RESOLVED_BINDINGS_PACKAGE_JSON" ]]; then
+ echo "Error: resolved bindings-package JSON is required for $COMPONENT release tag $GIT_TAG" >&2
+ exit 1
fi
-
- # exclude cython test artifacts
- if [[ "${p}" == *-tests ]]; then
- echo "Skipping test artifact: $p"
- continue
+ if ! command -v jq >/dev/null 2>&1; then
+ echo "Error: jq is required for bindings release artifact routing" >&2
+ exit 1
+ fi
+ if ! TOOLKIT_VERSION="$(jq -er '.toolkit_version | select(type == "string")' \
+ <<< "$RESOLVED_BINDINGS_PACKAGE_JSON")"; then
+ echo "Error: resolved bindings-package JSON has no string toolkit_version" >&2
+ exit 1
fi
+ if [[ ! "$TOOLKIT_VERSION" =~ ^[1-9][0-9]*\.[0-9]+\.[0-9]+([.-][A-Za-z0-9]+)*$ ]]; then
+ echo "Error: invalid toolkit_version in resolved bindings-package JSON: $TOOLKIT_VERSION" >&2
+ exit 1
+ fi
+fi
+
+if ! ARTIFACT_NAMES="$(gh api --paginate \
+ "repos/${REPOSITORY}/actions/runs/${RUN_ID}/artifacts?per_page=100" \
+ --jq '.artifacts[] | select(.expired == false) | .name')"; then
+ echo "Error: failed to inspect artifacts for run $RUN_ID" >&2
+ exit 1
+fi
- # If we're not downloading "all", only process matching component
- if [[ "$COMPONENT" != "all" && "$p" != ${COMPONENT}* ]]; then
- continue
+MATCHING_ARTIFACTS=()
+if [[ "$IS_BINDINGS_RELEASE" == true && "$COMPONENT" == "cuda-python" ]]; then
+ ARTIFACT_NAME="cuda-python-wheel-cuda${TOOLKIT_VERSION}"
+ if grep -Fxq "$ARTIFACT_NAME" <<< "$ARTIFACT_NAMES"; then
+ MATCHING_ARTIFACTS+=("$ARTIFACT_NAME")
fi
+else
+ if [[ "$IS_BINDINGS_RELEASE" == true ]]; then
+ ARTIFACT_PATTERN="cuda-bindings-python*-cuda${TOOLKIT_VERSION}-*"
+ fi
+ while IFS= read -r artifact_name; do
+ # Release patterns contain only the validated toolkit version. Other
+ # patterns preserve the component matching accepted by this script.
+ # shellcheck disable=SC2053
+ if [[ "$artifact_name" == $ARTIFACT_PATTERN && "$artifact_name" != *-tests ]]; then
+ MATCHING_ARTIFACTS+=("$artifact_name")
+ fi
+ done <<< "$ARTIFACT_NAMES"
+fi
- echo "Processing artifact: $p"
- # Move wheel files to output directory
- if [[ -d "$p" ]]; then
- find "$p" -name "*.whl" -exec mv {} "$OUTPUT_DIR/" \;
+if [[ ${#MATCHING_ARTIFACTS[@]} == 0 ]]; then
+ if [[ "$IS_BINDINGS_RELEASE" == true ]]; then
+ echo "Error: run $RUN_ID has no unexpired release artifact for CUDA $TOOLKIT_VERSION" >&2
+ else
+ echo "Error: run $RUN_ID has no unexpired releasable artifact matching $ARTIFACT_PATTERN" >&2
fi
+ exit 1
+fi
+
+for artifact_name in "${MATCHING_ARTIFACTS[@]}"; do
+ gh run download "$RUN_ID" --name "$artifact_name" \
+ -R "$REPOSITORY" --dir "$ARTIFACTS_DIR"
done
-# Clean up artifact directories
-rm -rf cuda-*
+# Create output directory
+mkdir -p "$OUTPUT_DIR"
+
+# `gh run download` extracts a sole selected artifact directly into --dir,
+# while multiple artifacts get per-artifact subdirectories. Find wheels
+# recursively so both layouts are handled identically.
+MOVED_WHEELS=0
+while IFS= read -r -d '' wheel; do
+ relative_path="${wheel#"$ARTIFACTS_DIR"/}"
+ echo "Processing wheel: $relative_path"
+ mv "$wheel" "$OUTPUT_DIR/"
+ MOVED_WHEELS=$((MOVED_WHEELS + 1))
+done < <(find "$ARTIFACTS_DIR" -type f -name "*.whl" -print0)
+
+if [[ "$MOVED_WHEELS" == 0 ]]; then
+ echo "Error: selected artifacts contain no releasable wheels" >&2
+ exit 1
+fi
echo "Downloaded wheels to: $OUTPUT_DIR"
ls -la "$OUTPUT_DIR"
diff --git a/ci/tools/env-vars b/ci/tools/env-vars
index 219c9db04f7..48da8700669 100755
--- a/ci/tools/env-vars
+++ b/ci/tools/env-vars
@@ -11,7 +11,13 @@ set -euo pipefail
# Check if the script was called with exactly 1 argument
if [[ ${#} -ne 1 ]]; then
echo "Error: This script requires exactly 1 argument (the build mode). You provided ${#}"
- echo "Usage: ${0} build_mode[build or test]"
+ echo "Usage: ${0} {build|test}"
+ exit 1
+fi
+BUILD_MODE=${1}
+if [[ "${BUILD_MODE}" != "build" && "${BUILD_MODE}" != "test" ]]; then
+ echo "Error: build mode must be build or test, got ${BUILD_MODE}" >&2
+ echo "Usage: ${0} {build|test}" >&2
exit 1
fi
@@ -29,16 +35,17 @@ 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}"
+CUDA_CORE_ARTIFACT_NAME="${CUDA_CORE_ARTIFACT_BASENAME}-${SHA}"
{
echo "CUDA_CORE_ARTIFACT_BASENAME=${CUDA_CORE_ARTIFACT_BASENAME}"
- echo "CUDA_CORE_ARTIFACT_NAME=${CUDA_CORE_ARTIFACT_BASENAME}-${SHA}"
+ echo "CUDA_CORE_ARTIFACT_NAME=${CUDA_CORE_ARTIFACT_NAME}"
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
-if [[ "${1}" == "build" ]]; then
+if [[ "${BUILD_MODE}" == "build" ]]; then
if [[ "${HOST_PLATFORM}" == "linux-64" ]]; then
CIBW_PLATFORM_TAG="manylinux_x86_64"
elif [[ "${HOST_PLATFORM}" == "linux-aarch64" ]]; then
@@ -52,68 +59,93 @@ if [[ "${1}" == "build" ]]; then
exit 1
fi
echo "CIBW_BUILD=cp${PYTHON_VERSION_FORMATTED}-${CIBW_PLATFORM_TAG}" >> "$GITHUB_ENV"
- 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}"
-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}"
+ # Native jq on Windows emits CRLF, so treat CR as an input delimiter too.
+ IFS=$'\t\r' read -r \
+ CURRENT_BINDINGS_ROOT CURRENT_CUDA_VERSION CURRENT_CUDA_MAJOR CURRENT_CUDA_VARIANT \
+ MAINTENANCE_BINDINGS_ROOT MAINTENANCE_CUDA_VERSION MAINTENANCE_CUDA_MAJOR MAINTENANCE_CUDA_VARIANT < <(
+ python -m ci.tools.bindings_config --package-roots \
+ | jq -er '
+ (.[] | select(.release_status == "current")) as $current
+ | (.[] | select(.release_status == "maintenance")) as $maintenance
+ | [$current.package_root, $current.toolkit_version, $current.cuda_major, $current.cuda_variant,
+ $maintenance.package_root, $maintenance.toolkit_version, $maintenance.cuda_major,
+ $maintenance.cuda_variant]
+ | @tsv
+ '
+ )
+ CURRENT_BINDINGS_ARTIFACT_BASENAME="cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda${CURRENT_CUDA_VERSION}-${HOST_PLATFORM}"
+ MAINTENANCE_BINDINGS_ARTIFACT_BASENAME="cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda${MAINTENANCE_CUDA_VERSION}-${HOST_PLATFORM}"
+
+ {
+ echo "CURRENT_BINDINGS_ROOT=${CURRENT_BINDINGS_ROOT}"
+ echo "CURRENT_CUDA_VERSION=${CURRENT_CUDA_VERSION}"
+ echo "CURRENT_CUDA_MAJOR=${CURRENT_CUDA_MAJOR}"
+ echo "CURRENT_CUDA_VARIANT=${CURRENT_CUDA_VARIANT}"
+ echo "MAINTENANCE_BINDINGS_ROOT=${MAINTENANCE_BINDINGS_ROOT}"
+ echo "MAINTENANCE_CUDA_VERSION=${MAINTENANCE_CUDA_VERSION}"
+ echo "MAINTENANCE_CUDA_MAJOR=${MAINTENANCE_CUDA_MAJOR}"
+ echo "MAINTENANCE_CUDA_VARIANT=${MAINTENANCE_CUDA_VARIANT}"
+ echo "CURRENT_BINDINGS_ARTIFACT_BASENAME=${CURRENT_BINDINGS_ARTIFACT_BASENAME}"
+ echo "CURRENT_BINDINGS_ARTIFACT_NAME=${CURRENT_BINDINGS_ARTIFACT_BASENAME}-${SHA}"
+ echo "CURRENT_BINDINGS_ARTIFACTS_DIR=$(realpath "${REPO_DIR}/${CURRENT_BINDINGS_ROOT}/dist")"
+ echo "CURRENT_BINDINGS_CYTHON_TESTS_DIR=$(realpath "${REPO_DIR}/${CURRENT_BINDINGS_ROOT}/tests/cython")"
+ echo "MAINTENANCE_BINDINGS_ARTIFACT_BASENAME=${MAINTENANCE_BINDINGS_ARTIFACT_BASENAME}"
+ echo "MAINTENANCE_BINDINGS_ARTIFACT_NAME=${MAINTENANCE_BINDINGS_ARTIFACT_BASENAME}-${SHA}"
+ echo "MAINTENANCE_BINDINGS_ARTIFACTS_DIR=$(realpath "${REPO_DIR}/${MAINTENANCE_BINDINGS_ROOT}/dist")"
+ echo "MAINTENANCE_BINDINGS_CYTHON_TESTS_DIR=$(realpath "${REPO_DIR}/${MAINTENANCE_BINDINGS_ROOT}/tests/cython")"
+ echo "CURRENT_PYTHON_ARTIFACT_NAME=cuda-python-wheel-cuda${CURRENT_CUDA_VERSION}"
+ echo "MAINTENANCE_PYTHON_ARTIFACT_NAME=cuda-python-wheel-cuda${MAINTENANCE_CUDA_VERSION}"
+ } >> $GITHUB_ENV
+else
+ TEST_CUDA_MAJOR="$(cut -d '.' -f 1 <<< "${CUDA_VER}")"
# BINDINGS_SOURCE controls which cuda-bindings to install at test time:
- # main — use the just-built bindings wheel from this CI run
- # backport — fetch bindings from the prior (N-1) branch
- # floor — install from PyPI the oldest cuda-bindings that the cuda-core
- # wheel under test supports, its per-major floor (see
- # cuda_core/cuda/core/_bindings_floor.py and ci/tools/run-tests).
- # Selected when the test CTK minor differs from the minor the
- # wheel was built against. Those rows exercise a new cuda-core
- # with the floor cuda-bindings and older CTK libraries, the skew
- # that cuda-core supports. cuda-bindings older than the floor is
- # unsupported and fails at import
- # (https://github.com/NVIDIA/cuda-python/issues/2783).
+ # local — use the matching configured-package wheel from this CI run
+ # floor — install the oldest bindings supported by the Core wheel,
+ # read from that wheel's METADATA, with older toolkit libraries
#
# SKIP_CUDA_BINDINGS_TEST / SKIP_CYTHON_TEST control which *tests* to run
# (they do NOT affect installation — that's BINDINGS_SOURCE's job).
- BUILD_CUDA_MINOR="$(cut -d '.' -f 2 <<< ${BUILD_CUDA_VER})"
TEST_CUDA_MINOR="$(cut -d '.' -f 2 <<< ${CUDA_VER})"
- # CI builds the prior-major half of the cuda-core wheel against the prev_build
- # toolkit in ci/versions.yml and the backport branch's bindings.
- if ! command -v yq >/dev/null 2>&1; then
- echo "Error: yq is required to read ci/versions.yml" >&2
+ if [[ "${PATHFINDER_SOURCE:-}" != "artifact" && "${PATHFINDER_SOURCE:-}" != "published" ]]; then
+ echo "Error: PATHFINDER_SOURCE must be artifact or published, got ${PATHFINDER_SOURCE:-}" >&2
exit 1
fi
- BUILD_PREV_CUDA_VER="$(yq -r '.cuda.prev_build.version' ci/versions.yml)"
- BUILD_PREV_CUDA_MINOR="$(cut -d '.' -f 2 <<< ${BUILD_PREV_CUDA_VER})"
- if [[ ${BUILD_CUDA_MAJOR} != ${TEST_CUDA_MAJOR} ]]; then
- SKIP_CUDA_BINDINGS_TEST=1
- SKIP_CYTHON_TEST=1
- if [[ ${BUILD_PREV_CUDA_MINOR} != ${TEST_CUDA_MINOR} ]]; then
- # Prior major, minor mismatch (e.g. built against 12.9, test=12.6): floor
- # bindings from PyPI with the older CTK libraries.
- BINDINGS_SOURCE=floor
- else
- # Prior major, same minor (e.g. build=13.x, test=12.9): the backport branch.
- BINDINGS_SOURCE=backport
- fi
- elif [[ ${BUILD_CUDA_MINOR} != ${TEST_CUDA_MINOR} ]]; then
- # Same major, minor mismatch (e.g. build=13.4, test=13.0): floor bindings
- # from PyPI with the older CTK libraries.
- BINDINGS_SOURCE=floor
+ if [[ "${BINDINGS_SOURCE}" == "local" ]]; then
+ : "${BINDINGS_SOURCE_DIR:?BINDINGS_SOURCE_DIR is required for local bindings}"
+ BINDINGS_BUILD_CUDA_VER="${CUDA_VER}"
+ CUDA_BINDINGS_ROOT="${BINDINGS_SOURCE_DIR}"
+ elif [[ "${BINDINGS_SOURCE}" == "floor" ]]; then
+ # The matrix selects floor bindings for toolkit rows without an exact
+ # registry match. Core declares the supported minimum for each major.
+ : "${DEFAULT_BINDINGS_SOURCE_DIR:?DEFAULT_BINDINGS_SOURCE_DIR is required for floor bindings}"
+ CUDA_BINDINGS_ROOT="${DEFAULT_BINDINGS_SOURCE_DIR}"
SKIP_CUDA_BINDINGS_TEST=1
SKIP_CYTHON_TEST=1
else
- # Exact match: use the just-built bindings wheel.
- BINDINGS_SOURCE=main
+ echo "Error: BINDINGS_SOURCE must be local or floor, got ${BINDINGS_SOURCE}" >&2
+ exit 1
+ fi
+
+ if [[ "${BINDINGS_SOURCE}" == "local" ]]; then
if [[ "${SKIP_BINDINGS_TEST_OVERRIDE:-0}" == "1" ]]; then
SKIP_CUDA_BINDINGS_TEST=1
else
SKIP_CUDA_BINDINGS_TEST=0
fi
SKIP_CYTHON_TEST=0
+ CUDA_BINDINGS_ARTIFACT_BASENAME="cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda${BINDINGS_BUILD_CUDA_VER}-${HOST_PLATFORM}"
+ CUDA_BINDINGS_ARTIFACTS_DIR="$(realpath "${REPO_DIR}/${CUDA_BINDINGS_ROOT}/dist")"
+ CUDA_BINDINGS_CYTHON_TESTS_DIR="$(realpath "${REPO_DIR}/${CUDA_BINDINGS_ROOT}/tests/cython")"
+ CUDA_PYTHON_ARTIFACT_NAME="cuda-python-wheel-cuda${BINDINGS_BUILD_CUDA_VER}"
+ else
+ # These are unused for floor bindings, but deterministic paths make
+ # environment dumps and ad-hoc invocations easier to understand.
+ CUDA_BINDINGS_ARTIFACT_BASENAME="cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda${CUDA_VER}-${HOST_PLATFORM}"
+ CUDA_BINDINGS_ARTIFACTS_DIR="$(realpath "${REPO_DIR}/${CUDA_BINDINGS_ROOT}/dist")"
+ CUDA_BINDINGS_CYTHON_TESTS_DIR="$(realpath "${REPO_DIR}/${CUDA_BINDINGS_ROOT}/tests/cython")"
fi
# We don't test compute-sanitizer on CTK<12 because backporting fixes is too much effort
# We only test compute-sanitizer on python 3.12 arbitrarily; we don't need to use sanitizer on the entire matrix
@@ -128,16 +160,19 @@ elif [[ "${1}" == "test" ]]; then
{
echo "SETUP_SANITIZER=${SETUP_SANITIZER}"
echo "BINDINGS_SOURCE=${BINDINGS_SOURCE}"
+ echo "CUDA_BINDINGS_ROOT=${CUDA_BINDINGS_ROOT}"
+ echo "PATHFINDER_SOURCE=${PATHFINDER_SOURCE}"
+ if [[ "${BINDINGS_SOURCE}" == "local" ]]; then
+ echo "CUDA_PYTHON_ARTIFACT_NAME=${CUDA_PYTHON_ARTIFACT_NAME}"
+ fi
echo "SKIP_CUDA_BINDINGS_TEST=${SKIP_CUDA_BINDINGS_TEST}"
echo "SKIP_CYTHON_TEST=${SKIP_CYTHON_TEST}"
echo "TEST_CUDA_MAJOR=${TEST_CUDA_MAJOR}"
echo "TEST_CUDA_MINOR=${TEST_CUDA_MINOR}"
+ echo "CUDA_BINDINGS_ARTIFACT_BASENAME=${CUDA_BINDINGS_ARTIFACT_BASENAME}"
+ echo "CUDA_BINDINGS_ARTIFACT_NAME=${CUDA_BINDINGS_ARTIFACT_BASENAME}-${SHA}"
+ echo "CUDA_BINDINGS_ARTIFACTS_DIR=${CUDA_BINDINGS_ARTIFACTS_DIR}"
+ echo "CUDA_BINDINGS_CYTHON_TESTS_DIR=${CUDA_BINDINGS_CYTHON_TESTS_DIR}"
+ echo "CUDA_CORE_CYTHON_TEST_ARTIFACT_NAME=${CUDA_CORE_ARTIFACT_NAME}-cu${TEST_CUDA_MAJOR}-tests"
} >> $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_13/dist")"
- echo "CUDA_BINDINGS_CYTHON_TESTS_DIR=$(realpath "${REPO_DIR}/cuda_bindings_13/tests/cython")"
-} >> $GITHUB_ENV
diff --git a/ci/tools/merge_cuda_core_wheels.py b/ci/tools/merge_cuda_core_wheels.py
index 374ea1f53ea..5670913bada 100644
--- a/ci/tools/merge_cuda_core_wheels.py
+++ b/ci/tools/merge_cuda_core_wheels.py
@@ -7,20 +7,20 @@
"""
Script to merge CUDA-specific wheels into a single multi-CUDA wheel.
-This script takes wheels built for different CUDA versions (cu12, cu13) and merges them
-into a single wheel that supports both CUDA versions.
+This script takes wheels built for different CUDA ABI majors and merges them into a
+single wheel that supports every supplied major.
In particular, each wheel contains a CUDA-specific build of the `cuda.core` library
and the associated bindings. This script merges these directories into a single wheel
-that supports both CUDA versions, i.e., containing both `cuda/core/cu12`
-and `cuda/core/cu13`. At runtime, the code in `cuda/core/__init__.py`
-is used to import the appropriate CUDA-specific bindings.
+with one `cuda/core/cuN` directory per input. At runtime, the code in
+`cuda/core/__init__.py` imports the appropriate CUDA-specific bindings.
This script is based on the one in NVIDIA/CCCL.
"""
import argparse
import os
+import re
import shutil
import subprocess
import sys
@@ -28,6 +28,16 @@
import zipfile
from pathlib import Path
+CUDA_WHEEL_SUFFIX = re.compile(r"\.cu(?P[1-9][0-9]*)\.whl$")
+
+
+def cuda_variant_from_wheel(wheel: Path) -> str:
+ """Return the ``cuN`` variant encoded in an intermediate wheel name."""
+ match = CUDA_WHEEL_SUFFIX.search(wheel.name)
+ if match is None:
+ raise ValueError(f"wheel name does not contain a .cuN suffix: {wheel.name}")
+ return f"cu{match.group('major')}"
+
def run_command(cmd: list[str], cwd: Path | None = None, env: dict = os.environ) -> subprocess.CompletedProcess:
"""Run a command with error handling."""
@@ -84,6 +94,9 @@ def merge_wheels(wheels: list[Path], output_dir: Path, show_wheel_contents: bool
if len(wheels) == 1:
raise RuntimeError("only one wheel is provided, nothing to merge")
+ cuda_variants = [cuda_variant_from_wheel(wheel) for wheel in wheels]
+ if len(set(cuda_variants)) != len(cuda_variants):
+ raise RuntimeError(f"input wheels must have unique CUDA variants: {cuda_variants}")
# Extract all wheels to temporary directories
with tempfile.TemporaryDirectory() as temp_dir:
@@ -133,11 +146,8 @@ def merge_wheels(wheels: list[Path], output_dir: Path, show_wheel_contents: bool
# Copy version-specific directories from each wheel into versioned subdirectories
base_dir = Path("cuda") / "core"
- versioned_dirs = set()
- for i, wheel_dir in enumerate(extracted_wheels):
- cuda_version = wheels[i].name.split(".cu")[1].split(".")[0]
- versioned_dir = base_wheel / base_dir / f"cu{cuda_version}"
- versioned_dirs.add(versioned_dir.name)
+ for wheel_dir, cuda_variant in zip(extracted_wheels, cuda_variants, strict=True):
+ versioned_dir = base_wheel / base_dir / cuda_variant
# Copy entire directory tree from source wheel to versioned directory
print(f" Copying {wheel_dir / base_dir} to {versioned_dir}", file=sys.stderr)
@@ -152,7 +162,7 @@ def merge_wheels(wheels: list[Path], output_dir: Path, show_wheel_contents: bool
# build-independent floor logic in _bindings_floor, then redirects every
# later import into the versioned tree. Anything else left at top level
# is a dead copy that nothing imports.
- items_to_keep = {"__init__.py", "_version.py", "_bindings_floor.py", *versioned_dirs}
+ items_to_keep = {"__init__.py", "_version.py", "_bindings_floor.py", *cuda_variants}
all_items = os.scandir(base_wheel / base_dir)
removed_count = 0
for f in all_items:
diff --git a/ci/tools/tests/test_compute_ci_plan.py b/ci/tools/tests/test_compute_ci_plan.py
index e1844384d54..0dfd4227468 100644
--- a/ci/tools/tests/test_compute_ci_plan.py
+++ b/ci/tools/tests/test_compute_ci_plan.py
@@ -4,58 +4,119 @@
from __future__ import annotations
+import json
+import os
+import re
+import subprocess
import tempfile
import unittest
from pathlib import Path
-from ci.tools.compute_ci_plan import _expand_linked_paths, compute_workplan
+import pytest
+import yaml
+
+from ci.tools.bindings_config import BindingsConfig, BindingsPackage, load_config
+from ci.tools.compute_ci_plan import _expand_linked_paths, compute_workplan, format_workplan_summary, main
ALL_MODULES = {"pathfinder", "bindings", "core", "python"}
ALL_PLATFORMS = {"linux", "windows"}
+VARIANT_MODULES = {"bindings", "core", "python"}
+DEFAULT_BINDINGS_CONFIG = load_config()
+CUDA_VARIANTS = {package.cuda_variant for package in DEFAULT_BINDINGS_CONFIG.package_roots}
+PACKAGE_VARIANTS = {package.package_root: package.cuda_variant for package in DEFAULT_BINDINGS_CONFIG.package_roots}
def plan_for(
*paths: str,
baseline: bool = True,
linked_paths: set[str] | None = None,
+ release_tag: str = "",
+ bindings_config: BindingsConfig | None = None,
) -> dict[str, object]:
return compute_workplan(
- list(paths),
+ bindings_config=bindings_config if bindings_config is not None else DEFAULT_BINDINGS_CONFIG,
+ paths=list(paths),
+ linked_paths=linked_paths if linked_paths is not None else set(),
merge_base="base",
baseline_run_id="123" if baseline else "",
- linked_paths=linked_paths,
+ release_tag=release_tag,
)
+def as_dict(value: object) -> dict[str, object]:
+ assert isinstance(value, dict)
+ return value
+
+
+def enabled(decisions: dict[str, object], key: str | None = None) -> set[str]:
+ if key is None:
+ return {name for name, value in decisions.items() if value}
+ return {name for name, value in decisions.items() if as_dict(value)[key]}
+
+
def selected(plan: dict[str, object], key: str) -> set[str]:
- modules = plan["modules"]
- assert isinstance(modules, dict)
- return {name for name, decision in modules.items() if decision[key]}
+ return enabled(as_dict(plan["modules"]), key)
def selected_platforms(plan: dict[str, object]) -> set[str]:
- jobs = plan["jobs"]
- assert isinstance(jobs, dict)
- platforms = jobs["platforms"]
- assert isinstance(platforms, dict)
+ platforms = as_dict(as_dict(plan["jobs"])["platforms"])
assert set(platforms) == ALL_PLATFORMS
- return {name for name, enabled in platforms.items() if enabled}
+ return enabled(platforms)
+
+
+def selected_variants(plan: dict[str, object], module: str, key: str) -> set[str]:
+ modules = as_dict(plan["modules"])
+ if module == "core":
+ return enabled(as_dict(as_dict(modules[module])["cuda_majors"]), key)
+ packages = as_dict(as_dict(modules[module])["package_roots"])
+ return {PACKAGE_VARIANTS[root] for root in enabled(packages, key)}
+
+
+def selected_package_roots(plan: dict[str, object], module: str, key: str) -> set[str]:
+ module_plan = as_dict(as_dict(plan["modules"])[module])
+ return enabled(as_dict(module_plan["package_roots"]), key)
+
+
+def selected_core_majors(plan: dict[str, object], key: str) -> set[str]:
+ core = as_dict(as_dict(plan["modules"])["core"])
+ return enabled(as_dict(core["cuda_majors"]), key)
+
+
+def selected_cuda_majors(plan: dict[str, object], key: str) -> set[str]:
+ majors = as_dict(as_dict(plan["jobs"])[key])
+ cuda_majors = as_dict(as_dict(as_dict(plan["modules"])["core"])["cuda_majors"])
+ assert set(majors) == set(cuda_majors)
+ return enabled(majors)
+
+
+def selected_sdist_variants(plan: dict[str, object]) -> set[str]:
+ jobs = as_dict(plan["jobs"])
+ selected_roots = enabled(as_dict(jobs["sdist_package_roots"]))
+ return {PACKAGE_VARIANTS[root] for root in selected_roots}
+
+
+def synthetic_package(package_root: str, ctk_target: str, release_status: str) -> BindingsPackage:
+ return BindingsPackage(
+ package_root=package_root,
+ toolkit_version=f"{ctk_target}.0",
+ release_status=release_status,
+ )
+
+
+def synthetic_config(
+ *packages: BindingsPackage,
+) -> BindingsConfig:
+ return BindingsConfig(
+ schema_version=2,
+ package_roots=packages,
+ )
class ComputeWorkplanTest(unittest.TestCase):
def test_path_impacts(self) -> None:
cases = {
- "cuda_pathfinder/cuda/pathfinder/_loader.py": (ALL_MODULES, ALL_MODULES, False),
- "cuda_bindings_13/cuda/bindings/driver.pyx": (
- {"bindings", "core", "python"},
- {"bindings", "core", "python"},
- False,
- ),
- "cuda_core/cuda/core/_device.py": ({"core"}, {"core", "python"}, True),
"cuda_core/cuda/core/examples/demo.py": ({"core"}, {"core", "python"}, True),
- "cuda_python/pyproject.toml": ({"bindings", "python"}, {"python"}, False),
"cuda_pathfinder/tests/test_loader.py": (set(), {"pathfinder"}, False),
- "cuda_bindings_13/examples/0_Introduction/vectorAddDrv.py": (set(), {"bindings"}, False),
"cuda_bindings_13/tests/README.md": (set(), {"bindings"}, False),
"cuda_core/pytest.ini": (set(), {"core"}, False),
"cuda_python/tests/test_import.py": (set(), {"python"}, False),
@@ -65,10 +126,6 @@ def test_path_impacts(self) -> None:
False,
),
"benchmarks/cuda_bindings/run_pyperf.py": (set(), ALL_MODULES, False),
- "benchmarks/cuda_core/runner.py": (set(), ALL_MODULES, False),
- "ci/tools/run-tests": (ALL_MODULES, ALL_MODULES, True),
- "ci/versions.yml": (ALL_MODULES, ALL_MODULES, True),
- "pytest.ini": (ALL_MODULES, ALL_MODULES, True),
}
for path, (builds, tests, core_api) in cases.items():
@@ -80,6 +137,119 @@ def test_path_impacts(self) -> None:
assert plan["jobs"]["sdist_tests"] == bool(builds)
assert plan["jobs"]["core_api_checks"] == core_api
+ self._check_variant_path_impacts()
+
+ def _check_variant_path_impacts(self) -> None:
+ all_variant_modules = dict.fromkeys(VARIANT_MODULES, CUDA_VARIANTS)
+ cases = (
+ (
+ ("cuda_bindings_12/cuda/bindings/driver.pyx",),
+ {"bindings": {"cu12"}, "core": CUDA_VARIANTS, "python": {"cu12"}},
+ {"bindings": {"cu12"}, "core": {"cu12"}, "python": {"cu12"}},
+ {"cu12"},
+ {"cu12"},
+ ),
+ (
+ ("cuda_bindings_13/cuda/bindings/driver.pyx",),
+ {"bindings": {"cu13"}, "core": CUDA_VARIANTS, "python": {"cu13"}},
+ {"bindings": {"cu13"}, "core": {"cu13"}, "python": {"cu13"}},
+ {"cu13"},
+ {"cu13"},
+ ),
+ (
+ ("cuda_bindings_12/examples/0_Introduction/vectorAddDrv.py",),
+ {},
+ {"bindings": {"cu12"}},
+ {"cu12"},
+ set(),
+ ),
+ (
+ ("cuda_bindings_13/tests/test_driver.py",),
+ {},
+ {"bindings": {"cu13"}},
+ {"cu13"},
+ set(),
+ ),
+ (
+ ("cuda_core/cuda/core/_device.py",),
+ {"core": CUDA_VARIANTS},
+ {"core": CUDA_VARIANTS, "python": CUDA_VARIANTS},
+ CUDA_VARIANTS,
+ CUDA_VARIANTS,
+ ),
+ (
+ ("cuda_python/pyproject.toml",),
+ {"bindings": CUDA_VARIANTS, "python": CUDA_VARIANTS},
+ {"python": CUDA_VARIANTS},
+ CUDA_VARIANTS,
+ CUDA_VARIANTS,
+ ),
+ (
+ ("cuda_pathfinder/cuda/pathfinder/_loader.py",),
+ all_variant_modules,
+ all_variant_modules,
+ CUDA_VARIANTS,
+ CUDA_VARIANTS,
+ ),
+ (
+ (
+ "cuda_bindings_12/cuda/bindings/driver.pyx",
+ "cuda_bindings_13/cuda/bindings/runtime.pyx",
+ ),
+ all_variant_modules,
+ all_variant_modules,
+ CUDA_VARIANTS,
+ CUDA_VARIANTS,
+ ),
+ )
+
+ for paths, variant_builds, variant_tests, test_majors, sdist_majors in cases:
+ with self.subTest(paths=paths):
+ plan = plan_for(*paths)
+ for module in VARIANT_MODULES:
+ builds = selected_variants(plan, module, "needs_build")
+ tests = selected_variants(plan, module, "needs_test")
+ assert builds == variant_builds.get(module, set())
+ assert tests == variant_tests.get(module, set())
+
+ modules = plan["modules"]
+ assert isinstance(modules, dict)
+ decision = modules[module]
+ assert decision["needs_build"] == bool(builds)
+ assert decision["needs_test"] == bool(tests)
+
+ assert selected_cuda_majors(plan, "test_cuda_majors") == test_majors
+ assert selected_sdist_variants(plan) == sdist_majors
+
+ @pytest.mark.agent_authored(model="gpt-5.6-sol")
+ def test_release_tags_select_only_the_matching_package(self) -> None:
+ for release_tag, package_root, variant in (
+ ("v12.9.9", "cuda_bindings_12", "cu12"),
+ ("v13.4.1", "cuda_bindings_13", "cu13"),
+ ("v13.4.1b1", "cuda_bindings_13", "cu13"),
+ ("v13.4.1rc1", "cuda_bindings_13", "cu13"),
+ ("v13.4.1.dev1", "cuda_bindings_13", "cu13"),
+ ("v12.9.9.post1", "cuda_bindings_12", "cu12"),
+ ("v13.4.1.post1", "cuda_bindings_13", "cu13"),
+ ):
+ with self.subTest(release_tag=release_tag):
+ plan = plan_for(baseline=False, release_tag=release_tag)
+ assert selected(plan, "needs_build") == {"bindings", "python"}
+ assert selected(plan, "needs_test") == {"bindings", "python"}
+ for module in ("bindings", "python"):
+ assert selected_package_roots(plan, module, "needs_build") == {package_root}
+ assert selected_package_roots(plan, module, "needs_test") == {package_root}
+ assert selected_variants(plan, module, "needs_build") == {variant}
+ assert selected_variants(plan, module, "needs_test") == {variant}
+ assert not selected_variants(plan, "core", "needs_build")
+ assert not selected_variants(plan, "core", "needs_test")
+ assert selected_cuda_majors(plan, "test_cuda_majors") == {variant}
+ assert selected_sdist_variants(plan) == {variant}
+ assert selected_platforms(plan) == ALL_PLATFORMS
+ assert plan["jobs"]["sdist_tests"]
+ assert not plan["jobs"]["core_api_checks"]
+ assert plan["baseline"] == {"run_id": "", "sha": ""}
+
def test_test_infrastructure_platforms(self) -> None:
cases = {
".github/workflows/test-wheel-linux.yml": {"linux"},
@@ -96,9 +266,13 @@ def test_test_infrastructure_platforms(self) -> None:
plan = plan_for(path)
assert not selected(plan, "needs_build")
assert selected(plan, "needs_test") == ALL_MODULES
+ for module in VARIANT_MODULES:
+ assert selected_variants(plan, module, "needs_test") == CUDA_VARIANTS
assert selected_platforms(plan) == platforms
assert not plan["jobs"]["sdist_tests"]
assert not plan["jobs"]["core_api_checks"]
+ assert selected_cuda_majors(plan, "test_cuda_majors") == CUDA_VARIANTS
+ assert not selected_sdist_variants(plan)
mixed_plan = plan_for("ci/tools/install_gpu_driver.sh", "ci/tools/install_gpu_driver.ps1")
assert selected_platforms(mixed_plan) == ALL_PLATFORMS
@@ -107,6 +281,7 @@ def test_test_infrastructure_platforms(self) -> None:
assert selected_platforms(source_plan) == ALL_PLATFORMS
def test_ignored_paths_select_no_work(self) -> None:
+ empty_plan = plan_for()
for path in (
"cuda_core/docs/index.rst",
"cuda_core/pixi.toml",
@@ -115,6 +290,8 @@ def test_ignored_paths_select_no_work(self) -> None:
"benchmarks/cuda_bindings/AGENTS.md",
"cuda_core/cuda/core/_cpp/rt/DESIGN.md",
"cuda_bindings_13/README.md",
+ "cuda_bindings_12/README.md",
+ "cuda_bindings_12/docs/index.rst",
"cuda_core/README.md",
"new-area/pixi.toml",
"notes.md",
@@ -123,10 +300,7 @@ def test_ignored_paths_select_no_work(self) -> None:
".github/ISSUE_TEMPLATE/bug.yml",
):
with self.subTest(path=path):
- plan = plan_for(path)
- assert not selected(plan, "needs_build")
- assert not selected(plan, "needs_test")
- assert not selected_platforms(plan)
+ assert plan_for(path) == empty_plan
def test_unknown_path_and_missing_baseline_force_all(self) -> None:
for plan in (
@@ -135,22 +309,86 @@ def test_unknown_path_and_missing_baseline_force_all(self) -> None:
plan_for(".github/workflows/new-main-ci-workflow.yml"),
plan_for(".github/actions/doc_preview/action.yml"),
plan_for("cuda_core/docs/index.rst", baseline=False),
- compute_workplan([], merge_base="", baseline_run_id="123"),
+ compute_workplan(
+ bindings_config=DEFAULT_BINDINGS_CONFIG,
+ paths=[],
+ linked_paths=set(),
+ merge_base="",
+ baseline_run_id="123",
+ release_tag="",
+ ),
+ plan_for(baseline=False, release_tag="cuda-core-v1.3.0"),
):
assert selected(plan, "needs_build") == ALL_MODULES
assert selected(plan, "needs_test") == ALL_MODULES
+ for module in VARIANT_MODULES:
+ assert selected_variants(plan, module, "needs_build") == CUDA_VARIANTS
+ assert selected_variants(plan, module, "needs_test") == CUDA_VARIANTS
assert selected_platforms(plan) == ALL_PLATFORMS
assert plan["jobs"]["core_api_checks"]
+ assert selected_cuda_majors(plan, "test_cuda_majors") == CUDA_VARIANTS
+ assert selected_sdist_variants(plan) == CUDA_VARIANTS
assert plan["baseline"] == {"run_id": "", "sha": ""}
+ for release_tag in ("v12.8.1", "v13.5.0", "v14.0.0"):
+ with (
+ self.subTest(release_tag=release_tag),
+ pytest.raises(ValueError, match="no configured CUDA bindings package root"),
+ ):
+ plan_for(baseline=False, release_tag=release_tag)
+
def test_mixed_changes_are_combined(self) -> None:
plan = plan_for("cuda_core/tests/test_device.py", "cuda_python/pyproject.toml")
assert selected(plan, "needs_build") == {"bindings", "python"}
assert selected(plan, "needs_test") == {"core", "python"}
+ assert selected_variants(plan, "bindings", "needs_build") == CUDA_VARIANTS
+ assert selected_variants(plan, "python", "needs_build") == CUDA_VARIANTS
+ assert selected_variants(plan, "core", "needs_test") == CUDA_VARIANTS
+ assert selected_variants(plan, "python", "needs_test") == CUDA_VARIANTS
assert selected_platforms(plan) == ALL_PLATFORMS
assert plan["jobs"]["sdist_tests"]
+ assert selected_cuda_majors(plan, "test_cuda_majors") == CUDA_VARIANTS
+ assert selected_sdist_variants(plan) == CUDA_VARIANTS
assert plan["baseline"] == {"run_id": "123", "sha": "base"}
+ @pytest.mark.agent_authored(model="gpt-5.6-sol")
+ def test_mixed_bindings_source_and_core_test_keep_line_specific_tests(self) -> None:
+ plan = plan_for(
+ "cuda_bindings_12/cuda/bindings/driver.pyx",
+ "cuda_core/tests/test_device.py",
+ )
+
+ assert selected(plan, "needs_test") == {"bindings", "core", "python"}
+ assert selected_package_roots(plan, "bindings", "needs_test") == {"cuda_bindings_12"}
+ assert selected_core_majors(plan, "needs_test") == CUDA_VARIANTS
+ assert selected_package_roots(plan, "python", "needs_test") == {"cuda_bindings_12"}
+ assert selected_cuda_majors(plan, "test_cuda_majors") == CUDA_VARIANTS
+
+ @pytest.mark.agent_authored(model="gpt-5.6-sol")
+ def test_wheel_workflows_consume_line_specific_test_gates(self) -> None:
+ expected_fragments = {
+ "TEST_BINDINGS": (
+ "matrix.BINDINGS_SOURCE == 'local'",
+ ".modules.bindings.package_roots[matrix.BINDINGS_SOURCE_DIR].needs_test",
+ ),
+ "TEST_CORE": (".modules.core.cuda_majors[matrix.CUDA_VARIANT].needs_test",),
+ "TEST_PYTHON": (
+ "matrix.BINDINGS_SOURCE == 'local'",
+ ".modules.python.package_roots[matrix.BINDINGS_SOURCE_DIR].needs_test",
+ ),
+ }
+
+ repo_root = Path(__file__).resolve().parents[3]
+ for platform in ("linux", "windows"):
+ with self.subTest(platform=platform):
+ workflow = yaml.safe_load(
+ (repo_root / ".github" / "workflows" / f"test-wheel-{platform}.yml").read_text(encoding="utf-8")
+ )
+ gates = workflow["jobs"]["test"]["env"]
+ for name, fragments in expected_fragments.items():
+ with self.subTest(platform=platform, gate=name):
+ assert all(fragment in gates[name] for fragment in fragments)
+
def test_changed_symlink_targets_include_their_consumers(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
@@ -168,13 +406,232 @@ def test_changed_symlink_targets_include_their_consumers(self) -> None:
assert paths == ["README.md", "cuda_python/README.md"]
plan = plan_for(*paths, linked_paths={"cuda_python/README.md"})
- assert selected(plan, "needs_build") == {"bindings", "python"}
- assert selected(plan, "needs_test") == {"python"}
-
removed_link = plan_for("cuda_python/README.md", linked_paths={"cuda_python/README.md"})
- assert selected(removed_link, "needs_build") == {"bindings", "python"}
- assert selected(removed_link, "needs_test") == {"python"}
+ for linked_plan in (plan, removed_link):
+ assert selected(linked_plan, "needs_build") == {"bindings", "python"}
+ assert selected(linked_plan, "needs_test") == {"python"}
+ for module in ("bindings", "python"):
+ assert selected_variants(linked_plan, module, "needs_build") == CUDA_VARIANTS
+ assert selected_variants(linked_plan, "python", "needs_test") == CUDA_VARIANTS
+
+ @pytest.mark.agent_authored(model="gpt-5.6-sol")
+ def test_current_package_can_move_to_a_new_cuda_major(self) -> None:
+ package_11 = synthetic_package("cuda_bindings_11", "11.8", "maintenance")
+ package_12 = synthetic_package("cuda_bindings_12", "12.0", "current")
+ config = synthetic_config(
+ package_11,
+ package_12,
+ )
+
+ plan = plan_for(
+ "cuda_bindings_12/cuda/bindings/runtime.pyx",
+ bindings_config=config,
+ )
+
+ assert selected_package_roots(plan, "bindings", "needs_build") == {package_12.package_root}
+ assert selected_package_roots(plan, "python", "needs_build") == {package_12.package_root}
+ assert selected_core_majors(plan, "needs_build") == {"cu11", "cu12"}
+ assert selected_core_majors(plan, "needs_test") == {"cu12"}
+ assert selected_cuda_majors(plan, "test_cuda_majors") == {"cu12"}
+ assert selected_sdist_variants(plan) == {"cu12"}
+
+ core_plan = plan_for("cuda_core/cuda/core/_device.py", bindings_config=config)
+ assert selected_core_majors(core_plan, "needs_build") == {"cu11", "cu12"}
+ assert selected_package_roots(core_plan, "python", "needs_test") == {
+ package_11.package_root,
+ package_12.package_root,
+ }
+
+
+@pytest.mark.agent_authored(model="gpt-5.6")
+def test_github_outputs_are_emitted_without_shell_json_transforms(tmp_path: Path) -> None:
+ output = tmp_path / "github-output"
+ summary = tmp_path / "github-summary"
+
+ main(
+ [
+ "--release-tag",
+ "v13.4.1",
+ "--github-output",
+ str(output),
+ "--github-step-summary",
+ str(summary),
+ ]
+ )
+
+ records = dict(line.split("=", maxsplit=1) for line in output.read_text(encoding="utf-8").splitlines())
+ assert set(records) == {"bindings-config", "workplan"}
+ registry = json.loads(records["bindings-config"])
+ assert [package["package_root"] for package in registry["package_roots"]] == [
+ "cuda_bindings_12",
+ "cuda_bindings_13",
+ ]
+ assert json.loads(records["workplan"])["jobs"]["sdist_package_roots"] == {
+ "cuda_bindings_12": False,
+ "cuda_bindings_13": True,
+ }
+ assert "### CI workplan" in summary.read_text(encoding="utf-8")
if __name__ == "__main__":
unittest.main()
+
+
+@pytest.mark.agent_authored(model="gpt-6.1-sol")
+def test_workplan_labels_and_summary_show_component_majors_and_exclusions() -> None:
+ plan = plan_for(release_tag="v12.9.11")
+ assert plan["labels"] == {
+ "build": "bindings12+meta12",
+ "build_platform": "bindings12",
+ "build_current": "baseline wheels and test extensions",
+ "sdist": {"cuda_bindings_12": "bindings12+meta12", "cuda_bindings_13": "excluded"},
+ }
+ assert plan["jobs"]["windows_arm64_build"] is False
+ summary = format_workplan_summary(plan, DEFAULT_BINDINGS_CONFIG)
+ assert "| cuda-bindings | 12 | build | required |" in summary
+ assert "| cuda-bindings | 13 | excluded | excluded by plan |" in summary
+ assert "published cuda-pathfinder" in summary
+ assert "CUDA 12 has no Windows ARM64 toolkit" in summary
+
+
+@pytest.mark.agent_authored(model="gpt-6.1-sol")
+def test_workplan_labels_keep_both_core_majors_for_a_bindings_change() -> None:
+ plan = plan_for("cuda_bindings_12/cuda/bindings/driver.pyx")
+ assert plan["labels"]["build_platform"] == "bindings12+core12+core13"
+ assert plan["labels"]["build_current"] == "core13"
+ assert plan["jobs"]["windows_arm64_build"] is True
+ summary = format_workplan_summary(plan, DEFAULT_BINDINGS_CONFIG)
+ assert "| cuda-bindings | 13 | reuse exact baseline | excluded by plan |" in summary
+ assert "Reusable artifacts: run `123` at `base`." in summary
+
+
+@pytest.mark.parametrize("platform", ["linux", "windows"])
+@pytest.mark.parametrize("scenario", ["full", "drop-local", "maintenance-only", "nightly-floor", "nightly-standard"])
+@pytest.mark.agent_authored(model="gpt-6.1-sol")
+def test_workflow_matrix_executes_component_selection_and_coverage_guard(
+ tmp_path: Path, platform: str, scenario: str
+) -> None:
+ workflow = Path(__file__).resolve().parents[3] / ".github" / "workflows" / f"test-wheel-{platform}.yml"
+ document = yaml.safe_load(workflow.read_text())
+ step = next(step for step in document["jobs"]["compute-matrix"]["steps"] if step.get("id") == "compute-matrix")
+ script = re.sub(r"^TEST_MATRIX=\$\(yq.*$", 'TEST_MATRIX="$CI_TEST_MATRIX"', step["run"], flags=re.MULTILINE)
+ matrix_filter = 'map(select(.CUDA_VER != "12.9.1"))' if scenario == "drop-local" else "."
+ script = script.replace("${{ inputs.matrix_filter }}", matrix_filter)
+ plan = (
+ plan_for("cuda_bindings_12/cuda/bindings/driver.pyx")
+ if scenario == "maintenance-only"
+ else plan_for(baseline=False)
+ )
+ versions = ["12.9.1", "13.4.2", "12.6.3", "13.0.2"]
+ if scenario == "nightly-floor":
+ versions = ["12.6.3"]
+ elif scenario == "nightly-standard":
+ versions = ["13.4.2"]
+ matrix = [
+ dict(CUDA_VER=version, PY_VER="3.12", LOCAL_CTK="0", GPU="l4", GPU_COUNT="1", DRIVER="latest")
+ for version in versions
+ ]
+ output = tmp_path / "output"
+ summary = tmp_path / "summary"
+ env = {
+ **os.environ,
+ "BINDINGS_CONFIG": DEFAULT_BINDINGS_CONFIG.to_json(),
+ "BUILD_TYPE": "nightly" if scenario.startswith("nightly-") else "pull-request",
+ "TEST_MODE": "nightly-pytorch" if scenario == "nightly-floor" else "standard",
+ "WORKPLAN": "" if scenario.startswith("nightly-") else json.dumps(plan),
+ "ARCH": "amd64",
+ "GITHUB_OUTPUT": str(output),
+ "GITHUB_STEP_SUMMARY": str(summary),
+ "CI_TEST_MATRIX": json.dumps(matrix),
+ }
+ result = subprocess.run( # noqa: S603 - executes the checked-in workflow script
+ ["/bin/bash", "-euo", "pipefail", "-c", script], env=env, text=True, capture_output=True, check=False
+ )
+ if scenario == "drop-local":
+ assert result.returncode != 0
+ assert "omits required local-wheel coverage: cuda_bindings_12" in result.stderr
+ return
+ assert result.returncode == 0, result.stderr
+ rows = json.loads(output.read_text().strip().removeprefix("MATRIX="))["include"]
+ if scenario == "maintenance-only":
+ assert [row["SUITES"] for row in rows] == ["bindings12+core12+python12", "core12"]
+ elif scenario == "nightly-floor":
+ assert rows[0]["BINDINGS_SOURCE"] == "floor"
+ assert rows[0]["SUITES"] == "nightly-pytorch"
+ elif scenario == "full":
+ assert rows[0]["SUITES"] == "pathfinder+bindings12+core12+python12"
+ assert rows[1]["SUITES"] == "pathfinder+bindings13+core13+python13"
+ assert rows[2]["SUITES"] == "pathfinder+core12"
+ assert "Selected wheel test coverage" in summary.read_text()
+
+
+@pytest.mark.parametrize("platform", ["linux", "windows"])
+@pytest.mark.parametrize(
+ ("major", "bindings", "core", "pathfinder_source", "resolve_dependencies"),
+ [
+ ("13", True, False, "published", True),
+ ("13", True, False, "artifact", True),
+ ("13", False, False, "artifact", True),
+ ("13", True, True, "artifact", False),
+ ("12", True, False, "published", False),
+ ],
+)
+@pytest.mark.agent_authored(model="gpt-6.1-sol")
+def test_metapackage_install_resolves_published_core_for_bindings_only_cuda13(
+ tmp_path: Path,
+ platform: str,
+ major: str,
+ bindings: bool,
+ core: bool,
+ pathfinder_source: str,
+ resolve_dependencies: bool,
+) -> None:
+ workflow = Path(__file__).resolve().parents[3] / ".github" / "workflows" / f"test-wheel-{platform}.yml"
+ document = yaml.safe_load(workflow.read_text())
+ step = next(
+ step for step in document["jobs"]["test"]["steps"] if step.get("name") == "Ensure cuda-python installable"
+ )
+ commands = tmp_path / "bin"
+ commands.mkdir()
+ pip = commands / "pip"
+ pip.write_text('#!/bin/bash\nprintf "%s\\n" "$@" > "$PIP_ARGUMENTS"\n')
+ pip.chmod(0o755)
+ python = commands / "python"
+ python.write_text('#!/bin/bash\nprintf "cuda-pathfinder==1.4.0\\n"\n')
+ python.chmod(0o755)
+ pathfinder = tmp_path / "cuda_pathfinder"
+ pathfinder.mkdir()
+ (pathfinder / "cuda_pathfinder-1.4.0-py3-none-any.whl").touch()
+ bindings_dist = tmp_path / "bindings"
+ bindings_dist.mkdir()
+ bindings_wheel = bindings_dist / f"cuda_bindings-{major}.4.0-cp312-cp312-linux_x86_64.whl"
+ bindings_wheel.touch()
+ (tmp_path / f"cuda_python-{major}.4.0-py3-none-any.whl").touch()
+ arguments = tmp_path / "pip-arguments"
+ result = subprocess.run( # noqa: S603 - executes the checked-in workflow script with stub installers
+ ["/bin/bash", "-euo", "pipefail", "-c", step["run"]],
+ cwd=tmp_path,
+ env={
+ **os.environ,
+ "PATH": f"{commands}{os.pathsep}{os.environ['PATH']}",
+ "PIP_ARGUMENTS": str(arguments),
+ "CUDA_BINDINGS_ARTIFACTS_DIR": str(bindings_dist),
+ "TEST_CUDA_MAJOR": major,
+ "TEST_BINDINGS": str(bindings).lower(),
+ "TEST_CORE": str(core).lower(),
+ "PATHFINDER_SOURCE": pathfinder_source,
+ "LOCAL_CTK": "0",
+ },
+ text=True,
+ capture_output=True,
+ check=False,
+ )
+ assert result.returncode == 0, result.stderr
+ captured = arguments.read_text().splitlines()
+ assert ("--no-deps" not in captured) is resolve_dependencies
+ if resolve_dependencies:
+ assert str(bindings_wheel) in captured
+ if pathfinder_source == "published":
+ assert "cuda-pathfinder==1.4.0" in captured
+ else:
+ assert "./cuda_pathfinder/cuda_pathfinder-1.4.0-py3-none-any.whl" in captured
diff --git a/ci/tools/tests/test_download_wheels.py b/ci/tools/tests/test_download_wheels.py
new file mode 100644
index 00000000000..323974b1b17
--- /dev/null
+++ b/ci/tools/tests/test_download_wheels.py
@@ -0,0 +1,209 @@
+# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+from __future__ import annotations
+
+import json
+import os
+import subprocess
+from pathlib import Path
+
+import pytest
+
+DOWNLOAD_WHEELS = Path(__file__).parent.parent / "download-wheels"
+
+FAKE_GH = r"""#!/usr/bin/env python3
+import fnmatch
+import json
+import os
+import sys
+from pathlib import Path
+
+args = sys.argv[1:]
+with Path(os.environ["FAKE_GH_LOG"]).open("a", encoding="utf-8") as stream:
+ print(json.dumps(args), file=stream)
+artifacts = json.loads(os.environ["FAKE_ARTIFACTS"])
+
+if args[:1] == ["api"]:
+ print(*artifacts, sep="\n")
+ raise SystemExit(0)
+
+if args[:2] == ["run", "download"]:
+ if "--name" in args:
+ name = args[args.index("--name") + 1]
+ selected = [artifact for artifact in artifacts if artifact == name]
+ else:
+ pattern = args[args.index("-p") + 1]
+ selected = [artifact for artifact in artifacts if fnmatch.fnmatchcase(artifact, pattern)]
+ if not selected:
+ raise SystemExit(1)
+ destination = Path(args[args.index("--dir") + 1])
+ for artifact in selected:
+ artifact_dir = destination / artifact
+ artifact_dir.mkdir(parents=True)
+ suffix = ".so" if artifact.endswith("-tests") else ".whl"
+ (artifact_dir / f"{artifact}{suffix}").touch()
+ raise SystemExit(0)
+
+raise SystemExit(2)
+"""
+
+
+@pytest.fixture
+def fake_gh(tmp_path):
+ fake_bin = tmp_path / "bin"
+ fake_bin.mkdir()
+ gh = fake_bin / "gh"
+ gh.write_text(FAKE_GH, encoding="utf-8")
+ gh.chmod(0o755)
+ return fake_bin
+
+
+def resolved_package(*, package_root="cuda_bindings", toolkit_version="13.3.0", release_version="13.3.1"):
+ return json.dumps(
+ {"package_root": package_root, "toolkit_version": toolkit_version, "release_version": release_version}
+ )
+
+
+def run_download(tmp_path, fake_gh, artifacts, component, *, tag="", package=None):
+ log = tmp_path / "gh.log"
+ env = os.environ.copy()
+ env.update(
+ {
+ "FAKE_ARTIFACTS": json.dumps(artifacts),
+ "FAKE_GH_LOG": str(log),
+ "GH_TOKEN": "test-token",
+ "PATH": f"{fake_gh}{os.pathsep}{env['PATH']}",
+ }
+ )
+ args = [str(DOWNLOAD_WHEELS), "123", component, "NVIDIA/cuda-python", str(tmp_path / "dist")]
+ if tag or package is not None:
+ args.append(tag)
+ if package is not None:
+ args.append(package)
+ result = subprocess.run( # noqa: S603
+ args,
+ cwd=tmp_path,
+ env=env,
+ check=False,
+ capture_output=True,
+ text=True,
+ )
+ commands = [json.loads(item) for item in log.read_text(encoding="utf-8").splitlines()] if log.exists() else []
+ return result, commands
+
+
+class TestDownloadWheels:
+ @pytest.mark.parametrize(
+ ("package_root", "toolkit_version", "release_version"),
+ (
+ ("cuda_bindings", "13.3.0", "13.3.1"),
+ ("cuda_bindings_12", "12.9.1", "12.9.9"),
+ ),
+ )
+ @pytest.mark.agent_authored(model="gpt-6")
+ def test_python_selects_exact_toolkit_artifact(
+ self, tmp_path, fake_gh, package_root, toolkit_version, release_version
+ ):
+ expected = f"cuda-python-wheel-cuda{toolkit_version}"
+ result, commands = run_download(
+ tmp_path,
+ fake_gh,
+ ["cuda-python-wheel", "cuda-python-wheel-cuda13.2.0", expected],
+ "cuda-python",
+ tag=f"v{release_version}",
+ package=resolved_package(
+ package_root=package_root, toolkit_version=toolkit_version, release_version=release_version
+ ),
+ )
+
+ assert result.returncode == 0, result.stderr
+ downloads = [command[command.index("--name") + 1] for command in commands if "--name" in command]
+ assert downloads == [expected]
+ assert [path.name for path in (tmp_path / "dist").glob("*.whl")] == [f"{expected}.whl"]
+
+ @pytest.mark.parametrize("artifact", ("cuda-python-wheel", "cuda-python-wheel-cuda13.2.0"))
+ @pytest.mark.agent_authored(model="gpt-6")
+ def test_python_requires_exact_toolkit_artifact(self, tmp_path, fake_gh, artifact):
+ result, commands = run_download(
+ tmp_path,
+ fake_gh,
+ [artifact],
+ "cuda-python",
+ tag="v13.3.1",
+ package=resolved_package(),
+ )
+
+ assert result.returncode == 1
+ assert "no unexpired release artifact for CUDA 13.3.0" in result.stderr
+ assert not any(command[:2] == ["run", "download"] for command in commands)
+
+ @pytest.mark.agent_authored(model="gpt-5.6-sol")
+ def test_bindings_pattern_contains_exact_toolkit_pin(self, tmp_path, fake_gh):
+ exact = "cuda-bindings-python312-cuda13.3.0-linux-64-sha"
+ result, commands = run_download(
+ tmp_path,
+ fake_gh,
+ ["cuda-bindings-python312-cuda13.2.0-linux-64-sha", exact, f"{exact}-tests"],
+ "cuda-bindings",
+ tag="v13.3.1",
+ package=resolved_package(),
+ )
+
+ assert result.returncode == 0, result.stderr
+ downloads = [command[command.index("--name") + 1] for command in commands if "--name" in command]
+ assert downloads == [exact]
+ assert [path.name for path in (tmp_path / "dist").glob("*.whl")] == [f"{exact}.whl"]
+
+ @pytest.mark.agent_authored(model="gpt-5.6-sol")
+ def test_release_routing_requires_resolved_package(self, tmp_path, fake_gh):
+ result, commands = run_download(
+ tmp_path,
+ fake_gh,
+ ["cuda-python-wheel-cuda13.3.0"],
+ "cuda-python",
+ tag="v13.3.1",
+ )
+
+ assert result.returncode == 1
+ assert commands == []
+
+ @pytest.mark.agent_authored(model="gpt-5.6-sol")
+ def test_bindings_test_artifact_is_not_releasable(self, tmp_path, fake_gh):
+ artifact = "cuda-bindings-python312-cuda13.3.0-linux-64-sha-tests"
+ result, _ = run_download(
+ tmp_path,
+ fake_gh,
+ [artifact],
+ "cuda-bindings",
+ tag="v13.3.1",
+ package=resolved_package(),
+ )
+
+ assert result.returncode == 1
+ assert "no unexpired release artifact" in result.stderr
+
+
+@pytest.mark.parametrize(
+ ("component", "artifacts", "expected"),
+ (
+ (
+ "all",
+ ["cuda-core-wheel", "cuda-core-wheel-tests", "cuda-python-wheel-cuda13.3.0", "build-metadata"],
+ ["cuda-core-wheel", "cuda-python-wheel-cuda13.3.0"],
+ ),
+ (
+ "cuda-core",
+ ["cuda-core-wheel", "cuda-core-wheel-tests", "cuda-python-wheel-cuda13.3.0"],
+ ["cuda-core-wheel"],
+ ),
+ ),
+)
+@pytest.mark.agent_authored(model="gpt-5.6")
+def test_non_release_selection_excludes_test_artifacts(tmp_path, fake_gh, component, artifacts, expected):
+ result, commands = run_download(tmp_path, fake_gh, artifacts, component)
+
+ assert result.returncode == 0, result.stderr
+ downloads = [command[command.index("--name") + 1] for command in commands if "--name" in command]
+ assert downloads == expected
+ assert all("-p" not in command for command in commands)
diff --git a/ci/tools/tests/test_merge_cuda_core_wheels.py b/ci/tools/tests/test_merge_cuda_core_wheels.py
new file mode 100644
index 00000000000..bdabb168819
--- /dev/null
+++ b/ci/tools/tests/test_merge_cuda_core_wheels.py
@@ -0,0 +1,57 @@
+# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+from pathlib import Path
+from zipfile import ZipFile
+
+import pytest
+from wheel.wheelfile import WheelFile
+
+from ci.tools.merge_cuda_core_wheels import cuda_variant_from_wheel, merge_wheels
+
+
+@pytest.mark.agent_authored(model="gpt-5.6-sol")
+@pytest.mark.parametrize(
+ ("name", "expected"),
+ [
+ ("cuda_core-1.2.3-cp312-cp312-manylinux_x86_64.cu12.whl", "cu12"),
+ ("cuda_core-2.0.0-cp315-cp315-win_amd64.cu14.whl", "cu14"),
+ ],
+)
+def test_cuda_variant_from_wheel(name, expected):
+ assert cuda_variant_from_wheel(Path(name)) == expected
+
+
+@pytest.mark.agent_authored(model="gpt-5.6-sol")
+def test_cuda_variant_from_wheel_rejects_missing_suffix():
+ with pytest.raises(ValueError, match=r"does not contain a \.cuN suffix"):
+ cuda_variant_from_wheel(Path("cuda_core-1.2.3-py3-none-any.whl"))
+
+
+@pytest.mark.agent_authored(model="gpt-6-astra")
+def test_merged_wheel_preserves_floor_helper_and_all_abi_variants(tmp_path):
+ wheels = []
+ floor_helper = "SUPPORTED_BINDINGS = {12: '12.9.8', 13: '13.4.1'}\n"
+ for major in (12, 13):
+ path = tmp_path / f"cuda_core-1.2.3-py3-none-any.cu{major}.whl"
+ with WheelFile(path, "w") as wheel:
+ wheel.writestr("cuda/core/__init__.py", "from . import _bindings_floor\n")
+ wheel.writestr("cuda/core/_version.py", "__version__ = '1.2.3'\n")
+ wheel.writestr("cuda/core/_bindings_floor.py", floor_helper)
+ wheel.writestr("cuda/core/_abi.py", f"CUDA_MAJOR = {major}\n")
+ wheel.writestr(
+ "cuda_core-1.2.3.dist-info/METADATA", "Metadata-Version: 2.1\nName: cuda-core\nVersion: 1.2.3\n"
+ )
+ wheel.writestr(
+ "cuda_core-1.2.3.dist-info/WHEEL",
+ "Wheel-Version: 1.0\nGenerator: test\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
+ )
+ wheels.append(path)
+
+ merged = merge_wheels(wheels, tmp_path / "merged", show_wheel_contents=False)
+
+ with ZipFile(merged) as wheel:
+ assert wheel.read("cuda/core/_bindings_floor.py").decode() == floor_helper
+ for major in (12, 13):
+ assert wheel.read(f"cuda/core/cu{major}/_abi.py").decode() == f"CUDA_MAJOR = {major}\n"
+ assert "cuda/core/_abi.py" not in wheel.namelist()