Skip to content

Commit ccb957b

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 e0a2d1a commit ccb957b

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
@@ -4,6 +4,7 @@
44
import functools
55
import os
66
from collections.abc import Iterable
7+
from pathlib import Path
78

89
from cuda.pathfinder._binaries import supported_nvidia_binaries, windows_nsight
910
from cuda.pathfinder._utils.ctk_root_canary import CTK_ROOT_CANARY_ANCHOR_LIBNAMES
@@ -29,22 +30,23 @@ def _normalize_utility_name(utility_name: str) -> str:
2930
return utility_name
3031

3132

32-
def _is_executable_candidate(path: str) -> bool:
33-
if not os.path.isfile(path):
33+
def _is_executable_candidate(path: Path) -> bool:
34+
if not path.is_file():
3435
return False
3536
if IS_WINDOWS:
3637
return True
38+
# pathlib has no access() equivalent.
3739
return os.access(path, os.X_OK)
3840

3941

40-
def _ctk_bin_subdirs(root: str) -> list[str]:
42+
def _ctk_bin_subdirs(root: Path) -> list[Path]:
4143
if IS_WINDOWS:
4244
return [
43-
os.path.join(root, "bin", "x64"),
44-
os.path.join(root, "bin", "x86_64"),
45-
os.path.join(root, "bin"),
45+
root / "bin" / "x64",
46+
root / "bin" / "x86_64",
47+
root / "bin",
4648
]
47-
return [os.path.join(root, "bin")]
49+
return [root / "bin"]
4850

4951

5052
def _resolve_candidate_paths(candidates: Iterable[str]) -> str | None:
@@ -54,7 +56,7 @@ def _resolve_candidate_paths(candidates: Iterable[str]) -> str | None:
5456
if candidate in seen:
5557
continue
5658
seen.add(candidate)
57-
if _is_executable_candidate(candidate):
59+
if _is_executable_candidate(Path(candidate)):
5860
return os.path.abspath(candidate)
5961
return None
6062

@@ -75,31 +77,34 @@ def _resolve_ctk_root_via_canary() -> str | None:
7577
return ctk_root
7678

7779

78-
def _resolve_in_trusted_dirs(normalized_name: str, dirs: list[str]) -> str | None:
80+
def _resolve_in_trusted_dirs(normalized_name: str, dirs: list[Path]) -> str | None:
7981
"""Resolve ``normalized_name`` against ``dirs`` in order."""
80-
seen: set[str] = set()
82+
seen: set[Path] = set()
8183
for directory in dirs:
8284
if directory in seen:
8385
continue
84-
assert directory
86+
# Path("") is Path("."), which would silently search the CWD (#2119).
87+
assert directory != Path()
8588
seen.add(directory)
86-
candidate = os.path.join(directory, normalized_name)
89+
candidate = directory / normalized_name
8790
if _is_executable_candidate(candidate):
8891
# Return an absolute path, as the docstring promises (a relative
89-
# search dir would otherwise leak a relative result).
92+
# search dir would otherwise leak a relative result). os.path.abspath
93+
# has no pathlib equivalent: Path.absolute() does not normalize and
94+
# Path.resolve() would also follow symlinks.
9095
return os.path.abspath(candidate)
9196
return None
9297

9398

94-
def _resolve_names_in_trusted_dirs(candidate_names: tuple[str, ...], dirs: list[str]) -> str | None:
99+
def _resolve_names_in_trusted_dirs(candidate_names: tuple[str, ...], dirs: list[Path]) -> str | None:
95100
"""Resolve ordered candidate names within each trusted directory."""
96-
seen: set[str] = set()
101+
seen: set[Path] = set()
97102
for directory in dirs:
98103
if directory in seen:
99104
continue
100-
assert directory
105+
assert directory != Path()
101106
seen.add(directory)
102-
found = _resolve_candidate_paths(os.path.join(directory, name) for name in candidate_names)
107+
found = _resolve_candidate_paths(str(directory / name) for name in candidate_names)
103108
if found is not None:
104109
return found
105110
return None
@@ -193,17 +198,15 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None:
193198

194199
# 1. Search in site-packages (NVIDIA wheels)
195200
candidate_dirs = supported_nvidia_binaries.SITE_PACKAGES_BINDIRS.get(utility_name, ())
196-
dirs = []
201+
dirs: list[Path] = []
197202

198203
for sub_dir in candidate_dirs:
199-
dirs.extend(find_sub_dirs_all_sitepackages(sub_dir.split(os.sep)))
204+
dirs.extend(Path(abs_dir) for abs_dir in find_sub_dirs_all_sitepackages(sub_dir))
200205

201206
# 2. Search in Conda environment
202207
if (conda_prefix := os.environ.get("CONDA_PREFIX")) is not None:
203-
if IS_WINDOWS:
204-
dirs.append(os.path.join(conda_prefix, "Library", "bin"))
205-
else:
206-
dirs.append(os.path.join(conda_prefix, "bin"))
208+
conda_root = Path(conda_prefix)
209+
dirs.append(conda_root / "Library" / "bin" if IS_WINDOWS else conda_root / "bin")
207210

208211
normalized_name = _normalize_utility_name(utility_name)
209212
if IS_WINDOWS and utility_name in ("compute-sanitizer", "ncu"):
@@ -235,5 +238,5 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None:
235238
if ctk_root is not None:
236239
if IS_WINDOWS and utility_name == "compute-sanitizer":
237240
return _find_windows_compute_sanitizer(ctk_root)
238-
return _resolve_in_trusted_dirs(normalized_name, _ctk_bin_subdirs(ctk_root))
241+
return _resolve_in_trusted_dirs(normalized_name, _ctk_bin_subdirs(Path(ctk_root)))
239242
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)