From f6200ec2de2a0c39ee6786b0ac128afeba1dd719 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 9 Jan 2026 13:29:09 -0800 Subject: [PATCH 01/17] Validate git tags availability before setuptools-scm runs Add pre-build validation that checks git tag availability directly to ensure builds fail early with clear error messages before setuptools-scm silently falls back to version '0.1.x'. Changes: - cuda_bindings/setup.py: Validate tags at import time (before setuptools-scm) - cuda_core/build_hooks.py: Validate tags in _build_cuda_core() before building - cuda_pathfinder/build_hooks.py: New custom build backend that validates tags before delegating to setuptools.build_meta - cuda_pathfinder/pyproject.toml: Configure custom build backend Benefits: - Fails immediately when pip install -e . is run, not during build - More direct: tests what setuptools-scm actually needs (git describe) - Cleaner: no dependency on generated files - Better UX: clear error messages with actionable fixes Error messages include: - Clear explanation of the problem - The actual git error output - Common causes (tags not fetched, wrong directory, etc.) - Package-specific debugging commands - Actionable fix: git fetch --tags --- cuda_bindings/setup.py | 63 ++++++++++++++++++++++++++++++++ cuda_core/build_hooks.py | 46 +++++++++++++++++++++++ cuda_pathfinder/build_hooks.py | 67 ++++++++++++++++++++++++++++++++++ cuda_pathfinder/pyproject.toml | 3 +- 4 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 cuda_pathfinder/build_hooks.py diff --git a/cuda_bindings/setup.py b/cuda_bindings/setup.py index bfa1ae7826..62653df5db 100644 --- a/cuda_bindings/setup.py +++ b/cuda_bindings/setup.py @@ -23,6 +23,69 @@ from setuptools.command.editable_wheel import _TopLevelFinder, editable_wheel from setuptools.extension import Extension + +# ---------------------------------------------------------------------- +# Validate git tags are available (fail early before setuptools-scm runs) +def _validate_git_tags_available(): + """Verify that git tags are available for setuptools-scm version detection.""" + import subprocess + + # Check if git is available + try: + subprocess.run(["git", "--version"], capture_output=True, check=True, timeout=5) # noqa: S607 + except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired): + raise RuntimeError( + "Git is not available in PATH. setuptools-scm requires git to determine version from tags.\n" + "Please ensure git is installed and available in your PATH." + ) from None + + # Check if we're in a git repository + try: + subprocess.run( + ["git", "rev-parse", "--git-dir"], # noqa: S607 + capture_output=True, + check=True, + timeout=5, + ) + except subprocess.CalledProcessError: + raise RuntimeError( + "Not in a git repository. setuptools-scm requires git tags to determine version.\n" + "Please run this from within the cuda-python git repository." + ) from None + + # Check if git describe works (this is what setuptools-scm uses) + try: + result = subprocess.run( + ["git", "describe", "--tags", "--long", "--match", "v*[0-9]*"], # noqa: S607 + capture_output=True, + text=True, + timeout=5, + ) + if result.returncode != 0: + raise RuntimeError( + f"git describe failed! This means setuptools-scm will fall back to version '0.1.x'.\n" + f"\n" + f"Error: {result.stderr.strip()}\n" + f"\n" + f"This usually means:\n" + f" 1. Git tags are not fetched (run: git fetch --tags)\n" + f" 2. Running from wrong directory (setuptools_scm root='..')\n" + f" 3. No matching tags found\n" + f"\n" + f"To fix:\n" + f" git fetch --tags\n" + f"\n" + f"To debug, run: git describe --tags --long --match 'v*[0-9]*'" + ) from None + except subprocess.TimeoutExpired: + raise RuntimeError( + "git describe command timed out. This may indicate git repository issues.\n" + "Please check your git repository state." + ) from None + + +_validate_git_tags_available() + # ---------------------------------------------------------------------- # Fetch configuration options diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index 4cb9223e88..06de3aab90 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -86,6 +86,52 @@ def _determine_cuda_major_version() -> str: def _build_cuda_core(): + # Validate git tags are available (fail early before setuptools-scm runs) + import subprocess + + # Check if git is available + try: + subprocess.run(["git", "--version"], capture_output=True, check=True, timeout=5) # noqa: S607 + except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired): + raise RuntimeError( + "Git is not available in PATH. setuptools-scm requires git to determine version from tags.\n" + "Please ensure git is installed and available in your PATH." + ) from None + + # Find git repository root (setuptools_scm root='..') + repo_root = os.path.dirname(os.path.dirname(__file__)) + + # Check if git describe works (this is what setuptools-scm uses) + try: + result = subprocess.run( + ["git", "describe", "--tags", "--long", "--match", "cuda-core-v*[0-9]*"], # noqa: S607 + cwd=repo_root, + capture_output=True, + text=True, + timeout=5, + ) + if result.returncode != 0: + raise RuntimeError( + f"git describe failed! This means setuptools-scm will fall back to version '0.1.x'.\n" + f"\n" + f"Error: {result.stderr.strip()}\n" + f"\n" + f"This usually means:\n" + f" 1. Git tags are not fetched (run: git fetch --tags)\n" + f" 2. Running from wrong directory (setuptools_scm root='..')\n" + f" 3. No matching tags found\n" + f"\n" + f"To fix:\n" + f" git fetch --tags\n" + f"\n" + f"To debug, run: git describe --tags --long --match 'cuda-core-v*[0-9]*'" + ) from None + except subprocess.TimeoutExpired: + raise RuntimeError( + "git describe command timed out. This may indicate git repository issues.\n" + "Please check your git repository state." + ) from None + # Customizing the build hooks is needed because we must defer cythonization until cuda-bindings, # now a required build-time dependency that's dynamically installed via the other hook below, # is installed. Otherwise, cimport any cuda.bindings modules would fail! diff --git a/cuda_pathfinder/build_hooks.py b/cuda_pathfinder/build_hooks.py new file mode 100644 index 0000000000..03a3de6727 --- /dev/null +++ b/cuda_pathfinder/build_hooks.py @@ -0,0 +1,67 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Custom build hooks for cuda-pathfinder. + +This module validates git tags are available before setuptools-scm runs, +ensuring proper version detection during pip install. All PEP 517 build +hooks are delegated to setuptools.build_meta. +""" + +import os +import subprocess + + +# Validate tags before importing setuptools.build_meta (which will use setuptools-scm) +def _validate_git_tags_available(): + """Verify that git tags are available for setuptools-scm version detection.""" + # Check if git is available + try: + subprocess.run(["git", "--version"], capture_output=True, check=True, timeout=5) # noqa: S607 + except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired): + raise RuntimeError( + "Git is not available in PATH. setuptools-scm requires git to determine version from tags.\n" + "Please ensure git is installed and available in your PATH." + ) from None + + # Find git repository root (setuptools_scm root='..') + repo_root = os.path.dirname(os.path.dirname(__file__)) + + # Check if git describe works (this is what setuptools-scm uses) + try: + result = subprocess.run( + ["git", "describe", "--tags", "--long", "--match", "cuda-pathfinder-v*[0-9]*"], # noqa: S607 + cwd=repo_root, + capture_output=True, + text=True, + timeout=5, + ) + if result.returncode != 0: + raise RuntimeError( + f"git describe failed! This means setuptools-scm will fall back to version '0.1.x'.\n" + f"\n" + f"Error: {result.stderr.strip()}\n" + f"\n" + f"This usually means:\n" + f" 1. Git tags are not fetched (run: git fetch --tags)\n" + f" 2. Running from wrong directory (setuptools_scm root='..')\n" + f" 3. No matching tags found\n" + f"\n" + f"To fix:\n" + f" git fetch --tags\n" + f"\n" + f"To debug, run: git describe --tags --long --match 'cuda-pathfinder-v*[0-9]*'" + ) from None + except subprocess.TimeoutExpired: + raise RuntimeError( + "git describe command timed out. This may indicate git repository issues.\n" + "Please check your git repository state." + ) from None + + +# Validate tags before any build operations +_validate_git_tags_available() + +# Import and re-export all PEP 517 hooks from setuptools.build_meta +from setuptools.build_meta import * # noqa: F403, E402 diff --git a/cuda_pathfinder/pyproject.toml b/cuda_pathfinder/pyproject.toml index 8f2e89eaab..4da3906ba2 100644 --- a/cuda_pathfinder/pyproject.toml +++ b/cuda_pathfinder/pyproject.toml @@ -70,7 +70,8 @@ requires = [ "setuptools_scm[simple]>=8", "wheel" ] -build-backend = "setuptools.build_meta" +build-backend = "build_hooks" +backend-path = ["."] [tool.setuptools_scm] root = ".." From 6ea0decb434c8aea3d7a9fcc9e36dfc5800fd012 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 9 Jan 2026 14:08:15 -0800 Subject: [PATCH 02/17] Add cuda-bindings version validation to prevent PyPI fallback Add validation in _get_cuda_bindings_require() to check if cuda-bindings is already installed and validate its version compatibility. Strategy: - If cuda-bindings is not installed: require matching CUDA major version - If installed from sources (editable): keep it regardless of version - If installed from wheel: validate major version matches CUDA major - Raise clear error if version mismatch detected This prevents accidentally using PyPI versions that don't match the CUDA toolkit version being used for compilation. Changes: - Add _check_cuda_bindings_installed() to detect installation status - Check for editable installs via direct_url.json, repo location, or .egg-link - Validate version compatibility in _get_cuda_bindings_require() - Move imports to module level (PEP 8 compliance) - Add noqa: S110 for broad exception handling (intentional) --- cuda_core/build_hooks.py | 99 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index 06de3aab90..6359407274 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -9,8 +9,11 @@ import functools import glob +import importlib.metadata +import json import os import re +from pathlib import Path from Cython.Build import cythonize from setuptools import Extension @@ -191,9 +194,103 @@ 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. + + Returns: + tuple: (is_installed: bool, is_editable: bool, major_version: str | None) + If not installed, returns (False, False, None) + """ + try: + bindings_dist = importlib.metadata.distribution("cuda-bindings") + except importlib.metadata.PackageNotFoundError: + return (False, False, None) + + bindings_version = bindings_dist.version + bindings_major_version = bindings_version.split(".")[0] + + # Check if it's an editable install + is_editable = False + + # Method 1: Check direct_url.json (PEP 610 - modern editable installs) + try: + dist_location = Path(bindings_dist.locate_file("")) + # dist-info or egg-info directory + metadata_dir = dist_location.parent + direct_url_path = metadata_dir / "direct_url.json" + if direct_url_path.exists(): + with open(direct_url_path) as f: + direct_url = json.load(f) + if direct_url.get("dir_info", {}).get("editable"): + is_editable = True + except Exception: # noqa: S110 + pass + + # Method 2: Check if package is in current repository (another way to detect editable) + if not is_editable: + try: + import cuda.bindings + + bindings_path = Path(cuda.bindings.__file__).resolve() + repo_root = Path(__file__).resolve().parent.parent.parent + if repo_root in bindings_path.parents: + is_editable = True + except Exception: # noqa: S110 + pass + + # Method 3: Check for .egg-link file (old editable install method) + if not is_editable: + try: + # Look for .egg-link files in site-packages + import site + + for site_dir in site.getsitepackages(): + egg_link_path = Path(site_dir) / "cuda-bindings.egg-link" + if egg_link_path.exists(): + is_editable = True + break + except Exception: # noqa: S110 + pass + + return (True, is_editable, bindings_major_version) + + def _get_cuda_bindings_require(): + """Determine cuda-bindings build requirement. + + Strategy: + 1. If cuda-bindings is already installed (any version), don't require it (keep existing) + 2. If installed from sources (editable), definitely keep it + 3. Otherwise, if installed version major doesn't match CUDA major, raise error + 4. If not installed, require matching CUDA major version + """ + 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 (don't require anything) + if bindings_editable: + return [] + + # 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 [] def get_requires_for_build_editable(config_settings=None): From a573a72e2cbf7cdf65a6f618d5fee1a1f64168a8 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 9 Jan 2026 14:29:24 -0800 Subject: [PATCH 03/17] Simplify editable install detection to PEP 610 only Remove Methods 2 and 3 for detecting editable installs, keeping only PEP 610 (direct_url.json) which is the standard for Python 3.10+ and pip 20.1+. Changes: - Remove Method 2: import cuda.bindings to check repo location (problematic during build requirement phase) - Remove Method 3: .egg-link file detection (obsolete for Python 3.10+) - Keep only PEP 610 method (direct_url.json) which is reliable and doesn't require importing modules during build This fixes build errors caused by importing cuda.bindings during the build requirement phase, which interfered with Cython compilation. --- cuda_core/build_hooks.py | 31 ++----------------------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index 6359407274..eca73c4ce2 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -209,10 +209,9 @@ def _check_cuda_bindings_installed(): bindings_version = bindings_dist.version bindings_major_version = bindings_version.split(".")[0] - # Check if it's an editable install + # Check if it's an editable install using PEP 610 (direct_url.json) + # This is the standard method for Python 3.10+ and pip 20.1+ is_editable = False - - # Method 1: Check direct_url.json (PEP 610 - modern editable installs) try: dist_location = Path(bindings_dist.locate_file("")) # dist-info or egg-info directory @@ -226,32 +225,6 @@ def _check_cuda_bindings_installed(): except Exception: # noqa: S110 pass - # Method 2: Check if package is in current repository (another way to detect editable) - if not is_editable: - try: - import cuda.bindings - - bindings_path = Path(cuda.bindings.__file__).resolve() - repo_root = Path(__file__).resolve().parent.parent.parent - if repo_root in bindings_path.parents: - is_editable = True - except Exception: # noqa: S110 - pass - - # Method 3: Check for .egg-link file (old editable install method) - if not is_editable: - try: - # Look for .egg-link files in site-packages - import site - - for site_dir in site.getsitepackages(): - egg_link_path = Path(site_dir) / "cuda-bindings.egg-link" - if egg_link_path.exists(): - is_editable = True - break - except Exception: # noqa: S110 - pass - return (True, is_editable, bindings_major_version) From f8cb7a5b5b014714168bb1d8b45ddfb5fc37ab4b Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 9 Jan 2026 14:31:26 -0800 Subject: [PATCH 04/17] Use _version.py import instead of importlib.metadata for cuda-bindings detection Replace importlib.metadata.distribution() with direct import of cuda.bindings._version module. The former 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. Also ensure cuda-bindings is always required in build environment by returning ['cuda-bindings'] instead of [] when already installed. This ensures pip makes it available in isolated build environments even if installed elsewhere. Fix import sorting inconsistency for _version import in cuda_pathfinder by adding 'isort: skip' directive. --- cuda_core/build_hooks.py | 58 +++++++++++---------- cuda_pathfinder/cuda/pathfinder/__init__.py | 4 +- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index eca73c4ce2..13248530ef 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -9,8 +9,6 @@ import functools import glob -import importlib.metadata -import json import os import re from pathlib import Path @@ -197,33 +195,32 @@ def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): 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: - bindings_dist = importlib.metadata.distribution("cuda-bindings") - except importlib.metadata.PackageNotFoundError: + import cuda.bindings._version as bv + except ModuleNotFoundError: return (False, False, None) - bindings_version = bindings_dist.version - bindings_major_version = bindings_version.split(".")[0] + # Determine repo root (parent of cuda_core) + repo_root = Path(__file__).resolve().parent.parent - # Check if it's an editable install using PEP 610 (direct_url.json) - # This is the standard method for Python 3.10+ and pip 20.1+ - is_editable = False - try: - dist_location = Path(bindings_dist.locate_file("")) - # dist-info or egg-info directory - metadata_dir = dist_location.parent - direct_url_path = metadata_dir / "direct_url.json" - if direct_url_path.exists(): - with open(direct_url_path) as f: - direct_url = json.load(f) - if direct_url.get("dir_info", {}).get("editable"): - is_editable = True - except Exception: # noqa: S110 - pass + # 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 tuple + bindings_version = bv.__version__ + bindings_major_version = bindings_version.split(".")[0] return (True, is_editable, bindings_major_version) @@ -232,10 +229,15 @@ def _get_cuda_bindings_require(): """Determine cuda-bindings build requirement. Strategy: - 1. If cuda-bindings is already installed (any version), don't require it (keep existing) - 2. If installed from sources (editable), definitely keep it - 3. Otherwise, if installed version major doesn't match CUDA major, raise error - 4. If not installed, require matching CUDA major version + 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() @@ -244,9 +246,9 @@ def _get_cuda_bindings_require(): cuda_major = _determine_cuda_major_version() return [f"cuda-bindings=={cuda_major}.*"] - # If installed from sources (editable), keep it (don't require anything) + # If installed from sources (editable), keep it if bindings_editable: - return [] + return ["cuda-bindings"] # If installed but not editable, check version matches CUDA major cuda_major = _determine_cuda_major_version() @@ -263,7 +265,7 @@ def _get_cuda_bindings_require(): ) # Installed and version matches (or is editable), keep it - return [] + return ["cuda-bindings"] def get_requires_for_build_editable(config_settings=None): diff --git a/cuda_pathfinder/cuda/pathfinder/__init__.py b/cuda_pathfinder/cuda/pathfinder/__init__.py index afffd54263..bd7081b525 100644 --- a/cuda_pathfinder/cuda/pathfinder/__init__.py +++ b/cuda_pathfinder/cuda/pathfinder/__init__.py @@ -3,8 +3,6 @@ """cuda.pathfinder public APIs""" -from cuda.pathfinder._version import __version__ # noqa: F401 - from cuda.pathfinder._dynamic_libs.load_dl_common import DynamicLibNotFoundError as DynamicLibNotFoundError from cuda.pathfinder._dynamic_libs.load_dl_common import LoadedDL as LoadedDL from cuda.pathfinder._dynamic_libs.load_nvidia_dynamic_lib import load_nvidia_dynamic_lib as load_nvidia_dynamic_lib @@ -14,6 +12,8 @@ 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__ # noqa: F401 # isort: skip + # Indirections to help Sphinx find the docstrings. #: Mapping from short CUDA Toolkit (CTK) library names to their canonical #: header basenames (used to validate a discovered include directory). From 0dadc89a21c21e7878e52a883bd8e3facfc3e4c7 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 9 Jan 2026 17:59:29 -0800 Subject: [PATCH 05/17] Standardize _validate_git_tags_available() across all three packages Make _validate_git_tags_available() take tag_pattern as parameter and ensure all three implementations (cuda-core, cuda-pathfinder, cuda-bindings) are identical. Add sync comments to remind maintainers to keep them in sync. Also fix ruff noqa comments: S603 on subprocess.run() line, S607 on list argument line. --- cuda_bindings/setup.py | 33 ++++++++++++++------------------- cuda_core/build_hooks.py | 20 +++++++++++++++----- cuda_pathfinder/build_hooks.py | 21 +++++++++++++-------- 3 files changed, 42 insertions(+), 32 deletions(-) diff --git a/cuda_bindings/setup.py b/cuda_bindings/setup.py index 62653df5db..51658b69c4 100644 --- a/cuda_bindings/setup.py +++ b/cuda_bindings/setup.py @@ -25,9 +25,14 @@ # ---------------------------------------------------------------------- +# Please keep the implementations in cuda-pathfinder, cuda-bindings, cuda-core in sync. # Validate git tags are available (fail early before setuptools-scm runs) -def _validate_git_tags_available(): - """Verify that git tags are available for setuptools-scm version detection.""" +def _validate_git_tags_available(tag_pattern: str) -> None: + """Verify that git tags are available for setuptools-scm version detection. + + Args: + tag_pattern: Git tag pattern to match (e.g., "v*[0-9]*") + """ import subprocess # Check if git is available @@ -39,24 +44,14 @@ def _validate_git_tags_available(): "Please ensure git is installed and available in your PATH." ) from None - # Check if we're in a git repository - try: - subprocess.run( - ["git", "rev-parse", "--git-dir"], # noqa: S607 - capture_output=True, - check=True, - timeout=5, - ) - except subprocess.CalledProcessError: - raise RuntimeError( - "Not in a git repository. setuptools-scm requires git tags to determine version.\n" - "Please run this from within the cuda-python git repository." - ) from None + # Find git repository root (setuptools_scm root='..') + repo_root = os.path.dirname(os.path.dirname(__file__)) # Check if git describe works (this is what setuptools-scm uses) try: - result = subprocess.run( - ["git", "describe", "--tags", "--long", "--match", "v*[0-9]*"], # noqa: S607 + result = subprocess.run( # noqa: S603 + ["git", "describe", "--tags", "--long", "--match", tag_pattern], # noqa: S607 + cwd=repo_root, capture_output=True, text=True, timeout=5, @@ -75,7 +70,7 @@ def _validate_git_tags_available(): f"To fix:\n" f" git fetch --tags\n" f"\n" - f"To debug, run: git describe --tags --long --match 'v*[0-9]*'" + f"To debug, run: git describe --tags --long --match '{tag_pattern}'" ) from None except subprocess.TimeoutExpired: raise RuntimeError( @@ -84,7 +79,7 @@ def _validate_git_tags_available(): ) from None -_validate_git_tags_available() +_validate_git_tags_available("v*[0-9]*") # ---------------------------------------------------------------------- # Fetch configuration options diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index 13248530ef..f71e53cc57 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -86,8 +86,13 @@ def _determine_cuda_major_version() -> str: _extensions = None -def _build_cuda_core(): - # Validate git tags are available (fail early before setuptools-scm runs) +# Please keep the implementations in cuda-pathfinder, cuda-bindings, cuda-core in sync. +def _validate_git_tags_available(tag_pattern: str) -> None: + """Verify that git tags are available for setuptools-scm version detection. + + Args: + tag_pattern: Git tag pattern to match (e.g., "v*[0-9]*") + """ import subprocess # Check if git is available @@ -104,8 +109,8 @@ def _build_cuda_core(): # Check if git describe works (this is what setuptools-scm uses) try: - result = subprocess.run( - ["git", "describe", "--tags", "--long", "--match", "cuda-core-v*[0-9]*"], # noqa: S607 + result = subprocess.run( # noqa: S603 + ["git", "describe", "--tags", "--long", "--match", tag_pattern], # noqa: S607 cwd=repo_root, capture_output=True, text=True, @@ -125,7 +130,7 @@ def _build_cuda_core(): f"To fix:\n" f" git fetch --tags\n" f"\n" - f"To debug, run: git describe --tags --long --match 'cuda-core-v*[0-9]*'" + f"To debug, run: git describe --tags --long --match '{tag_pattern}'" ) from None except subprocess.TimeoutExpired: raise RuntimeError( @@ -133,6 +138,11 @@ def _build_cuda_core(): "Please check your git repository state." ) from None + +def _build_cuda_core(): + # Validate git tags are available (fail early before setuptools-scm runs) + _validate_git_tags_available("cuda-core-v*[0-9]*") + # Customizing the build hooks is needed because we must defer cythonization until cuda-bindings, # now a required build-time dependency that's dynamically installed via the other hook below, # is installed. Otherwise, cimport any cuda.bindings modules would fail! diff --git a/cuda_pathfinder/build_hooks.py b/cuda_pathfinder/build_hooks.py index 03a3de6727..3f22c649c7 100644 --- a/cuda_pathfinder/build_hooks.py +++ b/cuda_pathfinder/build_hooks.py @@ -10,12 +10,17 @@ """ import os -import subprocess -# Validate tags before importing setuptools.build_meta (which will use setuptools-scm) -def _validate_git_tags_available(): - """Verify that git tags are available for setuptools-scm version detection.""" +# Please keep the implementations in cuda-pathfinder, cuda-bindings, cuda-core in sync. +def _validate_git_tags_available(tag_pattern: str) -> None: + """Verify that git tags are available for setuptools-scm version detection. + + Args: + tag_pattern: Git tag pattern to match (e.g., "v*[0-9]*") + """ + import subprocess + # Check if git is available try: subprocess.run(["git", "--version"], capture_output=True, check=True, timeout=5) # noqa: S607 @@ -30,8 +35,8 @@ def _validate_git_tags_available(): # Check if git describe works (this is what setuptools-scm uses) try: - result = subprocess.run( - ["git", "describe", "--tags", "--long", "--match", "cuda-pathfinder-v*[0-9]*"], # noqa: S607 + result = subprocess.run( # noqa: S603 + ["git", "describe", "--tags", "--long", "--match", tag_pattern], # noqa: S607 cwd=repo_root, capture_output=True, text=True, @@ -51,7 +56,7 @@ def _validate_git_tags_available(): f"To fix:\n" f" git fetch --tags\n" f"\n" - f"To debug, run: git describe --tags --long --match 'cuda-pathfinder-v*[0-9]*'" + f"To debug, run: git describe --tags --long --match '{tag_pattern}'" ) from None except subprocess.TimeoutExpired: raise RuntimeError( @@ -61,7 +66,7 @@ def _validate_git_tags_available(): # Validate tags before any build operations -_validate_git_tags_available() +_validate_git_tags_available("cuda-pathfinder-v*[0-9]*") # Import and re-export all PEP 517 hooks from setuptools.build_meta from setuptools.build_meta import * # noqa: F403, E402 From db4125c19cf9dc53af0e431b02dc855a8ac73a43 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 10 Jan 2026 12:47:40 -0800 Subject: [PATCH 06/17] Refactor git describe command: use shim/runner pattern Replace _validate_git_tags_available() functions with DRY shim/runner pattern: - Create scripts/git_describe_command_runner.py: shared implementation - Create git_describe_command_shim.py in each package: thin wrappers that check for scripts/ directory and delegate to the runner - Update pyproject.toml files to use git_describe_command_shim.py - Remove all three copies of _validate_git_tags_available() from build_hooks.py and setup.py Benefits: - DRY: single implementation in scripts/ - Portable: Python is always available (no git in PATH requirement) - Clear error messages: shims check for scripts/ and provide context - No import-time validation: only runs when setuptools-scm calls it - Cleaner code: cuda_pathfinder/build_hooks.py is now just 13 lines All three shim files are identical copies and must be kept in sync. --- .pre-commit-config.yaml | 6 ++ cuda_bindings/git_describe_command_shim.py | 54 ++++++++++++++++ cuda_bindings/pyproject.toml | 2 +- cuda_bindings/setup.py | 58 ------------------ cuda_core/build_hooks.py | 56 ----------------- cuda_core/git_describe_command_shim.py | 54 ++++++++++++++++ cuda_core/pyproject.toml | 2 +- cuda_pathfinder/build_hooks.py | 61 +------------------ cuda_pathfinder/git_describe_command_shim.py | 54 ++++++++++++++++ cuda_pathfinder/pyproject.toml | 2 +- scripts/git_describe_command_runner.py | 59 ++++++++++++++++++ .../check_git_describe_command_shim_code.py | 57 +++++++++++++++++ 12 files changed, 288 insertions(+), 177 deletions(-) create mode 100755 cuda_bindings/git_describe_command_shim.py create mode 100755 cuda_core/git_describe_command_shim.py create mode 100755 cuda_pathfinder/git_describe_command_shim.py create mode 100755 scripts/git_describe_command_runner.py create mode 100755 toolshed/check_git_describe_command_shim_code.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9727cb2d34..845a79c917 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,6 +31,12 @@ repos: - https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl exclude: '.*pixi\.lock' + - id: check-git-describe-command-shim-code + name: Check git_describe_command_shim.py files are identical + entry: python ./toolshed/check_git_describe_command_shim_code.py + language: python + files: '^(cuda_bindings|cuda_core|cuda_pathfinder)/git_describe_command_shim\.py$' + - id: no-markdown-in-docs-source name: Prevent markdown files in docs/source directories entry: bash -c diff --git a/cuda_bindings/git_describe_command_shim.py b/cuda_bindings/git_describe_command_shim.py new file mode 100755 index 0000000000..ccda632207 --- /dev/null +++ b/cuda_bindings/git_describe_command_shim.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE + +"""Shim that calls scripts/git_describe_command_runner.py if we're in a git repo. + +This shim checks if we're in a git repository (by looking for scripts/ directory) +and delegates to the shared git_describe_command_runner.py script. + +NOTE: +- cuda_bindings/git_describe_command_shim.py +- cuda_core/git_describe_command_shim.py +- cuda_pathfinder/git_describe_command_shim.py +are EXACT COPIES, PLEASE KEEP ALL FILES IN SYNC. +""" + +import subprocess +import sys +from pathlib import Path + +if len(sys.argv) < 2: + print("Usage: python git_describe_command_shim.py ", file=sys.stderr) # noqa: T201 + sys.exit(1) + +tag_pattern = sys.argv[1] + +# Find repo root (go up from package directory) +package_dir = Path(__file__).parent +repo_root = package_dir.parent +scripts_dir = repo_root / "scripts" +git_describe_script = scripts_dir / "git_describe_command_runner.py" + +# Check if we're in a git repo (scripts/ should exist) +if not scripts_dir.exists(): + print("ERROR: scripts/ directory not found.", file=sys.stderr) # noqa: T201 + print("This indicates we're not in a git repository.", file=sys.stderr) # noqa: T201 + print("git_describe_command_shim should not be called in this context.", file=sys.stderr) # noqa: T201 + sys.exit(1) + +# Check if the shared script exists +if not git_describe_script.exists(): + print(f"ERROR: {git_describe_script} not found.", file=sys.stderr) # noqa: T201 + print("The git_describe_command_runner script is missing.", file=sys.stderr) # noqa: T201 + sys.exit(1) + +# Call the shared script (from repo root so it can find .git) +result = subprocess.run( # noqa: S603 + [sys.executable, str(git_describe_script), tag_pattern], + cwd=repo_root, + timeout=10, +) + +sys.exit(result.returncode) diff --git a/cuda_bindings/pyproject.toml b/cuda_bindings/pyproject.toml index 7c4bddb434..463be9a6fb 100644 --- a/cuda_bindings/pyproject.toml +++ b/cuda_bindings/pyproject.toml @@ -82,4 +82,4 @@ root = ".." version_file = "cuda/bindings/_version.py" # We deliberately do not want to include the version suffixes (a/b/rc) in cuda-bindings versioning tag_regex = "^(?Pv\\d+\\.\\d+\\.\\d+)" -git_describe_command = ["git", "describe", "--dirty", "--tags", "--long", "--match", "v*[0-9]*"] +git_describe_command = ["python", "cuda_bindings/git_describe_command_shim.py", "v*[0-9]*"] diff --git a/cuda_bindings/setup.py b/cuda_bindings/setup.py index 51658b69c4..bfa1ae7826 100644 --- a/cuda_bindings/setup.py +++ b/cuda_bindings/setup.py @@ -23,64 +23,6 @@ from setuptools.command.editable_wheel import _TopLevelFinder, editable_wheel from setuptools.extension import Extension - -# ---------------------------------------------------------------------- -# Please keep the implementations in cuda-pathfinder, cuda-bindings, cuda-core in sync. -# Validate git tags are available (fail early before setuptools-scm runs) -def _validate_git_tags_available(tag_pattern: str) -> None: - """Verify that git tags are available for setuptools-scm version detection. - - Args: - tag_pattern: Git tag pattern to match (e.g., "v*[0-9]*") - """ - import subprocess - - # Check if git is available - try: - subprocess.run(["git", "--version"], capture_output=True, check=True, timeout=5) # noqa: S607 - except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired): - raise RuntimeError( - "Git is not available in PATH. setuptools-scm requires git to determine version from tags.\n" - "Please ensure git is installed and available in your PATH." - ) from None - - # Find git repository root (setuptools_scm root='..') - repo_root = os.path.dirname(os.path.dirname(__file__)) - - # Check if git describe works (this is what setuptools-scm uses) - try: - result = subprocess.run( # noqa: S603 - ["git", "describe", "--tags", "--long", "--match", tag_pattern], # noqa: S607 - cwd=repo_root, - capture_output=True, - text=True, - timeout=5, - ) - if result.returncode != 0: - raise RuntimeError( - f"git describe failed! This means setuptools-scm will fall back to version '0.1.x'.\n" - f"\n" - f"Error: {result.stderr.strip()}\n" - f"\n" - f"This usually means:\n" - f" 1. Git tags are not fetched (run: git fetch --tags)\n" - f" 2. Running from wrong directory (setuptools_scm root='..')\n" - f" 3. No matching tags found\n" - f"\n" - f"To fix:\n" - f" git fetch --tags\n" - f"\n" - f"To debug, run: git describe --tags --long --match '{tag_pattern}'" - ) from None - except subprocess.TimeoutExpired: - raise RuntimeError( - "git describe command timed out. This may indicate git repository issues.\n" - "Please check your git repository state." - ) from None - - -_validate_git_tags_available("v*[0-9]*") - # ---------------------------------------------------------------------- # Fetch configuration options diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index f71e53cc57..c2a10079f9 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -86,63 +86,7 @@ def _determine_cuda_major_version() -> str: _extensions = None -# Please keep the implementations in cuda-pathfinder, cuda-bindings, cuda-core in sync. -def _validate_git_tags_available(tag_pattern: str) -> None: - """Verify that git tags are available for setuptools-scm version detection. - - Args: - tag_pattern: Git tag pattern to match (e.g., "v*[0-9]*") - """ - import subprocess - - # Check if git is available - try: - subprocess.run(["git", "--version"], capture_output=True, check=True, timeout=5) # noqa: S607 - except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired): - raise RuntimeError( - "Git is not available in PATH. setuptools-scm requires git to determine version from tags.\n" - "Please ensure git is installed and available in your PATH." - ) from None - - # Find git repository root (setuptools_scm root='..') - repo_root = os.path.dirname(os.path.dirname(__file__)) - - # Check if git describe works (this is what setuptools-scm uses) - try: - result = subprocess.run( # noqa: S603 - ["git", "describe", "--tags", "--long", "--match", tag_pattern], # noqa: S607 - cwd=repo_root, - capture_output=True, - text=True, - timeout=5, - ) - if result.returncode != 0: - raise RuntimeError( - f"git describe failed! This means setuptools-scm will fall back to version '0.1.x'.\n" - f"\n" - f"Error: {result.stderr.strip()}\n" - f"\n" - f"This usually means:\n" - f" 1. Git tags are not fetched (run: git fetch --tags)\n" - f" 2. Running from wrong directory (setuptools_scm root='..')\n" - f" 3. No matching tags found\n" - f"\n" - f"To fix:\n" - f" git fetch --tags\n" - f"\n" - f"To debug, run: git describe --tags --long --match '{tag_pattern}'" - ) from None - except subprocess.TimeoutExpired: - raise RuntimeError( - "git describe command timed out. This may indicate git repository issues.\n" - "Please check your git repository state." - ) from None - - def _build_cuda_core(): - # Validate git tags are available (fail early before setuptools-scm runs) - _validate_git_tags_available("cuda-core-v*[0-9]*") - # Customizing the build hooks is needed because we must defer cythonization until cuda-bindings, # now a required build-time dependency that's dynamically installed via the other hook below, # is installed. Otherwise, cimport any cuda.bindings modules would fail! diff --git a/cuda_core/git_describe_command_shim.py b/cuda_core/git_describe_command_shim.py new file mode 100755 index 0000000000..ccda632207 --- /dev/null +++ b/cuda_core/git_describe_command_shim.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE + +"""Shim that calls scripts/git_describe_command_runner.py if we're in a git repo. + +This shim checks if we're in a git repository (by looking for scripts/ directory) +and delegates to the shared git_describe_command_runner.py script. + +NOTE: +- cuda_bindings/git_describe_command_shim.py +- cuda_core/git_describe_command_shim.py +- cuda_pathfinder/git_describe_command_shim.py +are EXACT COPIES, PLEASE KEEP ALL FILES IN SYNC. +""" + +import subprocess +import sys +from pathlib import Path + +if len(sys.argv) < 2: + print("Usage: python git_describe_command_shim.py ", file=sys.stderr) # noqa: T201 + sys.exit(1) + +tag_pattern = sys.argv[1] + +# Find repo root (go up from package directory) +package_dir = Path(__file__).parent +repo_root = package_dir.parent +scripts_dir = repo_root / "scripts" +git_describe_script = scripts_dir / "git_describe_command_runner.py" + +# Check if we're in a git repo (scripts/ should exist) +if not scripts_dir.exists(): + print("ERROR: scripts/ directory not found.", file=sys.stderr) # noqa: T201 + print("This indicates we're not in a git repository.", file=sys.stderr) # noqa: T201 + print("git_describe_command_shim should not be called in this context.", file=sys.stderr) # noqa: T201 + sys.exit(1) + +# Check if the shared script exists +if not git_describe_script.exists(): + print(f"ERROR: {git_describe_script} not found.", file=sys.stderr) # noqa: T201 + print("The git_describe_command_runner script is missing.", file=sys.stderr) # noqa: T201 + sys.exit(1) + +# Call the shared script (from repo root so it can find .git) +result = subprocess.run( # noqa: S603 + [sys.executable, str(git_describe_script), tag_pattern], + cwd=repo_root, + timeout=10, +) + +sys.exit(result.returncode) diff --git a/cuda_core/pyproject.toml b/cuda_core/pyproject.toml index 058148f97b..577432ebc3 100644 --- a/cuda_core/pyproject.toml +++ b/cuda_core/pyproject.toml @@ -83,7 +83,7 @@ root = ".." version_file = "cuda/core/_version.py" # We deliberately do not want to include the version suffixes (a/b/rc) in cuda-core versioning tag_regex = "^cuda-core-(?Pv\\d+\\.\\d+\\.\\d+)" -git_describe_command = ["git", "describe", "--dirty", "--tags", "--long", "--match", "cuda-core-v*[0-9]*"] +git_describe_command = ["python", "cuda_core/git_describe_command_shim.py", "cuda-core-v*[0-9]*"] [tool.cibuildwheel] skip = "*-musllinux_*" diff --git a/cuda_pathfinder/build_hooks.py b/cuda_pathfinder/build_hooks.py index 3f22c649c7..f6727873aa 100644 --- a/cuda_pathfinder/build_hooks.py +++ b/cuda_pathfinder/build_hooks.py @@ -9,64 +9,5 @@ hooks are delegated to setuptools.build_meta. """ -import os - - -# Please keep the implementations in cuda-pathfinder, cuda-bindings, cuda-core in sync. -def _validate_git_tags_available(tag_pattern: str) -> None: - """Verify that git tags are available for setuptools-scm version detection. - - Args: - tag_pattern: Git tag pattern to match (e.g., "v*[0-9]*") - """ - import subprocess - - # Check if git is available - try: - subprocess.run(["git", "--version"], capture_output=True, check=True, timeout=5) # noqa: S607 - except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired): - raise RuntimeError( - "Git is not available in PATH. setuptools-scm requires git to determine version from tags.\n" - "Please ensure git is installed and available in your PATH." - ) from None - - # Find git repository root (setuptools_scm root='..') - repo_root = os.path.dirname(os.path.dirname(__file__)) - - # Check if git describe works (this is what setuptools-scm uses) - try: - result = subprocess.run( # noqa: S603 - ["git", "describe", "--tags", "--long", "--match", tag_pattern], # noqa: S607 - cwd=repo_root, - capture_output=True, - text=True, - timeout=5, - ) - if result.returncode != 0: - raise RuntimeError( - f"git describe failed! This means setuptools-scm will fall back to version '0.1.x'.\n" - f"\n" - f"Error: {result.stderr.strip()}\n" - f"\n" - f"This usually means:\n" - f" 1. Git tags are not fetched (run: git fetch --tags)\n" - f" 2. Running from wrong directory (setuptools_scm root='..')\n" - f" 3. No matching tags found\n" - f"\n" - f"To fix:\n" - f" git fetch --tags\n" - f"\n" - f"To debug, run: git describe --tags --long --match '{tag_pattern}'" - ) from None - except subprocess.TimeoutExpired: - raise RuntimeError( - "git describe command timed out. This may indicate git repository issues.\n" - "Please check your git repository state." - ) from None - - -# Validate tags before any build operations -_validate_git_tags_available("cuda-pathfinder-v*[0-9]*") - # Import and re-export all PEP 517 hooks from setuptools.build_meta -from setuptools.build_meta import * # noqa: F403, E402 +from setuptools.build_meta import * # noqa: F403 diff --git a/cuda_pathfinder/git_describe_command_shim.py b/cuda_pathfinder/git_describe_command_shim.py new file mode 100755 index 0000000000..ccda632207 --- /dev/null +++ b/cuda_pathfinder/git_describe_command_shim.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE + +"""Shim that calls scripts/git_describe_command_runner.py if we're in a git repo. + +This shim checks if we're in a git repository (by looking for scripts/ directory) +and delegates to the shared git_describe_command_runner.py script. + +NOTE: +- cuda_bindings/git_describe_command_shim.py +- cuda_core/git_describe_command_shim.py +- cuda_pathfinder/git_describe_command_shim.py +are EXACT COPIES, PLEASE KEEP ALL FILES IN SYNC. +""" + +import subprocess +import sys +from pathlib import Path + +if len(sys.argv) < 2: + print("Usage: python git_describe_command_shim.py ", file=sys.stderr) # noqa: T201 + sys.exit(1) + +tag_pattern = sys.argv[1] + +# Find repo root (go up from package directory) +package_dir = Path(__file__).parent +repo_root = package_dir.parent +scripts_dir = repo_root / "scripts" +git_describe_script = scripts_dir / "git_describe_command_runner.py" + +# Check if we're in a git repo (scripts/ should exist) +if not scripts_dir.exists(): + print("ERROR: scripts/ directory not found.", file=sys.stderr) # noqa: T201 + print("This indicates we're not in a git repository.", file=sys.stderr) # noqa: T201 + print("git_describe_command_shim should not be called in this context.", file=sys.stderr) # noqa: T201 + sys.exit(1) + +# Check if the shared script exists +if not git_describe_script.exists(): + print(f"ERROR: {git_describe_script} not found.", file=sys.stderr) # noqa: T201 + print("The git_describe_command_runner script is missing.", file=sys.stderr) # noqa: T201 + sys.exit(1) + +# Call the shared script (from repo root so it can find .git) +result = subprocess.run( # noqa: S603 + [sys.executable, str(git_describe_script), tag_pattern], + cwd=repo_root, + timeout=10, +) + +sys.exit(result.returncode) diff --git a/cuda_pathfinder/pyproject.toml b/cuda_pathfinder/pyproject.toml index 4da3906ba2..b6e066e494 100644 --- a/cuda_pathfinder/pyproject.toml +++ b/cuda_pathfinder/pyproject.toml @@ -78,7 +78,7 @@ root = ".." version_file = "cuda/pathfinder/_version.py" # We deliberately do not want to include the version suffixes (a/b/rc) in cuda-pathfinder versioning tag_regex = "^cuda-pathfinder-(?Pv\\d+\\.\\d+\\.\\d+)" -git_describe_command = [ "git", "describe", "--dirty", "--tags", "--long", "--match", "cuda-pathfinder-v*[0-9]*" ] +git_describe_command = ["python", "cuda_pathfinder/git_describe_command_shim.py", "cuda-pathfinder-v*[0-9]*"] [tool.pytest.ini_options] addopts = "--showlocals" diff --git a/scripts/git_describe_command_runner.py b/scripts/git_describe_command_runner.py new file mode 100755 index 0000000000..8ed606b6c6 --- /dev/null +++ b/scripts/git_describe_command_runner.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE + +"""Git describe runner for setuptools-scm that fails loudly if no matching tags found. + +This script is used as a replacement for git_describe_command in pyproject.toml. +It provides better error messages and ensures setuptools-scm doesn't silently +fall back to 0.1.x when tags are missing. + +Usage: + python git_describe_command_runner.py + +Example: + python git_describe_command_runner.py "v*[0-9]*" +""" + +import subprocess +import sys + +if len(sys.argv) < 2: + print("Usage: python git_describe_command_runner.py ", file=sys.stderr) # noqa: T201 + sys.exit(1) + +tag_pattern = sys.argv[1] + +# Check if git is available +try: + subprocess.run(["git", "--version"], capture_output=True, check=True, timeout=5) # noqa: S607 +except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired): + print("ERROR: Git is not available in PATH.", file=sys.stderr) # noqa: T201 + print("setuptools-scm requires git to determine version from tags.", file=sys.stderr) # noqa: T201 + sys.exit(1) + +# Run git describe (setuptools-scm expects --dirty --tags --long) +result = subprocess.run( # noqa: S603 + ["git", "describe", "--dirty", "--tags", "--long", "--match", tag_pattern], # noqa: S607 + capture_output=True, + text=True, + timeout=5, +) + +if result.returncode != 0: + print(f"ERROR: git describe failed with pattern '{tag_pattern}'", file=sys.stderr) # noqa: T201 + print(f"Error: {result.stderr.strip()}", file=sys.stderr) # noqa: T201 + print("", file=sys.stderr) # noqa: T201 + print("This means setuptools-scm will fall back to version '0.1.x'.", file=sys.stderr) # noqa: T201 + print("", file=sys.stderr) # noqa: T201 + print("This usually means:", file=sys.stderr) # noqa: T201 + print(" 1. Git tags are not fetched (run: git fetch --tags)", file=sys.stderr) # noqa: T201 + print(" 2. Running from wrong directory", file=sys.stderr) # noqa: T201 + print(" 3. No matching tags found", file=sys.stderr) # noqa: T201 + print("", file=sys.stderr) # noqa: T201 + print("To fix:", file=sys.stderr) # noqa: T201 + print(" git fetch --tags", file=sys.stderr) # noqa: T201 + sys.exit(1) + +print(result.stdout.strip()) # noqa: T201 diff --git a/toolshed/check_git_describe_command_shim_code.py b/toolshed/check_git_describe_command_shim_code.py new file mode 100755 index 0000000000..c217288e41 --- /dev/null +++ b/toolshed/check_git_describe_command_shim_code.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Check that git_describe_command_shim.py files are identical across packages. + +This script verifies that the three shim files: +- cuda_bindings/git_describe_command_shim.py +- cuda_core/git_describe_command_shim.py +- cuda_pathfinder/git_describe_command_shim.py + +are exact copies of each other. +""" + +import sys +from pathlib import Path + +# Find repo root (assume script is in toolshed/) +repo_root = Path(__file__).parent.parent + +shim_files = [ + repo_root / "cuda_bindings" / "git_describe_command_shim.py", + repo_root / "cuda_core" / "git_describe_command_shim.py", + repo_root / "cuda_pathfinder" / "git_describe_command_shim.py", +] + +# Check all files exist +missing_files = [f for f in shim_files if not f.exists()] +if missing_files: + print("ERROR: Missing shim files:", file=sys.stderr) + for f in missing_files: + print(f" {f}", file=sys.stderr) + sys.exit(1) + +# Read all files +file_contents = {} +for shim_file in shim_files: + file_contents[shim_file] = shim_file.read_text() + +# Compare all pairs +errors = [] +for i, file1 in enumerate(shim_files): + for file2 in shim_files[i + 1 :]: + if file_contents[file1] != file_contents[file2]: + errors.append((file1, file2)) + +if errors: + print("ERROR: git_describe_command_shim.py files are not identical:", file=sys.stderr) + for file1, file2 in errors: + print(f" {file1.relative_to(repo_root)} != {file2.relative_to(repo_root)}", file=sys.stderr) + print("", file=sys.stderr) + print("These files must be kept in sync. Please copy one to the others.", file=sys.stderr) + sys.exit(1) + +print("✓ All git_describe_command_shim.py files are identical") +sys.exit(0) From 1b2e4c088cd3ddea052f3643788c3f6273495e40 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 10 Jan 2026 18:11:58 -0800 Subject: [PATCH 07/17] Simplify git describe: use shared wrapper directly Remove unnecessary shim files and use scripts/git_describe_wrapper.py directly. Since setuptools_scm runs from repo root (root = ".."), we can call the shared wrapper script directly without package-specific shims. Changes: - Remove all three git_describe_command_shim.py files - Update pyproject.toml files to use scripts/git_describe_wrapper.py - Remove cuda_pathfinder/build_hooks.py (was just pass-through) - Remove pre-commit hook for checking shim files - Rename git_describe_command_runner.py to git_describe_wrapper.py This simplifies the codebase while maintaining the same functionality: - Single shared implementation for git describe - Clear error messages when tags are missing - Works correctly from repo root where setuptools-scm runs --- .pre-commit-config.yaml | 6 -- cuda_bindings/git_describe_command_shim.py | 54 ------------------ cuda_bindings/pyproject.toml | 2 +- cuda_core/git_describe_command_shim.py | 54 ------------------ cuda_core/pyproject.toml | 2 +- cuda_pathfinder/build_hooks.py | 13 ----- cuda_pathfinder/git_describe_command_shim.py | 54 ------------------ cuda_pathfinder/pyproject.toml | 5 +- ...mand_runner.py => git_describe_wrapper.py} | 8 +-- .../check_git_describe_command_shim_code.py | 57 ------------------- 10 files changed, 8 insertions(+), 247 deletions(-) delete mode 100755 cuda_bindings/git_describe_command_shim.py delete mode 100755 cuda_core/git_describe_command_shim.py delete mode 100644 cuda_pathfinder/build_hooks.py delete mode 100755 cuda_pathfinder/git_describe_command_shim.py rename scripts/{git_describe_command_runner.py => git_describe_wrapper.py} (87%) delete mode 100755 toolshed/check_git_describe_command_shim_code.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 845a79c917..9727cb2d34 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,12 +31,6 @@ repos: - https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl exclude: '.*pixi\.lock' - - id: check-git-describe-command-shim-code - name: Check git_describe_command_shim.py files are identical - entry: python ./toolshed/check_git_describe_command_shim_code.py - language: python - files: '^(cuda_bindings|cuda_core|cuda_pathfinder)/git_describe_command_shim\.py$' - - id: no-markdown-in-docs-source name: Prevent markdown files in docs/source directories entry: bash -c diff --git a/cuda_bindings/git_describe_command_shim.py b/cuda_bindings/git_describe_command_shim.py deleted file mode 100755 index ccda632207..0000000000 --- a/cuda_bindings/git_describe_command_shim.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python3 - -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE - -"""Shim that calls scripts/git_describe_command_runner.py if we're in a git repo. - -This shim checks if we're in a git repository (by looking for scripts/ directory) -and delegates to the shared git_describe_command_runner.py script. - -NOTE: -- cuda_bindings/git_describe_command_shim.py -- cuda_core/git_describe_command_shim.py -- cuda_pathfinder/git_describe_command_shim.py -are EXACT COPIES, PLEASE KEEP ALL FILES IN SYNC. -""" - -import subprocess -import sys -from pathlib import Path - -if len(sys.argv) < 2: - print("Usage: python git_describe_command_shim.py ", file=sys.stderr) # noqa: T201 - sys.exit(1) - -tag_pattern = sys.argv[1] - -# Find repo root (go up from package directory) -package_dir = Path(__file__).parent -repo_root = package_dir.parent -scripts_dir = repo_root / "scripts" -git_describe_script = scripts_dir / "git_describe_command_runner.py" - -# Check if we're in a git repo (scripts/ should exist) -if not scripts_dir.exists(): - print("ERROR: scripts/ directory not found.", file=sys.stderr) # noqa: T201 - print("This indicates we're not in a git repository.", file=sys.stderr) # noqa: T201 - print("git_describe_command_shim should not be called in this context.", file=sys.stderr) # noqa: T201 - sys.exit(1) - -# Check if the shared script exists -if not git_describe_script.exists(): - print(f"ERROR: {git_describe_script} not found.", file=sys.stderr) # noqa: T201 - print("The git_describe_command_runner script is missing.", file=sys.stderr) # noqa: T201 - sys.exit(1) - -# Call the shared script (from repo root so it can find .git) -result = subprocess.run( # noqa: S603 - [sys.executable, str(git_describe_script), tag_pattern], - cwd=repo_root, - timeout=10, -) - -sys.exit(result.returncode) diff --git a/cuda_bindings/pyproject.toml b/cuda_bindings/pyproject.toml index 463be9a6fb..3bf1a45421 100644 --- a/cuda_bindings/pyproject.toml +++ b/cuda_bindings/pyproject.toml @@ -82,4 +82,4 @@ root = ".." version_file = "cuda/bindings/_version.py" # We deliberately do not want to include the version suffixes (a/b/rc) in cuda-bindings versioning tag_regex = "^(?Pv\\d+\\.\\d+\\.\\d+)" -git_describe_command = ["python", "cuda_bindings/git_describe_command_shim.py", "v*[0-9]*"] +git_describe_command = ["python", "scripts/git_describe_wrapper.py", "v*[0-9]*"] diff --git a/cuda_core/git_describe_command_shim.py b/cuda_core/git_describe_command_shim.py deleted file mode 100755 index ccda632207..0000000000 --- a/cuda_core/git_describe_command_shim.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python3 - -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE - -"""Shim that calls scripts/git_describe_command_runner.py if we're in a git repo. - -This shim checks if we're in a git repository (by looking for scripts/ directory) -and delegates to the shared git_describe_command_runner.py script. - -NOTE: -- cuda_bindings/git_describe_command_shim.py -- cuda_core/git_describe_command_shim.py -- cuda_pathfinder/git_describe_command_shim.py -are EXACT COPIES, PLEASE KEEP ALL FILES IN SYNC. -""" - -import subprocess -import sys -from pathlib import Path - -if len(sys.argv) < 2: - print("Usage: python git_describe_command_shim.py ", file=sys.stderr) # noqa: T201 - sys.exit(1) - -tag_pattern = sys.argv[1] - -# Find repo root (go up from package directory) -package_dir = Path(__file__).parent -repo_root = package_dir.parent -scripts_dir = repo_root / "scripts" -git_describe_script = scripts_dir / "git_describe_command_runner.py" - -# Check if we're in a git repo (scripts/ should exist) -if not scripts_dir.exists(): - print("ERROR: scripts/ directory not found.", file=sys.stderr) # noqa: T201 - print("This indicates we're not in a git repository.", file=sys.stderr) # noqa: T201 - print("git_describe_command_shim should not be called in this context.", file=sys.stderr) # noqa: T201 - sys.exit(1) - -# Check if the shared script exists -if not git_describe_script.exists(): - print(f"ERROR: {git_describe_script} not found.", file=sys.stderr) # noqa: T201 - print("The git_describe_command_runner script is missing.", file=sys.stderr) # noqa: T201 - sys.exit(1) - -# Call the shared script (from repo root so it can find .git) -result = subprocess.run( # noqa: S603 - [sys.executable, str(git_describe_script), tag_pattern], - cwd=repo_root, - timeout=10, -) - -sys.exit(result.returncode) diff --git a/cuda_core/pyproject.toml b/cuda_core/pyproject.toml index bf3f29f47a..d5adfdac0c 100644 --- a/cuda_core/pyproject.toml +++ b/cuda_core/pyproject.toml @@ -84,7 +84,7 @@ root = ".." version_file = "cuda/core/_version.py" # We deliberately do not want to include the version suffixes (a/b/rc) in cuda-core versioning tag_regex = "^cuda-core-(?Pv\\d+\\.\\d+\\.\\d+)" -git_describe_command = ["python", "cuda_core/git_describe_command_shim.py", "cuda-core-v*[0-9]*"] +git_describe_command = ["python", "scripts/git_describe_wrapper.py", "cuda-core-v*[0-9]*"] [tool.cibuildwheel] skip = "*-musllinux_*" diff --git a/cuda_pathfinder/build_hooks.py b/cuda_pathfinder/build_hooks.py deleted file mode 100644 index f6727873aa..0000000000 --- a/cuda_pathfinder/build_hooks.py +++ /dev/null @@ -1,13 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -""" -Custom build hooks for cuda-pathfinder. - -This module validates git tags are available before setuptools-scm runs, -ensuring proper version detection during pip install. All PEP 517 build -hooks are delegated to setuptools.build_meta. -""" - -# Import and re-export all PEP 517 hooks from setuptools.build_meta -from setuptools.build_meta import * # noqa: F403 diff --git a/cuda_pathfinder/git_describe_command_shim.py b/cuda_pathfinder/git_describe_command_shim.py deleted file mode 100755 index ccda632207..0000000000 --- a/cuda_pathfinder/git_describe_command_shim.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python3 - -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE - -"""Shim that calls scripts/git_describe_command_runner.py if we're in a git repo. - -This shim checks if we're in a git repository (by looking for scripts/ directory) -and delegates to the shared git_describe_command_runner.py script. - -NOTE: -- cuda_bindings/git_describe_command_shim.py -- cuda_core/git_describe_command_shim.py -- cuda_pathfinder/git_describe_command_shim.py -are EXACT COPIES, PLEASE KEEP ALL FILES IN SYNC. -""" - -import subprocess -import sys -from pathlib import Path - -if len(sys.argv) < 2: - print("Usage: python git_describe_command_shim.py ", file=sys.stderr) # noqa: T201 - sys.exit(1) - -tag_pattern = sys.argv[1] - -# Find repo root (go up from package directory) -package_dir = Path(__file__).parent -repo_root = package_dir.parent -scripts_dir = repo_root / "scripts" -git_describe_script = scripts_dir / "git_describe_command_runner.py" - -# Check if we're in a git repo (scripts/ should exist) -if not scripts_dir.exists(): - print("ERROR: scripts/ directory not found.", file=sys.stderr) # noqa: T201 - print("This indicates we're not in a git repository.", file=sys.stderr) # noqa: T201 - print("git_describe_command_shim should not be called in this context.", file=sys.stderr) # noqa: T201 - sys.exit(1) - -# Check if the shared script exists -if not git_describe_script.exists(): - print(f"ERROR: {git_describe_script} not found.", file=sys.stderr) # noqa: T201 - print("The git_describe_command_runner script is missing.", file=sys.stderr) # noqa: T201 - sys.exit(1) - -# Call the shared script (from repo root so it can find .git) -result = subprocess.run( # noqa: S603 - [sys.executable, str(git_describe_script), tag_pattern], - cwd=repo_root, - timeout=10, -) - -sys.exit(result.returncode) diff --git a/cuda_pathfinder/pyproject.toml b/cuda_pathfinder/pyproject.toml index b6e066e494..c01ba121fd 100644 --- a/cuda_pathfinder/pyproject.toml +++ b/cuda_pathfinder/pyproject.toml @@ -70,15 +70,14 @@ requires = [ "setuptools_scm[simple]>=8", "wheel" ] -build-backend = "build_hooks" -backend-path = ["."] +build-backend = "setuptools.build_meta" [tool.setuptools_scm] root = ".." version_file = "cuda/pathfinder/_version.py" # We deliberately do not want to include the version suffixes (a/b/rc) in cuda-pathfinder versioning tag_regex = "^cuda-pathfinder-(?Pv\\d+\\.\\d+\\.\\d+)" -git_describe_command = ["python", "cuda_pathfinder/git_describe_command_shim.py", "cuda-pathfinder-v*[0-9]*"] +git_describe_command = ["python", "scripts/git_describe_wrapper.py", "cuda-pathfinder-v*[0-9]*"] [tool.pytest.ini_options] addopts = "--showlocals" diff --git a/scripts/git_describe_command_runner.py b/scripts/git_describe_wrapper.py similarity index 87% rename from scripts/git_describe_command_runner.py rename to scripts/git_describe_wrapper.py index 8ed606b6c6..4edca79f23 100755 --- a/scripts/git_describe_command_runner.py +++ b/scripts/git_describe_wrapper.py @@ -3,24 +3,24 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE -"""Git describe runner for setuptools-scm that fails loudly if no matching tags found. +"""Git describe wrapper for setuptools-scm that fails loudly if no matching tags found. This script is used as a replacement for git_describe_command in pyproject.toml. It provides better error messages and ensures setuptools-scm doesn't silently fall back to 0.1.x when tags are missing. Usage: - python git_describe_command_runner.py + python git_describe_wrapper.py Example: - python git_describe_command_runner.py "v*[0-9]*" + python git_describe_wrapper.py "v*[0-9]*" """ import subprocess import sys if len(sys.argv) < 2: - print("Usage: python git_describe_command_runner.py ", file=sys.stderr) # noqa: T201 + print("Usage: python git_describe_wrapper.py ", file=sys.stderr) # noqa: T201 sys.exit(1) tag_pattern = sys.argv[1] diff --git a/toolshed/check_git_describe_command_shim_code.py b/toolshed/check_git_describe_command_shim_code.py deleted file mode 100755 index c217288e41..0000000000 --- a/toolshed/check_git_describe_command_shim_code.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env python3 - -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Check that git_describe_command_shim.py files are identical across packages. - -This script verifies that the three shim files: -- cuda_bindings/git_describe_command_shim.py -- cuda_core/git_describe_command_shim.py -- cuda_pathfinder/git_describe_command_shim.py - -are exact copies of each other. -""" - -import sys -from pathlib import Path - -# Find repo root (assume script is in toolshed/) -repo_root = Path(__file__).parent.parent - -shim_files = [ - repo_root / "cuda_bindings" / "git_describe_command_shim.py", - repo_root / "cuda_core" / "git_describe_command_shim.py", - repo_root / "cuda_pathfinder" / "git_describe_command_shim.py", -] - -# Check all files exist -missing_files = [f for f in shim_files if not f.exists()] -if missing_files: - print("ERROR: Missing shim files:", file=sys.stderr) - for f in missing_files: - print(f" {f}", file=sys.stderr) - sys.exit(1) - -# Read all files -file_contents = {} -for shim_file in shim_files: - file_contents[shim_file] = shim_file.read_text() - -# Compare all pairs -errors = [] -for i, file1 in enumerate(shim_files): - for file2 in shim_files[i + 1 :]: - if file_contents[file1] != file_contents[file2]: - errors.append((file1, file2)) - -if errors: - print("ERROR: git_describe_command_shim.py files are not identical:", file=sys.stderr) - for file1, file2 in errors: - print(f" {file1.relative_to(repo_root)} != {file2.relative_to(repo_root)}", file=sys.stderr) - print("", file=sys.stderr) - print("These files must be kept in sync. Please copy one to the others.", file=sys.stderr) - sys.exit(1) - -print("✓ All git_describe_command_shim.py files are identical") -sys.exit(0) From 5c1a216442f7273a1555cab52481a1a6f12b4728 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 10 Jan 2026 21:52:27 -0800 Subject: [PATCH 08/17] Add version validation to detect setuptools-scm fallback versions Add build-time validation to detect when setuptools-scm falls back to default versions (0.0.x or 0.1.dev*) due to shallow clones or missing git tags. This prevents silent failures that cause dependency conflicts. Changes: - scripts/validate_version.py: New DRY validation script that checks for fallback versions and validates against expected patterns - cuda_core/build_hooks.py: Add validation in prepare_metadata hooks - cuda_pathfinder/build_hooks.py: New build hooks with version validation - cuda_pathfinder/pyproject.toml: Use custom build_hooks backend - cuda_bindings/setup.py: Add ValidateVersion command class The validation runs after setuptools-scm generates _version.py files, ensuring we catch fallback versions before builds complete. This will cause the 18 Windows CI tests that currently use fallback versions to fail with clear error messages instead of silently using wrong versions. Related to shallow clone issue documented in PR #1454. --- cuda_bindings/setup.py | 52 ++++++++++++++++ cuda_core/build_hooks.py | 55 ++++++++++++++++- cuda_pathfinder/build_hooks.py | 81 +++++++++++++++++++++++++ cuda_pathfinder/pyproject.toml | 3 +- scripts/git_describe_wrapper.py | 22 +++++++ scripts/validate_version.py | 102 ++++++++++++++++++++++++++++++++ 6 files changed, 312 insertions(+), 3 deletions(-) create mode 100644 cuda_pathfinder/build_hooks.py create mode 100755 scripts/validate_version.py diff --git a/cuda_bindings/setup.py b/cuda_bindings/setup.py index bfa1ae7826..d4f53f1165 100644 --- a/cuda_bindings/setup.py +++ b/cuda_bindings/setup.py @@ -8,6 +8,7 @@ import pathlib import platform import shutil +import subprocess import sys import sysconfig import tempfile @@ -410,11 +411,62 @@ def build_extension(self, ext): super().build_extension(ext) +class ValidateVersion(build_py): + """Custom build_py that validates version after setuptools-scm generates it.""" + + def run(self): + # setuptools-scm generates _version.py during build_py phase + # Validate version after parent run() completes + super().run() + _validate_version() + + cmdclass = { "bdist_wheel": WheelsBuildExtensions, "build_ext": ParallelBuildExtensions, + "build_py": ValidateVersion, } +# ---------------------------------------------------------------------- +# Version validation + + +def _validate_version(): + """Validate that setuptools-scm did not fall back to default version. + + This checks if cuda-bindings version is a fallback (0.0.x or 0.1.dev*) which + indicates setuptools-scm failed to detect version from git tags. + """ + repo_root = pathlib.Path(__file__).resolve().parent.parent + validation_script = repo_root / "scripts" / "validate_version.py" + + if not validation_script.exists(): + # If validation script doesn't exist, skip validation (shouldn't happen) + return + + # Run validation script + result = subprocess.run( # noqa: S603 + [ + sys.executable, + str(validation_script), + "cuda-bindings", + "cuda/bindings/_version.py", + "12.9.*|13.*", + ], + cwd=repo_root, + capture_output=True, + text=True, + timeout=10, + ) + + if result.returncode != 0: + error_msg = result.stderr.strip() or result.stdout.strip() + raise RuntimeError( + f"Version validation failed for cuda-bindings:\n{error_msg}\n" + f"This build will fail to prevent using incorrect fallback version." + ) + + # ---------------------------------------------------------------------- # Setup diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index 710e17b297..faa1cb4a40 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -11,14 +11,29 @@ import glob import os import re +import subprocess +import sys from pathlib import Path from Cython.Build import cythonize from setuptools import Extension from setuptools import build_meta as _build_meta -prepare_metadata_for_build_editable = _build_meta.prepare_metadata_for_build_editable -prepare_metadata_for_build_wheel = _build_meta.prepare_metadata_for_build_wheel + +def prepare_metadata_for_build_editable(metadata_directory, config_settings=None): + result = _build_meta.prepare_metadata_for_build_editable(metadata_directory, config_settings) + # Validate version after metadata is prepared (which generates _version.py) + _validate_version() + return result + + +def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None): + result = _build_meta.prepare_metadata_for_build_wheel(metadata_directory, config_settings) + # Validate version after metadata is prepared (which generates _version.py) + _validate_version() + return result + + build_sdist = _build_meta.build_sdist get_requires_for_build_sdist = _build_meta.get_requires_for_build_sdist @@ -151,6 +166,42 @@ def get_sources(mod_name): return +def _validate_version(): + """Validate that setuptools-scm did not fall back to default version. + + This checks if cuda-core version is a fallback (0.0.x or 0.1.dev*) which + indicates setuptools-scm failed to detect version from git tags. + """ + repo_root = Path(__file__).resolve().parent.parent + validation_script = repo_root / "scripts" / "validate_version.py" + + if not validation_script.exists(): + # If validation script doesn't exist, skip validation (shouldn't happen) + return + + # Run validation script + result = subprocess.run( # noqa: S603 + [ + sys.executable, + str(validation_script), + "cuda-core", + "cuda/core/_version.py", + "0.5.*", + ], + cwd=repo_root, + capture_output=True, + text=True, + timeout=10, + ) + + if result.returncode != 0: + error_msg = result.stderr.strip() or result.stdout.strip() + raise RuntimeError( + f"Version validation failed for cuda-core:\n{error_msg}\n" + f"This build will fail to prevent using incorrect fallback version." + ) + + def build_editable(wheel_directory, config_settings=None, metadata_directory=None): _build_cuda_core() return _build_meta.build_editable(wheel_directory, config_settings, metadata_directory) diff --git a/cuda_pathfinder/build_hooks.py b/cuda_pathfinder/build_hooks.py new file mode 100644 index 0000000000..df4306fb64 --- /dev/null +++ b/cuda_pathfinder/build_hooks.py @@ -0,0 +1,81 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Custom build hooks for cuda-pathfinder to validate version detection. + +This module wraps setuptools.build_meta to add version validation that detects +when setuptools-scm falls back to default versions (0.0.x or 0.1.dev*) due to +shallow clones or missing git tags. +""" + +import subprocess +import sys +from pathlib import Path + +from setuptools import build_meta as _build_meta + + +def _validate_version(): + """Validate that setuptools-scm did not fall back to default version. + + This checks if cuda-pathfinder version is a fallback (0.0.x or 0.1.dev*) which + indicates setuptools-scm failed to detect version from git tags. + """ + repo_root = Path(__file__).resolve().parent.parent + validation_script = repo_root / "scripts" / "validate_version.py" + + if not validation_script.exists(): + # If validation script doesn't exist, skip validation (shouldn't happen) + return + + # Run validation script + result = subprocess.run( # noqa: S603 + [ + sys.executable, + str(validation_script), + "cuda-pathfinder", + "cuda/pathfinder/_version.py", + "1.3.*", + ], + cwd=repo_root, + capture_output=True, + text=True, + timeout=10, + ) + + if result.returncode != 0: + error_msg = result.stderr.strip() or result.stdout.strip() + raise RuntimeError( + f"Version validation failed for cuda-pathfinder:\n{error_msg}\n" + f"This build will fail to prevent using incorrect fallback version." + ) + + +# Delegate all PEP 517 hooks to setuptools.build_meta, but add version validation +def prepare_metadata_for_build_editable(metadata_directory, config_settings=None): + result = _build_meta.prepare_metadata_for_build_editable(metadata_directory, config_settings) + _validate_version() + return result + + +def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None): + result = _build_meta.prepare_metadata_for_build_wheel(metadata_directory, config_settings) + _validate_version() + return result + + +def build_editable(wheel_directory, config_settings=None, metadata_directory=None): + _validate_version() + return _build_meta.build_editable(wheel_directory, config_settings, metadata_directory) + + +def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): + _validate_version() + return _build_meta.build_wheel(wheel_directory, config_settings, metadata_directory) + + +# Delegate other hooks unchanged +build_sdist = _build_meta.build_sdist +get_requires_for_build_editable = _build_meta.get_requires_for_build_editable +get_requires_for_build_wheel = _build_meta.get_requires_for_build_wheel +get_requires_for_build_sdist = _build_meta.get_requires_for_build_sdist diff --git a/cuda_pathfinder/pyproject.toml b/cuda_pathfinder/pyproject.toml index c01ba121fd..a6d90c6546 100644 --- a/cuda_pathfinder/pyproject.toml +++ b/cuda_pathfinder/pyproject.toml @@ -70,7 +70,8 @@ requires = [ "setuptools_scm[simple]>=8", "wheel" ] -build-backend = "setuptools.build_meta" +build-backend = "build_hooks" +backend-path = ["."] [tool.setuptools_scm] root = ".." diff --git a/scripts/git_describe_wrapper.py b/scripts/git_describe_wrapper.py index 4edca79f23..0804701ad7 100755 --- a/scripts/git_describe_wrapper.py +++ b/scripts/git_describe_wrapper.py @@ -33,6 +33,28 @@ print("setuptools-scm requires git to determine version from tags.", file=sys.stderr) # noqa: T201 sys.exit(1) +# Check if repository is shallow (which can cause git describe to fail) +try: + result = subprocess.run( # noqa: S603 + ["git", "rev-parse", "--is-shallow-repository"], # noqa: S607 + capture_output=True, + text=True, + timeout=5, + ) + if result.returncode == 0 and result.stdout.strip() == "true": + print("ERROR: Repository is a shallow clone.", file=sys.stderr) # noqa: T201 + print("", file=sys.stderr) # noqa: T201 + print("Shallow clones may not have all tags, causing git describe to fail.", file=sys.stderr) # noqa: T201 + print("This will cause setuptools-scm to fall back to version '0.1.x'.", file=sys.stderr) # noqa: T201 + print("", file=sys.stderr) # noqa: T201 + print("To fix:", file=sys.stderr) # noqa: T201 + print(" git fetch --unshallow", file=sys.stderr) # noqa: T201 + print(" git fetch --tags", file=sys.stderr) # noqa: T201 + sys.exit(1) +except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired): + # If git rev-parse fails, continue anyway (might not be in a git repo) + pass + # Run git describe (setuptools-scm expects --dirty --tags --long) result = subprocess.run( # noqa: S603 ["git", "describe", "--dirty", "--tags", "--long", "--match", tag_pattern], # noqa: S607 diff --git a/scripts/validate_version.py b/scripts/validate_version.py new file mode 100755 index 0000000000..5ae9f2aaa7 --- /dev/null +++ b/scripts/validate_version.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE + +"""Validate that setuptools-scm did not fall back to default version. + +This script checks if a package version is a fallback version (0.0.x or 0.1.dev*) +which indicates setuptools-scm failed to detect version from git tags. + +Usage: + python scripts/validate_version.py + +Example: + python scripts/validate_version.py cuda-pathfinder cuda/pathfinder/_version.py "1.3.*|12.9.*|13.*" +""" + +import re +import sys +from pathlib import Path + + +def validate_version(package_name: str, version_file_path: str, expected_pattern: str) -> None: + """Validate that version matches expected pattern and is not a fallback. + + Args: + package_name: Name of the package (for error messages) + version_file_path: Path to _version.py file (relative to repo root) + expected_pattern: Regex pattern for expected version format (e.g., "1.3.*|12.9.*|13.*") + + Raises: + RuntimeError: If version is a fallback or doesn't match expected pattern + """ + version_file = Path(version_file_path) + if not version_file.exists(): + raise RuntimeError( + f"Version file not found: {version_file_path}\n" + f"This may indicate setuptools-scm failed to generate version metadata." + ) + + # Read version from _version.py + with open(version_file, encoding="utf-8") as f: + content = f.read() + + # Extract __version__ + version_match = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', content) + if not version_match: + raise RuntimeError( + f"Could not find __version__ in {version_file_path}\n" + f"This may indicate setuptools-scm failed to generate version metadata." + ) + + version = version_match.group(1) + + # Check for fallback versions + if version.startswith("0.0") or version.startswith("0.1.dev"): + raise RuntimeError( + f"ERROR: {package_name} has fallback version '{version}'.\n" + f"\n" + f"This indicates setuptools-scm failed to detect version from git tags.\n" + f"This usually happens when:\n" + f" 1. Repository is a shallow clone without tags in history\n" + f" 2. Git tags are not fetched (run: git fetch --tags)\n" + f" 3. Running from wrong directory\n" + f"\n" + f"To fix:\n" + f" git fetch --unshallow\n" + f" git fetch --tags\n" + f"\n" + f"If you cannot fix the git setup, ensure CI performs a full clone or\n" + f"fetches enough history to include tags." + ) + + # Check if version matches expected pattern + if not re.match(expected_pattern.replace("*", ".*"), version): + raise RuntimeError( + f"ERROR: {package_name} version '{version}' does not match expected pattern '{expected_pattern}'.\n" + f"\n" + f"This may indicate:\n" + f" 1. Wrong branch/commit is being built\n" + f" 2. Git tags are incorrect\n" + f" 3. Version detection logic is broken\n" + ) + + # Success - version is valid + print(f"✅ {package_name} version '{version}' is valid", file=sys.stderr) # noqa: T201 + + +if __name__ == "__main__": + if len(sys.argv) != 4: + print(f"Usage: {sys.argv[0]} ", file=sys.stderr) # noqa: T201 + sys.exit(1) + + package_name = sys.argv[1] + version_file_path = sys.argv[2] + expected_pattern = sys.argv[3] + + try: + validate_version(package_name, version_file_path, expected_pattern) + except RuntimeError as e: + print(str(e), file=sys.stderr) # noqa: T201 + sys.exit(1) From cd46c8161afe2c3fbc02baeae6237f1bc5cb2db0 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 10 Jan 2026 22:06:45 -0800 Subject: [PATCH 09/17] Fix version validation timing: validate in build hooks, not prepare_metadata Move validation from prepare_metadata_for_build_* to build_editable/build_wheel where _version.py definitely exists. This fixes build failures where validation ran before setuptools-scm wrote the version file. --- cuda_core/build_hooks.py | 18 ++++++++++-------- cuda_pathfinder/build_hooks.py | 14 ++++++++------ scripts/validate_version.py | 8 ++++---- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index faa1cb4a40..8e2caff1f2 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -21,17 +21,15 @@ def prepare_metadata_for_build_editable(metadata_directory, config_settings=None): - result = _build_meta.prepare_metadata_for_build_editable(metadata_directory, config_settings) - # Validate version after metadata is prepared (which generates _version.py) - _validate_version() - return result + # Don't validate here - _version.py might not be written yet + # Validation will happen in build_editable where the file definitely exists + return _build_meta.prepare_metadata_for_build_editable(metadata_directory, config_settings) def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None): - result = _build_meta.prepare_metadata_for_build_wheel(metadata_directory, config_settings) - # Validate version after metadata is prepared (which generates _version.py) - _validate_version() - return result + # Don't validate here - _version.py might not be written yet + # Validation will happen in build_wheel where the file definitely exists + return _build_meta.prepare_metadata_for_build_wheel(metadata_directory, config_settings) build_sdist = _build_meta.build_sdist @@ -203,11 +201,15 @@ def _validate_version(): def build_editable(wheel_directory, config_settings=None, metadata_directory=None): + # Validate version here - _version.py definitely exists by this point + _validate_version() _build_cuda_core() return _build_meta.build_editable(wheel_directory, config_settings, metadata_directory) def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): + # Validate version here - _version.py definitely exists by this point + _validate_version() _build_cuda_core() return _build_meta.build_wheel(wheel_directory, config_settings, metadata_directory) diff --git a/cuda_pathfinder/build_hooks.py b/cuda_pathfinder/build_hooks.py index df4306fb64..b5a8fb5f46 100644 --- a/cuda_pathfinder/build_hooks.py +++ b/cuda_pathfinder/build_hooks.py @@ -53,23 +53,25 @@ def _validate_version(): # Delegate all PEP 517 hooks to setuptools.build_meta, but add version validation def prepare_metadata_for_build_editable(metadata_directory, config_settings=None): - result = _build_meta.prepare_metadata_for_build_editable(metadata_directory, config_settings) - _validate_version() - return result + # Don't validate here - _version.py might not be written yet + # Validation will happen in build_editable where the file definitely exists + return _build_meta.prepare_metadata_for_build_editable(metadata_directory, config_settings) def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None): - result = _build_meta.prepare_metadata_for_build_wheel(metadata_directory, config_settings) - _validate_version() - return result + # Don't validate here - _version.py might not be written yet + # Validation will happen in build_wheel where the file definitely exists + return _build_meta.prepare_metadata_for_build_wheel(metadata_directory, config_settings) def build_editable(wheel_directory, config_settings=None, metadata_directory=None): + # Validate version here - _version.py definitely exists by this point _validate_version() return _build_meta.build_editable(wheel_directory, config_settings, metadata_directory) def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): + # Validate version here - _version.py definitely exists by this point _validate_version() return _build_meta.build_wheel(wheel_directory, config_settings, metadata_directory) diff --git a/scripts/validate_version.py b/scripts/validate_version.py index 5ae9f2aaa7..8435e7913d 100755 --- a/scripts/validate_version.py +++ b/scripts/validate_version.py @@ -33,10 +33,10 @@ def validate_version(package_name: str, version_file_path: str, expected_pattern """ version_file = Path(version_file_path) if not version_file.exists(): - raise RuntimeError( - f"Version file not found: {version_file_path}\n" - f"This may indicate setuptools-scm failed to generate version metadata." - ) + # Version file might not exist yet if validation runs during prepare_metadata + # In that case, skip validation silently (it will be validated later in build hooks) + # This allows prepare_metadata to complete, and validation will happen in build_editable/build_wheel + return # Read version from _version.py with open(version_file, encoding="utf-8") as f: From 475cab3cac3de02724a8c21d855364db6ea299e8 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 11 Jan 2026 10:59:24 -0800 Subject: [PATCH 10/17] Add skipif IS_WINDOWS for test_patterngen_seeds --- cuda_core/tests/test_helpers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cuda_core/tests/test_helpers.py b/cuda_core/tests/test_helpers.py index abe0d62622..51cf1af3f4 100644 --- a/cuda_core/tests/test_helpers.py +++ b/cuda_core/tests/test_helpers.py @@ -47,6 +47,10 @@ def test_latchkernel(): log("done") +@pytest.mark.skipif( + IS_WINDOWS, + reason="Extremely slow on Windows (issue #1455).", +) @pytest.mark.skipif( under_compute_sanitizer(), reason="Too slow under compute-sanitizer (UVM-heavy test).", From 9b5a69b75fc10fa4f25c11630e3d0182e288b40c Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 11 Jan 2026 14:01:59 -0800 Subject: [PATCH 11/17] Add late-stage version number validation tests This commit adds version number validation tests to detect when automatic version detection fails and falls back to invalid versions (e.g., 0.0.x or 0.1.dev*). This addresses two main concerns: 1. Fallback version numbers going undetected: When setuptools-scm cannot detect version from git tags (e.g., due to shallow clones), it silently falls back to default versions like 0.1.dev*. These invalid versions can cause dependency conflicts and confusion in production/SWQA environments. 2. PyPI wheel replacement: The critical issue of just-built cuda-bindings being replaced with PyPI wheels is already handled by _check_cuda_bindings_installed() in cuda_core/build_hooks.py. Rather than attempting complex early detection in build hooks (which proved fragile due to timing issues with when _version.py files are written), we implement late-stage detection via test files. This approach is: - Simpler: No complex build hook timing issues - Reliable: Tests run after installation when versions are definitely available - Sufficient: Catches issues before they reach production/SWQA Changes: - Add validate_version_number() function to cuda_python_test_helpers for centralized validation logic - Create minimal test_version_number.py files in cuda_bindings, cuda_core, and cuda_pathfinder that import the version and call the validation function - Add helpers/__init__.py files in cuda_bindings/tests and cuda_pathfinder/tests to enable importing from cuda_python_test_helpers - Update cuda_core/tests/helpers/__init__.py to use ModuleNotFoundError instead of ImportError for consistency The validation checks that versions have major.minor > 0.1, which is sufficient since all three packages are already at higher versions. Error messages explain the issue without referencing setuptools-scm internals. --- cuda_bindings/pyproject.toml | 2 +- cuda_bindings/setup.py | 52 --------- cuda_bindings/tests/helpers/__init__.py | 12 +++ cuda_bindings/tests/test_version_number.py | 9 ++ cuda_core/build_hooks.py | 57 +--------- cuda_core/pyproject.toml | 2 +- cuda_core/tests/helpers/__init__.py | 2 +- cuda_core/tests/test_version_number.py | 9 ++ cuda_pathfinder/build_hooks.py | 83 -------------- cuda_pathfinder/pyproject.toml | 5 +- cuda_pathfinder/tests/helpers/__init__.py | 12 +++ cuda_pathfinder/tests/test_version_number.py | 10 ++ .../cuda_python_test_helpers/__init__.py | 44 ++++++++ scripts/git_describe_wrapper.py | 81 -------------- scripts/validate_version.py | 102 ------------------ 15 files changed, 103 insertions(+), 379 deletions(-) create mode 100644 cuda_bindings/tests/helpers/__init__.py create mode 100644 cuda_bindings/tests/test_version_number.py create mode 100644 cuda_core/tests/test_version_number.py delete mode 100644 cuda_pathfinder/build_hooks.py create mode 100644 cuda_pathfinder/tests/helpers/__init__.py create mode 100644 cuda_pathfinder/tests/test_version_number.py delete mode 100755 scripts/git_describe_wrapper.py delete mode 100755 scripts/validate_version.py diff --git a/cuda_bindings/pyproject.toml b/cuda_bindings/pyproject.toml index 3bf1a45421..7c4bddb434 100644 --- a/cuda_bindings/pyproject.toml +++ b/cuda_bindings/pyproject.toml @@ -82,4 +82,4 @@ root = ".." version_file = "cuda/bindings/_version.py" # We deliberately do not want to include the version suffixes (a/b/rc) in cuda-bindings versioning tag_regex = "^(?Pv\\d+\\.\\d+\\.\\d+)" -git_describe_command = ["python", "scripts/git_describe_wrapper.py", "v*[0-9]*"] +git_describe_command = ["git", "describe", "--dirty", "--tags", "--long", "--match", "v*[0-9]*"] diff --git a/cuda_bindings/setup.py b/cuda_bindings/setup.py index d4f53f1165..bfa1ae7826 100644 --- a/cuda_bindings/setup.py +++ b/cuda_bindings/setup.py @@ -8,7 +8,6 @@ import pathlib import platform import shutil -import subprocess import sys import sysconfig import tempfile @@ -411,62 +410,11 @@ def build_extension(self, ext): super().build_extension(ext) -class ValidateVersion(build_py): - """Custom build_py that validates version after setuptools-scm generates it.""" - - def run(self): - # setuptools-scm generates _version.py during build_py phase - # Validate version after parent run() completes - super().run() - _validate_version() - - cmdclass = { "bdist_wheel": WheelsBuildExtensions, "build_ext": ParallelBuildExtensions, - "build_py": ValidateVersion, } -# ---------------------------------------------------------------------- -# Version validation - - -def _validate_version(): - """Validate that setuptools-scm did not fall back to default version. - - This checks if cuda-bindings version is a fallback (0.0.x or 0.1.dev*) which - indicates setuptools-scm failed to detect version from git tags. - """ - repo_root = pathlib.Path(__file__).resolve().parent.parent - validation_script = repo_root / "scripts" / "validate_version.py" - - if not validation_script.exists(): - # If validation script doesn't exist, skip validation (shouldn't happen) - return - - # Run validation script - result = subprocess.run( # noqa: S603 - [ - sys.executable, - str(validation_script), - "cuda-bindings", - "cuda/bindings/_version.py", - "12.9.*|13.*", - ], - cwd=repo_root, - capture_output=True, - text=True, - timeout=10, - ) - - if result.returncode != 0: - error_msg = result.stderr.strip() or result.stdout.strip() - raise RuntimeError( - f"Version validation failed for cuda-bindings:\n{error_msg}\n" - f"This build will fail to prevent using incorrect fallback version." - ) - - # ---------------------------------------------------------------------- # Setup 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..f44258620b --- /dev/null +++ b/cuda_bindings/tests/test_version_number.py @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE + +import cuda.bindings +from helpers import validate_version_number + + +def test_version_number(): + validate_version_number(cuda.bindings.__version__, "cuda-bindings") diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index 8e2caff1f2..710e17b297 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -11,27 +11,14 @@ import glob import os import re -import subprocess -import sys from pathlib import Path from Cython.Build import cythonize from setuptools import Extension from setuptools import build_meta as _build_meta - -def prepare_metadata_for_build_editable(metadata_directory, config_settings=None): - # Don't validate here - _version.py might not be written yet - # Validation will happen in build_editable where the file definitely exists - return _build_meta.prepare_metadata_for_build_editable(metadata_directory, config_settings) - - -def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None): - # Don't validate here - _version.py might not be written yet - # Validation will happen in build_wheel where the file definitely exists - return _build_meta.prepare_metadata_for_build_wheel(metadata_directory, config_settings) - - +prepare_metadata_for_build_editable = _build_meta.prepare_metadata_for_build_editable +prepare_metadata_for_build_wheel = _build_meta.prepare_metadata_for_build_wheel build_sdist = _build_meta.build_sdist get_requires_for_build_sdist = _build_meta.get_requires_for_build_sdist @@ -164,52 +151,12 @@ def get_sources(mod_name): return -def _validate_version(): - """Validate that setuptools-scm did not fall back to default version. - - This checks if cuda-core version is a fallback (0.0.x or 0.1.dev*) which - indicates setuptools-scm failed to detect version from git tags. - """ - repo_root = Path(__file__).resolve().parent.parent - validation_script = repo_root / "scripts" / "validate_version.py" - - if not validation_script.exists(): - # If validation script doesn't exist, skip validation (shouldn't happen) - return - - # Run validation script - result = subprocess.run( # noqa: S603 - [ - sys.executable, - str(validation_script), - "cuda-core", - "cuda/core/_version.py", - "0.5.*", - ], - cwd=repo_root, - capture_output=True, - text=True, - timeout=10, - ) - - if result.returncode != 0: - error_msg = result.stderr.strip() or result.stdout.strip() - raise RuntimeError( - f"Version validation failed for cuda-core:\n{error_msg}\n" - f"This build will fail to prevent using incorrect fallback version." - ) - - def build_editable(wheel_directory, config_settings=None, metadata_directory=None): - # Validate version here - _version.py definitely exists by this point - _validate_version() _build_cuda_core() return _build_meta.build_editable(wheel_directory, config_settings, metadata_directory) def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): - # Validate version here - _version.py definitely exists by this point - _validate_version() _build_cuda_core() return _build_meta.build_wheel(wheel_directory, config_settings, metadata_directory) diff --git a/cuda_core/pyproject.toml b/cuda_core/pyproject.toml index d5adfdac0c..f775ac8813 100644 --- a/cuda_core/pyproject.toml +++ b/cuda_core/pyproject.toml @@ -84,7 +84,7 @@ root = ".." version_file = "cuda/core/_version.py" # We deliberately do not want to include the version suffixes (a/b/rc) in cuda-core versioning tag_regex = "^cuda-core-(?Pv\\d+\\.\\d+\\.\\d+)" -git_describe_command = ["python", "scripts/git_describe_wrapper.py", "cuda-core-v*[0-9]*"] +git_describe_command = ["git", "describe", "--dirty", "--tags", "--long", "--match", "cuda-core-v*[0-9]*"] [tool.cibuildwheel] skip = "*-musllinux_*" diff --git a/cuda_core/tests/helpers/__init__.py b/cuda_core/tests/helpers/__init__.py index 02cbe6d8e9..10432795e4 100644 --- a/cuda_core/tests/helpers/__init__.py +++ b/cuda_core/tests/helpers/__init__.py @@ -20,7 +20,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..59ba932738 --- /dev/null +++ b/cuda_core/tests/test_version_number.py @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE + +import cuda.core +from helpers import validate_version_number + + +def test_version_number(): + validate_version_number(cuda.core.__version__, "cuda-core") diff --git a/cuda_pathfinder/build_hooks.py b/cuda_pathfinder/build_hooks.py deleted file mode 100644 index b5a8fb5f46..0000000000 --- a/cuda_pathfinder/build_hooks.py +++ /dev/null @@ -1,83 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Custom build hooks for cuda-pathfinder to validate version detection. - -This module wraps setuptools.build_meta to add version validation that detects -when setuptools-scm falls back to default versions (0.0.x or 0.1.dev*) due to -shallow clones or missing git tags. -""" - -import subprocess -import sys -from pathlib import Path - -from setuptools import build_meta as _build_meta - - -def _validate_version(): - """Validate that setuptools-scm did not fall back to default version. - - This checks if cuda-pathfinder version is a fallback (0.0.x or 0.1.dev*) which - indicates setuptools-scm failed to detect version from git tags. - """ - repo_root = Path(__file__).resolve().parent.parent - validation_script = repo_root / "scripts" / "validate_version.py" - - if not validation_script.exists(): - # If validation script doesn't exist, skip validation (shouldn't happen) - return - - # Run validation script - result = subprocess.run( # noqa: S603 - [ - sys.executable, - str(validation_script), - "cuda-pathfinder", - "cuda/pathfinder/_version.py", - "1.3.*", - ], - cwd=repo_root, - capture_output=True, - text=True, - timeout=10, - ) - - if result.returncode != 0: - error_msg = result.stderr.strip() or result.stdout.strip() - raise RuntimeError( - f"Version validation failed for cuda-pathfinder:\n{error_msg}\n" - f"This build will fail to prevent using incorrect fallback version." - ) - - -# Delegate all PEP 517 hooks to setuptools.build_meta, but add version validation -def prepare_metadata_for_build_editable(metadata_directory, config_settings=None): - # Don't validate here - _version.py might not be written yet - # Validation will happen in build_editable where the file definitely exists - return _build_meta.prepare_metadata_for_build_editable(metadata_directory, config_settings) - - -def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None): - # Don't validate here - _version.py might not be written yet - # Validation will happen in build_wheel where the file definitely exists - return _build_meta.prepare_metadata_for_build_wheel(metadata_directory, config_settings) - - -def build_editable(wheel_directory, config_settings=None, metadata_directory=None): - # Validate version here - _version.py definitely exists by this point - _validate_version() - return _build_meta.build_editable(wheel_directory, config_settings, metadata_directory) - - -def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): - # Validate version here - _version.py definitely exists by this point - _validate_version() - return _build_meta.build_wheel(wheel_directory, config_settings, metadata_directory) - - -# Delegate other hooks unchanged -build_sdist = _build_meta.build_sdist -get_requires_for_build_editable = _build_meta.get_requires_for_build_editable -get_requires_for_build_wheel = _build_meta.get_requires_for_build_wheel -get_requires_for_build_sdist = _build_meta.get_requires_for_build_sdist diff --git a/cuda_pathfinder/pyproject.toml b/cuda_pathfinder/pyproject.toml index a6d90c6546..8f2e89eaab 100644 --- a/cuda_pathfinder/pyproject.toml +++ b/cuda_pathfinder/pyproject.toml @@ -70,15 +70,14 @@ requires = [ "setuptools_scm[simple]>=8", "wheel" ] -build-backend = "build_hooks" -backend-path = ["."] +build-backend = "setuptools.build_meta" [tool.setuptools_scm] root = ".." version_file = "cuda/pathfinder/_version.py" # We deliberately do not want to include the version suffixes (a/b/rc) in cuda-pathfinder versioning tag_regex = "^cuda-pathfinder-(?Pv\\d+\\.\\d+\\.\\d+)" -git_describe_command = ["python", "scripts/git_describe_wrapper.py", "cuda-pathfinder-v*[0-9]*"] +git_describe_command = [ "git", "describe", "--dirty", "--tags", "--long", "--match", "cuda-pathfinder-v*[0-9]*" ] [tool.pytest.ini_options] addopts = "--showlocals" 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..a860761fb0 --- /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_version_number(): + 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 fca190c103..302ea15c92 100644 --- a/cuda_python_test_helpers/cuda_python_test_helpers/__init__.py +++ b/cuda_python_test_helpers/cuda_python_test_helpers/__init__.py @@ -17,6 +17,7 @@ "libc", "supports_ipc_mempool", "under_compute_sanitizer", + "validate_version_number", ] @@ -103,3 +104,46 @@ def supports_ipc_mempool(device_id: Union[int, object]) -> bool: return (int(mask) & int(posix_fd)) != 0 except Exception: return False + + +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" diff --git a/scripts/git_describe_wrapper.py b/scripts/git_describe_wrapper.py deleted file mode 100755 index 0804701ad7..0000000000 --- a/scripts/git_describe_wrapper.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 - -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE - -"""Git describe wrapper for setuptools-scm that fails loudly if no matching tags found. - -This script is used as a replacement for git_describe_command in pyproject.toml. -It provides better error messages and ensures setuptools-scm doesn't silently -fall back to 0.1.x when tags are missing. - -Usage: - python git_describe_wrapper.py - -Example: - python git_describe_wrapper.py "v*[0-9]*" -""" - -import subprocess -import sys - -if len(sys.argv) < 2: - print("Usage: python git_describe_wrapper.py ", file=sys.stderr) # noqa: T201 - sys.exit(1) - -tag_pattern = sys.argv[1] - -# Check if git is available -try: - subprocess.run(["git", "--version"], capture_output=True, check=True, timeout=5) # noqa: S607 -except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired): - print("ERROR: Git is not available in PATH.", file=sys.stderr) # noqa: T201 - print("setuptools-scm requires git to determine version from tags.", file=sys.stderr) # noqa: T201 - sys.exit(1) - -# Check if repository is shallow (which can cause git describe to fail) -try: - result = subprocess.run( # noqa: S603 - ["git", "rev-parse", "--is-shallow-repository"], # noqa: S607 - capture_output=True, - text=True, - timeout=5, - ) - if result.returncode == 0 and result.stdout.strip() == "true": - print("ERROR: Repository is a shallow clone.", file=sys.stderr) # noqa: T201 - print("", file=sys.stderr) # noqa: T201 - print("Shallow clones may not have all tags, causing git describe to fail.", file=sys.stderr) # noqa: T201 - print("This will cause setuptools-scm to fall back to version '0.1.x'.", file=sys.stderr) # noqa: T201 - print("", file=sys.stderr) # noqa: T201 - print("To fix:", file=sys.stderr) # noqa: T201 - print(" git fetch --unshallow", file=sys.stderr) # noqa: T201 - print(" git fetch --tags", file=sys.stderr) # noqa: T201 - sys.exit(1) -except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired): - # If git rev-parse fails, continue anyway (might not be in a git repo) - pass - -# Run git describe (setuptools-scm expects --dirty --tags --long) -result = subprocess.run( # noqa: S603 - ["git", "describe", "--dirty", "--tags", "--long", "--match", tag_pattern], # noqa: S607 - capture_output=True, - text=True, - timeout=5, -) - -if result.returncode != 0: - print(f"ERROR: git describe failed with pattern '{tag_pattern}'", file=sys.stderr) # noqa: T201 - print(f"Error: {result.stderr.strip()}", file=sys.stderr) # noqa: T201 - print("", file=sys.stderr) # noqa: T201 - print("This means setuptools-scm will fall back to version '0.1.x'.", file=sys.stderr) # noqa: T201 - print("", file=sys.stderr) # noqa: T201 - print("This usually means:", file=sys.stderr) # noqa: T201 - print(" 1. Git tags are not fetched (run: git fetch --tags)", file=sys.stderr) # noqa: T201 - print(" 2. Running from wrong directory", file=sys.stderr) # noqa: T201 - print(" 3. No matching tags found", file=sys.stderr) # noqa: T201 - print("", file=sys.stderr) # noqa: T201 - print("To fix:", file=sys.stderr) # noqa: T201 - print(" git fetch --tags", file=sys.stderr) # noqa: T201 - sys.exit(1) - -print(result.stdout.strip()) # noqa: T201 diff --git a/scripts/validate_version.py b/scripts/validate_version.py deleted file mode 100755 index 8435e7913d..0000000000 --- a/scripts/validate_version.py +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/env python3 - -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE - -"""Validate that setuptools-scm did not fall back to default version. - -This script checks if a package version is a fallback version (0.0.x or 0.1.dev*) -which indicates setuptools-scm failed to detect version from git tags. - -Usage: - python scripts/validate_version.py - -Example: - python scripts/validate_version.py cuda-pathfinder cuda/pathfinder/_version.py "1.3.*|12.9.*|13.*" -""" - -import re -import sys -from pathlib import Path - - -def validate_version(package_name: str, version_file_path: str, expected_pattern: str) -> None: - """Validate that version matches expected pattern and is not a fallback. - - Args: - package_name: Name of the package (for error messages) - version_file_path: Path to _version.py file (relative to repo root) - expected_pattern: Regex pattern for expected version format (e.g., "1.3.*|12.9.*|13.*") - - Raises: - RuntimeError: If version is a fallback or doesn't match expected pattern - """ - version_file = Path(version_file_path) - if not version_file.exists(): - # Version file might not exist yet if validation runs during prepare_metadata - # In that case, skip validation silently (it will be validated later in build hooks) - # This allows prepare_metadata to complete, and validation will happen in build_editable/build_wheel - return - - # Read version from _version.py - with open(version_file, encoding="utf-8") as f: - content = f.read() - - # Extract __version__ - version_match = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', content) - if not version_match: - raise RuntimeError( - f"Could not find __version__ in {version_file_path}\n" - f"This may indicate setuptools-scm failed to generate version metadata." - ) - - version = version_match.group(1) - - # Check for fallback versions - if version.startswith("0.0") or version.startswith("0.1.dev"): - raise RuntimeError( - f"ERROR: {package_name} has fallback version '{version}'.\n" - f"\n" - f"This indicates setuptools-scm failed to detect version from git tags.\n" - f"This usually happens when:\n" - f" 1. Repository is a shallow clone without tags in history\n" - f" 2. Git tags are not fetched (run: git fetch --tags)\n" - f" 3. Running from wrong directory\n" - f"\n" - f"To fix:\n" - f" git fetch --unshallow\n" - f" git fetch --tags\n" - f"\n" - f"If you cannot fix the git setup, ensure CI performs a full clone or\n" - f"fetches enough history to include tags." - ) - - # Check if version matches expected pattern - if not re.match(expected_pattern.replace("*", ".*"), version): - raise RuntimeError( - f"ERROR: {package_name} version '{version}' does not match expected pattern '{expected_pattern}'.\n" - f"\n" - f"This may indicate:\n" - f" 1. Wrong branch/commit is being built\n" - f" 2. Git tags are incorrect\n" - f" 3. Version detection logic is broken\n" - ) - - # Success - version is valid - print(f"✅ {package_name} version '{version}' is valid", file=sys.stderr) # noqa: T201 - - -if __name__ == "__main__": - if len(sys.argv) != 4: - print(f"Usage: {sys.argv[0]} ", file=sys.stderr) # noqa: T201 - sys.exit(1) - - package_name = sys.argv[1] - version_file_path = sys.argv[2] - expected_pattern = sys.argv[3] - - try: - validate_version(package_name, version_file_path, expected_pattern) - except RuntimeError as e: - print(str(e), file=sys.stderr) # noqa: T201 - sys.exit(1) From 5721e97e1dfc573e4ace5d18e76a54adb53f9413 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 11 Jan 2026 15:38:26 -0800 Subject: [PATCH 12/17] Move supports_ipc_mempool to cuda_core-specific helpers The supports_ipc_mempool function was previously defined in cuda_python_test_helpers, but it had a hard dependency on cuda.core._utils.cuda_utils.handle_return. This caused CI failures when cuda_bindings or cuda_pathfinder tests tried to import cuda_python_test_helpers, because cuda-core might not be installed in those test environments. By moving supports_ipc_mempool to cuda_core/tests/helpers/__init__.py, we ensure that: - cuda_python_test_helpers remains free of cuda-core-specific dependencies - The function is only available where cuda-core is guaranteed to be installed (i.e., in cuda_core tests) - cuda_bindings and cuda_pathfinder can safely import cuda_python_test_helpers without requiring cuda-core Changes: - Move supports_ipc_mempool from cuda_python_test_helpers to cuda_core/tests/helpers/__init__.py - Update cuda_core/tests/test_memory.py to import from helpers instead of cuda_python_test_helpers - Remove unused imports (functools, Union, handle_return) from cuda_python_test_helpers/__init__.py - Remove supports_ipc_mempool from cuda_python_test_helpers __all__ This fixes CI failures where importing cuda_python_test_helpers would fail due to missing cuda-core dependencies. --- cuda_core/tests/helpers/__init__.py | 53 ++++++++++++++++--- cuda_core/tests/test_memory.py | 3 +- .../cuda_python_test_helpers/__init__.py | 40 -------------- 3 files changed, 47 insertions(+), 49 deletions(-) diff --git a/cuda_core/tests/helpers/__init__.py b/cuda_core/tests/helpers/__init__.py index 10432795e4..e43e935c1e 100644 --- a/cuda_core/tests/helpers/__init__.py +++ b/cuda_core/tests/helpers/__init__.py @@ -1,9 +1,21 @@ -# 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 import os import pathlib import sys +from typing import Union + +from cuda.core._utils.cuda_utils import handle_return + +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 + CUDA_PATH = os.environ.get("CUDA_PATH") CUDA_INCLUDE_PATH = None @@ -18,9 +30,36 @@ CCCL_INCLUDE_PATHS = (path,) + CCCL_INCLUDE_PATHS -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 +@functools.cache +def supports_ipc_mempool(device_id: Union[int, object]) -> bool: + """Return True if mempool IPC via POSIX file descriptor is supported. + + Uses cuDeviceGetAttribute(CU_DEVICE_ATTRIBUTE_MEMPOOL_SUPPORTED_HANDLE_TYPES) + to check for CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR support. Does not + require an active CUDA context. + """ + if IS_WSL: # noqa: F405 + return False + + try: + # Lazy import to avoid hard dependency when not running GPU tests + try: + from cuda.bindings import driver # type: ignore + except Exception: + from cuda import cuda as driver # type: ignore + + # Initialize CUDA + handle_return(driver.cuInit(0)) + + # Resolve device id from int or Device-like object + dev_id = int(getattr(device_id, "device_id", device_id)) + + # Query supported mempool handle types bitmask + attr = driver.CUdevice_attribute.CU_DEVICE_ATTRIBUTE_MEMPOOL_SUPPORTED_HANDLE_TYPES + mask = handle_return(driver.cuDeviceGetAttribute(attr, dev_id)) + + # Check POSIX FD handle type support via bitmask + posix_fd = driver.CUmemAllocationHandleType.CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR + return (int(mask) & int(posix_fd)) != 0 + except Exception: + return False diff --git a/cuda_core/tests/test_memory.py b/cuda_core/tests/test_memory.py index 0f6288bcf7..71adb4ffc7 100644 --- a/cuda_core/tests/test_memory.py +++ b/cuda_core/tests/test_memory.py @@ -38,7 +38,7 @@ from cuda.core._memory import IPCBufferDescriptor from cuda.core._utils.cuda_utils import CUDAError, handle_return from cuda.core.utils import StridedMemoryView -from helpers import IS_WINDOWS +from helpers import IS_WINDOWS, supports_ipc_mempool from helpers.buffers import DummyUnifiedMemoryResource from conftest import ( @@ -46,7 +46,6 @@ skip_if_managed_memory_unsupported, skip_if_pinned_memory_unsupported, ) -from cuda_python_test_helpers import supports_ipc_mempool POOL_SIZE = 2097152 # 2MB size 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 302ea15c92..887845a640 100644 --- a/cuda_python_test_helpers/cuda_python_test_helpers/__init__.py +++ b/cuda_python_test_helpers/cuda_python_test_helpers/__init__.py @@ -2,20 +2,15 @@ # SPDX-License-Identifier: Apache-2.0 import ctypes -import functools import os import platform import sys from contextlib import suppress -from typing import Union - -from cuda.core._utils.cuda_utils import handle_return __all__ = [ "IS_WINDOWS", "IS_WSL", "libc", - "supports_ipc_mempool", "under_compute_sanitizer", "validate_version_number", ] @@ -71,41 +66,6 @@ def under_compute_sanitizer() -> bool: return "compute-sanitizer" in inj or "cuda-memcheck" in inj -@functools.cache -def supports_ipc_mempool(device_id: Union[int, object]) -> bool: - """Return True if mempool IPC via POSIX file descriptor is supported. - - Uses cuDeviceGetAttribute(CU_DEVICE_ATTRIBUTE_MEMPOOL_SUPPORTED_HANDLE_TYPES) - to check for CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR support. Does not - require an active CUDA context. - """ - if _detect_wsl(): - return False - - try: - # Lazy import to avoid hard dependency when not running GPU tests - try: - from cuda.bindings import driver # type: ignore - except Exception: - from cuda import cuda as driver # type: ignore - - # Initialize CUDA - handle_return(driver.cuInit(0)) - - # Resolve device id from int or Device-like object - dev_id = int(getattr(device_id, "device_id", device_id)) - - # Query supported mempool handle types bitmask - attr = driver.CUdevice_attribute.CU_DEVICE_ATTRIBUTE_MEMPOOL_SUPPORTED_HANDLE_TYPES - mask = handle_return(driver.cuDeviceGetAttribute(attr, dev_id)) - - # Check POSIX FD handle type support via bitmask - posix_fd = driver.CUmemAllocationHandleType.CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR - return (int(mask) & int(posix_fd)) != 0 - except Exception: - return False - - def validate_version_number(version: str, package_name: str) -> None: """Validate that a version number is valid (major.minor > 0.1). From 89a67e253c4bf17b46c261f7b9b5003bab98271d Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 11 Jan 2026 16:43:51 -0800 Subject: [PATCH 13/17] Test version numbers of dependencies in addition to own package The main purpose of these tests is to validate that dependencies have valid version numbers, not just the package being tested. This is critical for catching cases where a dependency (e.g., cuda-pathfinder) might be built with a fallback version (0.1.dev...) due to shallow git clones or missing tags. To ensure we see all invalid versions in a single test run, we organize the tests as separate test functions (test_bindings_version, test_core_version, test_pathfinder_version) rather than combining them into a single function. This way, if multiple packages have invalid versions, pytest will report all failures rather than stopping at the first one. Changes: - cuda_bindings/tests/test_version_number.py: Tests both cuda-bindings and cuda-pathfinder versions - cuda_core/tests/test_version_number.py: Tests cuda-bindings, cuda-core, and cuda-pathfinder versions - cuda_pathfinder/tests/test_version_number.py: Tests cuda-pathfinder version (renamed function for consistency) --- cuda_bindings/tests/test_version_number.py | 7 ++++++- cuda_core/tests/test_version_number.py | 12 +++++++++++- cuda_pathfinder/tests/test_version_number.py | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/cuda_bindings/tests/test_version_number.py b/cuda_bindings/tests/test_version_number.py index f44258620b..c912160d88 100644 --- a/cuda_bindings/tests/test_version_number.py +++ b/cuda_bindings/tests/test_version_number.py @@ -2,8 +2,13 @@ # SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE import cuda.bindings +import cuda.pathfinder from helpers import validate_version_number -def test_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/tests/test_version_number.py b/cuda_core/tests/test_version_number.py index 59ba932738..9820c3cc53 100644 --- a/cuda_core/tests/test_version_number.py +++ b/cuda_core/tests/test_version_number.py @@ -1,9 +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_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/tests/test_version_number.py b/cuda_pathfinder/tests/test_version_number.py index a860761fb0..8996ce3afa 100644 --- a/cuda_pathfinder/tests/test_version_number.py +++ b/cuda_pathfinder/tests/test_version_number.py @@ -6,5 +6,5 @@ import cuda.pathfinder -def test_version_number(): +def test_pathfinder_version(): validate_version_number(cuda.pathfinder.__version__, "cuda-pathfinder") From a1584a2907d9a6f650e2a7f8507b49b294e8335c Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 11 Jan 2026 20:21:26 -0800 Subject: [PATCH 14/17] Add fail-fast version assertions in __init__.py files Add minimal assertions immediately after importing __version__ in all three package __init__.py files to fail fast if an invalid version (e.g., 0.1.dev...) is detected. This prevents packages with fallback versions from being imported or used, catching the issue at the earliest possible point. The assertion checks that major.minor > (0, 1) using a minimal one-liner: assert tuple(int(_) for _ in __version__.split(".")[:2]) > (0, 1), "FATAL: invalid __version__" Strictly speaking this makes the unit tests redundant, but we want to keep the unit tests as a second line of defense. The assertions provide immediate feedback during import, while the unit tests provide explicit test coverage and clearer error messages in CI logs. Changes: - cuda_bindings/cuda/bindings/__init__.py: Add version assertion - cuda_core/cuda/core/__init__.py: Add version assertion - cuda_pathfinder/cuda/pathfinder/__init__.py: Add version assertion --- cuda_bindings/cuda/bindings/__init__.py | 2 ++ cuda_core/cuda/core/__init__.py | 2 ++ cuda_pathfinder/cuda/pathfinder/__init__.py | 4 +++- 3 files changed, 7 insertions(+), 1 deletion(-) 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_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_pathfinder/cuda/pathfinder/__init__.py b/cuda_pathfinder/cuda/pathfinder/__init__.py index bd7081b525..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__ # noqa: F401 # isort: skip +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 From 233447b1b8b9f2162104a5e1a68b8378c475d18b Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 11 Jan 2026 20:32:58 -0800 Subject: [PATCH 15/17] Harden test workflows with intentional shallow clone and version checks Use an intentionally shallow clone (fetch-depth: 1) to test wheel installation without full git history. This ensures we're testing the wheel artifacts themselves, not building from source. Changes: - Set fetch-depth: 1 explicitly (although it is the default) with comment emphasizing that shallow cloning is intentional - Add --only-binary=:all: to cuda-python installation to ensure we only test wheels, never build from source - Add "Verify installed package versions" step that imports cuda.pathfinder and cuda.bindings to trigger __version__ assertions immediately after installation, providing early detection of invalid versions - Update comments to accurately reflect that we're testing wheel artifacts This approach hardens the test workflows by: - Making the shallow clone intentional and explicit - Actually testing that __version__ assertions work (fail-fast on invalid versions) - Catching version issues immediately after installation, before tests run - Ensuring we only test wheels, not source builds Applied consistently to both: - .github/workflows/test-wheel-windows.yml - .github/workflows/test-wheel-linux.yml --- .github/workflows/test-wheel-linux.yml | 17 +++++++++++++---- .github/workflows/test-wheel-windows.yml | 15 +++++++++++++-- 2 files changed, 26 insertions(+), 6 deletions(-) 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..4b73bbc52f 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,11 +240,18 @@ 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: | From c574a94c2d1a91ab1d42ce33b033238f1fdb2e05 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 11 Jan 2026 23:06:42 -0800 Subject: [PATCH 16/17] Fix Windows workflow to use wheel files instead of directory Change pip install command from '.' to './*.whl' to prevent pip from building from source during metadata preparation. This matches the Linux workflow and eliminates the temporary 0.1.dev versions that were being built in shallow clones. See: https://github.com/NVIDIA/cuda-python/pull/1454#issuecomment-3737147324 --- .github/workflows/test-wheel-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-wheel-windows.yml b/.github/workflows/test-wheel-windows.yml index 4b73bbc52f..f0f16c276c 100644 --- a/.github/workflows/test-wheel-windows.yml +++ b/.github/workflows/test-wheel-windows.yml @@ -256,7 +256,7 @@ jobs: 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 From d889957c9aaad39ff3ff58479867ed831059687a Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 12 Jan 2026 10:48:00 -0800 Subject: [PATCH 17/17] =?UTF-8?q?Apply=20review=20suggestion=20(tuple=20?= =?UTF-8?q?=E2=86=92=20string=20in=20comment)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cuda_core/build_hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index 710e17b297..b7b24f4048 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -187,7 +187,7 @@ def _check_cuda_bindings_installed(): version_file_path = Path(bv.__file__).resolve() is_editable = repo_root in version_file_path.parents - # Extract major version from version tuple + # Extract major version from version string bindings_version = bv.__version__ bindings_major_version = bindings_version.split(".")[0]