Skip to content

Commit 1a16604

Browse files
committed
Add exact-name bitcode library lookup
1 parent 5be658a commit 1a16604

4 files changed

Lines changed: 171 additions & 16 deletions

File tree

cuda_pathfinder/cuda/pathfinder/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@
4141
from cuda.pathfinder._static_libs.find_bitcode_lib import (
4242
find_bitcode_lib as find_bitcode_lib,
4343
)
44+
from cuda.pathfinder._static_libs.find_bitcode_lib import (
45+
find_bitcode_lib_by_name as find_bitcode_lib_by_name,
46+
)
4447
from cuda.pathfinder._static_libs.find_bitcode_lib import (
4548
locate_bitcode_lib as locate_bitcode_lib,
4649
)
@@ -77,7 +80,8 @@
7780
SUPPORTED_BINARY_UTILITIES = _SUPPORTED_BINARIES
7881

7982
#: Tuple of supported bitcode library names that can be resolved
80-
#: via ``locate_bitcode_lib()`` and ``find_bitcode_lib()``.
83+
#: via ``locate_bitcode_lib()``, ``find_bitcode_lib()``, and
84+
#: ``find_bitcode_lib_by_name()``.
8185
#: Example value: ``"device"``.
8286
SUPPORTED_BITCODE_LIBS = _SUPPORTED_BITCODE_LIBS
8387

cuda_pathfinder/cuda/pathfinder/_static_libs/find_bitcode_lib.py

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

44
import functools
5+
import ntpath
56
import os
67
from dataclasses import dataclass
78
from typing import NoReturn, TypedDict
@@ -74,13 +75,21 @@ def _no_such_file_in_dir(dir_path: str, filename: str, error_messages: list[str]
7475
attachments.append(f' Directory does not exist: "{dir_path}"')
7576

7677

78+
def _validate_filename(filename: str) -> str:
79+
if not isinstance(filename, str):
80+
raise TypeError(f"filename must be a string, got {type(filename).__name__}")
81+
if filename in ("", ".", "..") or "\0" in filename or ntpath.basename(filename) != filename:
82+
raise ValueError(f"filename must be a file name without a directory: {filename!r}")
83+
return filename
84+
85+
7786
class _FindBitcodeLib:
78-
def __init__(self, name: str) -> None:
87+
def __init__(self, name: str, *, filename: str | None = None) -> None:
7988
if name not in _SUPPORTED_BITCODE_LIBS_INFO: # Updated reference
8089
raise ValueError(f"Unknown bitcode library: '{name}'. Supported: {', '.join(SUPPORTED_BITCODE_LIBS)}")
8190
self.name: str = name
8291
self.config: _BitcodeLibInfo = _SUPPORTED_BITCODE_LIBS_INFO[name] # Updated reference
83-
self.filename: str = self.config["filename"]
92+
self.filename: str = self.config["filename"] if filename is None else _validate_filename(filename)
8493
self.rel_path: str = self.config["rel_path"]
8594
self.site_packages_dirs: tuple[str, ...] = self.config["site_packages_dirs"]
8695
self.error_messages: list[str] = []
@@ -130,14 +139,8 @@ def raise_not_found_error(self) -> NoReturn:
130139
raise BitcodeLibNotFoundError(f'Failure finding "{self.filename}": {err}\n{att}')
131140

132141

133-
def locate_bitcode_lib(name: str) -> LocatedBitcodeLib:
134-
"""Locate a bitcode library by name.
135-
136-
Raises:
137-
ValueError: If ``name`` is not a supported bitcode library.
138-
BitcodeLibNotFoundError: If the bitcode library cannot be found.
139-
"""
140-
finder = _FindBitcodeLib(name)
142+
def _locate_bitcode_lib(name: str, *, filename: str | None = None) -> LocatedBitcodeLib:
143+
finder = _FindBitcodeLib(name, filename=filename)
141144

142145
abs_path = finder.try_site_packages()
143146
if abs_path is not None:
@@ -169,6 +172,16 @@ def locate_bitcode_lib(name: str) -> LocatedBitcodeLib:
169172
finder.raise_not_found_error()
170173

171174

175+
def locate_bitcode_lib(name: str) -> LocatedBitcodeLib:
176+
"""Locate a bitcode library by name.
177+
178+
Raises:
179+
ValueError: If ``name`` is not a supported bitcode library.
180+
BitcodeLibNotFoundError: If the bitcode library cannot be found.
181+
"""
182+
return _locate_bitcode_lib(name)
183+
184+
172185
@functools.cache
173186
def find_bitcode_lib(name: str) -> str:
174187
"""Find the absolute path to a bitcode library.
@@ -178,3 +191,23 @@ def find_bitcode_lib(name: str) -> str:
178191
BitcodeLibNotFoundError: If the bitcode library cannot be found.
179192
"""
180193
return locate_bitcode_lib(name).abs_path
194+
195+
196+
@functools.cache
197+
def find_bitcode_lib_by_name(name: str, filename: str) -> str:
198+
"""Find an exact bitcode filename using a supported library's search paths.
199+
200+
This lets a library define its own filename convention, including any
201+
architecture or other attributes encoded in the filename.
202+
203+
Args:
204+
name: Supported bitcode library whose configured directories to search.
205+
filename: Exact file name to find. Directory components are not allowed.
206+
207+
Raises:
208+
TypeError: If ``filename`` is not a string.
209+
ValueError: If ``name`` is unsupported or ``filename`` is not a file name.
210+
BitcodeLibNotFoundError: If ``filename`` cannot be found.
211+
"""
212+
_validate_filename(filename)
213+
return _locate_bitcode_lib(name, filename=filename).abs_path

cuda_pathfinder/docs/source/api.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ CUDA bitcode and static libraries.
3535

3636
SUPPORTED_BITCODE_LIBS
3737
find_bitcode_lib
38+
find_bitcode_lib_by_name
3839
locate_bitcode_lib
3940
LocatedBitcodeLib
4041
BitcodeLibNotFoundError

cuda_pathfinder/tests/test_find_bitcode_lib.py

Lines changed: 122 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
SUPPORTED_BITCODE_LIBS,
1212
BitcodeLibNotFoundError,
1313
find_bitcode_lib,
14+
find_bitcode_lib_by_name,
1415
locate_bitcode_lib,
1516
)
1617
from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home
@@ -30,15 +31,17 @@ def _bitcode_lib_filename(libname: str) -> str:
3031
@pytest.fixture
3132
def clear_find_bitcode_lib_cache():
3233
find_bitcode_lib_module.find_bitcode_lib.cache_clear()
34+
find_bitcode_lib_module.find_bitcode_lib_by_name.cache_clear()
3335
get_cuda_path_or_home.cache_clear()
3436
yield
3537
find_bitcode_lib_module.find_bitcode_lib.cache_clear()
38+
find_bitcode_lib_module.find_bitcode_lib_by_name.cache_clear()
3639
get_cuda_path_or_home.cache_clear()
3740

3841

39-
def _make_bitcode_lib_file(dir_path: Path, libname: str) -> str:
42+
def _make_bitcode_lib_file(dir_path: Path, filename: str) -> str:
4043
dir_path.mkdir(parents=True, exist_ok=True)
41-
file_path = dir_path / _bitcode_lib_filename(libname)
44+
file_path = dir_path / filename
4245
file_path.touch()
4346
return str(file_path)
4447

@@ -92,14 +95,15 @@ def test_locate_bitcode_lib(info_summary_append, libname):
9295
@pytest.mark.usefixtures("clear_find_bitcode_lib_cache")
9396
@pytest.mark.parametrize("libname", SUPPORTED_BITCODE_LIBS)
9497
def test_locate_bitcode_lib_search_order(monkeypatch, tmp_path, libname):
98+
filename = _bitcode_lib_filename(libname)
9599
site_packages_lib_dir = _site_packages_bitcode_lib_dir_under(tmp_path / "site-packages", libname)
96-
site_packages_path = _make_bitcode_lib_file(site_packages_lib_dir, libname)
100+
site_packages_path = _make_bitcode_lib_file(site_packages_lib_dir, filename)
97101

98102
conda_prefix = tmp_path / "conda-prefix"
99-
conda_path = _make_bitcode_lib_file(_bitcode_lib_dir_under(_conda_anchor(conda_prefix), libname), libname)
103+
conda_path = _make_bitcode_lib_file(_bitcode_lib_dir_under(_conda_anchor(conda_prefix), libname), filename)
100104

101105
cuda_home = tmp_path / "cuda-home"
102-
cuda_home_path = _make_bitcode_lib_file(_bitcode_lib_dir_under(cuda_home, libname), libname)
106+
cuda_home_path = _make_bitcode_lib_file(_bitcode_lib_dir_under(cuda_home, libname), filename)
103107

104108
site_packages_sub_dirs = tuple(
105109
tuple(rel_dir.split("/")) for rel_dir in _bitcode_lib_info(libname)["site_packages_dirs"]
@@ -135,6 +139,70 @@ def find_expected_sub_dir(sub_dir):
135139
assert located_lib.found_via == "CUDA_PATH"
136140

137141

142+
@pytest.mark.usefixtures("clear_find_bitcode_lib_cache")
143+
@pytest.mark.agent_authored(model="gpt-5")
144+
def test_find_bitcode_lib_by_name_search_order(monkeypatch, tmp_path):
145+
libname = "device"
146+
filename = "libdevice_sm_90.bc"
147+
site_packages_lib_dir = _site_packages_bitcode_lib_dir_under(tmp_path / "site-packages", libname)
148+
site_packages_path = _make_bitcode_lib_file(site_packages_lib_dir, filename)
149+
150+
conda_prefix = tmp_path / "conda-prefix"
151+
conda_path = _make_bitcode_lib_file(_bitcode_lib_dir_under(_conda_anchor(conda_prefix), libname), filename)
152+
153+
cuda_home = tmp_path / "cuda-home"
154+
cuda_home_path = _make_bitcode_lib_file(_bitcode_lib_dir_under(cuda_home, libname), filename)
155+
156+
site_packages_sub_dirs = tuple(
157+
tuple(rel_dir.split("/")) for rel_dir in _bitcode_lib_info(libname)["site_packages_dirs"]
158+
)
159+
160+
def find_expected_sub_dir(sub_dir):
161+
assert sub_dir in site_packages_sub_dirs
162+
if sub_dir == site_packages_sub_dirs[0]:
163+
return [str(site_packages_lib_dir)]
164+
return []
165+
166+
monkeypatch.setattr(
167+
find_bitcode_lib_module,
168+
"find_sub_dirs_all_sitepackages",
169+
find_expected_sub_dir,
170+
)
171+
monkeypatch.setenv("CONDA_PREFIX", str(conda_prefix))
172+
monkeypatch.setenv("CUDA_HOME", str(cuda_home))
173+
monkeypatch.delenv("CUDA_PATH", raising=False)
174+
175+
assert find_bitcode_lib_by_name(libname, filename) == site_packages_path
176+
os.remove(site_packages_path)
177+
find_bitcode_lib_by_name.cache_clear()
178+
179+
assert find_bitcode_lib_by_name(libname, filename) == conda_path
180+
os.remove(conda_path)
181+
find_bitcode_lib_by_name.cache_clear()
182+
183+
assert find_bitcode_lib_by_name(libname, filename) == cuda_home_path
184+
185+
186+
@pytest.mark.usefixtures("clear_find_bitcode_lib_cache")
187+
@pytest.mark.agent_authored(model="gpt-5")
188+
def test_find_bitcode_lib_by_name_cache_keeps_filenames_separate(monkeypatch, tmp_path):
189+
lib_dir = _site_packages_bitcode_lib_dir_under(tmp_path / "site-packages", "device")
190+
sm80_path = _make_bitcode_lib_file(lib_dir, "libdevice_sm_80.bc")
191+
sm90_path = _make_bitcode_lib_file(lib_dir, "libdevice_sm_90.bc")
192+
193+
monkeypatch.setattr(
194+
find_bitcode_lib_module,
195+
"find_sub_dirs_all_sitepackages",
196+
lambda _sub_dir: [str(lib_dir)],
197+
)
198+
monkeypatch.delenv("CONDA_PREFIX", raising=False)
199+
monkeypatch.delenv("CUDA_HOME", raising=False)
200+
monkeypatch.delenv("CUDA_PATH", raising=False)
201+
202+
assert find_bitcode_lib_by_name("device", "libdevice_sm_80.bc") == sm80_path
203+
assert find_bitcode_lib_by_name("device", "libdevice_sm_90.bc") == sm90_path
204+
205+
138206
@pytest.mark.usefixtures("clear_find_bitcode_lib_cache")
139207
def test_find_bitcode_lib_not_found_error_includes_cuda_home_directory_listing(monkeypatch, tmp_path):
140208
cuda_home = tmp_path / "cuda-home"
@@ -162,6 +230,32 @@ def test_find_bitcode_lib_not_found_error_includes_cuda_home_directory_listing(m
162230
assert "README.txt" in message
163231

164232

233+
@pytest.mark.usefixtures("clear_find_bitcode_lib_cache")
234+
@pytest.mark.agent_authored(model="gpt-5")
235+
def test_find_bitcode_lib_by_name_not_found_error_uses_requested_filename(monkeypatch, tmp_path):
236+
filename = "libdevice_sm_90.bc"
237+
cuda_home = tmp_path / "cuda-home"
238+
lib_dir = _bitcode_lib_dir_under(cuda_home, "device")
239+
lib_dir.mkdir(parents=True)
240+
(lib_dir / "libdevice.10.bc").touch()
241+
242+
monkeypatch.setattr(
243+
find_bitcode_lib_module,
244+
"find_sub_dirs_all_sitepackages",
245+
lambda _sub_dir: [],
246+
)
247+
monkeypatch.delenv("CONDA_PREFIX", raising=False)
248+
monkeypatch.setenv("CUDA_HOME", str(cuda_home))
249+
monkeypatch.delenv("CUDA_PATH", raising=False)
250+
251+
with pytest.raises(BitcodeLibNotFoundError, match=rf'Failure finding "{filename}"') as exc_info:
252+
find_bitcode_lib_by_name("device", filename)
253+
254+
message = str(exc_info.value)
255+
assert f"No such file: {lib_dir / filename}" in message
256+
assert "libdevice.10.bc" in message
257+
258+
165259
@pytest.mark.usefixtures("clear_find_bitcode_lib_cache")
166260
def test_find_bitcode_lib_not_found_error_without_cuda_home(monkeypatch):
167261
monkeypatch.setattr(
@@ -183,3 +277,26 @@ def test_find_bitcode_lib_not_found_error_without_cuda_home(monkeypatch):
183277
def test_find_bitcode_lib_invalid_name():
184278
with pytest.raises(ValueError, match="Unknown bitcode library"):
185279
find_bitcode_lib_module.locate_bitcode_lib("invalid")
280+
281+
282+
@pytest.mark.parametrize(
283+
"filename",
284+
("", ".", "..", "../file.bc", "subdir/file.bc", r"subdir\file.bc", r"C:\lib\file.bc", "bad\0name.bc"),
285+
)
286+
@pytest.mark.agent_authored(model="gpt-5")
287+
def test_find_bitcode_lib_by_name_rejects_paths(filename):
288+
with pytest.raises(ValueError, match="without a directory"):
289+
find_bitcode_lib_by_name("device", filename)
290+
291+
292+
@pytest.mark.parametrize("filename", (None, 90))
293+
@pytest.mark.agent_authored(model="gpt-5")
294+
def test_find_bitcode_lib_by_name_requires_string(filename):
295+
with pytest.raises(TypeError, match="filename must be a string"):
296+
find_bitcode_lib_by_name("device", filename)
297+
298+
299+
@pytest.mark.agent_authored(model="gpt-5")
300+
def test_find_bitcode_lib_by_name_invalid_project():
301+
with pytest.raises(ValueError, match="Unknown bitcode library"):
302+
find_bitcode_lib_by_name("not_a_real_lib", "libdevice_sm_90.bc")

0 commit comments

Comments
 (0)