diff --git a/cuda_bindings/pixi.lock b/cuda_bindings/pixi.lock index fb3d0ad393..3a9ae219de 100644 --- a/cuda_bindings/pixi.lock +++ b/cuda_bindings/pixi.lock @@ -515,7 +515,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - conda: . - build: py314h625260f_0 + build: py314hae7e39d_0 default: channels: - url: https://conda.anaconda.org/conda-forge/ @@ -1031,7 +1031,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - conda: . - build: py314h625260f_0 + build: py314hae7e39d_0 packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 @@ -1473,11 +1473,11 @@ packages: - conda: . name: cuda-bindings version: 13.1.0 - build: py314h625260f_0 - subdir: win-64 + build: py314ha479ada_0 + subdir: linux-aarch64 variants: python: 3.14.* - target_platform: win-64 + target_platform: linux-aarch64 depends: - python - cuda-pathfinder >=1.1,<2 @@ -1485,18 +1485,23 @@ packages: - cuda-nvrtc - cuda-nvrtc >=13.1.115,<14.0a0 - cuda-nvvm - - vc >=14.1,<15 - - vc14_runtime >=14.16.27033 + - libcufile + - libcufile >=1.16.1.26,<2.0a0 + - libgcc >=15 + - libgcc >=15 + - libstdcxx >=15 - python_abi 3.14.* *_cp314 license: LicenseRef-NVIDIA-SOFTWARE-LICENSE - conda: . name: cuda-bindings version: 13.1.0 - build: py314ha479ada_0 - subdir: linux-aarch64 + build: py314hae7e39d_0 + subdir: win-64 variants: + c_compiler: vs2022 + cxx_compiler: vs2022 python: 3.14.* - target_platform: linux-aarch64 + target_platform: win-64 depends: - python - cuda-pathfinder >=1.1,<2 @@ -1504,11 +1509,9 @@ packages: - cuda-nvrtc - cuda-nvrtc >=13.1.115,<14.0a0 - cuda-nvvm - - libcufile - - libcufile >=1.16.1.26,<2.0a0 - - libgcc >=15 - - libgcc >=15 - - libstdcxx >=15 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 - python_abi 3.14.* *_cp314 license: LicenseRef-NVIDIA-SOFTWARE-LICENSE - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cccl_linux-64-13.1.78-ha770c72_0.conda diff --git a/cuda_core/pixi.lock b/cuda_core/pixi.lock index b100dd71d2..e4c85d3624 100644 --- a/cuda_core/pixi.lock +++ b/cuda_core/pixi.lock @@ -280,7 +280,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - conda: . - build: py314h625260f_0 + build: py314hae7e39d_0 cu13: channels: - url: https://conda.anaconda.org/conda-forge/ @@ -536,7 +536,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - conda: . - build: py314h625260f_0 + build: py314hae7e39d_0 default: channels: - url: https://conda.anaconda.org/conda-forge/ @@ -792,7 +792,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - conda: . - build: py314h625260f_0 + build: py314hae7e39d_0 packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 @@ -1184,36 +1184,39 @@ packages: - conda: . name: cuda-core version: 0.5.0 - build: py314h625260f_0 - subdir: win-64 + build: py314ha479ada_0 + subdir: linux-aarch64 variants: python: 3.14.* - target_platform: win-64 + target_platform: linux-aarch64 depends: - python - numpy - cuda-bindings - - vc >=14.1,<15 - - vc14_runtime >=14.16.27033 + - libgcc >=15 + - libgcc >=15 + - libstdcxx >=15 - python_abi 3.14.* *_cp314 + - cuda-cudart >=13.1.80,<14.0a0 license: Apache-2.0 - conda: . name: cuda-core version: 0.5.0 - build: py314ha479ada_0 - subdir: linux-aarch64 + build: py314hae7e39d_0 + subdir: win-64 variants: + c_compiler: vs2022 + cxx_compiler: vs2022 python: 3.14.* - target_platform: linux-aarch64 + target_platform: win-64 depends: - python - numpy - cuda-bindings - - libgcc >=15 - - libgcc >=15 - - libstdcxx >=15 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 - python_abi 3.14.* *_cp314 - - cuda-cudart >=13.1.80,<14.0a0 license: Apache-2.0 - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-crt-dev_linux-64-12.9.86-ha770c72_2.conda sha256: e6257534c4b4b6b8a1192f84191c34906ab9968c92680fa09f639e7846a87304 diff --git a/cuda_pathfinder/cuda/pathfinder/__init__.py b/cuda_pathfinder/cuda/pathfinder/__init__.py index 8da4020116..12aa03a244 100644 --- a/cuda_pathfinder/cuda/pathfinder/__init__.py +++ b/cuda_pathfinder/cuda/pathfinder/__init__.py @@ -3,6 +3,10 @@ """cuda.pathfinder public APIs""" +from cuda.pathfinder._binaries.find_nvidia_binary_utility import ( + find_nvidia_binary_utility as find_nvidia_binary_utility, +) +from cuda.pathfinder._binaries.supported_nvidia_binaries import SUPPORTED_BINARIES as _SUPPORTED_BINARIES from cuda.pathfinder._dynamic_libs.load_dl_common import DynamicLibNotFoundError as DynamicLibNotFoundError from cuda.pathfinder._dynamic_libs.load_dl_common import LoadedDL as LoadedDL from cuda.pathfinder._dynamic_libs.load_nvidia_dynamic_lib import load_nvidia_dynamic_lib as load_nvidia_dynamic_lib @@ -11,6 +15,7 @@ ) from cuda.pathfinder._headers.find_nvidia_headers import find_nvidia_header_directory as find_nvidia_header_directory from cuda.pathfinder._headers.supported_nvidia_headers import SUPPORTED_HEADERS_CTK as _SUPPORTED_HEADERS_CTK +from cuda.pathfinder._headers.supported_nvidia_headers import SUPPORTED_HEADERS_NON_CTK as _SUPPORTED_HEADERS_NON_CTK from cuda.pathfinder._version import __version__ # isort: skip # noqa: F401 @@ -21,6 +26,16 @@ #: (e.g., ``"cufile"`` may be Linux-only). SUPPORTED_HEADERS_CTK = _SUPPORTED_HEADERS_CTK +#: Mapping from non-CTK library names to their canonical header basenames. +#: Example: ``"cutensor" → "cutensor.h"``. Platform-aware. +SUPPORTED_HEADERS_NON_CTK = _SUPPORTED_HEADERS_NON_CTK + +#: Tuple of supported CUDA binary utility names that can be located +#: via ``find_nvidia_binary_utility()``. Platform-aware (e.g., some +#: utilities may be available only on Linux or Windows). +#: Example utilities: ``"nvdisasm"``, ``"cuobjdump"``, ``"nvcc"``. +SUPPORTED_BINARY_UTILITIES = _SUPPORTED_BINARIES + # Backward compatibility: _find_nvidia_header_directory was added in release 1.2.2. # It will be removed in release 1.2.4. _find_nvidia_header_directory = find_nvidia_header_directory diff --git a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py new file mode 100644 index 0000000000..24925644c2 --- /dev/null +++ b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py @@ -0,0 +1,200 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import functools +import glob +import os + +from cuda.pathfinder._binaries import supported_nvidia_binaries +from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path +from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages +from cuda.pathfinder._utils.path_utils import _abs_norm, _is_executable +from cuda.pathfinder._utils.platform_aware import IS_WINDOWS + + +def _normalize_utility_name(utility_name: str) -> str: + """Normalize utility name by adding .exe on Windows if needed.""" + if IS_WINDOWS and not utility_name.lower().endswith((".exe", ".bat", ".cmd")): + return utility_name + ".exe" + return utility_name + + +def _find_under_site_packages(sub_dir: str, utility_name: str) -> str | None: + """Search for binary in site-packages subdirectories. + + Args: + sub_dir: Relative subdirectory path within site-packages + (e.g., "nvidia/cuda_nvcc/bin"). + utility_name: Name of the utility to find (will be normalized + for the current platform). + + Returns: + Absolute path to the binary if found, None otherwise. + """ + bin_path: str + normalized_name = _normalize_utility_name(utility_name) + for bin_dir in find_sub_dirs_all_sitepackages(tuple(sub_dir.split("/"))): + bin_path = os.path.join(bin_dir, normalized_name) + if _is_executable(bin_path): + return bin_path + return None + + +def _find_based_on_cuda_toolkit_layout(utility_name: str, anchor_point: str) -> str | None: + """Search in CUDA Toolkit style bin directories. + + Args: + utility_name: Name of the utility to find. + anchor_point: Base directory to search from (e.g., CUDA_HOME). + + Returns: + Absolute path to the binary if found, None otherwise. + """ + normalized_name = _normalize_utility_name(utility_name) + + # Windows: try bin/x64, bin/x86_64, bin; Linux: just bin + rel_paths = ["bin/x64", "bin/x86_64", "bin"] if IS_WINDOWS else ["bin"] + + for rel_path in rel_paths: + for bin_dir in sorted(glob.glob(os.path.join(anchor_point, rel_path))): + if not os.path.isdir(bin_dir): + continue + bin_path = os.path.join(bin_dir, normalized_name) + if _is_executable(bin_path): + return bin_path + + return None + + +def _find_based_on_conda_layout(utility_name: str) -> str | None: + """Search in Conda environment bin directories. + + Uses the CONDA_PREFIX environment variable to locate the Conda installation. + + Args: + utility_name: Name of the utility to find. + + Returns: + Absolute path to the binary if found, None otherwise. + """ + conda_prefix = os.environ.get("CONDA_PREFIX") + if not conda_prefix: + return None + + anchor_points = [os.path.join(conda_prefix, "Library"), conda_prefix] if IS_WINDOWS else [conda_prefix] + + for anchor_point in anchor_points: + if not os.path.isdir(anchor_point): + continue + if result := _find_based_on_cuda_toolkit_layout(utility_name, anchor_point): + return result + + return None + + +def _find_using_cuda_home(utility_name: str) -> str | None: + """Search using CUDA_HOME or CUDA_PATH environment variables. + + Checks CUDA_HOME first, then falls back to CUDA_PATH. + + Args: + utility_name: Name of the utility to find. + + Returns: + Absolute path to the binary if found, None otherwise. + """ + cuda_home = get_cuda_home_or_path() + if cuda_home is None: + return None + return _find_based_on_cuda_toolkit_layout(utility_name, cuda_home) + + +def _find_binary_utility(utility_name: str) -> str | None: + """Core search logic for finding a binary utility. + + Implements the search order: site-packages -> Conda -> CUDA Toolkit. + + Args: + utility_name: Name of the utility to find. + + Returns: + Absolute path to the binary if found, None otherwise. + """ + # 1. Search in site-packages (NVIDIA wheels) + candidate_dirs = supported_nvidia_binaries.SITE_PACKAGES_BINDIRS.get(utility_name, ()) + for cdir in candidate_dirs: + if bin_path := _find_under_site_packages(cdir, utility_name): + assert bin_path is not None + path: str = _abs_norm(bin_path) + return path + + # 2. Search in Conda environment + if bin_path := _find_based_on_conda_layout(utility_name): + assert bin_path is not None + path2: str = _abs_norm(bin_path) + return path2 + + # 3. Search in CUDA Toolkit (CUDA_HOME/CUDA_PATH) + if bin_path := _find_using_cuda_home(utility_name): + assert bin_path is not None + path3: str = _abs_norm(bin_path) + return path3 + + return None + + +@functools.cache +def find_nvidia_binary_utility(utility_name: str) -> str | None: + """Locate a CUDA binary utility executable. + + Args: + utility_name (str): The name of the binary utility to find + (e.g., ``"nvdisasm"``, ``"cuobjdump"``). On Windows, the ``.exe`` + extension will be automatically appended if not present. The function + also recognizes ``.bat`` and ``.cmd`` files on Windows. + + Returns: + str or None: Absolute path to the discovered executable, or ``None`` + if the utility cannot be found. The returned path is normalized + (absolute and with resolved separators). + + Raises: + RuntimeError: If ``utility_name`` is not in the supported set + (see ``SUPPORTED_BINARY_UTILITIES``). + + Search order: + 1. **NVIDIA Python wheels** + + - Scan installed distributions (``site-packages``) for binary layouts + shipped in NVIDIA wheels (e.g., ``cuda-nvcc``). + + 2. **Conda environments** + + - Check Conda-style installation prefixes via ``CONDA_PREFIX`` + environment variable, which use platform-specific bin directory + layouts (``Library/bin`` on Windows, ``bin`` on Linux). + + 3. **CUDA Toolkit environment variables** + + - Use ``CUDA_HOME`` or ``CUDA_PATH`` (in that order), searching + ``bin/x64``, ``bin/x86_64``, and ``bin`` subdirectories on Windows, + or just ``bin`` on Linux. + + Note: + Results are cached using ``@functools.cache`` for performance. The cache + persists for the lifetime of the process. + + On Windows, executables are identified by their file extensions + (``.exe``, ``.bat``, ``.cmd``). On Unix-like systems, executables + are identified by the ``X_OK`` (execute) permission bit. + + Example: + >>> from cuda.pathfinder import find_nvidia_binary_utility + >>> nvdisasm = find_nvidia_binary_utility("nvdisasm") + >>> if nvdisasm: + ... print(f"Found nvdisasm at: {nvdisasm}") + """ + if utility_name not in supported_nvidia_binaries.SUPPORTED_BINARIES: + raise RuntimeError(f"UNKNOWN {utility_name=}") + + return _find_binary_utility(utility_name) diff --git a/cuda_pathfinder/cuda/pathfinder/_binaries/supported_nvidia_binaries.py b/cuda_pathfinder/cuda/pathfinder/_binaries/supported_nvidia_binaries.py new file mode 100644 index 0000000000..ccf6f4fe9a --- /dev/null +++ b/cuda_pathfinder/cuda/pathfinder/_binaries/supported_nvidia_binaries.py @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + + +# Common CUDA binary utilities available on both Linux and Windows +SUPPORTED_BINARIES_ALL = ( + # Core compilation tools + "nvcc", + "nvdisasm", + "cuobjdump", + "nvprune", + "fatbinary", + "bin2c", + "nvlink", + # Runtime/debugging tools + "cuda-gdb", + "cuda-gdbserver", + "compute-sanitizer", + # Profiling tools + "nvprof", + "nsys", + "nsight-sys", + "ncu", + "nsight-compute", +) + +SUPPORTED_BINARIES = SUPPORTED_BINARIES_ALL + +# Site-packages bin directories where binaries might be found +# Based on NVIDIA wheel layouts (same for Linux and Windows) +SITE_PACKAGES_BINDIRS = { + "nvcc": ("nvidia/cuda_nvcc/bin",), + "nvdisasm": ("nvidia/cuda_nvcc/bin",), + "cuobjdump": ("nvidia/cuda_nvcc/bin",), + "nvprune": ("nvidia/cuda_nvcc/bin",), + "fatbinary": ("nvidia/cuda_nvcc/bin",), + "bin2c": ("nvidia/cuda_nvcc/bin",), + "nvlink": ("nvidia/cuda_nvcc/bin",), + "cuda-gdb": ("nvidia/cuda_nvcc/bin",), + "cuda-gdbserver": ("nvidia/cuda_nvcc/bin",), + "compute-sanitizer": ("nvidia/cuda_nvcc/bin",), + "nvprof": ("nvidia/cuda_nvcc/bin",), + "nsys": ("nvidia/nsight_systems/bin",), + "nsight-sys": ("nvidia/nsight_systems/bin",), + "ncu": ("nvidia/nsight_compute/bin",), + "nsight-compute": ("nvidia/nsight_compute/bin",), +} diff --git a/cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py b/cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py index 63f8a627fd..320f2957c4 100644 --- a/cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py +++ b/cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py @@ -8,15 +8,10 @@ from cuda.pathfinder._headers import supported_nvidia_headers from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages +from cuda.pathfinder._utils.path_utils import _abs_norm from cuda.pathfinder._utils.platform_aware import IS_WINDOWS -def _abs_norm(path: str | None) -> str | None: - if path: - return os.path.normpath(os.path.abspath(path)) - return None - - def _joined_isfile(dirpath: str, basename: str) -> bool: return os.path.isfile(os.path.join(dirpath, basename)) @@ -127,7 +122,11 @@ def find_nvidia_header_directory(libname: str) -> str | None: """ if libname in supported_nvidia_headers.SUPPORTED_HEADERS_CTK: - return _abs_norm(_find_ctk_header_directory(libname)) + ctk_dir = _find_ctk_header_directory(libname) + if ctk_dir is not None: + path: str = _abs_norm(ctk_dir) + return path + return None h_basename = supported_nvidia_headers.SUPPORTED_HEADERS_NON_CTK.get(libname) if h_basename is None: @@ -137,15 +136,21 @@ def find_nvidia_header_directory(libname: str) -> str | None: hdr_dir: str | None # help mypy for cdir in candidate_dirs: if hdr_dir := _find_under_site_packages(cdir, h_basename): - return _abs_norm(hdr_dir) + assert hdr_dir is not None + path2: str = _abs_norm(hdr_dir) + return path2 if hdr_dir := _find_based_on_conda_layout(libname, h_basename, False): - return _abs_norm(hdr_dir) + assert hdr_dir is not None + path3: str = _abs_norm(hdr_dir) + return path3 candidate_dirs = supported_nvidia_headers.SUPPORTED_INSTALL_DIRS_NON_CTK.get(libname, []) for cdir in candidate_dirs: for hdr_dir in sorted(glob.glob(cdir), reverse=True): if _joined_isfile(hdr_dir, h_basename): - return _abs_norm(hdr_dir) + assert hdr_dir is not None + path4: str = _abs_norm(hdr_dir) + return path4 return None diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/path_utils.py b/cuda_pathfinder/cuda/pathfinder/_utils/path_utils.py new file mode 100644 index 0000000000..7a1ccfd1eb --- /dev/null +++ b/cuda_pathfinder/cuda/pathfinder/_utils/path_utils.py @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import os + +from cuda.pathfinder._utils.platform_aware import IS_WINDOWS + + +def _abs_norm(path: str | None) -> str | None: + """Return normalized absolute path, or None if path is None. + + Converts relative paths to absolute and normalizes path separators + for the current platform. + + Args: + path: Path to normalize, or None. + + Returns: + Normalized absolute path, or None if input is None. + """ + if path is not None: + result: str = os.path.normpath(os.path.abspath(path)) + return result + return None + + +def _is_executable(filepath: str) -> bool: + """Check if a file exists and is executable. + + On Windows, checks if the file exists and has an executable extension + (.exe, .bat, .cmd). On Unix-like systems, checks if the file exists + and has the execute permission bit set (os.X_OK). + + Args: + filepath: Path to the file to check. + + Returns: + True if the file is executable, False otherwise. + """ + if not os.path.isfile(filepath): + return False + if IS_WINDOWS: + # On Windows, executables must have specific extensions (.exe, .bat, .cmd) + return filepath.lower().endswith((".exe", ".bat", ".cmd")) + else: + # On Unix, check execute permission + return os.access(filepath, os.X_OK) diff --git a/cuda_pathfinder/docs/source/api.rst b/cuda_pathfinder/docs/source/api.rst index 72e5e40724..19ade040b4 100644 --- a/cuda_pathfinder/docs/source/api.rst +++ b/cuda_pathfinder/docs/source/api.rst @@ -7,7 +7,7 @@ ================================= The ``cuda.pathfinder`` module provides utilities for loading NVIDIA dynamic libraries, -and experimental APIs for locating NVIDIA C/C++ header directories. +locating NVIDIA C/C++ header directories, and finding CUDA binary utilities. .. autosummary:: :toctree: generated/ @@ -20,3 +20,6 @@ and experimental APIs for locating NVIDIA C/C++ header directories. SUPPORTED_HEADERS_CTK SUPPORTED_HEADERS_NON_CTK find_nvidia_header_directory + + SUPPORTED_BINARY_UTILITIES + find_nvidia_binary_utility diff --git a/cuda_pathfinder/tests/test_find_nvidia_binaries.py b/cuda_pathfinder/tests/test_find_nvidia_binaries.py new file mode 100644 index 0000000000..44f2f5ecb6 --- /dev/null +++ b/cuda_pathfinder/tests/test_find_nvidia_binaries.py @@ -0,0 +1,67 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import os + +import pytest + +from cuda.pathfinder import find_nvidia_binary_utility +from cuda.pathfinder._binaries.supported_nvidia_binaries import ( + SUPPORTED_BINARIES, + SUPPORTED_BINARIES_ALL, +) + + +def test_unknown_utility_name(): + with pytest.raises(RuntimeError, match=r"^UNKNOWN utility_name='unknown-utility'$"): + find_nvidia_binary_utility("unknown-utility") + + +@pytest.mark.parametrize("utility_name", SUPPORTED_BINARIES) +def test_find_binary_utilities(info_summary_append, utility_name): + bin_path = find_nvidia_binary_utility(utility_name) + info_summary_append(f"{bin_path=!r}") + + if bin_path: + assert os.path.isfile(bin_path), f"Path exists but is not a file: {bin_path}" + # Note: We verify the file exists but don't check executability here because + # permissions may vary in test environments (e.g., mounted filesystems, CI + # containers). The _is_executable() check is tested separately in unit tests. + + +def test_supported_binaries_consistency(): + # Ensure SUPPORTED_BINARIES is a subset of SUPPORTED_BINARIES_ALL + assert set(SUPPORTED_BINARIES).issubset(set(SUPPORTED_BINARIES_ALL)) + + +def test_caching_behavior(): + # Call twice with same utility name to test @functools.cache + result1 = find_nvidia_binary_utility("nvdisasm") + result2 = find_nvidia_binary_utility("nvdisasm") + assert result1 is result2 # Should be the exact same object due to caching + + +def test_site_packages_bindirs_consistency(): + """Verify SITE_PACKAGES_BINDIRS keys are in SUPPORTED_BINARIES_ALL.""" + from cuda.pathfinder._binaries.supported_nvidia_binaries import SITE_PACKAGES_BINDIRS + + for utility_name in SITE_PACKAGES_BINDIRS: + assert utility_name in SUPPORTED_BINARIES_ALL, ( + f"Utility '{utility_name}' in SITE_PACKAGES_BINDIRS but not in SUPPORTED_BINARIES_ALL" + ) + + +def test_caching_per_utility(): + """Verify that different utilities have independent cache entries.""" + nvdisasm1 = find_nvidia_binary_utility("nvdisasm") + nvcc1 = find_nvidia_binary_utility("nvcc") + nvdisasm2 = find_nvidia_binary_utility("nvdisasm") + nvcc2 = find_nvidia_binary_utility("nvcc") + + # Same utility should return cached result + assert nvdisasm1 is nvdisasm2 + assert nvcc1 is nvcc2 + + # Different utilities should have different results (unless both None) + if nvdisasm1 is not None and nvcc1 is not None: + assert nvdisasm1 != nvcc1