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
111 changes: 64 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 @@ -24,13 +22,30 @@
from cuda.pathfinder._utils.windows_arch import windows_pe_matches_arch, windows_python_arch


def sorted_glob(directory: Path, pattern: str, *, reverse: bool = False) -> list[Path]:
"""Return ``directory.glob(pattern)`` matches in a deterministic order.

The ordering is deliberately taken from the string form rather than from
``Path`` comparison: ``PurePath`` ordering is case-insensitive on Windows,
so plain ``sorted()`` over ``Path`` objects would reorder mixed-case
filenames relative to the byte-wise ordering used up to now. Issue #1732
tracks the newest-first policy that rides on this ordering, so it is kept
unchanged on both platforms.
"""
return sorted(directory.glob(pattern), key=str, reverse=reverse)


def _sorted_dir_entry_names(directory: Path) -> list[str]:
return sorted(entry.name for entry in directory.iterdir())


def _no_such_file_in_sub_dirs(
sub_dirs: Sequence[str], file_wild: str, error_messages: list[str], attachments: list[str]
) -> None:
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_dir_entry_names(Path(sub_dir)):
attachments.append(f" {node}")


Expand All @@ -39,7 +54,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 +66,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_glob(abs_dir_path, 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_glob(dirpath, 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 +97,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 +116,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 +126,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 +146,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 +158,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_glob(lib_dir, 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_dir_entry_names(lib_dir):
attachments.append(f" {node}")
return None

Expand All @@ -186,8 +203,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 +215,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_dir_entry_names(lib_dir):
attachments.append(f" {node}")
return None

Expand Down
Loading
Loading