Skip to content

Commit 7cf4a1b

Browse files
authored
fix(pathfinder): Linux .so glob fallback prefers newest (#1732) (#1966)
* fix(pathfinder): glob fallback prefers newest lib<name>.so* (#1732) * test(pathfinder): cover Linux .so glob fallback newest-first policy (#1732)
1 parent 7b00098 commit 7cf4a1b

2 files changed

Lines changed: 114 additions & 2 deletions

File tree

cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_platform.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,16 @@ def _find_so_in_rel_dirs(
4343
for rel_dir in rel_dirs:
4444
sub_dir = tuple(rel_dir.split(os.path.sep))
4545
for abs_dir in find_sub_dirs_all_sitepackages(sub_dir):
46+
# Exact unversioned match first; fall back to versioned names because some
47+
# distros only ship lib<name>.so.<major> (e.g. conda libcupti). Only one match
48+
# is expected in practice. Sort in reverse so the newest-sorting name wins if
49+
# multiple coexist, matching the newest-first bias elsewhere in pathfinder
50+
# (see LinuxSearchPlatform.find_in_lib_dir and load_dl_linux._candidate_sonames).
51+
# Issue #1732 tracks the deferred question of raising on true ambiguity.
4652
so_name = os.path.join(abs_dir, so_basename)
4753
if os.path.isfile(so_name):
4854
return so_name
49-
for so_name in sorted(glob.glob(os.path.join(abs_dir, file_wild))):
55+
for so_name in sorted(glob.glob(os.path.join(abs_dir, file_wild)), reverse=True):
5056
if os.path.isfile(so_name):
5157
return so_name
5258
sub_dirs_searched.append(sub_dir)
@@ -150,7 +156,8 @@ def find_in_lib_dir(
150156
file_wild = lib_searched_for + "*"
151157
# Only one match is expected, but to ensure deterministic behavior in unexpected
152158
# situations, and to be internally consistent, we sort in reverse order with the
153-
# intent to return the newest version first.
159+
# intent to return the newest version first. Issue #1732 tracks the deferred
160+
# question of raising on true ambiguity.
154161
for so_name in sorted(glob.glob(os.path.join(lib_dir, file_wild)), reverse=True):
155162
if os.path.isfile(so_name):
156163
return so_name

cuda_pathfinder/tests/test_search_steps.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,63 @@ def test_not_found_appends_error(self, mocker, tmp_path):
148148
assert result is None
149149
assert any("No such file" in m for m in ctx.error_messages)
150150

151+
# The next three tests cover the Linux glob fallback in
152+
# cuda.pathfinder._dynamic_libs.search_platform._find_so_in_rel_dirs.
153+
# The fallback triggers when the unversioned libfoo.so is absent but
154+
# versioned libfoo.so.<major> files exist (e.g. some conda layouts).
155+
# Issue #1732 tracks the decision to return the newest-sorting match
156+
# deterministically; these tests lock in that policy at the
157+
# site-packages call site.
158+
159+
def test_glob_fallback_returns_single_versioned_match(self, mocker, tmp_path):
160+
lib_dir = tmp_path / "nvidia" / "cuda_runtime" / "lib"
161+
lib_dir.mkdir(parents=True)
162+
versioned = lib_dir / "libcudart.so.13"
163+
versioned.touch()
164+
165+
mocker.patch(
166+
f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages",
167+
return_value=[str(lib_dir)],
168+
)
169+
170+
result = find_in_site_packages(_ctx(platform=LinuxSearchPlatform()))
171+
assert result is not None
172+
assert result.abs_path == str(versioned)
173+
assert result.found_via == "site-packages"
174+
175+
def test_glob_fallback_returns_newest_of_multiple_matches(self, mocker, tmp_path):
176+
lib_dir = tmp_path / "nvidia" / "cuda_runtime" / "lib"
177+
lib_dir.mkdir(parents=True)
178+
older = lib_dir / "libcudart.so.12"
179+
newer = lib_dir / "libcudart.so.13"
180+
older.touch()
181+
newer.touch()
182+
183+
mocker.patch(
184+
f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages",
185+
return_value=[str(lib_dir)],
186+
)
187+
188+
result = find_in_site_packages(_ctx(platform=LinuxSearchPlatform()))
189+
assert result is not None
190+
assert result.abs_path == str(newer)
191+
assert result.found_via == "site-packages"
192+
193+
def test_glob_fallback_zero_matches_returns_none(self, mocker, tmp_path):
194+
lib_dir = tmp_path / "nvidia" / "cuda_runtime" / "lib"
195+
lib_dir.mkdir(parents=True)
196+
(lib_dir / "unrelated.txt").touch()
197+
198+
mocker.patch(
199+
f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages",
200+
return_value=[str(lib_dir)],
201+
)
202+
203+
ctx = _ctx(platform=LinuxSearchPlatform())
204+
result = find_in_site_packages(ctx)
205+
assert result is None
206+
assert any("No such file" in m and "libcudart.so" in m for m in ctx.error_messages)
207+
151208

152209
# ---------------------------------------------------------------------------
153210
# find_in_conda
@@ -189,6 +246,54 @@ def test_found_windows(self, mocker, tmp_path):
189246
assert result.abs_path == str(dll)
190247
assert result.found_via == "conda"
191248

249+
# The next three tests cover the Linux glob fallback in
250+
# cuda.pathfinder._dynamic_libs.search_platform.LinuxSearchPlatform.find_in_lib_dir,
251+
# which is exercised by find_in_conda (and find_in_cuda_path) when the
252+
# resolved lib dir contains only versioned libfoo.so.<major> files.
253+
# Issue #1732 tracks the decision to return the newest-sorting match
254+
# deterministically; these tests lock in that policy at the conda /
255+
# CUDA_PATH call site.
256+
257+
def test_glob_fallback_returns_single_versioned_match(self, mocker, tmp_path):
258+
lib_dir = tmp_path / "lib"
259+
lib_dir.mkdir()
260+
versioned = lib_dir / "libcudart.so.13"
261+
versioned.touch()
262+
263+
mocker.patch.dict(os.environ, {"CONDA_PREFIX": str(tmp_path)})
264+
265+
result = find_in_conda(_ctx(platform=LinuxSearchPlatform()))
266+
assert result is not None
267+
assert result.abs_path == str(versioned)
268+
assert result.found_via == "conda"
269+
270+
def test_glob_fallback_returns_newest_of_multiple_matches(self, mocker, tmp_path):
271+
lib_dir = tmp_path / "lib"
272+
lib_dir.mkdir()
273+
older = lib_dir / "libcudart.so.12"
274+
newer = lib_dir / "libcudart.so.13"
275+
older.touch()
276+
newer.touch()
277+
278+
mocker.patch.dict(os.environ, {"CONDA_PREFIX": str(tmp_path)})
279+
280+
result = find_in_conda(_ctx(platform=LinuxSearchPlatform()))
281+
assert result is not None
282+
assert result.abs_path == str(newer)
283+
assert result.found_via == "conda"
284+
285+
def test_glob_fallback_zero_matches_returns_none(self, mocker, tmp_path):
286+
lib_dir = tmp_path / "lib"
287+
lib_dir.mkdir()
288+
(lib_dir / "unrelated.txt").touch()
289+
290+
mocker.patch.dict(os.environ, {"CONDA_PREFIX": str(tmp_path)})
291+
292+
ctx = _ctx(platform=LinuxSearchPlatform())
293+
result = find_in_conda(ctx)
294+
assert result is None
295+
assert any("No such file" in m and "libcudart.so" in m for m in ctx.error_messages)
296+
192297

193298
# ---------------------------------------------------------------------------
194299
# find_in_cuda_path

0 commit comments

Comments
 (0)