Skip to content

Commit 59254f7

Browse files
committed
fix(pathfinder): address binary finder review
1 parent d14dc18 commit 59254f7

6 files changed

Lines changed: 33 additions & 61 deletions

File tree

cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py

Lines changed: 10 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,11 @@
55
import os
66

77
from cuda.pathfinder._binaries import supported_nvidia_binaries
8+
from cuda.pathfinder._utils.ctk_root_canary import CTK_ROOT_CANARY_ANCHOR_LIBNAMES
89
from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home
910
from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages
1011
from cuda.pathfinder._utils.platform_aware import IS_WINDOWS
1112

12-
# CUDA Toolkit canary library used to derive the toolkit root when it is only
13-
# visible through the dynamic loader. ``cudart`` always ships with the CTK and
14-
# matches the anchor used by the dynamic-library CTK-root canary flow.
15-
_CTK_ROOT_CANARY_ANCHOR_LIBNAME = "cudart"
16-
1713

1814
class UnsupportedBinaryError(Exception):
1915
def __init__(self, utility: str) -> None:
@@ -37,7 +33,7 @@ def _is_executable_file(path: str) -> bool:
3733
3834
On Windows executability is determined by the file extension (the
3935
candidate name already carries one), so existence is sufficient. On POSIX
40-
the execute permission bit must be set, matching ``shutil.which``.
36+
the execute permission bit must be set.
4137
"""
4238
if not os.path.isfile(path):
4339
return False
@@ -47,11 +43,6 @@ def _is_executable_file(path: str) -> bool:
4743

4844

4945
def _ctk_bin_subdirs(root: str) -> list[str]:
50-
"""Return the bin directories to search under a CUDA Toolkit ``root``.
51-
52-
On Windows the CTK ships binaries under ``bin/x64`` (CTK 13), ``bin/x86_64``,
53-
and ``bin`` (CTK 12); on Linux they live in ``bin``.
54-
"""
5546
if IS_WINDOWS:
5647
return [
5748
os.path.join(root, "bin", "x64"),
@@ -62,34 +53,19 @@ def _ctk_bin_subdirs(root: str) -> list[str]:
6253

6354

6455
def _resolve_ctk_root_via_canary() -> str | None:
65-
"""Derive the CUDA Toolkit root from the ``cudart`` canary library.
66-
67-
``cudart`` is resolved by the OS dynamic loader, which honors
68-
``LD_LIBRARY_PATH`` on Linux and the native DLL search on Windows, and the
69-
toolkit root is derived from its absolute path. The ambient ``PATH`` is
70-
never consulted. The loader module is imported lazily to avoid pulling the
71-
dynamic-library machinery in at import time.
72-
"""
7356
from cuda.pathfinder._dynamic_libs.load_nvidia_dynamic_lib import resolve_ctk_root_via_canary
7457

75-
ctk_root: str | None = resolve_ctk_root_via_canary(_CTK_ROOT_CANARY_ANCHOR_LIBNAME)
58+
ctk_root: str | None = resolve_ctk_root_via_canary(CTK_ROOT_CANARY_ANCHOR_LIBNAMES[0])
7659
return ctk_root
7760

7861

7962
def _resolve_in_trusted_dirs(normalized_name: str, dirs: list[str]) -> str | None:
80-
"""Resolve ``normalized_name`` against ``dirs`` only, in order.
81-
82-
Unlike ``shutil.which``, this never consults the current working directory
83-
or the ambient ``PATH``. On Windows ``shutil.which`` prepends the process
84-
CWD to the search even when an explicit ``path=`` is supplied, which lets a
85-
binary sitting in an arbitrary CWD shadow the trusted CUDA / Conda / wheel
86-
binary that pathfinder is contracted to discover. Searching the trusted
87-
directories explicitly keeps the lookup deterministic and bounded.
88-
"""
63+
"""Resolve ``normalized_name`` against ``dirs`` in order."""
8964
seen: set[str] = set()
9065
for directory in dirs:
91-
if not directory or directory in seen:
66+
if directory in seen:
9267
continue
68+
assert directory
9369
seen.add(directory)
9470
candidate = os.path.join(directory, normalized_name)
9571
if _is_executable_file(candidate):
@@ -137,11 +113,8 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None:
137113
4. **CTK-root canary fallback**
138114
139115
- Only when steps 1-3 miss: resolve the ``cudart`` library through the
140-
OS dynamic loader (which honors ``LD_LIBRARY_PATH`` on Linux and the
141-
native DLL search on Windows), derive the CUDA Toolkit root from it,
142-
and search that root's bin layout. This finds the utility for users
143-
who follow the CUDA install guide and set ``LD_LIBRARY_PATH`` for
144-
libraries without also setting ``CUDA_HOME`` / ``CUDA_PATH``.
116+
OS dynamic loader, derive the CUDA Toolkit root from it, and search
117+
that root's bin layout.
145118
146119
Note:
147120
Results are cached using ``@functools.cache`` for performance. The cache
@@ -152,8 +125,7 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None:
152125
are identified by the ``X_OK`` (execute) permission bit.
153126
154127
Lookup is restricted to the trusted directories and the canary-derived
155-
CTK root listed above; the process working directory and the ambient
156-
``PATH`` are never consulted.
128+
CTK root listed above.
157129
158130
Example:
159131
>>> from cuda.pathfinder import find_nvidia_binary_utility
@@ -187,9 +159,7 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None:
187159
if found is not None:
188160
return found
189161

190-
# 4. CTK-root canary fallback: only when the explicit trusted dirs above
191-
# miss. Resolve cudart via the dynamic loader (honors LD_LIBRARY_PATH),
192-
# derive the toolkit root, and search its bin layout. PATH is never used.
162+
# 4. CTK-root canary fallback.
193163
ctk_root = _resolve_ctk_root_via_canary()
194164
if ctk_root is not None:
195165
return _resolve_in_trusted_dirs(normalized_name, _ctk_bin_subdirs(ctk_root))

cuda_pathfinder/cuda/pathfinder/_dynamic_libs/descriptor_catalog.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from dataclasses import dataclass
99
from typing import Literal
1010

11+
from cuda.pathfinder._utils.ctk_root_canary import CTK_ROOT_CANARY_ANCHOR_LIBNAMES
12+
1113
PackagedWith = Literal["ctk", "other", "driver"]
1214

1315

@@ -73,7 +75,7 @@ class DescriptorSpec:
7375
site_packages_windows=("nvidia/cu13/bin/x86_64", "nvidia/cuda_nvcc/nvvm/bin"),
7476
anchor_rel_dirs_linux=("nvvm/lib64",),
7577
anchor_rel_dirs_windows=("nvvm/bin/*", "nvvm/bin"),
76-
ctk_root_canary_anchor_libnames=("cudart",),
78+
ctk_root_canary_anchor_libnames=CTK_ROOT_CANARY_ANCHOR_LIBNAMES,
7779
),
7880
DescriptorSpec(
7981
name="cublas",
@@ -291,7 +293,7 @@ class DescriptorSpec:
291293
site_packages_windows=("nvidia/cu13/bin/x86_64", "nvidia/cuda_cupti/bin"),
292294
anchor_rel_dirs_linux=("extras/CUPTI/lib64", "lib"),
293295
anchor_rel_dirs_windows=("extras/CUPTI/lib64", "bin"),
294-
ctk_root_canary_anchor_libnames=("cudart",),
296+
ctk_root_canary_anchor_libnames=CTK_ROOT_CANARY_ANCHOR_LIBNAMES,
295297
),
296298
DescriptorSpec(
297299
name="cudla",

cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
platform_include_subdirs,
2020
resolve_conda_anchor,
2121
)
22+
from cuda.pathfinder._utils.ctk_root_canary import CTK_ROOT_CANARY_ANCHOR_LIBNAMES
2223
from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home
2324
from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages
2425

@@ -121,7 +122,7 @@ def find_via_ctk_root_canary(desc: HeaderDescriptor) -> LocatedHeaderDir | None:
121122
"""
122123
if not desc.use_ctk_root_canary:
123124
return None
124-
canary_abs_path = _resolve_system_loaded_abs_path_in_subprocess("cudart")
125+
canary_abs_path = _resolve_system_loaded_abs_path_in_subprocess(CTK_ROOT_CANARY_ANCHOR_LIBNAMES[0])
125126
if canary_abs_path is None:
126127
return None
127128
ctk_root = derive_ctk_root(canary_abs_path)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
CTK_ROOT_CANARY_ANCHOR_LIBNAMES = ("cudart",)

cuda_pathfinder/docs/source/release/1.6.0-notes.rst

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,9 @@ Highlights
1010
----------
1111

1212
* :func:`find_nvidia_binary_utility` now resolves binaries through a bounded,
13-
deterministic search of trusted directories instead of ``shutil.which``. The
14-
process working directory and the ambient ``PATH`` are never consulted, which
15-
closes a lookup ambiguity on Windows where ``shutil.which`` prepends the CWD
16-
even when an explicit search path is supplied.
17-
(`PR #2196 <https://github.com/NVIDIA/cuda-python/pull/2196>`_)
18-
19-
* :func:`find_nvidia_binary_utility` gains a CTK-root canary fallback. When the
20-
NVIDIA wheel, ``CONDA_PREFIX``, and ``CUDA_HOME`` / ``CUDA_PATH`` directories
21-
all miss, ``cudart`` is resolved through the OS dynamic loader, which honors
22-
``LD_LIBRARY_PATH`` on Linux and the native DLL search on Windows. The CUDA
23-
Toolkit root is derived from that path and its ``bin`` layout is searched.
24-
This locates the utility for users who follow the CUDA installation guide and
25-
set ``LD_LIBRARY_PATH`` for libraries without also setting ``CUDA_HOME`` /
26-
``CUDA_PATH``, while still never falling back to ``PATH``.
13+
deterministic search of trusted directories instead of ``shutil.which`` and
14+
gains a CTK-root canary fallback. When the NVIDIA wheel, ``CONDA_PREFIX``, and
15+
``CUDA_HOME`` / ``CUDA_PATH`` directories all miss, ``cudart`` is resolved
16+
through the OS dynamic loader, the CUDA Toolkit root is derived from that
17+
path, and its ``bin`` layout is searched.
2718
(`PR #2196 <https://github.com/NVIDIA/cuda-python/pull/2196>`_)

cuda_pathfinder/tests/test_find_nvidia_binaries.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ def _make_executable(directory, name):
304304

305305
def test_cwd_is_not_searched(self, tmp_path, monkeypatch):
306306
# Regression for #2119: a binary in the process CWD must never shadow
307-
# the trusted directories the way shutil.which does on Windows.
307+
# the trusted directories.
308308
trusted = tmp_path / "trusted"
309309
trusted.mkdir()
310310
evil_cwd = tmp_path / "cwd"
@@ -315,7 +315,7 @@ def test_cwd_is_not_searched(self, tmp_path, monkeypatch):
315315
self._make_executable(evil_cwd, "nvcc") # the decoy that must be ignored
316316
monkeypatch.chdir(evil_cwd)
317317

318-
# The only trusted dir given has no binary -> None, never the CWD copy.
318+
# A trusted dir with no binary returns None, never the CWD copy.
319319
assert binary_finder_module._resolve_in_trusted_dirs("nvcc", [str(empty)]) is None
320320
# When a trusted dir holds it, that path wins regardless of CWD.
321321
assert binary_finder_module._resolve_in_trusted_dirs("nvcc", [str(empty), str(trusted)]) == trusted_nvcc
@@ -329,13 +329,17 @@ def test_first_trusted_dir_wins(self, tmp_path):
329329
self._make_executable(second, "nvcc")
330330
assert binary_finder_module._resolve_in_trusted_dirs("nvcc", [str(first), str(second)]) == first_nvcc
331331

332-
def test_empty_and_duplicate_dirs_skipped(self, tmp_path):
332+
def test_duplicate_dirs_skipped(self, tmp_path):
333333
present = tmp_path / "p"
334334
present.mkdir()
335335
nvcc = self._make_executable(present, "nvcc")
336-
assert binary_finder_module._resolve_in_trusted_dirs("nvcc", ["", str(present), str(present)]) == nvcc
336+
assert binary_finder_module._resolve_in_trusted_dirs("nvcc", [str(present), str(present)]) == nvcc
337337
assert binary_finder_module._resolve_in_trusted_dirs("nvcc", []) is None
338338

339+
def test_empty_dir_asserts(self):
340+
with pytest.raises(AssertionError):
341+
binary_finder_module._resolve_in_trusted_dirs("nvcc", [""])
342+
339343
@pytest.mark.skipif(binary_finder_module.IS_WINDOWS, reason="POSIX execute-bit semantics")
340344
def test_non_executable_file_rejected_on_posix(self, tmp_path):
341345
directory = tmp_path / "d"

0 commit comments

Comments
 (0)