diff --git a/.github/workflows/test-wheel-linux.yml b/.github/workflows/test-wheel-linux.yml index 3b7761fe4e..c332789afa 100644 --- a/.github/workflows/test-wheel-linux.yml +++ b/.github/workflows/test-wheel-linux.yml @@ -37,6 +37,8 @@ jobs: steps: - name: Checkout ${{ github.event.repository.name }} uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 1 # Intentionally shallow clone to test wheel installation without full git history - name: Validate Test Type run: | @@ -94,6 +96,8 @@ jobs: - name: Checkout ${{ github.event.repository.name }} uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 1 # Intentionally shallow clone to test wheel installation without full git history - name: Setup proxy cache uses: nv-gha-runners/setup-proxy-cache@main @@ -269,17 +273,22 @@ jobs: - name: Ensure cuda-python installable run: | if [[ "${{ matrix.LOCAL_CTK }}" == 1 ]]; then - pip install cuda_python*.whl + pip install --only-binary=:all: cuda_python*.whl else - pip install $(ls cuda_python*.whl)[all] + pip install --only-binary=:all: $(ls cuda_python*.whl)[all] fi + - name: Verify installed package versions + run: | + # Trigger __version__ assertions to guard against setuptools-scm fallback versions + python -c 'import cuda.pathfinder' + python -c 'import cuda.bindings' + - name: Install cuda.pathfinder extra wheels for testing run: | set -euo pipefail pushd cuda_pathfinder - # Install pathfinder from the pre-built wheel, since building from - # source won't work in this context (we don't have a git checkout). + # Install pathfinder from the pre-built wheel to test the wheel artifacts pip install --only-binary=:all: -v ./*.whl --group "test-cu${TEST_CUDA_MAJOR}" pip list popd diff --git a/.github/workflows/test-wheel-windows.yml b/.github/workflows/test-wheel-windows.yml index e9c20a625c..f0f16c276c 100644 --- a/.github/workflows/test-wheel-windows.yml +++ b/.github/workflows/test-wheel-windows.yml @@ -34,6 +34,8 @@ jobs: steps: - name: Checkout ${{ github.event.repository.name }} uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 1 # Intentionally shallow clone to test wheel installation without full git history - name: Validate Test Type run: | @@ -70,6 +72,8 @@ jobs: steps: - name: Checkout ${{ github.event.repository.name }} uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 1 # Intentionally shallow clone to test wheel installation without full git history - name: Setup proxy cache uses: nv-gha-runners/setup-proxy-cache@main @@ -236,16 +240,23 @@ jobs: - name: Ensure cuda-python installable run: | if ('${{ matrix.LOCAL_CTK }}' -eq '1') { - pip install (Get-ChildItem -Filter cuda_python*.whl).FullName + pip install --only-binary=:all: (Get-ChildItem -Filter cuda_python*.whl).FullName } else { - pip install "$((Get-ChildItem -Filter cuda_python*.whl).FullName)[all]" + pip install --only-binary=:all: "$((Get-ChildItem -Filter cuda_python*.whl).FullName)[all]" } + - name: Verify installed package versions + shell: bash --noprofile --norc -xeuo pipefail {0} + run: | + # Trigger __version__ assertions to guard against setuptools-scm fallback versions + python -c 'import cuda.pathfinder' + python -c 'import cuda.bindings' + - name: Install cuda.pathfinder extra wheels for testing shell: bash --noprofile --norc -xeuo pipefail {0} run: | pushd cuda_pathfinder - pip install --only-binary=:all: -v . --group "test-cu${TEST_CUDA_MAJOR}" + pip install --only-binary=:all: -v ./*.whl --group "test-cu${TEST_CUDA_MAJOR}" pip list popd diff --git a/cuda_bindings/cuda/bindings/__init__.py b/cuda_bindings/cuda/bindings/__init__.py index 38d71fcfde..3c05d524da 100644 --- a/cuda_bindings/cuda/bindings/__init__.py +++ b/cuda_bindings/cuda/bindings/__init__.py @@ -3,3 +3,5 @@ from cuda.bindings import utils from cuda.bindings._version import __version__ + +assert tuple(int(_) for _ in __version__.split(".")[:2]) > (0, 1), "FATAL: invalid __version__" diff --git a/cuda_bindings/tests/helpers/__init__.py b/cuda_bindings/tests/helpers/__init__.py new file mode 100644 index 0000000000..6464b99cef --- /dev/null +++ b/cuda_bindings/tests/helpers/__init__.py @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import pathlib +import sys + +try: + from cuda_python_test_helpers import * # noqa: F403 +except ModuleNotFoundError: + # Import shared platform helpers for tests across repos + sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[3] / "cuda_python_test_helpers")) + from cuda_python_test_helpers import * # noqa: F403 diff --git a/cuda_bindings/tests/test_version_number.py b/cuda_bindings/tests/test_version_number.py new file mode 100644 index 0000000000..c912160d88 --- /dev/null +++ b/cuda_bindings/tests/test_version_number.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE + +import cuda.bindings +import cuda.pathfinder +from helpers import validate_version_number + + +def test_bindings_version(): + validate_version_number(cuda.bindings.__version__, "cuda-bindings") + + +def test_pathfinder_version(): + validate_version_number(cuda.pathfinder.__version__, "cuda-pathfinder") diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index 63f32020d1..b7b24f4048 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -11,6 +11,7 @@ import glob import os import re +from pathlib import Path from Cython.Build import cythonize from setuptools import Extension @@ -160,9 +161,80 @@ def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): return _build_meta.build_wheel(wheel_directory, config_settings, metadata_directory) +def _check_cuda_bindings_installed(): + """Check if cuda-bindings is installed and validate its version. + + Uses cuda.bindings._version module (generated by setuptools-scm) instead of + importlib.metadata.distribution() because the latter may incorrectly return + the cuda-core distribution when queried for "cuda-bindings" in isolated build + environments (tested with Python 3.12 and pip 25.3). This may be due to + cuda-core metadata being written during the build process before cuda-bindings + is fully available, causing importlib.metadata to return the wrong distribution. + + Returns: + tuple: (is_installed: bool, is_editable: bool, major_version: str | None) + If not installed, returns (False, False, None) + """ + try: + import cuda.bindings._version as bv + except ModuleNotFoundError: + return (False, False, None) + + # Determine repo root (parent of cuda_core) + repo_root = Path(__file__).resolve().parent.parent + + # Check if _version.py is under repo root (editable install) + version_file_path = Path(bv.__file__).resolve() + is_editable = repo_root in version_file_path.parents + + # Extract major version from version string + bindings_version = bv.__version__ + bindings_major_version = bindings_version.split(".")[0] + + return (True, is_editable, bindings_major_version) + + def _get_cuda_bindings_require(): + """Determine cuda-bindings build requirement. + + Strategy: + 1. If not installed, require matching CUDA major version + 2. If installed from sources (editable), require it without version constraint + (pip will keep the existing editable install) + 3. If installed but not editable and version major doesn't match CUDA major, raise error + 4. If installed and version matches, require it without version constraint + (pip will keep the existing installation) + + Note: We always return a requirement (never empty list) to ensure cuda-bindings + is available in pip's isolated build environment, even if already installed elsewhere. + """ + bindings_installed, bindings_editable, bindings_major = _check_cuda_bindings_installed() + + # If not installed, require matching CUDA major version + if not bindings_installed: + cuda_major = _determine_cuda_major_version() + return [f"cuda-bindings=={cuda_major}.*"] + + # If installed from sources (editable), keep it + if bindings_editable: + return ["cuda-bindings"] + + # If installed but not editable, check version matches CUDA major cuda_major = _determine_cuda_major_version() - return [f"cuda-bindings=={cuda_major}.*"] + if bindings_major != cuda_major: + raise RuntimeError( + f"Installed cuda-bindings version has major version {bindings_major}, " + f"but CUDA major version is {cuda_major}.\n" + f"This mismatch could cause build or runtime errors.\n" + f"\n" + f"To fix:\n" + f" 1. Uninstall cuda-bindings: pip uninstall cuda-bindings\n" + f" 2. Or install from sources: pip install -e ./cuda_bindings\n" + f" 3. Or install matching version: pip install 'cuda-bindings=={cuda_major}.*'" + ) + + # Installed and version matches (or is editable), keep it + return ["cuda-bindings"] def get_requires_for_build_editable(config_settings=None): diff --git a/cuda_core/cuda/core/__init__.py b/cuda_core/cuda/core/__init__.py index 9817179b72..3daf6954d8 100644 --- a/cuda_core/cuda/core/__init__.py +++ b/cuda_core/cuda/core/__init__.py @@ -15,6 +15,8 @@ import importlib +assert tuple(int(_) for _ in __version__.split(".")[:2]) > (0, 1), "FATAL: invalid __version__" + # The _resource_handles module exports a PyCapsule dispatch table that other # extension modules access via PyCapsule_Import. We import it here to ensure # it's loaded before other modules try to use it. diff --git a/cuda_core/tests/helpers/__init__.py b/cuda_core/tests/helpers/__init__.py index ad9d281c16..d7cad78d7f 100644 --- a/cuda_core/tests/helpers/__init__.py +++ b/cuda_core/tests/helpers/__init__.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 import functools @@ -24,7 +24,7 @@ try: from cuda_python_test_helpers import * # noqa: F403 -except ImportError: +except ModuleNotFoundError: # Import shared platform helpers for tests across repos sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[3] / "cuda_python_test_helpers")) from cuda_python_test_helpers import * # noqa: F403 diff --git a/cuda_core/tests/test_version_number.py b/cuda_core/tests/test_version_number.py new file mode 100644 index 0000000000..9820c3cc53 --- /dev/null +++ b/cuda_core/tests/test_version_number.py @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE + +import cuda.bindings +import cuda.core +import cuda.pathfinder +from helpers import validate_version_number + + +def test_bindings_version(): + validate_version_number(cuda.bindings.__version__, "cuda-bindings") + + +def test_core_version(): + validate_version_number(cuda.core.__version__, "cuda-core") + + +def test_pathfinder_version(): + validate_version_number(cuda.pathfinder.__version__, "cuda-pathfinder") diff --git a/cuda_pathfinder/cuda/pathfinder/__init__.py b/cuda_pathfinder/cuda/pathfinder/__init__.py index 8da4020116..04509b25ab 100644 --- a/cuda_pathfinder/cuda/pathfinder/__init__.py +++ b/cuda_pathfinder/cuda/pathfinder/__init__.py @@ -12,7 +12,9 @@ from cuda.pathfinder._headers.find_nvidia_headers import find_nvidia_header_directory as find_nvidia_header_directory from cuda.pathfinder._headers.supported_nvidia_headers import SUPPORTED_HEADERS_CTK as _SUPPORTED_HEADERS_CTK -from cuda.pathfinder._version import __version__ # isort: skip # noqa: F401 +from cuda.pathfinder._version import __version__ # isort: skip + +assert tuple(int(_) for _ in __version__.split(".")[:2]) > (0, 1), "FATAL: invalid __version__" # Indirections to help Sphinx find the docstrings. #: Mapping from short CUDA Toolkit (CTK) library names to their canonical diff --git a/cuda_pathfinder/tests/helpers/__init__.py b/cuda_pathfinder/tests/helpers/__init__.py new file mode 100644 index 0000000000..6464b99cef --- /dev/null +++ b/cuda_pathfinder/tests/helpers/__init__.py @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import pathlib +import sys + +try: + from cuda_python_test_helpers import * # noqa: F403 +except ModuleNotFoundError: + # Import shared platform helpers for tests across repos + sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[3] / "cuda_python_test_helpers")) + from cuda_python_test_helpers import * # noqa: F403 diff --git a/cuda_pathfinder/tests/test_version_number.py b/cuda_pathfinder/tests/test_version_number.py new file mode 100644 index 0000000000..8996ce3afa --- /dev/null +++ b/cuda_pathfinder/tests/test_version_number.py @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE + +from helpers import validate_version_number + +import cuda.pathfinder + + +def test_pathfinder_version(): + validate_version_number(cuda.pathfinder.__version__, "cuda-pathfinder") diff --git a/cuda_python_test_helpers/cuda_python_test_helpers/__init__.py b/cuda_python_test_helpers/cuda_python_test_helpers/__init__.py index 70ecf42ea0..887845a640 100644 --- a/cuda_python_test_helpers/cuda_python_test_helpers/__init__.py +++ b/cuda_python_test_helpers/cuda_python_test_helpers/__init__.py @@ -12,6 +12,7 @@ "IS_WSL", "libc", "under_compute_sanitizer", + "validate_version_number", ] @@ -63,3 +64,46 @@ def under_compute_sanitizer() -> bool: # Another common indicator: sanitizer injectors are configured via env vars. inj = os.environ.get("CUDA_INJECTION64_PATH", "") return "compute-sanitizer" in inj or "cuda-memcheck" in inj + + +def validate_version_number(version: str, package_name: str) -> None: + """Validate that a version number is valid (major.minor > 0.1). + + This function is meant to detect issues in the procedure for automatically + generating version numbers. It is only a late-stage detection, but assumed + to be sufficient to catch issues before they cause problems in production. + + Args: + version: The version string to validate (e.g., "1.3.4.dev79+g123") + package_name: Name of the package (for error messages) + + Raises: + AssertionError: If the version is invalid or appears to be a fallback value + """ + parts = version.split(".") + + if len(parts) < 3: + raise AssertionError(f"Invalid version format: '{version}'. Expected format: major.minor.patch") + + try: + major = int(parts[0]) + minor = int(parts[1]) + except ValueError: + raise AssertionError( + f"Invalid version format: '{version}'. Major and minor version numbers must be integers." + ) from None + + if major == 0 and minor <= 1: + raise AssertionError( + f"Invalid version number detected: '{version}'.\n" + f"\n" + f"Apparently the procedure for automatically generating version numbers failed silently.\n" + f"Common causes include:\n" + f" - Shallow git clone without tags\n" + f" - Missing git tags in repository history\n" + f" - Running from incorrect directory\n" + f"\n" + f"To fix, ensure the repository has full git history and tags available." + ) + + assert major > 0 or (major == 0 and minor > 1), f"Version '{version}' should have major.minor > 0.1"