From 417a5d7fe7a0dff4fb22a3338bbbe4ae769a0dba Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 20 Mar 2026 11:42:08 -0700 Subject: [PATCH 01/15] Squash-merge of PR #1519 (rparolin/env_var_improvements) rebased onto main. Adds cuda.pathfinder._utils.env_var_constants with canonical search order, enhances get_cuda_home_or_path() with robust path comparison and caching, and updates documentation across all packages to reflect the new priority. Co-authored-by: Rob Parolin Made-with: Cursor --- conftest.py | 7 +- cuda_bindings/README.md | 2 +- .../docs/source/environment_variables.rst | 17 ++- cuda_bindings/docs/source/install.rst | 4 +- cuda_bindings/pyproject.toml | 1 + cuda_core/README.md | 2 +- cuda_core/pyproject.toml | 3 +- cuda_core/tests/helpers/__init__.py | 5 +- cuda_core/tests/test_build_hooks.py | 26 ++++ .../_dynamic_libs/load_nvidia_dynamic_lib.py | 10 +- .../_headers/find_nvidia_headers.py | 6 +- .../cuda/pathfinder/_utils/env_vars.py | 114 +++++++++++++--- cuda_pathfinder/docs/source/api.rst | 18 ++- .../docs/source/release/1.5.0-notes.rst | 47 +++++++ cuda_pathfinder/tests/test_utils_env_vars.py | 125 ++++++++++++++---- 15 files changed, 326 insertions(+), 61 deletions(-) create mode 100644 cuda_pathfinder/docs/source/release/1.5.0-notes.rst diff --git a/conftest.py b/conftest.py index 7cea6e6612..c09a182c2c 100644 --- a/conftest.py +++ b/conftest.py @@ -1,13 +1,14 @@ -# 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 os import pytest +from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path + def pytest_collection_modifyitems(config, items): # noqa: ARG001 - cuda_home = os.environ.get("CUDA_HOME") + cuda_home = get_cuda_home_or_path() for item in items: nodeid = item.nodeid.replace("\\", "/") diff --git a/cuda_bindings/README.md b/cuda_bindings/README.md index a0657706d0..f1df49b67e 100644 --- a/cuda_bindings/README.md +++ b/cuda_bindings/README.md @@ -33,7 +33,7 @@ To run these tests: Cython tests are located in `tests/cython` and need to be built. These builds have the same CUDA Toolkit header requirements as [Installing from Source](https://nvidia.github.io/cuda-python/cuda-bindings/latest/install.html#requirements) where the major.minor version must match `cuda.bindings`. To build them: -1. Setup environment variable `CUDA_HOME` with the path to the CUDA Toolkit installation. +1. Setup environment variable `CUDA_PATH` (or `CUDA_HOME`) with the path to the CUDA Toolkit installation. Note: If both are set, `CUDA_PATH` takes precedence (see `cuda.pathfinder._utils.env_vars.CUDA_ENV_VARS_ORDERED`). 2. Run `build_tests` script located in `test/cython` appropriate to your platform. This will both cythonize the tests and build them. To run these tests: diff --git a/cuda_bindings/docs/source/environment_variables.rst b/cuda_bindings/docs/source/environment_variables.rst index 7a49fb66a3..2625e8d8d9 100644 --- a/cuda_bindings/docs/source/environment_variables.rst +++ b/cuda_bindings/docs/source/environment_variables.rst @@ -15,7 +15,22 @@ Runtime Environment Variables Build-Time Environment Variables -------------------------------- -- ``CUDA_HOME`` or ``CUDA_PATH``: Specifies the location of the CUDA Toolkit. +- ``CUDA_PATH`` or ``CUDA_HOME``: Specifies the location of the CUDA Toolkit. If both are set, ``CUDA_PATH`` takes precedence. This search order is defined in :py:data:`cuda.pathfinder._utils.env_vars.CUDA_ENV_VARS_ORDERED`. + + .. note:: + **Breaking Change in v1.4.0**: The priority order changed from ``CUDA_HOME`` > ``CUDA_PATH`` to ``CUDA_PATH`` > ``CUDA_HOME``. + + **Migration Guide**: + + - If you only set one variable, no changes are needed + - If you set both variables to the same location, no changes are needed + - If you set both variables to different locations and relied on ``CUDA_HOME`` taking precedence, you should either: + + - Switch to using only ``CUDA_PATH`` (recommended) + - Ensure both variables point to the same CUDA Toolkit installation + - Be aware that ``CUDA_PATH`` will now be used + + A warning will be issued if both variables are set but point to different locations. - ``CUDA_PYTHON_PARSER_CACHING`` : bool, toggles the caching of parsed header files during the cuda-bindings build process. If caching is enabled (``CUDA_PYTHON_PARSER_CACHING`` is True), the cache path is set to ./cache_, where is derived from the cuda toolkit libraries used to build cuda-bindings. diff --git a/cuda_bindings/docs/source/install.rst b/cuda_bindings/docs/source/install.rst index 58a6a0f31c..d647360ff2 100644 --- a/cuda_bindings/docs/source/install.rst +++ b/cuda_bindings/docs/source/install.rst @@ -87,11 +87,11 @@ Requirements [^2]: The CUDA Runtime static library (``libcudart_static.a`` on Linux, ``cudart_static.lib`` on Windows) is part of the CUDA Toolkit. If using conda packages, it is contained in the ``cuda-cudart-static`` package. -Source builds require that the provided CUDA headers are of the same major.minor version as the ``cuda.bindings`` you're trying to build. Despite this requirement, note that the minor version compatibility is still maintained. Use the ``CUDA_HOME`` (or ``CUDA_PATH``) environment variable to specify the location of your headers. For example, if your headers are located in ``/usr/local/cuda/include``, then you should set ``CUDA_HOME`` with: +Source builds require that the provided CUDA headers are of the same major.minor version as the ``cuda.bindings`` you're trying to build. Despite this requirement, note that the minor version compatibility is still maintained. Use the ``CUDA_PATH`` (or ``CUDA_HOME``) environment variable to specify the location of your headers. If both are set, ``CUDA_PATH`` takes precedence (see :py:data:`cuda.pathfinder._utils.env_vars.CUDA_ENV_VARS_ORDERED`). For example, if your headers are located in ``/usr/local/cuda/include``, then you should set ``CUDA_PATH`` with: .. code-block:: console - $ export CUDA_HOME=/usr/local/cuda + $ export CUDA_PATH=/usr/local/cuda See `Environment Variables `_ for a description of other build-time environment variables. diff --git a/cuda_bindings/pyproject.toml b/cuda_bindings/pyproject.toml index 96cfb4dd07..ae625db2db 100644 --- a/cuda_bindings/pyproject.toml +++ b/cuda_bindings/pyproject.toml @@ -6,6 +6,7 @@ requires = [ "setuptools_scm[simple]>=8", "cython>=3.2,<3.3", "pyclibrary>=0.1.7", + "cuda-pathfinder", ] build-backend = "build_hooks" backend-path = ["."] diff --git a/cuda_core/README.md b/cuda_core/README.md index 9925511ef9..aa2d1c3fbe 100644 --- a/cuda_core/README.md +++ b/cuda_core/README.md @@ -26,7 +26,7 @@ Alternatively, from the repository root you can use a simple script: Cython tests are located in `tests/cython` and need to be built. These builds have the same CUDA Toolkit header requirements as [those of cuda.bindings](https://nvidia.github.io/cuda-python/cuda-bindings/latest/install.html#requirements) where the major.minor version must match `cuda.bindings`. To build them: -1. Set up environment variable `CUDA_HOME` with the path to the CUDA Toolkit installation. +1. Set up environment variable `CUDA_PATH` (or `CUDA_HOME`) with the path to the CUDA Toolkit installation. Note: If both are set, `CUDA_PATH` takes precedence (see `cuda.pathfinder._utils.env_vars.CUDA_ENV_VARS_ORDERED`). 2. Run `build_tests` script located in `tests/cython` appropriate to your platform. This will both cythonize the tests and build them. To run these tests: diff --git a/cuda_core/pyproject.toml b/cuda_core/pyproject.toml index 9b3e5a37c5..438d171ec2 100644 --- a/cuda_core/pyproject.toml +++ b/cuda_core/pyproject.toml @@ -6,7 +6,8 @@ requires = [ "setuptools>=80", "setuptools-scm[simple]>=8", - "Cython>=3.2,<3.3" + "Cython>=3.2,<3.3", + "cuda-pathfinder" ] build-backend = "build_hooks" backend-path = ["."] diff --git a/cuda_core/tests/helpers/__init__.py b/cuda_core/tests/helpers/__init__.py index efe41f7015..8a08fa1f04 100644 --- a/cuda_core/tests/helpers/__init__.py +++ b/cuda_core/tests/helpers/__init__.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 import functools @@ -6,9 +6,10 @@ from typing import Union from cuda.core._utils.cuda_utils import handle_return +from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path from cuda_python_test_helpers import * -CUDA_PATH = os.environ.get("CUDA_PATH") +CUDA_PATH = get_cuda_home_or_path() CUDA_INCLUDE_PATH = None CCCL_INCLUDE_PATHS = None if CUDA_PATH is not None: diff --git a/cuda_core/tests/test_build_hooks.py b/cuda_core/tests/test_build_hooks.py index 419efbe065..4bca00f789 100644 --- a/cuda_core/tests/test_build_hooks.py +++ b/cuda_core/tests/test_build_hooks.py @@ -130,3 +130,29 @@ def test_missing_cuda_path_raises_error(self): pytest.raises(RuntimeError, match="CUDA_PATH or CUDA_HOME"), ): build_hooks._determine_cuda_major_version() + + def test_multiple_cuda_paths(self): + """Multiple CUDA paths separated by os.pathsep are correctly handled.""" + with tempfile.TemporaryDirectory() as tmpdir1, tempfile.TemporaryDirectory() as tmpdir2: + # Create mock CUDA installations in both directories + for tmpdir in [tmpdir1, tmpdir2]: + include_dir = Path(tmpdir) / "include" + include_dir.mkdir() + cuda_h = include_dir / "cuda.h" + cuda_h.write_text("#define CUDA_VERSION 12080\n") + + build_hooks._get_cuda_paths.cache_clear() + build_hooks._determine_cuda_major_version.cache_clear() + + # Set CUDA_PATH with multiple paths + multiple_paths = os.pathsep.join([tmpdir1, tmpdir2]) + with mock.patch.dict(os.environ, {"CUDA_PATH": multiple_paths}, clear=True): + # Should return list of both paths + paths = build_hooks._get_cuda_paths() + assert len(paths) == 2 + assert paths[0] == tmpdir1 + assert paths[1] == tmpdir2 + + # Version detection should use first path + result = build_hooks._determine_cuda_major_version() + assert result == "12" diff --git a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_nvidia_dynamic_lib.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_nvidia_dynamic_lib.py index acf9bd62d7..f8df1f75e4 100644 --- a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_nvidia_dynamic_lib.py +++ b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_nvidia_dynamic_lib.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations @@ -50,7 +50,7 @@ # Driver libraries: shipped with the NVIDIA display driver, always on the # system linker path. These skip all CTK search steps (site-packages, -# conda, CUDA_HOME, canary) and go straight to system search. +# conda, CUDA_PATH, canary) and go straight to system search. _DRIVER_ONLY_LIBNAMES = frozenset(name for name, desc in LIB_DESCRIPTORS.items() if desc.packaged_with == "driver") @@ -60,7 +60,7 @@ def _load_driver_lib_no_cache(desc: LibDescriptor) -> LoadedDL: Driver libs (libcuda, libnvidia-ml) are part of the display driver, not the CUDA Toolkit. They are expected to be discoverable via the platform's native loader mechanisms, so the full CTK search cascade (site-packages, - conda, CUDA_HOME, canary) is unnecessary. + conda, CUDA_PATH, canary) is unnecessary. """ loaded = LOADER.check_if_already_loaded_from_elsewhere(desc, False) if loaded is not None: @@ -246,7 +246,7 @@ def load_nvidia_dynamic_lib(libname: str) -> LoadedDL: 4. **Environment variables** - - If set, use ``CUDA_HOME`` or ``CUDA_PATH`` (in that order). + - If set, use ``CUDA_PATH`` or ``CUDA_HOME`` (in that order). On Windows, this is the typical way system-installed CTK DLLs are located. Note that the NVIDIA CTK installer automatically adds ``CUDA_PATH`` to the system-wide environment. @@ -269,7 +269,7 @@ def load_nvidia_dynamic_lib(libname: str) -> LoadedDL: 0. Already loaded in the current process 1. OS default mechanisms (``dlopen`` / ``LoadLibraryExW``) - The CTK-specific steps (site-packages, conda, ``CUDA_HOME``, canary + The CTK-specific steps (site-packages, conda, ``CUDA_PATH``, canary probe) are skipped entirely. Notes: diff --git a/cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py b/cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py index 6e2ae100d8..04c608a771 100644 --- a/cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py +++ b/cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py @@ -101,7 +101,7 @@ def find_in_conda(desc: HeaderDescriptor) -> LocatedHeaderDir | None: def find_in_cuda_home(desc: HeaderDescriptor) -> LocatedHeaderDir | None: - """Search ``$CUDA_HOME`` / ``$CUDA_PATH``.""" + """Search ``$CUDA_PATH`` / ``$CUDA_HOME``.""" cuda_home = get_cuda_home_or_path() if cuda_home is None: return None @@ -189,7 +189,7 @@ def locate_nvidia_header_directory(libname: str) -> LocatedHeaderDir | None: Search order: 1. **NVIDIA Python wheels** — site-packages directories from the descriptor. 2. **Conda environments** — platform-specific conda include layouts. - 3. **CUDA Toolkit environment variables** — ``CUDA_HOME`` / ``CUDA_PATH``. + 3. **CUDA Toolkit environment variables** — ``CUDA_PATH`` / ``CUDA_HOME``. 4. **CTK root canary probe** — subprocess canary (descriptors with ``use_ctk_root_canary=True`` only). 5. **System install directories** — glob patterns from the descriptor. @@ -217,7 +217,7 @@ def find_nvidia_header_directory(libname: str) -> str | None: Search order: 1. **NVIDIA Python wheels** — site-packages directories from the descriptor. 2. **Conda environments** — platform-specific conda include layouts. - 3. **CUDA Toolkit environment variables** — ``CUDA_HOME`` / ``CUDA_PATH``. + 3. **CUDA Toolkit environment variables** — ``CUDA_PATH`` / ``CUDA_HOME``. 4. **CTK root canary probe** — subprocess canary (descriptors with ``use_ctk_root_canary=True`` only). 5. **System install directories** — glob patterns from the descriptor. diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py b/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py index cf78a627cb..710fcafd38 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py @@ -1,9 +1,43 @@ # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 +"""Centralized CUDA environment variable handling. + +This module defines the canonical search order for CUDA Toolkit environment variables +used throughout cuda-python packages (cuda.pathfinder, cuda.core, cuda.bindings). + +Search Order Priority: + 1. CUDA_PATH (higher priority) + 2. CUDA_HOME (lower priority) + +If both are set and differ, CUDA_PATH takes precedence and a warning is issued. + +Important Note on Caching: + The result of get_cuda_home_or_path() is cached for the process lifetime. The first + call determines the CUDA Toolkit path, and all subsequent calls return the cached + value, even if environment variables change later. This ensures consistent behavior + throughout the application lifecycle. +""" + +import functools import os import warnings +#: Canonical search order for CUDA Toolkit environment variables. +#: +#: This tuple defines the priority order used by :py:func:`get_cuda_home_or_path` +#: and throughout cuda-python packages when determining which CUDA Toolkit to use. +#: +#: The first variable in the tuple has the highest priority. If multiple variables are set +#: and point to different locations, the first one is used and a warning is issued. +#: +#: .. note:: +#: **Breaking Change in v1.4.0**: The order changed from ``("CUDA_HOME", "CUDA_PATH")`` +#: to ``("CUDA_PATH", "CUDA_HOME")``, making ``CUDA_PATH`` the highest priority. +#: +#: :type: tuple[str, ...] +CUDA_ENV_VARS_ORDERED = ("CUDA_PATH", "CUDA_HOME") + def _paths_differ(a: str, b: str) -> bool: """ @@ -32,20 +66,68 @@ def _paths_differ(a: str, b: str) -> bool: return True +@functools.cache def get_cuda_home_or_path() -> str | None: - cuda_home = os.environ.get("CUDA_HOME") - cuda_path = os.environ.get("CUDA_PATH") - - if cuda_home and cuda_path and _paths_differ(cuda_home, cuda_path): - warnings.warn( - "Both CUDA_HOME and CUDA_PATH are set but differ:\n" - f" CUDA_HOME={cuda_home}\n" - f" CUDA_PATH={cuda_path}\n" - "Using CUDA_HOME (higher priority).", - UserWarning, - stacklevel=2, - ) - - if cuda_home is not None: - return cuda_home - return cuda_path + """Get CUDA Toolkit path from environment variables. + + Returns the value of CUDA_PATH or CUDA_HOME following the canonical search order + defined in CUDA_ENV_VARS_ORDERED. If both are set and differ, CUDA_PATH takes + precedence and a warning is issued. + + The result is cached for the process lifetime. The first call determines the CUDA + Toolkit path, and subsequent calls return the cached value. + + Returns: + Path to CUDA Toolkit, or None if neither variable is set. Empty strings are + preserved and returned as-is if explicitly set in the environment. + + Warnings: + UserWarning: If multiple CUDA environment variables are set but point to + different locations (only on the first call). + + See Also: + CUDA_ENV_VARS_ORDERED: The canonical search order for CUDA environment variables. + """ + # Collect all set environment variables in priority order + # Note: We check 'is not None' to preserve empty strings (which are valid but unusual). + # Empty strings are falsy in Python but may indicate an intentional "unset" by the user. + set_vars = {} + for var in CUDA_ENV_VARS_ORDERED: + val = os.environ.get(var) + if val is not None: + set_vars[var] = val + + if not set_vars: + return None + + # If multiple variables are set, check if they differ and warn + if len(set_vars) > 1: + # Check if any non-empty values actually differ + non_empty_values = [(var, val) for var, val in set_vars.items() if val] + + if len(non_empty_values) > 1: + # Check if any pair of non-empty values differs + values_differ = False + for i in range(len(non_empty_values) - 1): + if _paths_differ(non_empty_values[i][1], non_empty_values[i + 1][1]): + values_differ = True + break + + if values_differ: + # Build a generic warning message that works for any number of variables + var_list = "\n".join(f" {var}={val}" for var, val in set_vars.items()) + highest_priority = CUDA_ENV_VARS_ORDERED[0] + warnings.warn( + f"Multiple CUDA environment variables are set but differ:\n" + f"{var_list}\n" + f"Using {highest_priority} (highest priority as defined in CUDA_ENV_VARS_ORDERED).", + UserWarning, + stacklevel=2, + ) + + # Return the first (highest priority) set variable + for var in CUDA_ENV_VARS_ORDERED: + if var in set_vars: + return set_vars[var] + + return None diff --git a/cuda_pathfinder/docs/source/api.rst b/cuda_pathfinder/docs/source/api.rst index 5e842330a2..19a4b43b3a 100644 --- a/cuda_pathfinder/docs/source/api.rst +++ b/cuda_pathfinder/docs/source/api.rst @@ -1,4 +1,4 @@ -.. SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. .. SPDX-License-Identifier: Apache-2.0 .. module:: cuda.pathfinder @@ -42,3 +42,19 @@ CUDA bitcode and static libraries. locate_static_lib LocatedStaticLib StaticLibNotFoundError + +Environment Variable Utilities +------------------------------- + +The ``cuda.pathfinder._utils.env_vars`` module provides centralized handling of CUDA +environment variables used across all cuda-python packages. + +.. autosummary:: + :toctree: generated/ + + _utils.env_vars.get_cuda_home_or_path + _utils.env_vars.CUDA_ENV_VARS_ORDERED + +.. autofunction:: cuda.pathfinder._utils.env_vars.get_cuda_home_or_path + +.. autodata:: cuda.pathfinder._utils.env_vars.CUDA_ENV_VARS_ORDERED diff --git a/cuda_pathfinder/docs/source/release/1.5.0-notes.rst b/cuda_pathfinder/docs/source/release/1.5.0-notes.rst new file mode 100644 index 0000000000..9867149315 --- /dev/null +++ b/cuda_pathfinder/docs/source/release/1.5.0-notes.rst @@ -0,0 +1,47 @@ +.. SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. SPDX-License-Identifier: Apache-2.0 + +.. py:currentmodule:: cuda.pathfinder + +``cuda-pathfinder`` 1.5.0 Release notes +======================================= + +Highlights +---------- + +Breaking Changes +~~~~~~~~~~~~~~~~ + +* **CUDA environment variable priority changed**: ``CUDA_PATH`` now takes precedence over ``CUDA_HOME`` when both are set. Previously, ``CUDA_HOME`` had higher priority. If both variables are set and point to different locations, a warning will be issued and ``CUDA_PATH`` will be used. This change aligns with industry standards and NVIDIA's recommended practices. + + **Migration Guide**: + + - If you rely on ``CUDA_HOME``, consider switching to ``CUDA_PATH`` + - If you set both variables, ensure they point to the same CUDA Toolkit installation + - If they differ intentionally, be aware that ``CUDA_PATH`` will now be used + - The canonical search order is defined in :py:data:`cuda.pathfinder._utils.env_vars.CUDA_ENV_VARS_ORDERED` + +New Features +~~~~~~~~~~~~ + +* Added centralized CUDA environment variable handling with :py:func:`cuda.pathfinder._utils.env_vars.get_cuda_home_or_path()`. This function provides: + + - Consistent behavior across all cuda-python packages (cuda.pathfinder, cuda.core, cuda.bindings) + - Intelligent path comparison that handles symlinks, case sensitivity, and trailing slashes + - Result caching for performance (first call determines the path for the process lifetime) + - Clear warnings when multiple environment variables are set but differ + +* Added :py:data:`cuda.pathfinder._utils.env_vars.CUDA_ENV_VARS_ORDERED` constant that explicitly documents the canonical search order for CUDA environment variables. + +Documentation +~~~~~~~~~~~~~ + +* Updated documentation across all packages to reflect the new ``CUDA_PATH`` priority +* Added detailed caching behavior documentation for :py:func:`cuda.pathfinder._utils.env_vars.get_cuda_home_or_path()` +* Clarified environment variable precedence in installation guides + +Bug Fixes +~~~~~~~~~ + +* Improved robustness of test collection by adding fallback when cuda.pathfinder is not yet installed +* Enhanced path comparison logic to properly handle edge cases (nonexistent paths, symlinks, OS-specific behavior) diff --git a/cuda_pathfinder/tests/test_utils_env_vars.py b/cuda_pathfinder/tests/test_utils_env_vars.py index 40c7d4930d..789aa868dd 100644 --- a/cuda_pathfinder/tests/test_utils_env_vars.py +++ b/cuda_pathfinder/tests/test_utils_env_vars.py @@ -8,7 +8,11 @@ import pytest -from cuda.pathfinder._utils.env_vars import _paths_differ, get_cuda_home_or_path +from cuda.pathfinder._utils.env_vars import ( + CUDA_ENV_VARS_ORDERED, + _paths_differ, + get_cuda_home_or_path, +) skip_symlink_tests = pytest.mark.skipif( sys.platform == "win32", @@ -20,6 +24,8 @@ def unset_env(monkeypatch): """Helper to clear both env vars for each test.""" monkeypatch.delenv("CUDA_HOME", raising=False) monkeypatch.delenv("CUDA_PATH", raising=False) + # Clear the cache so each test gets fresh behavior + get_cuda_home_or_path.cache_clear() def test_returns_none_when_unset(monkeypatch): @@ -27,14 +33,15 @@ def test_returns_none_when_unset(monkeypatch): assert get_cuda_home_or_path() is None -def test_empty_cuda_home_preserved(monkeypatch): +def test_empty_cuda_path_preserved(monkeypatch): # empty string is returned as-is if set. - monkeypatch.setenv("CUDA_HOME", "") - monkeypatch.setenv("CUDA_PATH", "/does/not/matter") + unset_env(monkeypatch) + monkeypatch.setenv("CUDA_PATH", "") + monkeypatch.setenv("CUDA_HOME", "/does/not/matter") assert get_cuda_home_or_path() == "" -def test_prefers_cuda_home_over_cuda_path(monkeypatch, tmp_path): +def test_prefers_cuda_path_over_cuda_home(monkeypatch, tmp_path): unset_env(monkeypatch) home = tmp_path / "home" path = tmp_path / "path" @@ -44,18 +51,18 @@ def test_prefers_cuda_home_over_cuda_path(monkeypatch, tmp_path): monkeypatch.setenv("CUDA_HOME", str(home)) monkeypatch.setenv("CUDA_PATH", str(path)) - # Different directories -> warning + prefer CUDA_HOME - with pytest.warns(UserWarning, match="Both CUDA_HOME and CUDA_PATH are set but differ"): + # Different directories -> warning + prefer CUDA_PATH + with pytest.warns(UserWarning, match="Multiple CUDA environment variables are set but differ"): result = get_cuda_home_or_path() - assert pathlib.Path(result) == home + assert pathlib.Path(result) == path -def test_uses_cuda_path_if_home_missing(monkeypatch, tmp_path): +def test_uses_cuda_home_if_path_missing(monkeypatch, tmp_path): unset_env(monkeypatch) - only_path = tmp_path / "path" - only_path.mkdir() - monkeypatch.setenv("CUDA_PATH", str(only_path)) - assert pathlib.Path(get_cuda_home_or_path()) == only_path + only_home = tmp_path / "home" + only_home.mkdir() + monkeypatch.setenv("CUDA_HOME", str(only_home)) + assert pathlib.Path(get_cuda_home_or_path()) == only_home def test_no_warning_when_textually_equal_after_normalization(monkeypatch, tmp_path): @@ -68,8 +75,8 @@ def test_no_warning_when_textually_equal_after_normalization(monkeypatch, tmp_pa d.mkdir() with_slash = str(d) + ("/" if os.sep == "/" else "\\") - monkeypatch.setenv("CUDA_HOME", str(d)) - monkeypatch.setenv("CUDA_PATH", with_slash) + monkeypatch.setenv("CUDA_PATH", str(d)) + monkeypatch.setenv("CUDA_HOME", with_slash) # No warning; same logical directory with warnings.catch_warnings(record=True) as record: @@ -89,8 +96,8 @@ def test_no_warning_on_windows_case_only_difference(monkeypatch, tmp_path): upper = str(d).upper() lower = str(d).lower() - monkeypatch.setenv("CUDA_HOME", upper) - monkeypatch.setenv("CUDA_PATH", lower) + monkeypatch.setenv("CUDA_PATH", upper) + monkeypatch.setenv("CUDA_HOME", lower) with warnings.catch_warnings(record=True) as record: warnings.simplefilter("always") @@ -106,11 +113,11 @@ def test_warning_when_both_exist_and_are_different(monkeypatch, tmp_path): a.mkdir() b.mkdir() - monkeypatch.setenv("CUDA_HOME", str(a)) - monkeypatch.setenv("CUDA_PATH", str(b)) + monkeypatch.setenv("CUDA_PATH", str(a)) + monkeypatch.setenv("CUDA_HOME", str(b)) # Different actual dirs -> warning - with pytest.warns(UserWarning, match="Both CUDA_HOME and CUDA_PATH are set but differ"): + with pytest.warns(UserWarning, match="Multiple CUDA environment variables are set but differ"): result = get_cuda_home_or_path() assert pathlib.Path(result) == a @@ -124,10 +131,10 @@ def test_nonexistent_paths_fall_back_to_text_comparison(monkeypatch, tmp_path): a = tmp_path / "does_not_exist_a" b = tmp_path / "does_not_exist_b" - monkeypatch.setenv("CUDA_HOME", str(a)) - monkeypatch.setenv("CUDA_PATH", str(b)) + monkeypatch.setenv("CUDA_PATH", str(a)) + monkeypatch.setenv("CUDA_HOME", str(b)) - with pytest.warns(UserWarning, match="Both CUDA_HOME and CUDA_PATH are set but differ"): + with pytest.warns(UserWarning, match="Multiple CUDA environment variables are set but differ"): result = get_cuda_home_or_path() assert pathlib.Path(result) == a @@ -146,8 +153,8 @@ def test_samefile_equivalence_via_symlink_when_possible(monkeypatch, tmp_path): os.symlink(str(real_dir), str(link_dir), target_is_directory=True) # Set env vars to real and alias - monkeypatch.setenv("CUDA_HOME", str(real_dir)) - monkeypatch.setenv("CUDA_PATH", str(link_dir)) + monkeypatch.setenv("CUDA_PATH", str(real_dir)) + monkeypatch.setenv("CUDA_HOME", str(link_dir)) # Because they resolve to the same entry, no warning should be raised with warnings.catch_warnings(record=True) as record: @@ -157,6 +164,41 @@ def test_samefile_equivalence_via_symlink_when_possible(monkeypatch, tmp_path): assert len(record) == 0 +def test_cuda_env_vars_ordered_constant(): + """ + Verify the canonical search order constant is defined correctly. + CUDA_PATH must have higher priority than CUDA_HOME. + """ + assert CUDA_ENV_VARS_ORDERED == ("CUDA_PATH", "CUDA_HOME") + assert CUDA_ENV_VARS_ORDERED[0] == "CUDA_PATH" # highest priority + assert CUDA_ENV_VARS_ORDERED[1] == "CUDA_HOME" # lower priority + + +def test_search_order_matches_implementation(monkeypatch, tmp_path): + """ + Verify that get_cuda_home_or_path() follows the documented search order. + """ + unset_env(monkeypatch) + path_dir = tmp_path / "path_dir" + home_dir = tmp_path / "home_dir" + path_dir.mkdir() + home_dir.mkdir() + + # Set both env vars to different values + monkeypatch.setenv("CUDA_PATH", str(path_dir)) + monkeypatch.setenv("CUDA_HOME", str(home_dir)) + + # The result should match the first (highest priority) variable in CUDA_ENV_VARS_ORDERED + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + result = get_cuda_home_or_path() + + highest_priority_var = CUDA_ENV_VARS_ORDERED[0] + expected = os.environ.get(highest_priority_var) + assert result == expected + assert pathlib.Path(result) == path_dir # CUDA_PATH should win + + # --- unit tests for the helper itself (optional but nice to have) --- @@ -179,3 +221,36 @@ def test_paths_differ_samefile(tmp_path): # Should detect equivalence via samefile assert _paths_differ(str(real_dir), str(alias)) is False + + +def test_caching_behavior(monkeypatch, tmp_path): + """ + Verify that get_cuda_home_or_path() caches the result and returns the same + value even if environment variables change after the first call. + """ + unset_env(monkeypatch) + + first_dir = tmp_path / "first" + second_dir = tmp_path / "second" + first_dir.mkdir() + second_dir.mkdir() + + # Set initial value + monkeypatch.setenv("CUDA_PATH", str(first_dir)) + + # First call should return first_dir + result1 = get_cuda_home_or_path() + assert pathlib.Path(result1) == first_dir + + # Change the environment variable + monkeypatch.setenv("CUDA_PATH", str(second_dir)) + + # Second call should still return first_dir (cached value) + result2 = get_cuda_home_or_path() + assert pathlib.Path(result2) == first_dir + assert result1 == result2 + + # After clearing cache, should get new value + get_cuda_home_or_path.cache_clear() + result3 = get_cuda_home_or_path() + assert pathlib.Path(result3) == second_dir From 2164c33aeab77aabe33e0591a75a23604cd00da3 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 20 Mar 2026 12:32:17 -0700 Subject: [PATCH 02/15] replace _get_cuda_paths() with _get_cuda_path() using pathfinder Drop os.pathsep splitting of CUDA_PATH/CUDA_HOME in both build_hooks.py files. Both functions now delegate to get_cuda_home_or_path() from cuda.pathfinder, returning a single path. See https://github.com/NVIDIA/cuda-python/pull/1801#issuecomment-4100499549 Made-with: Cursor --- cuda_bindings/build_hooks.py | 25 +++++++++++----------- cuda_core/build_hooks.py | 40 ++++++++++++++++++------------------ 2 files changed, 33 insertions(+), 32 deletions(-) diff --git a/cuda_bindings/build_hooks.py b/cuda_bindings/build_hooks.py index b2d3029c69..7f2987c618 100644 --- a/cuda_bindings/build_hooks.py +++ b/cuda_bindings/build_hooks.py @@ -34,13 +34,14 @@ @functools.cache -def _get_cuda_paths() -> list[str]: - CUDA_HOME = os.environ.get("CUDA_HOME", os.environ.get("CUDA_PATH", None)) - if not CUDA_HOME: - raise RuntimeError("Environment variable CUDA_HOME or CUDA_PATH is not set") - CUDA_HOME = CUDA_HOME.split(os.pathsep) - print("CUDA paths:", CUDA_HOME) - return CUDA_HOME +def _get_cuda_path() -> str: + from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path + + cuda_path = get_cuda_home_or_path() + if not cuda_path: + raise RuntimeError("Environment variable CUDA_PATH or CUDA_HOME is not set") + print("CUDA path:", cuda_path) + return cuda_path # ----------------------------------------------------------------------- @@ -133,8 +134,8 @@ def _fetch_header_paths(required_headers, include_path_list): if missing_headers: error_message = "Couldn't find required headers: " error_message += ", ".join(missing_headers) - cuda_paths = _get_cuda_paths() - raise RuntimeError(f'{error_message}\nIs CUDA_HOME setup correctly? (CUDA_HOME="{cuda_paths}")') + cuda_path = _get_cuda_path() + raise RuntimeError(f'{error_message}\nIs CUDA_PATH setup correctly? (CUDA_PATH="{cuda_path}")') return header_dict @@ -291,7 +292,7 @@ def _build_cuda_bindings(strip=False): global _extensions - cuda_paths = _get_cuda_paths() + cuda_path = _get_cuda_path() if os.environ.get("PARALLEL_LEVEL") is not None: warn( @@ -307,7 +308,7 @@ def _build_cuda_bindings(strip=False): compile_for_coverage = bool(int(os.environ.get("CUDA_PYTHON_COVERAGE", "0"))) # Parse CUDA headers - include_path_list = [os.path.join(path, "include") for path in cuda_paths] + include_path_list = [os.path.join(cuda_path, "include")] header_dict = _fetch_header_paths(_REQUIRED_HEADERS, include_path_list) found_types, found_functions, found_values, found_struct, struct_list = _parse_headers( header_dict, include_path_list, parser_caching @@ -347,7 +348,7 @@ def _build_cuda_bindings(strip=False): ] + include_path_list library_dirs = [sysconfig.get_path("platlib"), os.path.join(os.sys.prefix, "lib")] cudalib_subdirs = [r"lib\x64"] if sys.platform == "win32" else ["lib64", "lib"] - library_dirs.extend(os.path.join(prefix, subdir) for prefix in cuda_paths for subdir in cudalib_subdirs) + library_dirs.extend(os.path.join(cuda_path, subdir) for subdir in cudalib_subdirs) extra_compile_args = [] extra_link_args = [] diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index a98a33b6fb..d66093a862 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -29,12 +29,13 @@ @functools.cache -def _get_cuda_paths() -> list[str]: - cuda_path = os.environ.get("CUDA_PATH", os.environ.get("CUDA_HOME", None)) +def _get_cuda_path() -> str: + from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path + + cuda_path = get_cuda_home_or_path() if not cuda_path: raise RuntimeError("Environment variable CUDA_PATH or CUDA_HOME is not set") - cuda_path = cuda_path.split(os.pathsep) - print("CUDA paths:", cuda_path) + print("CUDA path:", cuda_path) return cuda_path @@ -60,21 +61,20 @@ def _determine_cuda_major_version() -> str: return cuda_major # Derive from the CUDA headers (the authoritative source for what we compile against). - cuda_path = _get_cuda_paths() - for root in cuda_path: - cuda_h = os.path.join(root, "include", "cuda.h") - try: - with open(cuda_h, encoding="utf-8") as f: - for line in f: - m = re.match(r"^#\s*define\s+CUDA_VERSION\s+(\d+)\s*$", line) - if m: - v = int(m.group(1)) - # CUDA_VERSION is e.g. 12020 for 12.2. - cuda_major = str(v // 1000) - print("CUDA MAJOR VERSION:", cuda_major) - return cuda_major - except OSError: - continue + cuda_path = _get_cuda_path() + cuda_h = os.path.join(cuda_path, "include", "cuda.h") + try: + with open(cuda_h, encoding="utf-8") as f: + for line in f: + m = re.match(r"^#\s*define\s+CUDA_VERSION\s+(\d+)\s*$", line) + if m: + v = int(m.group(1)) + # CUDA_VERSION is e.g. 12020 for 12.2. + cuda_major = str(v // 1000) + print("CUDA MAJOR VERSION:", cuda_major) + return cuda_major + except OSError: + pass # CUDA_PATH or CUDA_HOME is required for the build, so we should not reach here # in normal circumstances. Raise an error to make the issue clear. @@ -132,7 +132,7 @@ def get_sources(mod_name): return sources - all_include_dirs = [os.path.join(root, "include") for root in _get_cuda_paths()] + all_include_dirs = [os.path.join(_get_cuda_path(), "include")] extra_compile_args = [] if COMPILE_FOR_COVERAGE: # CYTHON_TRACE_NOGIL indicates to trace nogil functions. It is not From c00434265944982570c777adc9083d4b7d218a19 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 20 Mar 2026 12:49:26 -0700 Subject: [PATCH 03/15] treat empty env vars as undefined in get_cuda_home_or_path() See https://github.com/NVIDIA/cuda-python/pull/1801#issuecomment-4100591498 for the rationale Made-with: Cursor --- .../cuda/pathfinder/_utils/env_vars.py | 55 ++++++++----------- cuda_pathfinder/tests/test_utils_env_vars.py | 14 +++-- 2 files changed, 32 insertions(+), 37 deletions(-) diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py b/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py index 710fcafd38..a7cba0fc9a 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py @@ -78,8 +78,7 @@ def get_cuda_home_or_path() -> str | None: Toolkit path, and subsequent calls return the cached value. Returns: - Path to CUDA Toolkit, or None if neither variable is set. Empty strings are - preserved and returned as-is if explicitly set in the environment. + Path to CUDA Toolkit, or None if neither variable is set or all are empty. Warnings: UserWarning: If multiple CUDA environment variables are set but point to @@ -88,13 +87,12 @@ def get_cuda_home_or_path() -> str | None: See Also: CUDA_ENV_VARS_ORDERED: The canonical search order for CUDA environment variables. """ - # Collect all set environment variables in priority order - # Note: We check 'is not None' to preserve empty strings (which are valid but unusual). - # Empty strings are falsy in Python but may indicate an intentional "unset" by the user. + # Collect non-empty environment variables in priority order. + # Empty strings are treated as undefined — no valid CUDA path is empty. set_vars = {} for var in CUDA_ENV_VARS_ORDERED: val = os.environ.get(var) - if val is not None: + if val: set_vars[var] = val if not set_vars: @@ -102,32 +100,23 @@ def get_cuda_home_or_path() -> str | None: # If multiple variables are set, check if they differ and warn if len(set_vars) > 1: - # Check if any non-empty values actually differ - non_empty_values = [(var, val) for var, val in set_vars.items() if val] - - if len(non_empty_values) > 1: - # Check if any pair of non-empty values differs - values_differ = False - for i in range(len(non_empty_values) - 1): - if _paths_differ(non_empty_values[i][1], non_empty_values[i + 1][1]): - values_differ = True - break - - if values_differ: - # Build a generic warning message that works for any number of variables - var_list = "\n".join(f" {var}={val}" for var, val in set_vars.items()) - highest_priority = CUDA_ENV_VARS_ORDERED[0] - warnings.warn( - f"Multiple CUDA environment variables are set but differ:\n" - f"{var_list}\n" - f"Using {highest_priority} (highest priority as defined in CUDA_ENV_VARS_ORDERED).", - UserWarning, - stacklevel=2, - ) + values = list(set_vars.items()) + values_differ = False + for i in range(len(values) - 1): + if _paths_differ(values[i][1], values[i + 1][1]): + values_differ = True + break + + if values_differ: + var_list = "\n".join(f" {var}={val}" for var, val in set_vars.items()) + highest_priority = CUDA_ENV_VARS_ORDERED[0] + warnings.warn( + f"Multiple CUDA environment variables are set but differ:\n" + f"{var_list}\n" + f"Using {highest_priority} (highest priority as defined in CUDA_ENV_VARS_ORDERED).", + UserWarning, + stacklevel=2, + ) # Return the first (highest priority) set variable - for var in CUDA_ENV_VARS_ORDERED: - if var in set_vars: - return set_vars[var] - - return None + return next(iter(set_vars.values())) diff --git a/cuda_pathfinder/tests/test_utils_env_vars.py b/cuda_pathfinder/tests/test_utils_env_vars.py index 789aa868dd..cec986c84a 100644 --- a/cuda_pathfinder/tests/test_utils_env_vars.py +++ b/cuda_pathfinder/tests/test_utils_env_vars.py @@ -33,12 +33,18 @@ def test_returns_none_when_unset(monkeypatch): assert get_cuda_home_or_path() is None -def test_empty_cuda_path_preserved(monkeypatch): - # empty string is returned as-is if set. +def test_empty_cuda_path_falls_through(monkeypatch): unset_env(monkeypatch) monkeypatch.setenv("CUDA_PATH", "") - monkeypatch.setenv("CUDA_HOME", "/does/not/matter") - assert get_cuda_home_or_path() == "" + monkeypatch.setenv("CUDA_HOME", "/usr/local/cuda") + assert get_cuda_home_or_path() == "/usr/local/cuda" + + +def test_all_empty_returns_none(monkeypatch): + unset_env(monkeypatch) + monkeypatch.setenv("CUDA_PATH", "") + monkeypatch.setenv("CUDA_HOME", "") + assert get_cuda_home_or_path() is None def test_prefers_cuda_path_over_cuda_home(monkeypatch, tmp_path): From 3ba64aef9e2e3b82ecf97c26f308999937b412c2 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 20 Mar 2026 14:09:57 -0700 Subject: [PATCH 04/15] fix(pathfinder): clear get_cuda_home_or_path cache in test fixtures Made-with: Cursor --- cuda_pathfinder/tests/test_find_bitcode_lib.py | 3 +++ cuda_pathfinder/tests/test_find_nvidia_headers.py | 3 +++ cuda_pathfinder/tests/test_find_static_lib.py | 3 +++ 3 files changed, 9 insertions(+) diff --git a/cuda_pathfinder/tests/test_find_bitcode_lib.py b/cuda_pathfinder/tests/test_find_bitcode_lib.py index 2da138c31d..5b9343033e 100644 --- a/cuda_pathfinder/tests/test_find_bitcode_lib.py +++ b/cuda_pathfinder/tests/test_find_bitcode_lib.py @@ -13,6 +13,7 @@ find_bitcode_lib, locate_bitcode_lib, ) +from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path STRICTNESS = os.environ.get("CUDA_PATHFINDER_TEST_FIND_NVIDIA_BITCODE_LIB_STRICTNESS", "see_what_works") assert STRICTNESS in ("see_what_works", "all_must_work") @@ -23,8 +24,10 @@ @pytest.fixture def clear_find_bitcode_lib_cache(): find_bitcode_lib_module.find_bitcode_lib.cache_clear() + get_cuda_home_or_path.cache_clear() yield find_bitcode_lib_module.find_bitcode_lib.cache_clear() + get_cuda_home_or_path.cache_clear() def _make_bitcode_lib_file(dir_path: Path) -> str: diff --git a/cuda_pathfinder/tests/test_find_nvidia_headers.py b/cuda_pathfinder/tests/test_find_nvidia_headers.py index 2732de216b..567533d981 100644 --- a/cuda_pathfinder/tests/test_find_nvidia_headers.py +++ b/cuda_pathfinder/tests/test_find_nvidia_headers.py @@ -33,6 +33,7 @@ SUPPORTED_INSTALL_DIRS_NON_CTK, SUPPORTED_SITE_PACKAGE_HEADER_DIRS_CTK, ) +from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path from cuda.pathfinder._utils.platform_aware import IS_WINDOWS STRICTNESS = os.environ.get("CUDA_PATHFINDER_TEST_FIND_NVIDIA_HEADERS_STRICTNESS", "see_what_works") @@ -78,9 +79,11 @@ def have_distribution_for(libname: str) -> bool: def clear_locate_nvidia_header_cache(): locate_nvidia_header_directory.cache_clear() _resolve_system_loaded_abs_path_in_subprocess.cache_clear() + get_cuda_home_or_path.cache_clear() yield locate_nvidia_header_directory.cache_clear() _resolve_system_loaded_abs_path_in_subprocess.cache_clear() + get_cuda_home_or_path.cache_clear() def _create_ctk_header(ctk_root: Path, libname: str) -> str: diff --git a/cuda_pathfinder/tests/test_find_static_lib.py b/cuda_pathfinder/tests/test_find_static_lib.py index 80e593f166..8f793f8c1e 100644 --- a/cuda_pathfinder/tests/test_find_static_lib.py +++ b/cuda_pathfinder/tests/test_find_static_lib.py @@ -13,6 +13,7 @@ find_static_lib, locate_static_lib, ) +from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path from cuda.pathfinder._utils.platform_aware import quote_for_shell STRICTNESS = os.environ.get("CUDA_PATHFINDER_TEST_FIND_NVIDIA_STATIC_LIB_STRICTNESS", "see_what_works") @@ -24,8 +25,10 @@ @pytest.fixture def clear_find_static_lib_cache(): find_static_lib_module.find_static_lib.cache_clear() + get_cuda_home_or_path.cache_clear() yield find_static_lib_module.find_static_lib.cache_clear() + get_cuda_home_or_path.cache_clear() def _make_static_lib_file(dir_path: Path, filename: str) -> str: From 0c6c6559506e2818aca127b7995295bef48ac1e9 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 20 Mar 2026 14:20:40 -0700 Subject: [PATCH 05/15] fix(core): update test_build_hooks for _get_cuda_path rename, drop multi-path test Made-with: Cursor --- cuda_core/tests/test_build_hooks.py | 37 +++++++---------------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/cuda_core/tests/test_build_hooks.py b/cuda_core/tests/test_build_hooks.py index 4bca00f789..845e8673ac 100644 --- a/cuda_core/tests/test_build_hooks.py +++ b/cuda_core/tests/test_build_hooks.py @@ -24,6 +24,8 @@ import pytest +from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path + # build_hooks.py imports Cython and setuptools at the top level, so skip if not available pytest.importorskip("Cython") pytest.importorskip("setuptools") @@ -66,8 +68,9 @@ def _check_version_detection( cuda_h = include_dir / "cuda.h" cuda_h.write_text(f"#define CUDA_VERSION {cuda_version}\n") - build_hooks._get_cuda_paths.cache_clear() + build_hooks._get_cuda_path.cache_clear() build_hooks._determine_cuda_major_version.cache_clear() + get_cuda_home_or_path.cache_clear() mock_env = { k: v @@ -90,8 +93,9 @@ class TestGetCudaMajorVersion: @pytest.mark.parametrize("version", ["11", "12", "13", "14"]) def test_env_var_override(self, version): """CUDA_CORE_BUILD_MAJOR env var override works with various versions.""" - build_hooks._get_cuda_paths.cache_clear() + build_hooks._get_cuda_path.cache_clear() build_hooks._determine_cuda_major_version.cache_clear() + get_cuda_home_or_path.cache_clear() with mock.patch.dict(os.environ, {"CUDA_CORE_BUILD_MAJOR": version}, clear=False): result = build_hooks._determine_cuda_major_version() assert result == version @@ -123,36 +127,11 @@ def test_env_var_takes_priority_over_headers(self): def test_missing_cuda_path_raises_error(self): """RuntimeError is raised when CUDA_PATH/CUDA_HOME not set and no env var override.""" - build_hooks._get_cuda_paths.cache_clear() + build_hooks._get_cuda_path.cache_clear() build_hooks._determine_cuda_major_version.cache_clear() + get_cuda_home_or_path.cache_clear() with ( mock.patch.dict(os.environ, {}, clear=True), pytest.raises(RuntimeError, match="CUDA_PATH or CUDA_HOME"), ): build_hooks._determine_cuda_major_version() - - def test_multiple_cuda_paths(self): - """Multiple CUDA paths separated by os.pathsep are correctly handled.""" - with tempfile.TemporaryDirectory() as tmpdir1, tempfile.TemporaryDirectory() as tmpdir2: - # Create mock CUDA installations in both directories - for tmpdir in [tmpdir1, tmpdir2]: - include_dir = Path(tmpdir) / "include" - include_dir.mkdir() - cuda_h = include_dir / "cuda.h" - cuda_h.write_text("#define CUDA_VERSION 12080\n") - - build_hooks._get_cuda_paths.cache_clear() - build_hooks._determine_cuda_major_version.cache_clear() - - # Set CUDA_PATH with multiple paths - multiple_paths = os.pathsep.join([tmpdir1, tmpdir2]) - with mock.patch.dict(os.environ, {"CUDA_PATH": multiple_paths}, clear=True): - # Should return list of both paths - paths = build_hooks._get_cuda_paths() - assert len(paths) == 2 - assert paths[0] == tmpdir1 - assert paths[1] == tmpdir2 - - # Version detection should use first path - result = build_hooks._determine_cuda_major_version() - assert result == "12" From a4e38d0a0ccbddcdc98173b17ae39b5053b40666 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 20 Mar 2026 14:38:54 -0700 Subject: [PATCH 06/15] refactor(core): use get_cuda_home_or_path() in test conftest skipif Made-with: Cursor --- cuda_core/tests/conftest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cuda_core/tests/conftest.py b/cuda_core/tests/conftest.py index 71d2f30573..462a6db8ca 100644 --- a/cuda_core/tests/conftest.py +++ b/cuda_core/tests/conftest.py @@ -9,6 +9,8 @@ import pytest +from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path + try: from cuda.bindings import driver except ImportError: @@ -253,6 +255,6 @@ def test_something(memory_resource_factory): skipif_need_cuda_headers = pytest.mark.skipif( - not os.path.isdir(os.path.join(os.environ.get("CUDA_PATH", ""), "include")), + get_cuda_home_or_path() is None, reason="need CUDA header", ) From 3cb531e367ce7626e1e0739fabc87d1174f1f37d Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 20 Mar 2026 14:45:39 -0700 Subject: [PATCH 07/15] refactor(core): use get_cuda_home_or_path() in examples Made-with: Cursor --- cuda_core/examples/thread_block_cluster.py | 5 +++-- cuda_core/examples/tma_tensor_map.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cuda_core/examples/thread_block_cluster.py b/cuda_core/examples/thread_block_cluster.py index 495fe882a9..d4a5d38f9e 100644 --- a/cuda_core/examples/thread_block_cluster.py +++ b/cuda_core/examples/thread_block_cluster.py @@ -23,6 +23,7 @@ ProgramOptions, launch, ) +from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path # print cluster info using a kernel and store results in pinned memory code = r""" @@ -65,9 +66,9 @@ def main(): print("This example requires NumPy 2.2.5 or later", file=sys.stderr) sys.exit(1) - cuda_path = os.environ.get("CUDA_PATH", os.environ.get("CUDA_HOME")) + cuda_path = get_cuda_home_or_path() if cuda_path is None: - print("this example requires a valid CUDA_PATH environment variable set", file=sys.stderr) + print("This example requires CUDA_PATH or CUDA_HOME to point to a CUDA toolkit.", file=sys.stderr) sys.exit(1) cuda_include = os.path.join(cuda_path, "include") if not os.path.isdir(cuda_include): diff --git a/cuda_core/examples/tma_tensor_map.py b/cuda_core/examples/tma_tensor_map.py index b914651089..8ddcc647c6 100644 --- a/cuda_core/examples/tma_tensor_map.py +++ b/cuda_core/examples/tma_tensor_map.py @@ -36,6 +36,7 @@ StridedMemoryView, launch, ) +from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path # --------------------------------------------------------------------------- # CUDA kernel that uses TMA to load a 1-D tile into shared memory, then @@ -103,7 +104,7 @@ def _get_cccl_include_paths(): - cuda_path = os.environ.get("CUDA_PATH", os.environ.get("CUDA_HOME")) + cuda_path = get_cuda_home_or_path() if cuda_path is None: print("This example requires CUDA_PATH or CUDA_HOME to point to a CUDA toolkit.", file=sys.stderr) sys.exit(1) From c174cd81f30acaa70e0f7915964509ef5bbe1e5f Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 20 Mar 2026 14:50:25 -0700 Subject: [PATCH 08/15] rename get_cuda_home_or_path -> get_cuda_path_or_home Safe: currently an internal-only API (not yet public). Made-with: Cursor --- conftest.py | 4 +- cuda_bindings/build_hooks.py | 4 +- cuda_core/build_hooks.py | 4 +- cuda_core/examples/thread_block_cluster.py | 4 +- cuda_core/examples/tma_tensor_map.py | 4 +- cuda_core/tests/conftest.py | 4 +- cuda_core/tests/helpers/__init__.py | 4 +- cuda_core/tests/test_build_hooks.py | 8 ++-- .../_binaries/find_nvidia_binary_utility.py | 4 +- .../pathfinder/_dynamic_libs/search_steps.py | 4 +- .../_headers/find_nvidia_headers.py | 4 +- .../_static_libs/find_bitcode_lib.py | 4 +- .../_static_libs/find_static_lib.py | 4 +- .../cuda/pathfinder/_utils/env_vars.py | 6 +-- cuda_pathfinder/docs/source/api.rst | 4 +- .../docs/source/release/1.5.0-notes.rst | 4 +- .../tests/test_ctk_root_discovery.py | 6 +-- .../tests/test_find_bitcode_lib.py | 6 +-- .../tests/test_find_nvidia_binaries.py | 10 ++--- .../tests/test_find_nvidia_headers.py | 6 +-- cuda_pathfinder/tests/test_find_static_lib.py | 6 +-- ...st_load_nvidia_dynamic_lib_using_mocker.py | 6 +-- cuda_pathfinder/tests/test_search_steps.py | 8 ++-- cuda_pathfinder/tests/test_utils_env_vars.py | 38 +++++++++---------- 24 files changed, 78 insertions(+), 78 deletions(-) diff --git a/conftest.py b/conftest.py index c09a182c2c..5f26c9211e 100644 --- a/conftest.py +++ b/conftest.py @@ -4,11 +4,11 @@ import pytest -from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path +from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home def pytest_collection_modifyitems(config, items): # noqa: ARG001 - cuda_home = get_cuda_home_or_path() + cuda_home = get_cuda_path_or_home() for item in items: nodeid = item.nodeid.replace("\\", "/") diff --git a/cuda_bindings/build_hooks.py b/cuda_bindings/build_hooks.py index 7f2987c618..afbd1246a0 100644 --- a/cuda_bindings/build_hooks.py +++ b/cuda_bindings/build_hooks.py @@ -35,9 +35,9 @@ @functools.cache def _get_cuda_path() -> str: - from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path + from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home - cuda_path = get_cuda_home_or_path() + cuda_path = get_cuda_path_or_home() if not cuda_path: raise RuntimeError("Environment variable CUDA_PATH or CUDA_HOME is not set") print("CUDA path:", cuda_path) diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index d66093a862..898b76119d 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -30,9 +30,9 @@ @functools.cache def _get_cuda_path() -> str: - from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path + from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home - cuda_path = get_cuda_home_or_path() + cuda_path = get_cuda_path_or_home() if not cuda_path: raise RuntimeError("Environment variable CUDA_PATH or CUDA_HOME is not set") print("CUDA path:", cuda_path) diff --git a/cuda_core/examples/thread_block_cluster.py b/cuda_core/examples/thread_block_cluster.py index d4a5d38f9e..bfd621de6c 100644 --- a/cuda_core/examples/thread_block_cluster.py +++ b/cuda_core/examples/thread_block_cluster.py @@ -23,7 +23,7 @@ ProgramOptions, launch, ) -from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path +from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home # print cluster info using a kernel and store results in pinned memory code = r""" @@ -66,7 +66,7 @@ def main(): print("This example requires NumPy 2.2.5 or later", file=sys.stderr) sys.exit(1) - cuda_path = get_cuda_home_or_path() + cuda_path = get_cuda_path_or_home() if cuda_path is None: print("This example requires CUDA_PATH or CUDA_HOME to point to a CUDA toolkit.", file=sys.stderr) sys.exit(1) diff --git a/cuda_core/examples/tma_tensor_map.py b/cuda_core/examples/tma_tensor_map.py index 8ddcc647c6..bc66d2e908 100644 --- a/cuda_core/examples/tma_tensor_map.py +++ b/cuda_core/examples/tma_tensor_map.py @@ -36,7 +36,7 @@ StridedMemoryView, launch, ) -from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path +from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home # --------------------------------------------------------------------------- # CUDA kernel that uses TMA to load a 1-D tile into shared memory, then @@ -104,7 +104,7 @@ def _get_cccl_include_paths(): - cuda_path = get_cuda_home_or_path() + cuda_path = get_cuda_path_or_home() if cuda_path is None: print("This example requires CUDA_PATH or CUDA_HOME to point to a CUDA toolkit.", file=sys.stderr) sys.exit(1) diff --git a/cuda_core/tests/conftest.py b/cuda_core/tests/conftest.py index 462a6db8ca..330cb7e393 100644 --- a/cuda_core/tests/conftest.py +++ b/cuda_core/tests/conftest.py @@ -9,7 +9,7 @@ import pytest -from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path +from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home try: from cuda.bindings import driver @@ -255,6 +255,6 @@ def test_something(memory_resource_factory): skipif_need_cuda_headers = pytest.mark.skipif( - get_cuda_home_or_path() is None, + get_cuda_path_or_home() is None, reason="need CUDA header", ) diff --git a/cuda_core/tests/helpers/__init__.py b/cuda_core/tests/helpers/__init__.py index 8a08fa1f04..cd628be395 100644 --- a/cuda_core/tests/helpers/__init__.py +++ b/cuda_core/tests/helpers/__init__.py @@ -6,10 +6,10 @@ from typing import Union from cuda.core._utils.cuda_utils import handle_return -from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path +from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home from cuda_python_test_helpers import * -CUDA_PATH = get_cuda_home_or_path() +CUDA_PATH = get_cuda_path_or_home() CUDA_INCLUDE_PATH = None CCCL_INCLUDE_PATHS = None if CUDA_PATH is not None: diff --git a/cuda_core/tests/test_build_hooks.py b/cuda_core/tests/test_build_hooks.py index 845e8673ac..3f13a1c720 100644 --- a/cuda_core/tests/test_build_hooks.py +++ b/cuda_core/tests/test_build_hooks.py @@ -24,7 +24,7 @@ import pytest -from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path +from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home # build_hooks.py imports Cython and setuptools at the top level, so skip if not available pytest.importorskip("Cython") @@ -70,7 +70,7 @@ def _check_version_detection( build_hooks._get_cuda_path.cache_clear() build_hooks._determine_cuda_major_version.cache_clear() - get_cuda_home_or_path.cache_clear() + get_cuda_path_or_home.cache_clear() mock_env = { k: v @@ -95,7 +95,7 @@ def test_env_var_override(self, version): """CUDA_CORE_BUILD_MAJOR env var override works with various versions.""" build_hooks._get_cuda_path.cache_clear() build_hooks._determine_cuda_major_version.cache_clear() - get_cuda_home_or_path.cache_clear() + get_cuda_path_or_home.cache_clear() with mock.patch.dict(os.environ, {"CUDA_CORE_BUILD_MAJOR": version}, clear=False): result = build_hooks._determine_cuda_major_version() assert result == version @@ -129,7 +129,7 @@ def test_missing_cuda_path_raises_error(self): """RuntimeError is raised when CUDA_PATH/CUDA_HOME not set and no env var override.""" build_hooks._get_cuda_path.cache_clear() build_hooks._determine_cuda_major_version.cache_clear() - get_cuda_home_or_path.cache_clear() + get_cuda_path_or_home.cache_clear() with ( mock.patch.dict(os.environ, {}, clear=True), pytest.raises(RuntimeError, match="CUDA_PATH or CUDA_HOME"), diff --git a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py index 1d8278e103..10ca2e041b 100644 --- a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py +++ b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py @@ -6,7 +6,7 @@ import shutil from cuda.pathfinder._binaries import supported_nvidia_binaries -from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path +from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages from cuda.pathfinder._utils.platform_aware import IS_WINDOWS @@ -97,7 +97,7 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None: dirs.append(os.path.join(conda_prefix, "bin")) # 3. Search in CUDA Toolkit (CUDA_HOME/CUDA_PATH) - if (cuda_home := get_cuda_home_or_path()) is not None: + if (cuda_home := get_cuda_path_or_home()) is not None: if IS_WINDOWS: dirs.append(os.path.join(cuda_home, "bin", "x64")) dirs.append(os.path.join(cuda_home, "bin", "x86_64")) diff --git a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_steps.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_steps.py index 216c4e1a63..e6061690ac 100644 --- a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_steps.py +++ b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_steps.py @@ -28,7 +28,7 @@ from cuda.pathfinder._dynamic_libs.lib_descriptor import LibDescriptor from cuda.pathfinder._dynamic_libs.load_dl_common import DynamicLibNotFoundError from cuda.pathfinder._dynamic_libs.search_platform import PLATFORM, SearchPlatform -from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path +from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home # --------------------------------------------------------------------------- # Data types @@ -190,7 +190,7 @@ def find_in_cuda_home(ctx: SearchContext) -> FindResult | None: path used by :func:`cuda.pathfinder._dynamic_libs.load_dl_windows.load_with_system_search`. Python 3.8+ does not include ``PATH`` in that native DLL search. """ - cuda_home = get_cuda_home_or_path() + cuda_home = get_cuda_path_or_home() if cuda_home is None: return None lib_dir = _find_lib_dir_using_anchor(ctx.desc, ctx.platform, cuda_home) diff --git a/cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py b/cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py index 04c608a771..4614e5a952 100644 --- a/cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py +++ b/cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py @@ -19,7 +19,7 @@ platform_include_subdirs, resolve_conda_anchor, ) -from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path +from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages if TYPE_CHECKING: @@ -102,7 +102,7 @@ def find_in_conda(desc: HeaderDescriptor) -> LocatedHeaderDir | None: def find_in_cuda_home(desc: HeaderDescriptor) -> LocatedHeaderDir | None: """Search ``$CUDA_PATH`` / ``$CUDA_HOME``.""" - cuda_home = get_cuda_home_or_path() + cuda_home = get_cuda_path_or_home() if cuda_home is None: return None result = _locate_in_anchor_layout(desc, cuda_home) diff --git a/cuda_pathfinder/cuda/pathfinder/_static_libs/find_bitcode_lib.py b/cuda_pathfinder/cuda/pathfinder/_static_libs/find_bitcode_lib.py index 109f9eb53f..f8de3aecf2 100644 --- a/cuda_pathfinder/cuda/pathfinder/_static_libs/find_bitcode_lib.py +++ b/cuda_pathfinder/cuda/pathfinder/_static_libs/find_bitcode_lib.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from typing import NoReturn, TypedDict -from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path +from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages from cuda.pathfinder._utils.platform_aware import IS_WINDOWS @@ -89,7 +89,7 @@ def try_with_conda_prefix(self) -> str | None: return None def try_with_cuda_home(self) -> str | None: - cuda_home = get_cuda_home_or_path() + cuda_home = get_cuda_path_or_home() if cuda_home is None: self.error_messages.append("CUDA_HOME/CUDA_PATH not set") return None diff --git a/cuda_pathfinder/cuda/pathfinder/_static_libs/find_static_lib.py b/cuda_pathfinder/cuda/pathfinder/_static_libs/find_static_lib.py index 0b78f881fd..27e397527b 100644 --- a/cuda_pathfinder/cuda/pathfinder/_static_libs/find_static_lib.py +++ b/cuda_pathfinder/cuda/pathfinder/_static_libs/find_static_lib.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from typing import NoReturn, TypedDict -from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path +from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages from cuda.pathfinder._utils.platform_aware import IS_WINDOWS @@ -92,7 +92,7 @@ def try_with_conda_prefix(self) -> str | None: return None def try_with_cuda_home(self) -> str | None: - cuda_home = get_cuda_home_or_path() + cuda_home = get_cuda_path_or_home() if cuda_home is None: self.error_messages.append("CUDA_HOME/CUDA_PATH not set") return None diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py b/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py index a7cba0fc9a..8ae60d007a 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py @@ -13,7 +13,7 @@ If both are set and differ, CUDA_PATH takes precedence and a warning is issued. Important Note on Caching: - The result of get_cuda_home_or_path() is cached for the process lifetime. The first + The result of get_cuda_path_or_home() is cached for the process lifetime. The first call determines the CUDA Toolkit path, and all subsequent calls return the cached value, even if environment variables change later. This ensures consistent behavior throughout the application lifecycle. @@ -25,7 +25,7 @@ #: Canonical search order for CUDA Toolkit environment variables. #: -#: This tuple defines the priority order used by :py:func:`get_cuda_home_or_path` +#: This tuple defines the priority order used by :py:func:`get_cuda_path_or_home` #: and throughout cuda-python packages when determining which CUDA Toolkit to use. #: #: The first variable in the tuple has the highest priority. If multiple variables are set @@ -67,7 +67,7 @@ def _paths_differ(a: str, b: str) -> bool: @functools.cache -def get_cuda_home_or_path() -> str | None: +def get_cuda_path_or_home() -> str | None: """Get CUDA Toolkit path from environment variables. Returns the value of CUDA_PATH or CUDA_HOME following the canonical search order diff --git a/cuda_pathfinder/docs/source/api.rst b/cuda_pathfinder/docs/source/api.rst index 19a4b43b3a..81a30bfeb7 100644 --- a/cuda_pathfinder/docs/source/api.rst +++ b/cuda_pathfinder/docs/source/api.rst @@ -52,9 +52,9 @@ environment variables used across all cuda-python packages. .. autosummary:: :toctree: generated/ - _utils.env_vars.get_cuda_home_or_path + _utils.env_vars.get_cuda_path_or_home _utils.env_vars.CUDA_ENV_VARS_ORDERED -.. autofunction:: cuda.pathfinder._utils.env_vars.get_cuda_home_or_path +.. autofunction:: cuda.pathfinder._utils.env_vars.get_cuda_path_or_home .. autodata:: cuda.pathfinder._utils.env_vars.CUDA_ENV_VARS_ORDERED diff --git a/cuda_pathfinder/docs/source/release/1.5.0-notes.rst b/cuda_pathfinder/docs/source/release/1.5.0-notes.rst index 9867149315..9388a19eef 100644 --- a/cuda_pathfinder/docs/source/release/1.5.0-notes.rst +++ b/cuda_pathfinder/docs/source/release/1.5.0-notes.rst @@ -24,7 +24,7 @@ Breaking Changes New Features ~~~~~~~~~~~~ -* Added centralized CUDA environment variable handling with :py:func:`cuda.pathfinder._utils.env_vars.get_cuda_home_or_path()`. This function provides: +* Added centralized CUDA environment variable handling with :py:func:`cuda.pathfinder._utils.env_vars.get_cuda_path_or_home()`. This function provides: - Consistent behavior across all cuda-python packages (cuda.pathfinder, cuda.core, cuda.bindings) - Intelligent path comparison that handles symlinks, case sensitivity, and trailing slashes @@ -37,7 +37,7 @@ Documentation ~~~~~~~~~~~~~ * Updated documentation across all packages to reflect the new ``CUDA_PATH`` priority -* Added detailed caching behavior documentation for :py:func:`cuda.pathfinder._utils.env_vars.get_cuda_home_or_path()` +* Added detailed caching behavior documentation for :py:func:`cuda.pathfinder._utils.env_vars.get_cuda_path_or_home()` * Clarified environment variable precedence in installation guides Bug Fixes diff --git a/cuda_pathfinder/tests/test_ctk_root_discovery.py b/cuda_pathfinder/tests/test_ctk_root_discovery.py index 403851f492..59160ec238 100644 --- a/cuda_pathfinder/tests/test_ctk_root_discovery.py +++ b/cuda_pathfinder/tests/test_ctk_root_discovery.py @@ -413,7 +413,7 @@ def test_cuda_home_takes_priority_over_canary(tmp_path, mocker): # Canary subprocess probe would find cudart if consulted. mocker.patch(f"{_MODULE}._resolve_system_loaded_abs_path_in_subprocess", side_effect=canary_mock) # CUDA_HOME points to a separate root that also has nvvm - mocker.patch(f"{_STEPS_MODULE}.get_cuda_home_or_path", return_value=str(cuda_home_root)) + mocker.patch(f"{_STEPS_MODULE}.get_cuda_path_or_home", return_value=str(cuda_home_root)) # Capture the final load call mocker.patch.object( load_mod.LOADER, @@ -443,7 +443,7 @@ def test_canary_fires_only_after_all_earlier_steps_fail(tmp_path, mocker): return_value=_fake_canary_path(canary_root), ) # No CUDA_HOME set - mocker.patch(f"{_STEPS_MODULE}.get_cuda_home_or_path", return_value=None) + mocker.patch(f"{_STEPS_MODULE}.get_cuda_path_or_home", return_value=None) # Capture the final load call mocker.patch.object( load_mod.LOADER, @@ -461,7 +461,7 @@ def test_canary_fires_only_after_all_earlier_steps_fail(tmp_path, mocker): def test_non_discoverable_lib_skips_canary_probe(mocker): # Force fallback path for a lib that is not canary-discoverable. mocker.patch.object(load_mod.LOADER, "load_with_system_search", return_value=None) - mocker.patch(f"{_STEPS_MODULE}.get_cuda_home_or_path", return_value=None) + mocker.patch(f"{_STEPS_MODULE}.get_cuda_path_or_home", return_value=None) canary_probe = mocker.patch(f"{_MODULE}._resolve_system_loaded_abs_path_in_subprocess") with pytest.raises(DynamicLibNotFoundError): diff --git a/cuda_pathfinder/tests/test_find_bitcode_lib.py b/cuda_pathfinder/tests/test_find_bitcode_lib.py index 5b9343033e..021fd66453 100644 --- a/cuda_pathfinder/tests/test_find_bitcode_lib.py +++ b/cuda_pathfinder/tests/test_find_bitcode_lib.py @@ -13,7 +13,7 @@ find_bitcode_lib, locate_bitcode_lib, ) -from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path +from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home STRICTNESS = os.environ.get("CUDA_PATHFINDER_TEST_FIND_NVIDIA_BITCODE_LIB_STRICTNESS", "see_what_works") assert STRICTNESS in ("see_what_works", "all_must_work") @@ -24,10 +24,10 @@ @pytest.fixture def clear_find_bitcode_lib_cache(): find_bitcode_lib_module.find_bitcode_lib.cache_clear() - get_cuda_home_or_path.cache_clear() + get_cuda_path_or_home.cache_clear() yield find_bitcode_lib_module.find_bitcode_lib.cache_clear() - get_cuda_home_or_path.cache_clear() + get_cuda_path_or_home.cache_clear() def _make_bitcode_lib_file(dir_path: Path) -> str: diff --git a/cuda_pathfinder/tests/test_find_nvidia_binaries.py b/cuda_pathfinder/tests/test_find_nvidia_binaries.py index 4f9eef223a..ec9740cd85 100644 --- a/cuda_pathfinder/tests/test_find_nvidia_binaries.py +++ b/cuda_pathfinder/tests/test_find_nvidia_binaries.py @@ -57,7 +57,7 @@ def test_find_binary_search_path_includes_site_packages_conda_cuda(monkeypatch, binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[site_dir] ) monkeypatch.setenv("CONDA_PREFIX", conda_prefix) - mocker.patch.object(binary_finder_module, "get_cuda_home_or_path", return_value=cuda_home) + mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=cuda_home) which_mock = mocker.patch.object( binary_finder_module.shutil, "which", return_value=os.path.join(os.sep, "resolved", "nvcc") ) @@ -91,7 +91,7 @@ def test_find_binary_windows_extension_and_search_dirs(monkeypatch, mocker): binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[site_dir] ) monkeypatch.setenv("CONDA_PREFIX", conda_prefix) - mocker.patch.object(binary_finder_module, "get_cuda_home_or_path", return_value=cuda_home) + mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=cuda_home) which_mock = mocker.patch.object( binary_finder_module.shutil, "which", return_value=os.path.join(os.sep, "resolved", "nvcc.exe") ) @@ -122,7 +122,7 @@ def test_find_binary_returns_none_with_no_candidates(monkeypatch, mocker): ) find_sub_dirs_mock = mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[]) monkeypatch.delenv("CONDA_PREFIX", raising=False) - mocker.patch.object(binary_finder_module, "get_cuda_home_or_path", return_value=None) + mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=None) which_mock = mocker.patch.object(binary_finder_module.shutil, "which", return_value=None) result = find_nvidia_binary_utility("nvcc") @@ -141,7 +141,7 @@ def test_find_binary_without_site_packages_entry(monkeypatch, mocker): mocker.patch.object(binary_finder_module.supported_nvidia_binaries, "SITE_PACKAGES_BINDIRS", {}) find_sub_dirs_mock = mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[]) monkeypatch.setenv("CONDA_PREFIX", conda_prefix) - mocker.patch.object(binary_finder_module, "get_cuda_home_or_path", return_value=cuda_home) + mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=cuda_home) which_mock = mocker.patch.object(binary_finder_module.shutil, "which", return_value=None) result = find_nvidia_binary_utility("nvcc") @@ -161,7 +161,7 @@ def test_find_binary_cache_negative_result(monkeypatch, mocker): mocker.patch.object(binary_finder_module.supported_nvidia_binaries, "SITE_PACKAGES_BINDIRS", {}) mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[]) monkeypatch.delenv("CONDA_PREFIX", raising=False) - mocker.patch.object(binary_finder_module, "get_cuda_home_or_path", return_value=None) + mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=None) which_mock = mocker.patch.object(binary_finder_module.shutil, "which", return_value=None) first = find_nvidia_binary_utility("nvcc") diff --git a/cuda_pathfinder/tests/test_find_nvidia_headers.py b/cuda_pathfinder/tests/test_find_nvidia_headers.py index 567533d981..2a0ad3e7ab 100644 --- a/cuda_pathfinder/tests/test_find_nvidia_headers.py +++ b/cuda_pathfinder/tests/test_find_nvidia_headers.py @@ -33,7 +33,7 @@ SUPPORTED_INSTALL_DIRS_NON_CTK, SUPPORTED_SITE_PACKAGE_HEADER_DIRS_CTK, ) -from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path +from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home from cuda.pathfinder._utils.platform_aware import IS_WINDOWS STRICTNESS = os.environ.get("CUDA_PATHFINDER_TEST_FIND_NVIDIA_HEADERS_STRICTNESS", "see_what_works") @@ -79,11 +79,11 @@ def have_distribution_for(libname: str) -> bool: def clear_locate_nvidia_header_cache(): locate_nvidia_header_directory.cache_clear() _resolve_system_loaded_abs_path_in_subprocess.cache_clear() - get_cuda_home_or_path.cache_clear() + get_cuda_path_or_home.cache_clear() yield locate_nvidia_header_directory.cache_clear() _resolve_system_loaded_abs_path_in_subprocess.cache_clear() - get_cuda_home_or_path.cache_clear() + get_cuda_path_or_home.cache_clear() def _create_ctk_header(ctk_root: Path, libname: str) -> str: diff --git a/cuda_pathfinder/tests/test_find_static_lib.py b/cuda_pathfinder/tests/test_find_static_lib.py index 8f793f8c1e..a03179f393 100644 --- a/cuda_pathfinder/tests/test_find_static_lib.py +++ b/cuda_pathfinder/tests/test_find_static_lib.py @@ -13,7 +13,7 @@ find_static_lib, locate_static_lib, ) -from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path +from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home from cuda.pathfinder._utils.platform_aware import quote_for_shell STRICTNESS = os.environ.get("CUDA_PATHFINDER_TEST_FIND_NVIDIA_STATIC_LIB_STRICTNESS", "see_what_works") @@ -25,10 +25,10 @@ @pytest.fixture def clear_find_static_lib_cache(): find_static_lib_module.find_static_lib.cache_clear() - get_cuda_home_or_path.cache_clear() + get_cuda_path_or_home.cache_clear() yield find_static_lib_module.find_static_lib.cache_clear() - get_cuda_home_or_path.cache_clear() + get_cuda_path_or_home.cache_clear() def _make_static_lib_file(dir_path: Path, filename: str) -> str: diff --git a/cuda_pathfinder/tests/test_load_nvidia_dynamic_lib_using_mocker.py b/cuda_pathfinder/tests/test_load_nvidia_dynamic_lib_using_mocker.py index 3510d1933e..f46ad43356 100644 --- a/cuda_pathfinder/tests/test_load_nvidia_dynamic_lib_using_mocker.py +++ b/cuda_pathfinder/tests/test_load_nvidia_dynamic_lib_using_mocker.py @@ -81,7 +81,7 @@ def _run_find_steps_without_site_packages(ctx, steps): mocker.patch.object(load_mod.LOADER, "check_if_already_loaded_from_elsewhere", return_value=None) mocker.patch(f"{_MODULE}.load_dependencies") mocker.patch.object(load_mod.LOADER, "load_with_system_search", return_value=None) - mocker.patch(f"{_STEPS_MODULE}.get_cuda_home_or_path", return_value=None) + mocker.patch(f"{_STEPS_MODULE}.get_cuda_path_or_home", return_value=None) mocker.patch(f"{_MODULE}._resolve_system_loaded_abs_path_in_subprocess", return_value=None) mocker.patch.object( load_mod.LOADER, @@ -112,7 +112,7 @@ def _run_find_steps_disabled(ctx, steps): mocker.patch.object(load_mod.LOADER, "check_if_already_loaded_from_elsewhere", return_value=None) mocker.patch(f"{_MODULE}.load_dependencies") mocker.patch.object(load_mod.LOADER, "load_with_system_search", return_value=None) - mocker.patch(f"{_STEPS_MODULE}.get_cuda_home_or_path", return_value=None) + mocker.patch(f"{_STEPS_MODULE}.get_cuda_path_or_home", return_value=None) mocker.patch( f"{_MODULE}._resolve_system_loaded_abs_path_in_subprocess", return_value=None, @@ -161,7 +161,7 @@ def _run_find_steps_without_site_packages(ctx, steps): mocker.patch.object(load_mod.LOADER, "check_if_already_loaded_from_elsewhere", return_value=None) mocker.patch(f"{_MODULE}.load_dependencies") mocker.patch.object(load_mod.LOADER, "load_with_system_search", return_value=None) - mocker.patch(f"{_STEPS_MODULE}.get_cuda_home_or_path", return_value=str(ctk_root)) + mocker.patch(f"{_STEPS_MODULE}.get_cuda_path_or_home", return_value=str(ctk_root)) mocker.patch.object( load_mod.LOADER, "load_with_abs_path", diff --git a/cuda_pathfinder/tests/test_search_steps.py b/cuda_pathfinder/tests/test_search_steps.py index cf018562a6..a3e185dc68 100644 --- a/cuda_pathfinder/tests/test_search_steps.py +++ b/cuda_pathfinder/tests/test_search_steps.py @@ -197,7 +197,7 @@ def test_found_windows(self, mocker, tmp_path): class TestFindInCudaHome: def test_returns_none_without_env_var(self, mocker): - mocker.patch(f"{_STEPS_MOD}.get_cuda_home_or_path", return_value=None) + mocker.patch(f"{_STEPS_MOD}.get_cuda_path_or_home", return_value=None) assert find_in_cuda_home(_ctx(platform=LinuxSearchPlatform())) is None def test_found_linux(self, mocker, tmp_path): @@ -206,7 +206,7 @@ def test_found_linux(self, mocker, tmp_path): so_file = lib_dir / "libcudart.so" so_file.touch() - mocker.patch(f"{_STEPS_MOD}.get_cuda_home_or_path", return_value=str(tmp_path)) + mocker.patch(f"{_STEPS_MOD}.get_cuda_path_or_home", return_value=str(tmp_path)) result = find_in_cuda_home(_ctx(platform=LinuxSearchPlatform())) assert result is not None @@ -219,7 +219,7 @@ def test_found_windows(self, mocker, tmp_path): dll = bin_dir / "cudart64_12.dll" dll.touch() - mocker.patch(f"{_STEPS_MOD}.get_cuda_home_or_path", return_value=str(tmp_path)) + mocker.patch(f"{_STEPS_MOD}.get_cuda_path_or_home", return_value=str(tmp_path)) result = find_in_cuda_home(_ctx(platform=WindowsSearchPlatform())) assert result is not None @@ -319,7 +319,7 @@ def test_find_lib_dir_returns_none_when_no_match(self, tmp_path): def test_nvvm_cuda_home_linux(self, mocker, tmp_path): """End-to-end: find_in_cuda_home resolves nvvm under its custom subdir.""" - mocker.patch(f"{_STEPS_MOD}.get_cuda_home_or_path", return_value=str(tmp_path)) + mocker.patch(f"{_STEPS_MOD}.get_cuda_path_or_home", return_value=str(tmp_path)) nvvm_dir = tmp_path / "nvvm" / "lib64" nvvm_dir.mkdir(parents=True) diff --git a/cuda_pathfinder/tests/test_utils_env_vars.py b/cuda_pathfinder/tests/test_utils_env_vars.py index cec986c84a..7a6277d4f1 100644 --- a/cuda_pathfinder/tests/test_utils_env_vars.py +++ b/cuda_pathfinder/tests/test_utils_env_vars.py @@ -11,7 +11,7 @@ from cuda.pathfinder._utils.env_vars import ( CUDA_ENV_VARS_ORDERED, _paths_differ, - get_cuda_home_or_path, + get_cuda_path_or_home, ) skip_symlink_tests = pytest.mark.skipif( @@ -25,26 +25,26 @@ def unset_env(monkeypatch): monkeypatch.delenv("CUDA_HOME", raising=False) monkeypatch.delenv("CUDA_PATH", raising=False) # Clear the cache so each test gets fresh behavior - get_cuda_home_or_path.cache_clear() + get_cuda_path_or_home.cache_clear() def test_returns_none_when_unset(monkeypatch): unset_env(monkeypatch) - assert get_cuda_home_or_path() is None + assert get_cuda_path_or_home() is None def test_empty_cuda_path_falls_through(monkeypatch): unset_env(monkeypatch) monkeypatch.setenv("CUDA_PATH", "") monkeypatch.setenv("CUDA_HOME", "/usr/local/cuda") - assert get_cuda_home_or_path() == "/usr/local/cuda" + assert get_cuda_path_or_home() == "/usr/local/cuda" def test_all_empty_returns_none(monkeypatch): unset_env(monkeypatch) monkeypatch.setenv("CUDA_PATH", "") monkeypatch.setenv("CUDA_HOME", "") - assert get_cuda_home_or_path() is None + assert get_cuda_path_or_home() is None def test_prefers_cuda_path_over_cuda_home(monkeypatch, tmp_path): @@ -59,7 +59,7 @@ def test_prefers_cuda_path_over_cuda_home(monkeypatch, tmp_path): # Different directories -> warning + prefer CUDA_PATH with pytest.warns(UserWarning, match="Multiple CUDA environment variables are set but differ"): - result = get_cuda_home_or_path() + result = get_cuda_path_or_home() assert pathlib.Path(result) == path @@ -68,7 +68,7 @@ def test_uses_cuda_home_if_path_missing(monkeypatch, tmp_path): only_home = tmp_path / "home" only_home.mkdir() monkeypatch.setenv("CUDA_HOME", str(only_home)) - assert pathlib.Path(get_cuda_home_or_path()) == only_home + assert pathlib.Path(get_cuda_path_or_home()) == only_home def test_no_warning_when_textually_equal_after_normalization(monkeypatch, tmp_path): @@ -86,7 +86,7 @@ def test_no_warning_when_textually_equal_after_normalization(monkeypatch, tmp_pa # No warning; same logical directory with warnings.catch_warnings(record=True) as record: - result = get_cuda_home_or_path() + result = get_cuda_path_or_home() assert pathlib.Path(result) == d assert len(record) == 0 @@ -107,7 +107,7 @@ def test_no_warning_on_windows_case_only_difference(monkeypatch, tmp_path): with warnings.catch_warnings(record=True) as record: warnings.simplefilter("always") - result = get_cuda_home_or_path() + result = get_cuda_path_or_home() assert pathlib.Path(result).samefile(d) assert len(record) == 0 @@ -124,7 +124,7 @@ def test_warning_when_both_exist_and_are_different(monkeypatch, tmp_path): # Different actual dirs -> warning with pytest.warns(UserWarning, match="Multiple CUDA environment variables are set but differ"): - result = get_cuda_home_or_path() + result = get_cuda_path_or_home() assert pathlib.Path(result) == a @@ -141,7 +141,7 @@ def test_nonexistent_paths_fall_back_to_text_comparison(monkeypatch, tmp_path): monkeypatch.setenv("CUDA_HOME", str(b)) with pytest.warns(UserWarning, match="Multiple CUDA environment variables are set but differ"): - result = get_cuda_home_or_path() + result = get_cuda_path_or_home() assert pathlib.Path(result) == a @@ -165,7 +165,7 @@ def test_samefile_equivalence_via_symlink_when_possible(monkeypatch, tmp_path): # Because they resolve to the same entry, no warning should be raised with warnings.catch_warnings(record=True) as record: warnings.simplefilter("always") - result = get_cuda_home_or_path() + result = get_cuda_path_or_home() assert pathlib.Path(result) == real_dir assert len(record) == 0 @@ -182,7 +182,7 @@ def test_cuda_env_vars_ordered_constant(): def test_search_order_matches_implementation(monkeypatch, tmp_path): """ - Verify that get_cuda_home_or_path() follows the documented search order. + Verify that get_cuda_path_or_home() follows the documented search order. """ unset_env(monkeypatch) path_dir = tmp_path / "path_dir" @@ -197,7 +197,7 @@ def test_search_order_matches_implementation(monkeypatch, tmp_path): # The result should match the first (highest priority) variable in CUDA_ENV_VARS_ORDERED with warnings.catch_warnings(record=True): warnings.simplefilter("always") - result = get_cuda_home_or_path() + result = get_cuda_path_or_home() highest_priority_var = CUDA_ENV_VARS_ORDERED[0] expected = os.environ.get(highest_priority_var) @@ -231,7 +231,7 @@ def test_paths_differ_samefile(tmp_path): def test_caching_behavior(monkeypatch, tmp_path): """ - Verify that get_cuda_home_or_path() caches the result and returns the same + Verify that get_cuda_path_or_home() caches the result and returns the same value even if environment variables change after the first call. """ unset_env(monkeypatch) @@ -245,18 +245,18 @@ def test_caching_behavior(monkeypatch, tmp_path): monkeypatch.setenv("CUDA_PATH", str(first_dir)) # First call should return first_dir - result1 = get_cuda_home_or_path() + result1 = get_cuda_path_or_home() assert pathlib.Path(result1) == first_dir # Change the environment variable monkeypatch.setenv("CUDA_PATH", str(second_dir)) # Second call should still return first_dir (cached value) - result2 = get_cuda_home_or_path() + result2 = get_cuda_path_or_home() assert pathlib.Path(result2) == first_dir assert result1 == result2 # After clearing cache, should get new value - get_cuda_home_or_path.cache_clear() - result3 = get_cuda_home_or_path() + get_cuda_path_or_home.cache_clear() + result3 = get_cuda_path_or_home() assert pathlib.Path(result3) == second_dir From 24b89b5f5b1d6353af8413fe67344e91c1722831 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 20 Mar 2026 15:48:44 -0700 Subject: [PATCH 09/15] make get_cuda_path_or_home a public API, privatize CUDA_ENV_VARS_ORDERED Export get_cuda_path_or_home from cuda.pathfinder.__init__. External consumers now import from cuda.pathfinder directly. Rename constant to _CUDA_PATH_ENV_VARS_ORDERED and remove all public references to it. Made-with: Cursor --- conftest.py | 2 +- cuda_bindings/README.md | 2 +- cuda_bindings/build_hooks.py | 2 +- .../docs/source/environment_variables.rst | 2 +- cuda_bindings/docs/source/install.rst | 2 +- cuda_core/README.md | 2 +- cuda_core/build_hooks.py | 2 +- cuda_core/examples/thread_block_cluster.py | 2 +- cuda_core/examples/tma_tensor_map.py | 2 +- cuda_core/tests/conftest.py | 2 +- cuda_core/tests/helpers/__init__.py | 2 +- cuda_core/tests/test_build_hooks.py | 2 +- cuda_pathfinder/cuda/pathfinder/__init__.py | 1 + .../cuda/pathfinder/_utils/env_vars.py | 27 ++++--------------- cuda_pathfinder/docs/source/api.rst | 18 ++----------- .../docs/source/release/1.5.0-notes.rst | 3 --- cuda_pathfinder/tests/test_utils_env_vars.py | 14 +--------- 17 files changed, 21 insertions(+), 66 deletions(-) diff --git a/conftest.py b/conftest.py index 5f26c9211e..3881b7d647 100644 --- a/conftest.py +++ b/conftest.py @@ -4,7 +4,7 @@ import pytest -from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home +from cuda.pathfinder import get_cuda_path_or_home def pytest_collection_modifyitems(config, items): # noqa: ARG001 diff --git a/cuda_bindings/README.md b/cuda_bindings/README.md index f1df49b67e..b79b0febff 100644 --- a/cuda_bindings/README.md +++ b/cuda_bindings/README.md @@ -33,7 +33,7 @@ To run these tests: Cython tests are located in `tests/cython` and need to be built. These builds have the same CUDA Toolkit header requirements as [Installing from Source](https://nvidia.github.io/cuda-python/cuda-bindings/latest/install.html#requirements) where the major.minor version must match `cuda.bindings`. To build them: -1. Setup environment variable `CUDA_PATH` (or `CUDA_HOME`) with the path to the CUDA Toolkit installation. Note: If both are set, `CUDA_PATH` takes precedence (see `cuda.pathfinder._utils.env_vars.CUDA_ENV_VARS_ORDERED`). +1. Setup environment variable `CUDA_PATH` (or `CUDA_HOME`) with the path to the CUDA Toolkit installation. Note: If both are set, `CUDA_PATH` takes precedence. 2. Run `build_tests` script located in `test/cython` appropriate to your platform. This will both cythonize the tests and build them. To run these tests: diff --git a/cuda_bindings/build_hooks.py b/cuda_bindings/build_hooks.py index afbd1246a0..4433304ab3 100644 --- a/cuda_bindings/build_hooks.py +++ b/cuda_bindings/build_hooks.py @@ -35,7 +35,7 @@ @functools.cache def _get_cuda_path() -> str: - from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home + from cuda.pathfinder import get_cuda_path_or_home cuda_path = get_cuda_path_or_home() if not cuda_path: diff --git a/cuda_bindings/docs/source/environment_variables.rst b/cuda_bindings/docs/source/environment_variables.rst index 2625e8d8d9..c1853a8486 100644 --- a/cuda_bindings/docs/source/environment_variables.rst +++ b/cuda_bindings/docs/source/environment_variables.rst @@ -15,7 +15,7 @@ Runtime Environment Variables Build-Time Environment Variables -------------------------------- -- ``CUDA_PATH`` or ``CUDA_HOME``: Specifies the location of the CUDA Toolkit. If both are set, ``CUDA_PATH`` takes precedence. This search order is defined in :py:data:`cuda.pathfinder._utils.env_vars.CUDA_ENV_VARS_ORDERED`. +- ``CUDA_PATH`` or ``CUDA_HOME``: Specifies the location of the CUDA Toolkit. If both are set, ``CUDA_PATH`` takes precedence. .. note:: **Breaking Change in v1.4.0**: The priority order changed from ``CUDA_HOME`` > ``CUDA_PATH`` to ``CUDA_PATH`` > ``CUDA_HOME``. diff --git a/cuda_bindings/docs/source/install.rst b/cuda_bindings/docs/source/install.rst index d647360ff2..00db4b5911 100644 --- a/cuda_bindings/docs/source/install.rst +++ b/cuda_bindings/docs/source/install.rst @@ -87,7 +87,7 @@ Requirements [^2]: The CUDA Runtime static library (``libcudart_static.a`` on Linux, ``cudart_static.lib`` on Windows) is part of the CUDA Toolkit. If using conda packages, it is contained in the ``cuda-cudart-static`` package. -Source builds require that the provided CUDA headers are of the same major.minor version as the ``cuda.bindings`` you're trying to build. Despite this requirement, note that the minor version compatibility is still maintained. Use the ``CUDA_PATH`` (or ``CUDA_HOME``) environment variable to specify the location of your headers. If both are set, ``CUDA_PATH`` takes precedence (see :py:data:`cuda.pathfinder._utils.env_vars.CUDA_ENV_VARS_ORDERED`). For example, if your headers are located in ``/usr/local/cuda/include``, then you should set ``CUDA_PATH`` with: +Source builds require that the provided CUDA headers are of the same major.minor version as the ``cuda.bindings`` you're trying to build. Despite this requirement, note that the minor version compatibility is still maintained. Use the ``CUDA_PATH`` (or ``CUDA_HOME``) environment variable to specify the location of your headers. If both are set, ``CUDA_PATH`` takes precedence. For example, if your headers are located in ``/usr/local/cuda/include``, then you should set ``CUDA_PATH`` with: .. code-block:: console diff --git a/cuda_core/README.md b/cuda_core/README.md index aa2d1c3fbe..d7dfe83bfa 100644 --- a/cuda_core/README.md +++ b/cuda_core/README.md @@ -26,7 +26,7 @@ Alternatively, from the repository root you can use a simple script: Cython tests are located in `tests/cython` and need to be built. These builds have the same CUDA Toolkit header requirements as [those of cuda.bindings](https://nvidia.github.io/cuda-python/cuda-bindings/latest/install.html#requirements) where the major.minor version must match `cuda.bindings`. To build them: -1. Set up environment variable `CUDA_PATH` (or `CUDA_HOME`) with the path to the CUDA Toolkit installation. Note: If both are set, `CUDA_PATH` takes precedence (see `cuda.pathfinder._utils.env_vars.CUDA_ENV_VARS_ORDERED`). +1. Set up environment variable `CUDA_PATH` (or `CUDA_HOME`) with the path to the CUDA Toolkit installation. Note: If both are set, `CUDA_PATH` takes precedence. 2. Run `build_tests` script located in `tests/cython` appropriate to your platform. This will both cythonize the tests and build them. To run these tests: diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index 898b76119d..c4a79ea059 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -30,7 +30,7 @@ @functools.cache def _get_cuda_path() -> str: - from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home + from cuda.pathfinder import get_cuda_path_or_home cuda_path = get_cuda_path_or_home() if not cuda_path: diff --git a/cuda_core/examples/thread_block_cluster.py b/cuda_core/examples/thread_block_cluster.py index bfd621de6c..c056c59a86 100644 --- a/cuda_core/examples/thread_block_cluster.py +++ b/cuda_core/examples/thread_block_cluster.py @@ -23,7 +23,7 @@ ProgramOptions, launch, ) -from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home +from cuda.pathfinder import get_cuda_path_or_home # print cluster info using a kernel and store results in pinned memory code = r""" diff --git a/cuda_core/examples/tma_tensor_map.py b/cuda_core/examples/tma_tensor_map.py index bc66d2e908..415f390819 100644 --- a/cuda_core/examples/tma_tensor_map.py +++ b/cuda_core/examples/tma_tensor_map.py @@ -36,7 +36,7 @@ StridedMemoryView, launch, ) -from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home +from cuda.pathfinder import get_cuda_path_or_home # --------------------------------------------------------------------------- # CUDA kernel that uses TMA to load a 1-D tile into shared memory, then diff --git a/cuda_core/tests/conftest.py b/cuda_core/tests/conftest.py index 330cb7e393..a5e79dea74 100644 --- a/cuda_core/tests/conftest.py +++ b/cuda_core/tests/conftest.py @@ -9,7 +9,7 @@ import pytest -from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home +from cuda.pathfinder import get_cuda_path_or_home try: from cuda.bindings import driver diff --git a/cuda_core/tests/helpers/__init__.py b/cuda_core/tests/helpers/__init__.py index cd628be395..6680a1a07b 100644 --- a/cuda_core/tests/helpers/__init__.py +++ b/cuda_core/tests/helpers/__init__.py @@ -6,7 +6,7 @@ from typing import Union from cuda.core._utils.cuda_utils import handle_return -from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home +from cuda.pathfinder import get_cuda_path_or_home from cuda_python_test_helpers import * CUDA_PATH = get_cuda_path_or_home() diff --git a/cuda_core/tests/test_build_hooks.py b/cuda_core/tests/test_build_hooks.py index 3f13a1c720..af32f8e444 100644 --- a/cuda_core/tests/test_build_hooks.py +++ b/cuda_core/tests/test_build_hooks.py @@ -24,7 +24,7 @@ import pytest -from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home +from cuda.pathfinder import get_cuda_path_or_home # build_hooks.py imports Cython and setuptools at the top level, so skip if not available pytest.importorskip("Cython") diff --git a/cuda_pathfinder/cuda/pathfinder/__init__.py b/cuda_pathfinder/cuda/pathfinder/__init__.py index 89402370b3..dc818dfd08 100644 --- a/cuda_pathfinder/cuda/pathfinder/__init__.py +++ b/cuda_pathfinder/cuda/pathfinder/__init__.py @@ -59,6 +59,7 @@ from cuda.pathfinder._static_libs.find_static_lib import ( locate_static_lib as locate_static_lib, ) +from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home as get_cuda_path_or_home from cuda.pathfinder._version import __version__ # isort: skip diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py b/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py index 8ae60d007a..12198ac9f7 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py @@ -23,20 +23,7 @@ import os import warnings -#: Canonical search order for CUDA Toolkit environment variables. -#: -#: This tuple defines the priority order used by :py:func:`get_cuda_path_or_home` -#: and throughout cuda-python packages when determining which CUDA Toolkit to use. -#: -#: The first variable in the tuple has the highest priority. If multiple variables are set -#: and point to different locations, the first one is used and a warning is issued. -#: -#: .. note:: -#: **Breaking Change in v1.4.0**: The order changed from ``("CUDA_HOME", "CUDA_PATH")`` -#: to ``("CUDA_PATH", "CUDA_HOME")``, making ``CUDA_PATH`` the highest priority. -#: -#: :type: tuple[str, ...] -CUDA_ENV_VARS_ORDERED = ("CUDA_PATH", "CUDA_HOME") +_CUDA_PATH_ENV_VARS_ORDERED = ("CUDA_PATH", "CUDA_HOME") def _paths_differ(a: str, b: str) -> bool: @@ -70,9 +57,8 @@ def _paths_differ(a: str, b: str) -> bool: def get_cuda_path_or_home() -> str | None: """Get CUDA Toolkit path from environment variables. - Returns the value of CUDA_PATH or CUDA_HOME following the canonical search order - defined in CUDA_ENV_VARS_ORDERED. If both are set and differ, CUDA_PATH takes - precedence and a warning is issued. + Returns the value of CUDA_PATH or CUDA_HOME. If both are set and differ, + CUDA_PATH takes precedence and a warning is issued. The result is cached for the process lifetime. The first call determines the CUDA Toolkit path, and subsequent calls return the cached value. @@ -84,13 +70,11 @@ def get_cuda_path_or_home() -> str | None: UserWarning: If multiple CUDA environment variables are set but point to different locations (only on the first call). - See Also: - CUDA_ENV_VARS_ORDERED: The canonical search order for CUDA environment variables. """ # Collect non-empty environment variables in priority order. # Empty strings are treated as undefined — no valid CUDA path is empty. set_vars = {} - for var in CUDA_ENV_VARS_ORDERED: + for var in _CUDA_PATH_ENV_VARS_ORDERED: val = os.environ.get(var) if val: set_vars[var] = val @@ -109,11 +93,10 @@ def get_cuda_path_or_home() -> str | None: if values_differ: var_list = "\n".join(f" {var}={val}" for var, val in set_vars.items()) - highest_priority = CUDA_ENV_VARS_ORDERED[0] warnings.warn( f"Multiple CUDA environment variables are set but differ:\n" f"{var_list}\n" - f"Using {highest_priority} (highest priority as defined in CUDA_ENV_VARS_ORDERED).", + f"Using {_CUDA_PATH_ENV_VARS_ORDERED[0]} (highest priority).", UserWarning, stacklevel=2, ) diff --git a/cuda_pathfinder/docs/source/api.rst b/cuda_pathfinder/docs/source/api.rst index 81a30bfeb7..e49478c09e 100644 --- a/cuda_pathfinder/docs/source/api.rst +++ b/cuda_pathfinder/docs/source/api.rst @@ -16,6 +16,8 @@ CUDA bitcode and static libraries. .. autosummary:: :toctree: generated/ + get_cuda_path_or_home + SUPPORTED_NVIDIA_LIBNAMES load_nvidia_dynamic_lib LoadedDL @@ -42,19 +44,3 @@ CUDA bitcode and static libraries. locate_static_lib LocatedStaticLib StaticLibNotFoundError - -Environment Variable Utilities -------------------------------- - -The ``cuda.pathfinder._utils.env_vars`` module provides centralized handling of CUDA -environment variables used across all cuda-python packages. - -.. autosummary:: - :toctree: generated/ - - _utils.env_vars.get_cuda_path_or_home - _utils.env_vars.CUDA_ENV_VARS_ORDERED - -.. autofunction:: cuda.pathfinder._utils.env_vars.get_cuda_path_or_home - -.. autodata:: cuda.pathfinder._utils.env_vars.CUDA_ENV_VARS_ORDERED diff --git a/cuda_pathfinder/docs/source/release/1.5.0-notes.rst b/cuda_pathfinder/docs/source/release/1.5.0-notes.rst index 9388a19eef..c611a70f3c 100644 --- a/cuda_pathfinder/docs/source/release/1.5.0-notes.rst +++ b/cuda_pathfinder/docs/source/release/1.5.0-notes.rst @@ -19,7 +19,6 @@ Breaking Changes - If you rely on ``CUDA_HOME``, consider switching to ``CUDA_PATH`` - If you set both variables, ensure they point to the same CUDA Toolkit installation - If they differ intentionally, be aware that ``CUDA_PATH`` will now be used - - The canonical search order is defined in :py:data:`cuda.pathfinder._utils.env_vars.CUDA_ENV_VARS_ORDERED` New Features ~~~~~~~~~~~~ @@ -31,8 +30,6 @@ New Features - Result caching for performance (first call determines the path for the process lifetime) - Clear warnings when multiple environment variables are set but differ -* Added :py:data:`cuda.pathfinder._utils.env_vars.CUDA_ENV_VARS_ORDERED` constant that explicitly documents the canonical search order for CUDA environment variables. - Documentation ~~~~~~~~~~~~~ diff --git a/cuda_pathfinder/tests/test_utils_env_vars.py b/cuda_pathfinder/tests/test_utils_env_vars.py index 7a6277d4f1..99a55324fc 100644 --- a/cuda_pathfinder/tests/test_utils_env_vars.py +++ b/cuda_pathfinder/tests/test_utils_env_vars.py @@ -9,7 +9,6 @@ import pytest from cuda.pathfinder._utils.env_vars import ( - CUDA_ENV_VARS_ORDERED, _paths_differ, get_cuda_path_or_home, ) @@ -170,16 +169,6 @@ def test_samefile_equivalence_via_symlink_when_possible(monkeypatch, tmp_path): assert len(record) == 0 -def test_cuda_env_vars_ordered_constant(): - """ - Verify the canonical search order constant is defined correctly. - CUDA_PATH must have higher priority than CUDA_HOME. - """ - assert CUDA_ENV_VARS_ORDERED == ("CUDA_PATH", "CUDA_HOME") - assert CUDA_ENV_VARS_ORDERED[0] == "CUDA_PATH" # highest priority - assert CUDA_ENV_VARS_ORDERED[1] == "CUDA_HOME" # lower priority - - def test_search_order_matches_implementation(monkeypatch, tmp_path): """ Verify that get_cuda_path_or_home() follows the documented search order. @@ -194,12 +183,11 @@ def test_search_order_matches_implementation(monkeypatch, tmp_path): monkeypatch.setenv("CUDA_PATH", str(path_dir)) monkeypatch.setenv("CUDA_HOME", str(home_dir)) - # The result should match the first (highest priority) variable in CUDA_ENV_VARS_ORDERED with warnings.catch_warnings(record=True): warnings.simplefilter("always") result = get_cuda_path_or_home() - highest_priority_var = CUDA_ENV_VARS_ORDERED[0] + highest_priority_var = "CUDA_PATH" # as hard-wired in the documentation expected = os.environ.get(highest_priority_var) assert result == expected assert pathlib.Path(result) == path_dir # CUDA_PATH should win From feecb82a4f321479fe1f720153c03c9a0267cb82 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 20 Mar 2026 16:07:47 -0700 Subject: [PATCH 10/15] docs(pathfinder): manually edit 1.5.0 release notes, fix RST formatting (Cursor) Made-with: Cursor --- .../docs/source/release/1.5.0-notes.rst | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/cuda_pathfinder/docs/source/release/1.5.0-notes.rst b/cuda_pathfinder/docs/source/release/1.5.0-notes.rst index c611a70f3c..596cddca36 100644 --- a/cuda_pathfinder/docs/source/release/1.5.0-notes.rst +++ b/cuda_pathfinder/docs/source/release/1.5.0-notes.rst @@ -1,4 +1,4 @@ -.. SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. .. SPDX-License-Identifier: Apache-2.0 .. py:currentmodule:: cuda.pathfinder @@ -6,13 +6,14 @@ ``cuda-pathfinder`` 1.5.0 Release notes ======================================= -Highlights ----------- +Breaking Change +--------------- -Breaking Changes -~~~~~~~~~~~~~~~~ - -* **CUDA environment variable priority changed**: ``CUDA_PATH`` now takes precedence over ``CUDA_HOME`` when both are set. Previously, ``CUDA_HOME`` had higher priority. If both variables are set and point to different locations, a warning will be issued and ``CUDA_PATH`` will be used. This change aligns with industry standards and NVIDIA's recommended practices. +* **CUDA environment variable priority changed**: ``CUDA_PATH`` now takes + precedence over ``CUDA_HOME`` when both are set. Previously, ``CUDA_HOME`` had + higher priority. If both variables are set and point to different locations, + a warning will be issued and ``CUDA_PATH`` will be used. This change aligns + with industry standards and NVIDIA's recommended practices. **Migration Guide**: @@ -20,25 +21,20 @@ Breaking Changes - If you set both variables, ensure they point to the same CUDA Toolkit installation - If they differ intentionally, be aware that ``CUDA_PATH`` will now be used -New Features -~~~~~~~~~~~~ +Highlights +---------- -* Added centralized CUDA environment variable handling with :py:func:`cuda.pathfinder._utils.env_vars.get_cuda_path_or_home()`. This function provides: +* Added centralized CUDA environment variable handling with + :py:func:`cuda.pathfinder.get_cuda_path_or_home`. This function provides: - Consistent behavior across all cuda-python packages (cuda.pathfinder, cuda.core, cuda.bindings) - Intelligent path comparison that handles symlinks, case sensitivity, and trailing slashes - Result caching for performance (first call determines the path for the process lifetime) - - Clear warnings when multiple environment variables are set but differ - -Documentation -~~~~~~~~~~~~~ - -* Updated documentation across all packages to reflect the new ``CUDA_PATH`` priority -* Added detailed caching behavior documentation for :py:func:`cuda.pathfinder._utils.env_vars.get_cuda_path_or_home()` -* Clarified environment variable precedence in installation guides + - Clear warnings when both ``CUDA_PATH`` and ``CUDA_HOME`` are set but differ -Bug Fixes -~~~~~~~~~ + (`PR #1801 `_) -* Improved robustness of test collection by adding fallback when cuda.pathfinder is not yet installed -* Enhanced path comparison logic to properly handle edge cases (nonexistent paths, symlinks, OS-specific behavior) +* Clarified that Python 3.8+ excludes ``PATH`` from the native Windows DLL + search used by ``load_nvidia_dynamic_lib()``, so CTK installs are typically + found via ``CUDA_PATH``/``CUDA_HOME`` or other explicit search steps instead. + (`PR #1795 `_) From 74e2d5b2d2d1618eb7de39e548a157ae5d2d62bc Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 20 Mar 2026 16:14:40 -0700 Subject: [PATCH 11/15] Add 1.5.0, 1.4.3, 1.4.2 in cuda_pathfinder/docs/nv-versions.json --- cuda_pathfinder/docs/nv-versions.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/cuda_pathfinder/docs/nv-versions.json b/cuda_pathfinder/docs/nv-versions.json index eb0e60239e..eb3abd2abe 100644 --- a/cuda_pathfinder/docs/nv-versions.json +++ b/cuda_pathfinder/docs/nv-versions.json @@ -3,6 +3,18 @@ "version": "latest", "url": "https://nvidia.github.io/cuda-python/cuda-pathfinder/latest/" }, + { + "version": "1.5.0", + "url": "https://nvidia.github.io/cuda-python/cuda-pathfinder/1.4.1/" + }, + { + "version": "1.4.3", + "url": "https://nvidia.github.io/cuda-python/cuda-pathfinder/1.4.1/" + }, + { + "version": "1.4.2", + "url": "https://nvidia.github.io/cuda-python/cuda-pathfinder/1.4.1/" + }, { "version": "1.4.1", "url": "https://nvidia.github.io/cuda-python/cuda-pathfinder/1.4.1/" From 5ae2976d4f87ba134bac1de40cc842c6a4053914 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 20 Mar 2026 16:28:05 -0700 Subject: [PATCH 12/15] docs: clarify that CUDA_PATH/CUDA_HOME priority comes from pathfinder Pathfinder 1.5.0 release notes no longer claim cross-package consistency (that depends on future bindings/core releases). cuda_bindings env var docs now defer to pathfinder release notes for migration guidance. Made-with: Cursor --- .../docs/source/environment_variables.rst | 18 +++++------------- .../docs/source/release/1.5.0-notes.rst | 8 +++++--- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/cuda_bindings/docs/source/environment_variables.rst b/cuda_bindings/docs/source/environment_variables.rst index c1853a8486..f38916549a 100644 --- a/cuda_bindings/docs/source/environment_variables.rst +++ b/cuda_bindings/docs/source/environment_variables.rst @@ -18,19 +18,11 @@ Build-Time Environment Variables - ``CUDA_PATH`` or ``CUDA_HOME``: Specifies the location of the CUDA Toolkit. If both are set, ``CUDA_PATH`` takes precedence. .. note:: - **Breaking Change in v1.4.0**: The priority order changed from ``CUDA_HOME`` > ``CUDA_PATH`` to ``CUDA_PATH`` > ``CUDA_HOME``. - - **Migration Guide**: - - - If you only set one variable, no changes are needed - - If you set both variables to the same location, no changes are needed - - If you set both variables to different locations and relied on ``CUDA_HOME`` taking precedence, you should either: - - - Switch to using only ``CUDA_PATH`` (recommended) - - Ensure both variables point to the same CUDA Toolkit installation - - Be aware that ``CUDA_PATH`` will now be used - - A warning will be issued if both variables are set but point to different locations. + The ``CUDA_PATH`` > ``CUDA_HOME`` priority is determined by ``cuda-pathfinder``. + Earlier versions of ``cuda-pathfinder`` (before 1.5.0) used the opposite order + (``CUDA_HOME`` > ``CUDA_PATH``). See the + `cuda-pathfinder 1.5.0 release notes `_ + for details and migration guidance. - ``CUDA_PYTHON_PARSER_CACHING`` : bool, toggles the caching of parsed header files during the cuda-bindings build process. If caching is enabled (``CUDA_PYTHON_PARSER_CACHING`` is True), the cache path is set to ./cache_, where is derived from the cuda toolkit libraries used to build cuda-bindings. diff --git a/cuda_pathfinder/docs/source/release/1.5.0-notes.rst b/cuda_pathfinder/docs/source/release/1.5.0-notes.rst index 596cddca36..e893c347ad 100644 --- a/cuda_pathfinder/docs/source/release/1.5.0-notes.rst +++ b/cuda_pathfinder/docs/source/release/1.5.0-notes.rst @@ -24,14 +24,16 @@ Breaking Change Highlights ---------- -* Added centralized CUDA environment variable handling with - :py:func:`cuda.pathfinder.get_cuda_path_or_home`. This function provides: +* Added :py:func:`cuda.pathfinder.get_cuda_path_or_home`, a centralized + function for resolving ``CUDA_PATH``/``CUDA_HOME``. Features: - - Consistent behavior across all cuda-python packages (cuda.pathfinder, cuda.core, cuda.bindings) - Intelligent path comparison that handles symlinks, case sensitivity, and trailing slashes - Result caching for performance (first call determines the path for the process lifetime) - Clear warnings when both ``CUDA_PATH`` and ``CUDA_HOME`` are set but differ + Future releases of ``cuda-bindings`` and ``cuda-core`` will adopt this + function to ensure consistent ``CUDA_PATH`` > ``CUDA_HOME`` priority + across all cuda-python packages. (`PR #1801 `_) * Clarified that Python 3.8+ excludes ``PATH`` from the native Windows DLL From 6d065e90cba17de4302016ef145a9790493c3001 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 20 Mar 2026 17:38:45 -0700 Subject: [PATCH 13/15] fix oversights that slipped in when manually editing cuda_pathfinder/docs/nv-versions.json before Discovered via independent review from GPT-5.4 Extra High --- cuda_pathfinder/docs/nv-versions.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cuda_pathfinder/docs/nv-versions.json b/cuda_pathfinder/docs/nv-versions.json index eb3abd2abe..67c0b2b696 100644 --- a/cuda_pathfinder/docs/nv-versions.json +++ b/cuda_pathfinder/docs/nv-versions.json @@ -5,15 +5,15 @@ }, { "version": "1.5.0", - "url": "https://nvidia.github.io/cuda-python/cuda-pathfinder/1.4.1/" + "url": "https://nvidia.github.io/cuda-python/cuda-pathfinder/1.5.0/" }, { "version": "1.4.3", - "url": "https://nvidia.github.io/cuda-python/cuda-pathfinder/1.4.1/" + "url": "https://nvidia.github.io/cuda-python/cuda-pathfinder/1.4.3/" }, { "version": "1.4.2", - "url": "https://nvidia.github.io/cuda-python/cuda-pathfinder/1.4.1/" + "url": "https://nvidia.github.io/cuda-python/cuda-pathfinder/1.4.2/" }, { "version": "1.4.1", From 8d3ed030597d3638e466bcc118d84346144f2527 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 20 Mar 2026 21:02:04 -0700 Subject: [PATCH 14/15] fix(pathfinder): change found_via from "CUDA_HOME" to "CUDA_PATH" Aligns the provenance label with the new CUDA_PATH-first priority. The label signals the highest-priority env var name, not necessarily which variable supplied the value. Discovered via independent review from GPT-5.4 Extra High Made-with: Cursor --- .../cuda/pathfinder/_dynamic_libs/load_dl_common.py | 2 +- .../cuda/pathfinder/_dynamic_libs/search_steps.py | 7 +++++-- .../cuda/pathfinder/_headers/find_nvidia_headers.py | 2 +- .../cuda/pathfinder/_static_libs/find_bitcode_lib.py | 2 +- .../cuda/pathfinder/_static_libs/find_static_lib.py | 2 +- cuda_pathfinder/docs/source/release/1.5.0-notes.rst | 7 +++++++ cuda_pathfinder/tests/test_ctk_root_discovery.py | 2 +- cuda_pathfinder/tests/test_find_bitcode_lib.py | 4 ++-- cuda_pathfinder/tests/test_find_nvidia_headers.py | 4 ++-- cuda_pathfinder/tests/test_find_static_lib.py | 4 ++-- cuda_pathfinder/tests/test_search_steps.py | 6 +++--- 11 files changed, 26 insertions(+), 16 deletions(-) diff --git a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_common.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_common.py index 64f0bbd60a..8d7987d00e 100644 --- a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_common.py +++ b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_common.py @@ -28,7 +28,7 @@ class LoadedDL: abs_path: str | None was_already_loaded_from_elsewhere: bool _handle_uint: int # Platform-agnostic unsigned pointer value - found_via: str + found_via: str # "CUDA_PATH" covers both CUDA_PATH and CUDA_HOME env vars def load_dependencies(desc: LibDescriptor, load_func: Callable[[str], LoadedDL]) -> None: diff --git a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_steps.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_steps.py index e6061690ac..9e62756ed3 100644 --- a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_steps.py +++ b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_steps.py @@ -183,12 +183,15 @@ def find_in_conda(ctx: SearchContext) -> FindResult | None: def find_in_cuda_home(ctx: SearchContext) -> FindResult | None: - """Search ``$CUDA_HOME`` / ``$CUDA_PATH``. + """Search ``$CUDA_PATH`` / ``$CUDA_HOME``. On Windows, this is the normal fallback for system-installed CTK DLLs when they are not already discoverable via the native ``LoadLibraryExW(..., 0)`` path used by :func:`cuda.pathfinder._dynamic_libs.load_dl_windows.load_with_system_search`. Python 3.8+ does not include ``PATH`` in that native DLL search. + + The returned ``found_via`` is always ``"CUDA_PATH"`` regardless of which + environment variable actually provided the value. """ cuda_home = get_cuda_path_or_home() if cuda_home is None: @@ -196,7 +199,7 @@ def find_in_cuda_home(ctx: SearchContext) -> FindResult | None: lib_dir = _find_lib_dir_using_anchor(ctx.desc, ctx.platform, cuda_home) abs_path = _find_using_lib_dir(ctx, lib_dir) if abs_path is not None: - return FindResult(abs_path, "CUDA_HOME") + return FindResult(abs_path, "CUDA_PATH") return None diff --git a/cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py b/cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py index 4614e5a952..22800c87c5 100644 --- a/cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py +++ b/cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py @@ -107,7 +107,7 @@ def find_in_cuda_home(desc: HeaderDescriptor) -> LocatedHeaderDir | None: return None result = _locate_in_anchor_layout(desc, cuda_home) if result is not None: - return LocatedHeaderDir(abs_path=result, found_via="CUDA_HOME") + return LocatedHeaderDir(abs_path=result, found_via="CUDA_PATH") return None diff --git a/cuda_pathfinder/cuda/pathfinder/_static_libs/find_bitcode_lib.py b/cuda_pathfinder/cuda/pathfinder/_static_libs/find_bitcode_lib.py index f8de3aecf2..ea04bbda6f 100644 --- a/cuda_pathfinder/cuda/pathfinder/_static_libs/find_bitcode_lib.py +++ b/cuda_pathfinder/cuda/pathfinder/_static_libs/find_bitcode_lib.py @@ -145,7 +145,7 @@ def locate_bitcode_lib(name: str) -> LocatedBitcodeLib: name=name, abs_path=abs_path, filename=finder.filename, - found_via="CUDA_HOME", + found_via="CUDA_PATH", ) finder.raise_not_found_error() diff --git a/cuda_pathfinder/cuda/pathfinder/_static_libs/find_static_lib.py b/cuda_pathfinder/cuda/pathfinder/_static_libs/find_static_lib.py index 27e397527b..22cea7daad 100644 --- a/cuda_pathfinder/cuda/pathfinder/_static_libs/find_static_lib.py +++ b/cuda_pathfinder/cuda/pathfinder/_static_libs/find_static_lib.py @@ -149,7 +149,7 @@ def locate_static_lib(name: str) -> LocatedStaticLib: name=name, abs_path=abs_path, filename=finder.filename, - found_via="CUDA_HOME", + found_via="CUDA_PATH", ) finder.raise_not_found_error() diff --git a/cuda_pathfinder/docs/source/release/1.5.0-notes.rst b/cuda_pathfinder/docs/source/release/1.5.0-notes.rst index e893c347ad..ae67367d9e 100644 --- a/cuda_pathfinder/docs/source/release/1.5.0-notes.rst +++ b/cuda_pathfinder/docs/source/release/1.5.0-notes.rst @@ -15,6 +15,13 @@ Breaking Change a warning will be issued and ``CUDA_PATH`` will be used. This change aligns with industry standards and NVIDIA's recommended practices. + The ``found_via`` provenance field on ``LoadedDL``, ``LocatedHeaderDir``, + ``LocatedStaticLib``, and ``LocatedBitcodeLib`` now reports ``"CUDA_PATH"`` + (previously ``"CUDA_HOME"``) when a library or header was discovered through + the ``CUDA_PATH``/``CUDA_HOME`` environment variables. The label reflects + the highest-priority variable name, not necessarily the variable that + supplied the value. + **Migration Guide**: - If you rely on ``CUDA_HOME``, consider switching to ``CUDA_PATH`` diff --git a/cuda_pathfinder/tests/test_ctk_root_discovery.py b/cuda_pathfinder/tests/test_ctk_root_discovery.py index 59160ec238..19cbe847d2 100644 --- a/cuda_pathfinder/tests/test_ctk_root_discovery.py +++ b/cuda_pathfinder/tests/test_ctk_root_discovery.py @@ -424,7 +424,7 @@ def test_cuda_home_takes_priority_over_canary(tmp_path, mocker): result = _load_lib_no_cache("nvvm") # CUDA_HOME must win; the canary should never have been consulted - assert result.found_via == "CUDA_HOME" + assert result.found_via == "CUDA_PATH" assert result.abs_path == str(nvvm_home_lib) canary_mock.assert_not_called() diff --git a/cuda_pathfinder/tests/test_find_bitcode_lib.py b/cuda_pathfinder/tests/test_find_bitcode_lib.py index 021fd66453..7368722d29 100644 --- a/cuda_pathfinder/tests/test_find_bitcode_lib.py +++ b/cuda_pathfinder/tests/test_find_bitcode_lib.py @@ -54,7 +54,7 @@ def _located_bitcode_lib_asserts(located_bitcode_lib): assert isinstance(located_bitcode_lib.abs_path, str) assert isinstance(located_bitcode_lib.filename, str) assert isinstance(located_bitcode_lib.found_via, str) - assert located_bitcode_lib.found_via in ("site-packages", "conda", "CUDA_HOME") + assert located_bitcode_lib.found_via in ("site-packages", "conda", "CUDA_PATH") assert os.path.isfile(located_bitcode_lib.abs_path) @@ -110,7 +110,7 @@ def test_locate_bitcode_lib_search_order(monkeypatch, tmp_path): located_lib = locate_bitcode_lib("device") assert located_lib.abs_path == cuda_home_path - assert located_lib.found_via == "CUDA_HOME" + assert located_lib.found_via == "CUDA_PATH" @pytest.mark.usefixtures("clear_find_bitcode_lib_cache") diff --git a/cuda_pathfinder/tests/test_find_nvidia_headers.py b/cuda_pathfinder/tests/test_find_nvidia_headers.py index 2a0ad3e7ab..a47a235b2d 100644 --- a/cuda_pathfinder/tests/test_find_nvidia_headers.py +++ b/cuda_pathfinder/tests/test_find_nvidia_headers.py @@ -56,7 +56,7 @@ def _located_hdr_dir_asserts(located_hdr_dir): assert located_hdr_dir.found_via in ( "site-packages", "conda", - "CUDA_HOME", + "CUDA_PATH", "system-ctk-root", "supported_install_dir", ) @@ -201,7 +201,7 @@ def test_locate_ctk_headers_cuda_home_takes_priority_over_canary(tmp_path, monke assert located_hdr_dir is not None assert located_hdr_dir.abs_path == expected_hdr_dir - assert located_hdr_dir.found_via == "CUDA_HOME" + assert located_hdr_dir.found_via == "CUDA_PATH" probe.assert_not_called() diff --git a/cuda_pathfinder/tests/test_find_static_lib.py b/cuda_pathfinder/tests/test_find_static_lib.py index a03179f393..2b30aa1201 100644 --- a/cuda_pathfinder/tests/test_find_static_lib.py +++ b/cuda_pathfinder/tests/test_find_static_lib.py @@ -51,7 +51,7 @@ def _located_static_lib_asserts(located_static_lib): assert isinstance(located_static_lib.abs_path, str) assert isinstance(located_static_lib.filename, str) assert isinstance(located_static_lib.found_via, str) - assert located_static_lib.found_via in ("site-packages", "conda", "CUDA_HOME") + assert located_static_lib.found_via in ("site-packages", "conda", "CUDA_PATH") assert os.path.isfile(located_static_lib.abs_path) @@ -114,7 +114,7 @@ def test_locate_static_lib_search_order(monkeypatch, tmp_path): located_lib = locate_static_lib("cudadevrt") assert located_lib.abs_path == cuda_home_path - assert located_lib.found_via == "CUDA_HOME" + assert located_lib.found_via == "CUDA_PATH" @pytest.mark.usefixtures("clear_find_static_lib_cache") diff --git a/cuda_pathfinder/tests/test_search_steps.py b/cuda_pathfinder/tests/test_search_steps.py index a3e185dc68..acccf75a55 100644 --- a/cuda_pathfinder/tests/test_search_steps.py +++ b/cuda_pathfinder/tests/test_search_steps.py @@ -211,7 +211,7 @@ def test_found_linux(self, mocker, tmp_path): result = find_in_cuda_home(_ctx(platform=LinuxSearchPlatform())) assert result is not None assert result.abs_path == str(so_file) - assert result.found_via == "CUDA_HOME" + assert result.found_via == "CUDA_PATH" def test_found_windows(self, mocker, tmp_path): bin_dir = tmp_path / "bin" @@ -224,7 +224,7 @@ def test_found_windows(self, mocker, tmp_path): result = find_in_cuda_home(_ctx(platform=WindowsSearchPlatform())) assert result is not None assert result.abs_path == str(dll) - assert result.found_via == "CUDA_HOME" + assert result.found_via == "CUDA_PATH" # --------------------------------------------------------------------------- @@ -334,4 +334,4 @@ def test_nvvm_cuda_home_linux(self, mocker, tmp_path): result = find_in_cuda_home(_ctx(desc, platform=LinuxSearchPlatform())) assert result is not None assert result.abs_path == str(so_file) - assert result.found_via == "CUDA_HOME" + assert result.found_via == "CUDA_PATH" From 5e445bf64490cbb2bb582e435549921356a62e55 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 20 Mar 2026 22:10:15 -0700 Subject: [PATCH 15/15] fix(build): don't import cuda.pathfinder in build_hooks.py The build backends run in an isolated venv created by pyproject-build. Although cuda-pathfinder is listed in build-system.requires and gets installed, the cuda namespace package from backend-path=["."] shadows the installed cuda-pathfinder, making `from cuda.pathfinder import ...` fail with ModuleNotFoundError. This broke all CI wheel builds. Revert _get_cuda_path() to use os.environ.get() directly with CUDA_PATH > CUDA_HOME priority, and remove cuda-pathfinder from build-system.requires (it was not there on main; our PR added it). Made-with: Cursor --- cuda_bindings/build_hooks.py | 7 ++++--- cuda_bindings/pyproject.toml | 1 - cuda_core/build_hooks.py | 7 ++++--- cuda_core/pyproject.toml | 1 - cuda_core/tests/test_build_hooks.py | 5 ----- 5 files changed, 8 insertions(+), 13 deletions(-) diff --git a/cuda_bindings/build_hooks.py b/cuda_bindings/build_hooks.py index 4433304ab3..2ebbc7f86e 100644 --- a/cuda_bindings/build_hooks.py +++ b/cuda_bindings/build_hooks.py @@ -35,9 +35,10 @@ @functools.cache def _get_cuda_path() -> str: - from cuda.pathfinder import get_cuda_path_or_home - - cuda_path = get_cuda_path_or_home() + # Not using cuda.pathfinder.get_cuda_path_or_home() here because this + # build backend runs in an isolated venv where the cuda namespace package + # from backend-path shadows the installed cuda-pathfinder. + cuda_path = os.environ.get("CUDA_PATH", os.environ.get("CUDA_HOME")) if not cuda_path: raise RuntimeError("Environment variable CUDA_PATH or CUDA_HOME is not set") print("CUDA path:", cuda_path) diff --git a/cuda_bindings/pyproject.toml b/cuda_bindings/pyproject.toml index ae625db2db..96cfb4dd07 100644 --- a/cuda_bindings/pyproject.toml +++ b/cuda_bindings/pyproject.toml @@ -6,7 +6,6 @@ requires = [ "setuptools_scm[simple]>=8", "cython>=3.2,<3.3", "pyclibrary>=0.1.7", - "cuda-pathfinder", ] build-backend = "build_hooks" backend-path = ["."] diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index c4a79ea059..45d153ae82 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -30,9 +30,10 @@ @functools.cache def _get_cuda_path() -> str: - from cuda.pathfinder import get_cuda_path_or_home - - cuda_path = get_cuda_path_or_home() + # Not using cuda.pathfinder.get_cuda_path_or_home() here because this + # build backend runs in an isolated venv where the cuda namespace package + # from backend-path shadows the installed cuda-pathfinder. + cuda_path = os.environ.get("CUDA_PATH", os.environ.get("CUDA_HOME")) if not cuda_path: raise RuntimeError("Environment variable CUDA_PATH or CUDA_HOME is not set") print("CUDA path:", cuda_path) diff --git a/cuda_core/pyproject.toml b/cuda_core/pyproject.toml index 438d171ec2..28f35c824b 100644 --- a/cuda_core/pyproject.toml +++ b/cuda_core/pyproject.toml @@ -7,7 +7,6 @@ requires = [ "setuptools>=80", "setuptools-scm[simple]>=8", "Cython>=3.2,<3.3", - "cuda-pathfinder" ] build-backend = "build_hooks" backend-path = ["."] diff --git a/cuda_core/tests/test_build_hooks.py b/cuda_core/tests/test_build_hooks.py index af32f8e444..b298e7a977 100644 --- a/cuda_core/tests/test_build_hooks.py +++ b/cuda_core/tests/test_build_hooks.py @@ -24,8 +24,6 @@ import pytest -from cuda.pathfinder import get_cuda_path_or_home - # build_hooks.py imports Cython and setuptools at the top level, so skip if not available pytest.importorskip("Cython") pytest.importorskip("setuptools") @@ -70,7 +68,6 @@ def _check_version_detection( build_hooks._get_cuda_path.cache_clear() build_hooks._determine_cuda_major_version.cache_clear() - get_cuda_path_or_home.cache_clear() mock_env = { k: v @@ -95,7 +92,6 @@ def test_env_var_override(self, version): """CUDA_CORE_BUILD_MAJOR env var override works with various versions.""" build_hooks._get_cuda_path.cache_clear() build_hooks._determine_cuda_major_version.cache_clear() - get_cuda_path_or_home.cache_clear() with mock.patch.dict(os.environ, {"CUDA_CORE_BUILD_MAJOR": version}, clear=False): result = build_hooks._determine_cuda_major_version() assert result == version @@ -129,7 +125,6 @@ def test_missing_cuda_path_raises_error(self): """RuntimeError is raised when CUDA_PATH/CUDA_HOME not set and no env var override.""" build_hooks._get_cuda_path.cache_clear() build_hooks._determine_cuda_major_version.cache_clear() - get_cuda_path_or_home.cache_clear() with ( mock.patch.dict(os.environ, {}, clear=True), pytest.raises(RuntimeError, match="CUDA_PATH or CUDA_HOME"),