Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -183,15 +183,15 @@ def _load_lib_no_cache(libname: str) -> LoadedDL:

# Phase 3: Load from found path, or fall back to system search + late find.
if find is not None:
return LOADER.load_with_abs_path(desc, find.abs_path, find.found_via)
return LOADER.load_with_abs_path(desc, str(find.abs_path), find.found_via)

loaded = LOADER.load_with_system_search(desc)
if loaded is not None:
return loaded

find = run_find_steps(ctx, LATE_FIND_STEPS)
if find is not None:
return LOADER.load_with_abs_path(desc, find.abs_path, find.found_via)
return LOADER.load_with_abs_path(desc, str(find.abs_path), find.found_via)

if desc.ctk_root_canary_anchor_libnames:
canary_abs_path = _try_ctk_root_canary(ctx)
Expand Down
94 changes: 47 additions & 47 deletions cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@

from __future__ import annotations

import glob
import os
from collections.abc import Sequence
from dataclasses import dataclass
from pathlib import PurePath
from pathlib import Path, PurePath
from typing import Protocol, cast

from cuda.pathfinder._dynamic_libs.lib_descriptor import LibDescriptor
Expand All @@ -30,7 +28,7 @@ def _no_such_file_in_sub_dirs(
error_messages.append(f"No such file: {file_wild}")
for sub_dir in find_sub_dirs_all_sitepackages(sub_dirs):
attachments.append(f' listdir("{sub_dir}"):')
for node in sorted(os.listdir(sub_dir)):
for node in sorted(entry.name for entry in Path(sub_dir).iterdir()):
attachments.append(f" {node}")


Expand All @@ -39,7 +37,7 @@ def _find_so_in_rel_dirs(
so_basename: str,
error_messages: list[str],
attachments: list[str],
) -> str | None:
) -> Path | None:
sub_dirs_searched: list[tuple[str, ...]] = []
file_wild = so_basename + "*"
for rel_dir in rel_dirs:
Expand All @@ -51,25 +49,27 @@ def _find_so_in_rel_dirs(
# multiple coexist, matching the newest-first bias elsewhere in pathfinder
# (see LinuxSearchPlatform.find_in_lib_dir and load_dl_linux._candidate_sonames).
# Issue #1732 tracks the deferred question of raising on true ambiguity.
so_name = os.path.join(abs_dir, so_basename)
if os.path.isfile(so_name):
return so_name
for so_name in sorted(glob.glob(os.path.join(abs_dir, file_wild)), reverse=True):
if os.path.isfile(so_name):
return so_name
abs_dir_path = Path(abs_dir)
so_path = abs_dir_path / so_basename
if so_path.is_file():
return so_path
for so_path in sorted(abs_dir_path.glob(file_wild), reverse=True):
if so_path.is_file():
return so_path
sub_dirs_searched.append(sub_dir)
for sub_dir in sub_dirs_searched:
_no_such_file_in_sub_dirs(sub_dir, file_wild, error_messages, attachments)
return None


def _find_dll_under_dir(dirpath: str, file_wild: str, target_arch: str | None = None) -> str | None:
for path in sorted(glob.glob(os.path.join(dirpath, file_wild))):
if not os.path.isfile(path):
def _find_dll_under_dir(dirpath: Path, file_wild: str, target_arch: str | None = None) -> Path | None:
for path in sorted(dirpath.glob(file_wild)):
if not path.is_file():
continue
if is_suppressed_dll_file(os.path.basename(path)):
if is_suppressed_dll_file(path.name):
continue
if target_arch is not None and not windows_pe_matches_arch(path, target_arch):
# windows_pe_matches_arch() lives in _utils and is still str-typed.
if target_arch is not None and not windows_pe_matches_arch(str(path), target_arch):
continue
return path
return None
Expand All @@ -80,14 +80,14 @@ def _find_dll_in_rel_dirs(
lib_searched_for: str,
error_messages: list[str],
attachments: list[str],
) -> str | None:
) -> Path | None:
sub_dirs_searched: list[tuple[str, ...]] = []
for rel_dir in rel_dirs:
sub_dir = PurePath(rel_dir).parts
for abs_dir in find_sub_dirs_all_sitepackages(sub_dir):
dll_name = _find_dll_under_dir(abs_dir, lib_searched_for)
if dll_name is not None:
return dll_name
dll_path = _find_dll_under_dir(Path(abs_dir), lib_searched_for)
if dll_path is not None:
return dll_path
sub_dirs_searched.append(sub_dir)
for sub_dir in sub_dirs_searched:
_no_such_file_in_sub_dirs(sub_dir, lib_searched_for, error_messages, attachments)
Expand All @@ -99,7 +99,7 @@ def lib_searched_for(self, libname: str) -> str: ...

def site_packages_rel_dirs(self, desc: LibDescriptor) -> tuple[str, ...]: ...

def conda_anchor_point(self, conda_prefix: str) -> str: ...
def conda_anchor_point(self, conda_prefix: str) -> Path: ...

def anchor_rel_dirs(self, desc: LibDescriptor) -> tuple[str, ...]: ...

Expand All @@ -109,16 +109,16 @@ def find_in_site_packages(
lib_searched_for: str,
error_messages: list[str],
attachments: list[str],
) -> str | None: ...
) -> Path | None: ...

def find_in_lib_dir(
self,
lib_dir: str,
lib_dir: Path,
desc: LibDescriptor,
lib_searched_for: str,
error_messages: list[str],
attachments: list[str],
) -> str | None: ...
) -> Path | None: ...


@dataclass(frozen=True, slots=True)
Expand All @@ -129,8 +129,8 @@ def lib_searched_for(self, libname: str) -> str:
def site_packages_rel_dirs(self, desc: LibDescriptor) -> tuple[str, ...]:
return cast(tuple[str, ...], desc.site_packages_linux)

def conda_anchor_point(self, conda_prefix: str) -> str:
return conda_prefix
def conda_anchor_point(self, conda_prefix: str) -> Path:
return Path(conda_prefix)

def anchor_rel_dirs(self, desc: LibDescriptor) -> tuple[str, ...]:
return cast(tuple[str, ...], desc.anchor_rel_dirs_linux)
Expand All @@ -141,37 +141,37 @@ def find_in_site_packages(
lib_searched_for: str,
error_messages: list[str],
attachments: list[str],
) -> str | None:
) -> Path | None:
return _find_so_in_rel_dirs(rel_dirs, lib_searched_for, error_messages, attachments)

def find_in_lib_dir(
self,
lib_dir: str,
lib_dir: Path,
_desc: LibDescriptor,
lib_searched_for: str,
error_messages: list[str],
attachments: list[str],
) -> str | None:
) -> Path | None:
# Most libraries have both unversioned and versioned files/symlinks (exact match first)
so_name = os.path.join(lib_dir, lib_searched_for)
if os.path.isfile(so_name):
return so_name
so_path = lib_dir / lib_searched_for
if so_path.is_file():
return so_path
# Some libraries only exist as versioned files (e.g., libcupti.so.13 in conda),
# so the glob fallback is needed
file_wild = lib_searched_for + "*"
# Only one match is expected, but to ensure deterministic behavior in unexpected
# situations, and to be internally consistent, we sort in reverse order with the
# intent to return the newest version first. Issue #1732 tracks the deferred
# question of raising on true ambiguity.
for so_name in sorted(glob.glob(os.path.join(lib_dir, file_wild)), reverse=True):
if os.path.isfile(so_name):
return so_name
for so_path in sorted(lib_dir.glob(file_wild), reverse=True):
if so_path.is_file():
return so_path
error_messages.append(f"No such file: {file_wild}")
attachments.append(f' listdir("{lib_dir}"):')
if not os.path.isdir(lib_dir):
if not lib_dir.is_dir():
attachments.append(" DIRECTORY DOES NOT EXIST")
else:
for node in sorted(os.listdir(lib_dir)):
for node in sorted(entry.name for entry in lib_dir.iterdir()):
attachments.append(f" {node}")
return None

Expand All @@ -186,8 +186,8 @@ def lib_searched_for(self, libname: str) -> str:
def site_packages_rel_dirs(self, desc: LibDescriptor) -> tuple[str, ...]:
return cast(tuple[str, ...], desc.site_packages_windows.for_arch(self.target_arch))

def conda_anchor_point(self, conda_prefix: str) -> str:
return os.path.join(conda_prefix, "Library")
def conda_anchor_point(self, conda_prefix: str) -> Path:
return Path(conda_prefix, "Library")

def anchor_rel_dirs(self, desc: LibDescriptor) -> tuple[str, ...]:
return cast(tuple[str, ...], desc.anchor_rel_dirs_windows.for_arch(self.target_arch))
Expand All @@ -198,31 +198,31 @@ def find_in_site_packages(
lib_searched_for: str,
error_messages: list[str],
attachments: list[str],
) -> str | None:
) -> Path | None:
return _find_dll_in_rel_dirs(rel_dirs, lib_searched_for, error_messages, attachments)

def find_in_lib_dir(
self,
lib_dir: str,
lib_dir: Path,
desc: LibDescriptor,
_lib_searched_for: str,
error_messages: list[str],
attachments: list[str],
) -> str | None:
) -> Path | None:
file_wild = desc.name + "*.dll"
target_arch = self.target_arch if desc.requires_windows_binary_arch_check else None
dll_name = _find_dll_under_dir(lib_dir, file_wild, target_arch)
if dll_name is not None:
return dll_name
dll_path = _find_dll_under_dir(lib_dir, file_wild, target_arch)
if dll_path is not None:
return dll_path
if target_arch is None:
error_messages.append(f"No such file: {file_wild}")
else:
error_messages.append(f"No {target_arch}-compatible PE file: {file_wild}")
attachments.append(f' listdir("{lib_dir}"):')
if not os.path.isdir(lib_dir):
if not lib_dir.is_dir():
attachments.append(" DIRECTORY DOES NOT EXIST")
else:
for node in sorted(os.listdir(lib_dir)):
for node in sorted(entry.name for entry in lib_dir.iterdir()):
attachments.append(f" {node}")
return None

Expand Down
48 changes: 30 additions & 18 deletions cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@
:data:`~cuda.pathfinder._dynamic_libs.search_platform.PLATFORM` instance.
"""

import glob
import os
from collections.abc import Callable
from dataclasses import dataclass, field
from pathlib import Path, PurePath
from typing import NoReturn, cast

from cuda.pathfinder._dynamic_libs.lib_descriptor import LibDescriptor
Expand All @@ -39,7 +39,7 @@
class FindResult:
"""A library file located on disk (not yet loaded)."""

abs_path: str
abs_path: Path
found_via: str


Expand Down Expand Up @@ -70,22 +70,26 @@ def raise_not_found(self) -> NoReturn:
FindStep = Callable[[SearchContext], FindResult | None]


def _find_lib_dir_using_anchor(desc: LibDescriptor, platform: SearchPlatform, anchor_point: str) -> str | None:
def _find_lib_dir_using_anchor(desc: LibDescriptor, platform: SearchPlatform, anchor_point: Path) -> Path | None:
"""Find the library directory under *anchor_point* using the descriptor's relative paths."""
rel_dirs = platform.anchor_rel_dirs(desc)
for rel_path in rel_dirs:
for dirname in sorted(glob.glob(os.path.join(anchor_point, rel_path))):
if os.path.isdir(dirname):
return os.path.normpath(dirname)
for match in sorted(anchor_point.glob(rel_path)):
if match.is_dir():
# os.path.normpath has no pathlib equivalent: PurePath deliberately does
# not collapse "..", and Path.resolve() would also follow symlinks. Keeping
# normpath preserves the exact path reported for an anchor such as a
# CUDA_PATH containing "..".
return Path(os.path.normpath(match))
return None


def _find_using_lib_dir(ctx: SearchContext, lib_dir: str | None) -> str | None:
def _find_using_lib_dir(ctx: SearchContext, lib_dir: Path | None) -> Path | None:
"""Find a library file in a resolved lib directory."""
if lib_dir is None:
return None
return cast(
str | None,
Path | None,
ctx.platform.find_in_lib_dir(
lib_dir,
ctx.desc,
Expand All @@ -104,15 +108,17 @@ def _derive_ctk_root_linux(resolved_lib_path: str) -> str | None:
- ``$CTK_ROOT/lib/libfoo.so.*``
- ``$CTK_ROOT/targets/<triple>/lib64/libfoo.so.*``
- ``$CTK_ROOT/targets/<triple>/lib/libfoo.so.*``

Uses the host path flavour (``PurePath``); the Windows canary layouts are
handled by :func:`_derive_ctk_root_windows`.
"""
lib_dir = os.path.dirname(resolved_lib_path)
basename = os.path.basename(lib_dir)
if basename in ("lib64", "lib"):
parent = os.path.dirname(lib_dir)
grandparent = os.path.dirname(parent)
if os.path.basename(grandparent) == "targets":
return os.path.dirname(grandparent)
return parent
lib_dir = PurePath(resolved_lib_path).parent
if lib_dir.name in ("lib64", "lib"):
parent = lib_dir.parent
grandparent = parent.parent
if grandparent.name == "targets":
return str(grandparent.parent)
return str(parent)
return None


Expand All @@ -123,6 +129,12 @@ def _derive_ctk_root_windows(resolved_lib_path: str) -> str | None:
- ``$CTK_ROOT/bin/x64/foo.dll`` (CTK 13 style)
- ``$CTK_ROOT/bin/arm64/foo.dll`` (Windows on Arm CTK 13 style)
- ``$CTK_ROOT/bin/foo.dll`` (CTK 12 style)

Uses ``ntpath`` rather than ``PureWindowsPath``: ``ntpath.dirname`` slices the
input and keeps whichever separator the caller used, while ``PureWindowsPath``
rewrites them to backslashes. :func:`derive_ctk_root` also reaches this function
on Linux, where rewriting would yield an unusable root for a POSIX path whose
parent directory happens to be named ``bin``.
"""
import ntpath

Expand All @@ -147,7 +159,7 @@ def derive_ctk_root(resolved_lib_path: str) -> str | None:

def find_via_ctk_root(ctx: SearchContext, ctk_root: str) -> FindResult | None:
"""Find a library under a previously derived CTK root."""
lib_dir = _find_lib_dir_using_anchor(ctx.desc, ctx.platform, ctk_root)
lib_dir = _find_lib_dir_using_anchor(ctx.desc, ctx.platform, Path(ctk_root))
abs_path = _find_using_lib_dir(ctx, lib_dir)
if abs_path is None:
return None
Expand Down Expand Up @@ -197,7 +209,7 @@ def find_in_cuda_path(ctx: SearchContext) -> FindResult | None:
cuda_home = get_cuda_path_or_home()
if cuda_home is None:
return None
lib_dir = _find_lib_dir_using_anchor(ctx.desc, ctx.platform, cuda_home)
lib_dir = _find_lib_dir_using_anchor(ctx.desc, ctx.platform, Path(cuda_home))
abs_path = _find_using_lib_dir(ctx, lib_dir)
if abs_path is not None:
return FindResult(abs_path, "CUDA_PATH")
Expand Down
Loading
Loading