diff --git a/conftest.py b/conftest.py index 7cea6e6612..3881b7d647 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 import get_cuda_path_or_home + def pytest_collection_modifyitems(config, items): # noqa: ARG001 - cuda_home = os.environ.get("CUDA_HOME") + cuda_home = get_cuda_path_or_home() for item in items: nodeid = item.nodeid.replace("\\", "/") diff --git a/cuda_bindings/README.md b/cuda_bindings/README.md index a0657706d0..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_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. 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 b2d3029c69..2ebbc7f86e 100644 --- a/cuda_bindings/build_hooks.py +++ b/cuda_bindings/build_hooks.py @@ -34,13 +34,15 @@ @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: + # 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) + return cuda_path # ----------------------------------------------------------------------- @@ -133,8 +135,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 +293,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 +309,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 +349,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_bindings/docs/source/environment_variables.rst b/cuda_bindings/docs/source/environment_variables.rst index 7a49fb66a3..f38916549a 100644 --- a/cuda_bindings/docs/source/environment_variables.rst +++ b/cuda_bindings/docs/source/environment_variables.rst @@ -15,7 +15,14 @@ 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. + + .. note:: + 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_bindings/docs/source/install.rst b/cuda_bindings/docs/source/install.rst index 58a6a0f31c..00db4b5911 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. 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_core/README.md b/cuda_core/README.md index 9925511ef9..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_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. 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 a98a33b6fb..45d153ae82 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -29,12 +29,14 @@ @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: + # 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") - cuda_path = cuda_path.split(os.pathsep) - print("CUDA paths:", cuda_path) + print("CUDA path:", cuda_path) return cuda_path @@ -60,21 +62,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 +133,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 diff --git a/cuda_core/examples/thread_block_cluster.py b/cuda_core/examples/thread_block_cluster.py index 495fe882a9..c056c59a86 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 import get_cuda_path_or_home # 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_path_or_home() 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..415f390819 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 import get_cuda_path_or_home # --------------------------------------------------------------------------- # 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_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/pyproject.toml b/cuda_core/pyproject.toml index 9b3e5a37c5..28f35c824b 100644 --- a/cuda_core/pyproject.toml +++ b/cuda_core/pyproject.toml @@ -6,7 +6,7 @@ requires = [ "setuptools>=80", "setuptools-scm[simple]>=8", - "Cython>=3.2,<3.3" + "Cython>=3.2,<3.3", ] build-backend = "build_hooks" backend-path = ["."] diff --git a/cuda_core/tests/conftest.py b/cuda_core/tests/conftest.py index 71d2f30573..a5e79dea74 100644 --- a/cuda_core/tests/conftest.py +++ b/cuda_core/tests/conftest.py @@ -9,6 +9,8 @@ import pytest +from cuda.pathfinder import get_cuda_path_or_home + 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_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 efe41f7015..6680a1a07b 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 import get_cuda_path_or_home from cuda_python_test_helpers import * -CUDA_PATH = os.environ.get("CUDA_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 419efbe065..b298e7a977 100644 --- a/cuda_core/tests/test_build_hooks.py +++ b/cuda_core/tests/test_build_hooks.py @@ -66,7 +66,7 @@ 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() mock_env = { @@ -90,7 +90,7 @@ 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() with mock.patch.dict(os.environ, {"CUDA_CORE_BUILD_MAJOR": version}, clear=False): result = build_hooks._determine_cuda_major_version() @@ -123,7 +123,7 @@ 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() with ( mock.patch.dict(os.environ, {}, clear=True), 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/_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/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/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/_dynamic_libs/search_steps.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_steps.py index 216c4e1a63..9e62756ed3 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 @@ -183,20 +183,23 @@ 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_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) 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 6e2ae100d8..22800c87c5 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: @@ -101,13 +101,13 @@ def find_in_conda(desc: HeaderDescriptor) -> LocatedHeaderDir | None: def find_in_cuda_home(desc: HeaderDescriptor) -> LocatedHeaderDir | None: - """Search ``$CUDA_HOME`` / ``$CUDA_PATH``.""" - cuda_home = get_cuda_home_or_path() + """Search ``$CUDA_PATH`` / ``$CUDA_HOME``.""" + cuda_home = get_cuda_path_or_home() if cuda_home is 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 @@ -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/_static_libs/find_bitcode_lib.py b/cuda_pathfinder/cuda/pathfinder/_static_libs/find_bitcode_lib.py index 109f9eb53f..ea04bbda6f 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 @@ -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 0b78f881fd..22cea7daad 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 @@ -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/cuda/pathfinder/_utils/env_vars.py b/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py index cf78a627cb..12198ac9f7 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py @@ -1,9 +1,30 @@ # 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_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. +""" + +import functools import os import warnings +_CUDA_PATH_ENV_VARS_ORDERED = ("CUDA_PATH", "CUDA_HOME") + def _paths_differ(a: str, b: str) -> bool: """ @@ -32,20 +53,53 @@ def _paths_differ(a: str, b: str) -> bool: return True -def get_cuda_home_or_path() -> str | None: - cuda_home = os.environ.get("CUDA_HOME") - cuda_path = os.environ.get("CUDA_PATH") +@functools.cache +def get_cuda_path_or_home() -> str | None: + """Get CUDA Toolkit path from environment variables. + + 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. + + Returns: + 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 + different locations (only on the first call). + + """ + # 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_PATH_ENV_VARS_ORDERED: + val = os.environ.get(var) + if val: + 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: + 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 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 values_differ: + var_list = "\n".join(f" {var}={val}" for var, val in set_vars.items()) + warnings.warn( + f"Multiple CUDA environment variables are set but differ:\n" + f"{var_list}\n" + f"Using {_CUDA_PATH_ENV_VARS_ORDERED[0]} (highest priority).", + UserWarning, + stacklevel=2, + ) - if cuda_home is not None: - return cuda_home - return cuda_path + # Return the first (highest priority) set variable + return next(iter(set_vars.values())) diff --git a/cuda_pathfinder/docs/nv-versions.json b/cuda_pathfinder/docs/nv-versions.json index eb0e60239e..67c0b2b696 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.5.0/" + }, + { + "version": "1.4.3", + "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.2/" + }, { "version": "1.4.1", "url": "https://nvidia.github.io/cuda-python/cuda-pathfinder/1.4.1/" diff --git a/cuda_pathfinder/docs/source/api.rst b/cuda_pathfinder/docs/source/api.rst index 5e842330a2..e49478c09e 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 @@ -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 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..ae67367d9e --- /dev/null +++ b/cuda_pathfinder/docs/source/release/1.5.0-notes.rst @@ -0,0 +1,49 @@ +.. SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. SPDX-License-Identifier: Apache-2.0 + +.. py:currentmodule:: cuda.pathfinder + +``cuda-pathfinder`` 1.5.0 Release notes +======================================= + +Breaking Change +--------------- + +* **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. + + 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`` + - 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 + +Highlights +---------- + +* Added :py:func:`cuda.pathfinder.get_cuda_path_or_home`, a centralized + function for resolving ``CUDA_PATH``/``CUDA_HOME``. Features: + + - 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 + 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 `_) diff --git a/cuda_pathfinder/tests/test_ctk_root_discovery.py b/cuda_pathfinder/tests/test_ctk_root_discovery.py index 403851f492..19cbe847d2 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, @@ -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() @@ -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 2da138c31d..7368722d29 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_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") @@ -23,8 +24,10 @@ @pytest.fixture def clear_find_bitcode_lib_cache(): find_bitcode_lib_module.find_bitcode_lib.cache_clear() + get_cuda_path_or_home.cache_clear() yield find_bitcode_lib_module.find_bitcode_lib.cache_clear() + get_cuda_path_or_home.cache_clear() def _make_bitcode_lib_file(dir_path: Path) -> str: @@ -51,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) @@ -107,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_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 2732de216b..a47a235b2d 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_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") @@ -55,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", ) @@ -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_path_or_home.cache_clear() yield locate_nvidia_header_directory.cache_clear() _resolve_system_loaded_abs_path_in_subprocess.cache_clear() + get_cuda_path_or_home.cache_clear() def _create_ctk_header(ctk_root: Path, libname: str) -> str: @@ -198,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 80e593f166..2b30aa1201 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_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") @@ -24,8 +25,10 @@ @pytest.fixture def clear_find_static_lib_cache(): find_static_lib_module.find_static_lib.cache_clear() + get_cuda_path_or_home.cache_clear() yield find_static_lib_module.find_static_lib.cache_clear() + get_cuda_path_or_home.cache_clear() def _make_static_lib_file(dir_path: Path, filename: str) -> str: @@ -48,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) @@ -111,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_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..acccf75a55 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,12 +206,12 @@ 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 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" @@ -219,12 +219,12 @@ 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 assert result.abs_path == str(dll) - assert result.found_via == "CUDA_HOME" + assert result.found_via == "CUDA_PATH" # --------------------------------------------------------------------------- @@ -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) @@ -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" diff --git a/cuda_pathfinder/tests/test_utils_env_vars.py b/cuda_pathfinder/tests/test_utils_env_vars.py index 40c7d4930d..99a55324fc 100644 --- a/cuda_pathfinder/tests/test_utils_env_vars.py +++ b/cuda_pathfinder/tests/test_utils_env_vars.py @@ -8,7 +8,10 @@ import pytest -from cuda.pathfinder._utils.env_vars import _paths_differ, get_cuda_home_or_path +from cuda.pathfinder._utils.env_vars import ( + _paths_differ, + get_cuda_path_or_home, +) skip_symlink_tests = pytest.mark.skipif( sys.platform == "win32", @@ -20,21 +23,30 @@ 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_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_path_or_home() == "/usr/local/cuda" -def test_empty_cuda_home_preserved(monkeypatch): - # empty string is returned as-is if set. +def test_all_empty_returns_none(monkeypatch): + unset_env(monkeypatch) + monkeypatch.setenv("CUDA_PATH", "") monkeypatch.setenv("CUDA_HOME", "") - monkeypatch.setenv("CUDA_PATH", "/does/not/matter") - assert get_cuda_home_or_path() == "" + assert get_cuda_path_or_home() is None -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 +56,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"): - result = get_cuda_home_or_path() - assert pathlib.Path(result) == home + # Different directories -> warning + prefer CUDA_PATH + with pytest.warns(UserWarning, match="Multiple CUDA environment variables are set but differ"): + result = get_cuda_path_or_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_path_or_home()) == only_home def test_no_warning_when_textually_equal_after_normalization(monkeypatch, tmp_path): @@ -68,12 +80,12 @@ 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: - result = get_cuda_home_or_path() + result = get_cuda_path_or_home() assert pathlib.Path(result) == d assert len(record) == 0 @@ -89,12 +101,12 @@ 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") - result = get_cuda_home_or_path() + result = get_cuda_path_or_home() assert pathlib.Path(result).samefile(d) assert len(record) == 0 @@ -106,12 +118,12 @@ 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"): - result = get_cuda_home_or_path() + with pytest.warns(UserWarning, match="Multiple CUDA environment variables are set but differ"): + result = get_cuda_path_or_home() assert pathlib.Path(result) == a @@ -124,11 +136,11 @@ 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"): - result = get_cuda_home_or_path() + with pytest.warns(UserWarning, match="Multiple CUDA environment variables are set but differ"): + result = get_cuda_path_or_home() assert pathlib.Path(result) == a @@ -146,17 +158,41 @@ 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: 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 +def test_search_order_matches_implementation(monkeypatch, tmp_path): + """ + Verify that get_cuda_path_or_home() 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)) + + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + result = get_cuda_path_or_home() + + 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 + + # --- unit tests for the helper itself (optional but nice to have) --- @@ -179,3 +215,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_path_or_home() 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_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_path_or_home() + assert pathlib.Path(result2) == first_dir + assert result1 == result2 + + # After clearing cache, should get new value + get_cuda_path_or_home.cache_clear() + result3 = get_cuda_path_or_home() + assert pathlib.Path(result3) == second_dir