Skip to content

Commit a876ed4

Browse files
committed
Migrate _binaries and _utils path handling to pathlib
Part 3 of the series proposed in #2410. _binaries/find_nvidia_binary_utility.py now works in Path internally: _is_executable_candidate, _ctk_bin_subdirs and _resolve_in_trusted_dirs take and return Path. str() is applied once, on the public return of find_nvidia_binary_utility(), which is unchanged. SITE_PACKAGES_BINDIRS holds path components instead of joined strings. The caller immediately did sub_dir.split(os.sep) to undo the join, and find_sub_dirs_all_sitepackages wants components anyway. find_sub_dirs_no_cache walks in Path. Its return type stays list[str]: _binaries, _dynamic_libs, _headers and _static_libs all consume it, so flipping it is better done on its own once those have moved. Its directory test goes through a small _is_dir() helper rather than calling Path.is_dir() directly. The two are not interchangeable here: os.path.isdir() returns False for any stat error, while Path.is_dir() only swallows the errnos in pathlib's ignore list and propagates the rest. This function walks site-packages trees that nobody here controls, so a single unreadable directory would have turned a clean "not found" into a PermissionError. _utils/env_vars.py uses Path.exists/Path.samefile. Both calls are already inside the existing try/except OSError, so the same error-handling difference does not apply. os.path.normcase and os.path.normpath stay in _paths_differ: PurePath does not collapse "..", which test_paths_differ_text_only depends on, and Path.resolve() would also follow symlinks. os.path.abspath likewise stays in _resolve_in_trusted_dirs, since Path.absolute() does not normalize. test_find_nvidia_binaries.py moves with the module: it asserts on the exact values passed to and returned by these private helpers, so it cannot be separated from the signature change. Verified by differential fuzzing of find_sub_dirs_no_cache against the previous implementation: 3000 calls over randomized trees comparing result order, plus 720 calls over trees containing unreadable directories. Identical, except that a parent dir spelled with redundant separators now yields the normalized form. Signed-off-by: LeSingh1 <sshaurya914@gmail.com>
1 parent 29acb74 commit a876ed4

5 files changed

Lines changed: 142 additions & 119 deletions

File tree

cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import importlib
66
import os
77
from collections.abc import Iterable
8+
from pathlib import Path
89
from typing import Any
910

1011
from cuda.pathfinder._binaries import supported_nvidia_binaries
@@ -44,22 +45,23 @@ def _normalize_utility_name(utility_name: str) -> str:
4445
return utility_name
4546

4647

47-
def _is_executable_candidate(path: str) -> bool:
48-
if not os.path.isfile(path):
48+
def _is_executable_candidate(path: Path) -> bool:
49+
if not path.is_file():
4950
return False
5051
if IS_WINDOWS:
5152
return True
53+
# pathlib has no access() equivalent.
5254
return os.access(path, os.X_OK)
5355

5456

55-
def _ctk_bin_subdirs(root: str) -> list[str]:
57+
def _ctk_bin_subdirs(root: Path) -> list[Path]:
5658
if IS_WINDOWS:
5759
return [
58-
os.path.join(root, "bin", "x64"),
59-
os.path.join(root, "bin", "x86_64"),
60-
os.path.join(root, "bin"),
60+
root / "bin" / "x64",
61+
root / "bin" / "x86_64",
62+
root / "bin",
6163
]
62-
return [os.path.join(root, "bin")]
64+
return [root / "bin"]
6365

6466

6567
def _resolve_candidate_paths(candidates: Iterable[str]) -> str | None:
@@ -69,7 +71,7 @@ def _resolve_candidate_paths(candidates: Iterable[str]) -> str | None:
6971
if candidate in seen:
7072
continue
7173
seen.add(candidate)
72-
if _is_executable_candidate(candidate):
74+
if _is_executable_candidate(Path(candidate)):
7375
return os.path.abspath(candidate)
7476
return None
7577

@@ -145,31 +147,34 @@ def _resolve_ctk_root_via_canary() -> str | None:
145147
return ctk_root
146148

147149

148-
def _resolve_in_trusted_dirs(normalized_name: str, dirs: list[str]) -> str | None:
150+
def _resolve_in_trusted_dirs(normalized_name: str, dirs: list[Path]) -> str | None:
149151
"""Resolve ``normalized_name`` against ``dirs`` in order."""
150-
seen: set[str] = set()
152+
seen: set[Path] = set()
151153
for directory in dirs:
152154
if directory in seen:
153155
continue
154-
assert directory
156+
# Path("") is Path("."), which would silently search the CWD (#2119).
157+
assert directory != Path()
155158
seen.add(directory)
156-
candidate = os.path.join(directory, normalized_name)
159+
candidate = directory / normalized_name
157160
if _is_executable_candidate(candidate):
158161
# Return an absolute path, as the docstring promises (a relative
159-
# search dir would otherwise leak a relative result).
162+
# search dir would otherwise leak a relative result). os.path.abspath
163+
# has no pathlib equivalent: Path.absolute() does not normalize and
164+
# Path.resolve() would also follow symlinks.
160165
return os.path.abspath(candidate)
161166
return None
162167

163168

164-
def _resolve_names_in_trusted_dirs(candidate_names: tuple[str, ...], dirs: list[str]) -> str | None:
169+
def _resolve_names_in_trusted_dirs(candidate_names: tuple[str, ...], dirs: list[Path]) -> str | None:
165170
"""Resolve ordered candidate names within each trusted directory."""
166-
seen: set[str] = set()
171+
seen: set[Path] = set()
167172
for directory in dirs:
168173
if directory in seen:
169174
continue
170-
assert directory
175+
assert directory != Path()
171176
seen.add(directory)
172-
found = _resolve_candidate_paths(os.path.join(directory, name) for name in candidate_names)
177+
found = _resolve_candidate_paths(str(directory / name) for name in candidate_names)
173178
if found is not None:
174179
return found
175180
return None
@@ -263,17 +268,15 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None:
263268

264269
# 1. Search in site-packages (NVIDIA wheels)
265270
candidate_dirs = supported_nvidia_binaries.SITE_PACKAGES_BINDIRS.get(utility_name, ())
266-
dirs = []
271+
dirs: list[Path] = []
267272

268273
for sub_dir in candidate_dirs:
269-
dirs.extend(find_sub_dirs_all_sitepackages(sub_dir.split(os.sep)))
274+
dirs.extend(Path(abs_dir) for abs_dir in find_sub_dirs_all_sitepackages(sub_dir))
270275

271276
# 2. Search in Conda environment
272277
if (conda_prefix := os.environ.get("CONDA_PREFIX")) is not None:
273-
if IS_WINDOWS:
274-
dirs.append(os.path.join(conda_prefix, "Library", "bin"))
275-
else:
276-
dirs.append(os.path.join(conda_prefix, "bin"))
278+
conda_root = Path(conda_prefix)
279+
dirs.append(conda_root / "Library" / "bin" if IS_WINDOWS else conda_root / "bin")
277280

278281
normalized_name = _normalize_utility_name(utility_name)
279282
if IS_WINDOWS and utility_name in ("compute-sanitizer", "ncu"):
@@ -305,5 +308,5 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None:
305308
if ctk_root is not None:
306309
if IS_WINDOWS and utility_name == "compute-sanitizer":
307310
return _find_windows_compute_sanitizer(ctk_root)
308-
return _resolve_in_trusted_dirs(normalized_name, _ctk_bin_subdirs(ctk_root))
311+
return _resolve_in_trusted_dirs(normalized_name, _ctk_bin_subdirs(Path(ctk_root)))
309312
return None

cuda_pathfinder/cuda/pathfinder/_binaries/supported_nvidia_binaries.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
# SPDX-License-Identifier: Apache-2.0
3-
import os
43

54
# Site-packages bin directories where binaries might be found
6-
# Based on NVIDIA wheel layouts (same for Linux and Windows)
7-
_CUDA_NVCC_BIN = os.path.join("nvidia", "cuda_nvcc", "bin")
8-
_CUDA13_BIN = os.path.join("nvidia", "cu13", "bin")
9-
_NSIGHT_SYSTEMS_BIN = os.path.join("nvidia", "nsight_systems", "bin")
10-
_NSIGHT_COMPUTE_BIN = os.path.join("nvidia", "nsight_compute", "bin")
5+
# Based on NVIDIA wheel layouts (same for Linux and Windows).
6+
# Path components, because that is what find_sub_dirs_all_sitepackages takes.
7+
_CUDA_NVCC_BIN = ("nvidia", "cuda_nvcc", "bin")
8+
_CUDA13_BIN = ("nvidia", "cu13", "bin")
9+
_NSIGHT_SYSTEMS_BIN = ("nvidia", "nsight_systems", "bin")
10+
_NSIGHT_COMPUTE_BIN = ("nvidia", "nsight_compute", "bin")
1111

1212
# Common CUDA binary utilities available on both Linux and Windows
1313
SITE_PACKAGES_BINDIRS = {

cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import functools
2323
import os
2424
import warnings
25+
from pathlib import Path
2526

2627
_CUDA_PATH_ENV_VARS_ORDERED = ("CUDA_PATH", "CUDA_HOME")
2728

@@ -36,15 +37,19 @@ def _paths_differ(a: str, b: str) -> bool:
3637
2) If still different AND both exist, use os.path.samefile to resolve symlinks/junctions.
3738
3) Otherwise (nonexistent paths or samefile unavailable), treat as different.
3839
"""
40+
# normcase/normpath have no pathlib equivalent: PurePath does not collapse
41+
# "..", Path.resolve() would also follow symlinks, and comparing PurePath
42+
# objects would only case-fold on Windows.
3943
norm_a = os.path.normcase(os.path.normpath(a))
4044
norm_b = os.path.normcase(os.path.normpath(b))
4145
if norm_a == norm_b:
4246
return False
4347

48+
path_a, path_b = Path(a), Path(b)
4449
try:
45-
if os.path.exists(a) and os.path.exists(b):
50+
if path_a.exists() and path_b.exists():
4651
# samefile raises on non-existent paths; only call when both exist.
47-
return not os.path.samefile(a, b)
52+
return not path_a.samefile(path_b)
4853
except OSError:
4954
# Fall through to "different" if samefile isn't applicable/available.
5055
pass

cuda_pathfinder/cuda/pathfinder/_utils/find_sub_dirs.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,51 @@
22
# SPDX-License-Identifier: Apache-2.0
33

44
import functools
5-
import os
65
import site
76
import sys
87
from collections.abc import Sequence
8+
from pathlib import Path
9+
10+
11+
def _is_dir(path: Path) -> bool:
12+
"""``path.is_dir()``, but False instead of raising on an inaccessible path.
13+
14+
This walks directories nobody here controls, so it has to tolerate whatever
15+
it runs into. Path.is_dir() only swallows the errnos in pathlib's ignore
16+
list, and raises for the rest (EACCES, ENAMETOOLONG); os.path.isdir, which
17+
this replaces, returned False for all of them.
18+
"""
19+
try:
20+
return path.is_dir()
21+
except OSError:
22+
return False
923

1024

1125
def find_sub_dirs_no_cache(parent_dirs: Sequence[str], sub_dirs: Sequence[str]) -> list[str]:
26+
# Results stay str: they are consumed by _binaries, _dynamic_libs, _headers
27+
# and _static_libs, so the type flip belongs in its own change.
1228
results = []
1329
for base in parent_dirs:
14-
stack = [(base, 0)] # (current_path, index into sub_dirs)
30+
stack = [(Path(base), 0)] # (current_path, index into sub_dirs)
1531
while stack:
1632
current_path, idx = stack.pop()
1733
if idx == len(sub_dirs):
18-
if os.path.isdir(current_path):
19-
results.append(current_path)
34+
if _is_dir(current_path):
35+
results.append(str(current_path))
2036
continue
2137

2238
sub = sub_dirs[idx]
2339
if sub == "*":
2440
try:
25-
entries = sorted(os.listdir(current_path))
41+
entries = sorted(current_path.iterdir(), key=lambda entry: entry.name)
2642
except OSError:
2743
continue
28-
for entry in entries:
29-
entry_path = os.path.join(current_path, entry)
30-
if os.path.isdir(entry_path):
44+
for entry_path in entries:
45+
if _is_dir(entry_path):
3146
stack.append((entry_path, idx + 1))
3247
else:
33-
next_path = os.path.join(current_path, sub)
34-
if os.path.isdir(next_path):
48+
next_path = current_path / sub
49+
if _is_dir(next_path):
3550
stack.append((next_path, idx + 1))
3651
return results
3752

0 commit comments

Comments
 (0)