From 091b75d1d2837820f288c272b753775845143eb4 Mon Sep 17 00:00:00 2001 From: 0z5a Date: Wed, 4 Nov 2026 16:39:29 +0100 Subject: [PATCH 1/2] [pathfinder] Respect site-packages root priority across wheel layouts Signed-off-by: 0z5a --- .../_binaries/find_nvidia_binary_utility.py | 8 +- .../_dynamic_libs/descriptor_catalog.py | 2 +- .../_dynamic_libs/search_platform.py | 31 ++-- .../_headers/find_nvidia_headers.py | 11 +- .../_static_libs/find_bitcode_lib.py | 13 +- .../_static_libs/find_static_lib.py | 13 +- .../cuda/pathfinder/_utils/find_sub_dirs.py | 18 ++- .../tests/test_find_bitcode_lib.py | 14 +- .../tests/test_find_nvidia_binaries.py | 38 ++--- .../tests/test_find_nvidia_headers.py | 10 +- cuda_pathfinder/tests/test_find_static_lib.py | 8 +- cuda_pathfinder/tests/test_search_steps.py | 24 ++-- .../tests/test_site_packages_precedence.py | 133 ++++++++++++++++++ 13 files changed, 235 insertions(+), 88 deletions(-) create mode 100644 cuda_pathfinder/tests/test_site_packages_precedence.py diff --git a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py index 42dcfda1cfb..ad31a13a473 100644 --- a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py +++ b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py @@ -8,7 +8,7 @@ from cuda.pathfinder._binaries import supported_nvidia_binaries, windows_nsight from cuda.pathfinder._utils.ctk_root_canary import CTK_ROOT_CANARY_ANCHOR_LIBNAMES from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home -from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages +from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages_ordered from cuda.pathfinder._utils.platform_aware import IS_WINDOWS @@ -193,10 +193,8 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None: # 1. Search in site-packages (NVIDIA wheels) candidate_dirs = supported_nvidia_binaries.SITE_PACKAGES_BINDIRS.get(utility_name, ()) - dirs = [] - - for sub_dir in candidate_dirs: - dirs.extend(find_sub_dirs_all_sitepackages(sub_dir.split(os.sep))) + patterns = tuple(tuple(sub_dir.split(os.sep)) for sub_dir in candidate_dirs) + dirs = find_sub_dirs_all_sitepackages_ordered(patterns) if patterns else [] # 2. Search in Conda environment if (conda_prefix := os.environ.get("CONDA_PREFIX")) is not None: diff --git a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/descriptor_catalog.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/descriptor_catalog.py index 4a5035e2577..ccaa28bad7f 100644 --- a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/descriptor_catalog.py +++ b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/descriptor_catalog.py @@ -47,7 +47,7 @@ def for_arch(self, target_arch: str) -> tuple[str, ...]: def _ctk_windows_wheel_dirs(cuda13_bin_dir: str, cuda12_dir: str) -> WindowsSearchDirs: - """Search CUDA 13 first, with the x64-only CUDA 12 wheel as an x64 fallback.""" + """Within each site root, prefer CUDA 13, with CUDA 12 as an x64 fallback.""" cuda13_bin_path = PurePosixPath(cuda13_bin_dir) return WindowsSearchDirs( x64=((cuda13_bin_path / "x86_64").as_posix(), cuda12_dir), diff --git a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_platform.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_platform.py index 71a92b00924..74e366d613c 100644 --- a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_platform.py +++ b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/search_platform.py @@ -18,7 +18,10 @@ from cuda.pathfinder._dynamic_libs.lib_descriptor import LibDescriptor from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import is_suppressed_dll_file -from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages +from cuda.pathfinder._utils.find_sub_dirs import ( + find_sub_dirs_all_sitepackages, + find_sub_dirs_all_sitepackages_ordered, +) from cuda.pathfinder._utils.platform_aware import IS_WINDOWS from cuda.pathfinder._utils.windows_arch import windows_pe_matches_arch, windows_python_arch @@ -48,14 +51,11 @@ def _find_so_in_rel_dirs( error_messages: list[str], attachments: list[str], ) -> str | 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): - so_path = _find_descriptor_so_under_dir(abs_dir, desc) - if so_path is not None: - return so_path - sub_dirs_searched.append(sub_dir) + sub_dirs_searched = tuple(PurePath(rel_dir).parts for rel_dir in rel_dirs) + for abs_dir in find_sub_dirs_all_sitepackages_ordered(sub_dirs_searched): + so_path = _find_descriptor_so_under_dir(abs_dir, desc) + if so_path is not None: + return so_path for sub_dir in sub_dirs_searched: _no_such_file_in_sub_dirs(sub_dir, file_description, error_messages, attachments) return None @@ -88,15 +88,12 @@ def _find_dll_in_rel_dirs( error_messages: list[str], attachments: list[str], ) -> str | None: - sub_dirs_searched: list[tuple[str, ...]] = [] + sub_dirs_searched = tuple(PurePath(rel_dir).parts for rel_dir in rel_dirs) checked_arch = target_arch if desc.requires_windows_binary_arch_check else None - 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_descriptor_dll_under_dir(abs_dir, desc, checked_arch) - if dll_name is not None: - return dll_name - sub_dirs_searched.append(sub_dir) + for abs_dir in find_sub_dirs_all_sitepackages_ordered(sub_dirs_searched): + dll_name = _find_descriptor_dll_under_dir(abs_dir, desc, checked_arch) + if dll_name is not None: + return dll_name for sub_dir in sub_dirs_searched: _no_such_file_in_sub_dirs(sub_dir, lib_searched_for, error_messages, attachments) return None diff --git a/cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py b/cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py index 86a86b2999c..84a64d00ea2 100644 --- a/cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py +++ b/cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py @@ -22,7 +22,7 @@ ) from cuda.pathfinder._utils.ctk_root_canary import CTK_ROOT_CANARY_ANCHOR_LIBNAMES from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home -from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages +from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages_ordered from cuda.pathfinder._utils.path_sort import numeric_aware_path_sort_key if TYPE_CHECKING: @@ -81,11 +81,10 @@ def _locate_in_anchor_layout(desc: HeaderDescriptor, anchor_point: str) -> str | def find_in_site_packages(desc: HeaderDescriptor) -> LocatedHeaderDir | None: """Search pip wheel install locations.""" - for sub_dir in desc.site_packages_dirs: - hdr_dir: str # help mypy - for hdr_dir in find_sub_dirs_all_sitepackages(tuple(sub_dir.split("/"))): - if _joined_isfile(hdr_dir, desc.header_basename): - return LocatedHeaderDir(abs_path=hdr_dir, found_via="site-packages") + patterns = tuple(tuple(sub_dir.split("/")) for sub_dir in desc.site_packages_dirs) + for hdr_dir in find_sub_dirs_all_sitepackages_ordered(patterns): + if _joined_isfile(hdr_dir, desc.header_basename): + return LocatedHeaderDir(abs_path=hdr_dir, found_via="site-packages") return None diff --git a/cuda_pathfinder/cuda/pathfinder/_static_libs/find_bitcode_lib.py b/cuda_pathfinder/cuda/pathfinder/_static_libs/find_bitcode_lib.py index fd9f3cdfe55..ebd5974500d 100644 --- a/cuda_pathfinder/cuda/pathfinder/_static_libs/find_bitcode_lib.py +++ b/cuda_pathfinder/cuda/pathfinder/_static_libs/find_bitcode_lib.py @@ -8,7 +8,7 @@ from typing import NoReturn, TypedDict from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home -from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages +from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages_ordered from cuda.pathfinder._utils.platform_aware import IS_WINDOWS @@ -90,12 +90,11 @@ def __init__(self, name: str) -> None: self.attachments: list[str] = [] def try_site_packages(self) -> Path | None: - for rel_dir in self.site_packages_dirs: - sub_dir = tuple(rel_dir.split("/")) - for abs_dir in find_sub_dirs_all_sitepackages(sub_dir): - file_path = Path(abs_dir, self.filename) - if file_path.is_file(): - return file_path + patterns = tuple(tuple(rel_dir.split("/")) for rel_dir in self.site_packages_dirs) + for abs_dir in find_sub_dirs_all_sitepackages_ordered(patterns): + file_path = Path(abs_dir, self.filename) + if file_path.is_file(): + return file_path return None def try_with_conda_prefix(self) -> Path | None: diff --git a/cuda_pathfinder/cuda/pathfinder/_static_libs/find_static_lib.py b/cuda_pathfinder/cuda/pathfinder/_static_libs/find_static_lib.py index f0f1c85f538..7aa53fff6fd 100644 --- a/cuda_pathfinder/cuda/pathfinder/_static_libs/find_static_lib.py +++ b/cuda_pathfinder/cuda/pathfinder/_static_libs/find_static_lib.py @@ -8,7 +8,7 @@ from typing import NoReturn, TypedDict from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home -from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages +from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages_ordered from cuda.pathfinder._utils.platform_aware import IS_WINDOWS from cuda.pathfinder._utils.windows_arch import windows_python_arch @@ -87,12 +87,11 @@ def __init__(self, name: str) -> None: self.attachments: list[str] = [] def try_site_packages(self) -> Path | None: - for rel_dir in self.site_packages_dirs: - sub_dir = tuple(rel_dir.split("/")) - for abs_dir in find_sub_dirs_all_sitepackages(sub_dir): - file_path = Path(abs_dir, self.filename) - if file_path.is_file(): - return file_path + patterns = tuple(tuple(rel_dir.split("/")) for rel_dir in self.site_packages_dirs) + for abs_dir in find_sub_dirs_all_sitepackages_ordered(patterns): + file_path = Path(abs_dir, self.filename) + if file_path.is_file(): + return file_path return None def try_with_conda_prefix(self) -> Path | None: diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/find_sub_dirs.py b/cuda_pathfinder/cuda/pathfinder/_utils/find_sub_dirs.py index 55705ba6393..aa6d88c906a 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/find_sub_dirs.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/find_sub_dirs.py @@ -49,7 +49,7 @@ def find_sub_dirs_sys_path(sub_dirs: Sequence[str]) -> list[str]: return find_sub_dirs(sys.path, sub_dirs) -def find_sub_dirs_all_sitepackages(sub_dirs: Sequence[str]) -> list[str]: +def _sitepackages_in_order() -> list[str]: parent_dirs = list(site.getsitepackages()) if site.ENABLE_USER_SITE: user_site = site.getusersitepackages() @@ -62,4 +62,18 @@ def find_sub_dirs_all_sitepackages(sub_dirs: Sequence[str]) -> list[str]: # Detect venv by checking if sys.prefix differs from sys.base_prefix insert_idx = 1 if sys.prefix != sys.base_prefix else 0 parent_dirs.insert(insert_idx, user_site) - return find_sub_dirs(parent_dirs, sub_dirs) + return parent_dirs + + +def find_sub_dirs_all_sitepackages(sub_dirs: Sequence[str]) -> list[str]: + return find_sub_dirs(_sitepackages_in_order(), sub_dirs) + + +def find_sub_dirs_all_sitepackages_ordered(sub_dir_patterns: Sequence[Sequence[str]]) -> list[str]: + """Search site roots in priority order, then the wheel layouts within each root.""" + return [ + directory + for parent in _sitepackages_in_order() + for pattern in sub_dir_patterns + for directory in find_sub_dirs((parent,), pattern) + ] diff --git a/cuda_pathfinder/tests/test_find_bitcode_lib.py b/cuda_pathfinder/tests/test_find_bitcode_lib.py index 6b5f2de49eb..ef85af98123 100644 --- a/cuda_pathfinder/tests/test_find_bitcode_lib.py +++ b/cuda_pathfinder/tests/test_find_bitcode_lib.py @@ -105,15 +105,13 @@ def test_locate_bitcode_lib_search_order(monkeypatch, tmp_path, libname): tuple(rel_dir.split("/")) for rel_dir in _bitcode_lib_info(libname)["site_packages_dirs"] ) - def find_expected_sub_dir(sub_dir): - assert sub_dir in site_packages_sub_dirs - if sub_dir == site_packages_sub_dirs[0]: - return [str(site_packages_lib_dir)] - return [] + def find_expected_sub_dir(patterns): + assert patterns == site_packages_sub_dirs + return [str(site_packages_lib_dir)] monkeypatch.setattr( find_bitcode_lib_module, - "find_sub_dirs_all_sitepackages", + "find_sub_dirs_all_sitepackages_ordered", find_expected_sub_dir, ) monkeypatch.setenv("CONDA_PREFIX", str(conda_prefix)) @@ -145,7 +143,7 @@ def test_find_bitcode_lib_not_found_error_includes_cuda_home_directory_listing(m monkeypatch.setattr( find_bitcode_lib_module, - "find_sub_dirs_all_sitepackages", + "find_sub_dirs_all_sitepackages_ordered", lambda _sub_dir: [], ) monkeypatch.delenv("CONDA_PREFIX", raising=False) @@ -166,7 +164,7 @@ def test_find_bitcode_lib_not_found_error_includes_cuda_home_directory_listing(m def test_find_bitcode_lib_not_found_error_without_cuda_home(monkeypatch): monkeypatch.setattr( find_bitcode_lib_module, - "find_sub_dirs_all_sitepackages", + "find_sub_dirs_all_sitepackages_ordered", lambda _sub_dir: [], ) monkeypatch.delenv("CONDA_PREFIX", raising=False) diff --git a/cuda_pathfinder/tests/test_find_nvidia_binaries.py b/cuda_pathfinder/tests/test_find_nvidia_binaries.py index 1fc9cb29906..7fe0dff078f 100644 --- a/cuda_pathfinder/tests/test_find_nvidia_binaries.py +++ b/cuda_pathfinder/tests/test_find_nvidia_binaries.py @@ -91,7 +91,7 @@ def test_find_binary_search_path_includes_site_packages_conda_cuda(monkeypatch, {"nvcc": (site_key,)}, ) find_sub_dirs_mock = mocker.patch.object( - binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[site_dir] + binary_finder_module, "find_sub_dirs_all_sitepackages_ordered", return_value=[site_dir] ) monkeypatch.setenv("CONDA_PREFIX", conda_prefix) mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=cuda_home) @@ -107,7 +107,7 @@ def test_find_binary_search_path_includes_site_packages_conda_cuda(monkeypatch, # No directory contains the binary, so every trusted dir is probed in order. assert result is None - find_sub_dirs_mock.assert_called_once_with(site_key.split(os.sep)) + find_sub_dirs_mock.assert_called_once_with((tuple(site_key.split(os.sep)),)) assert checked == [os.path.join(d, "nvcc") for d in expected_dirs] @@ -125,7 +125,7 @@ def test_find_binary_windows_extension_and_search_dirs(monkeypatch, mocker): {"nvcc": (site_key,)}, ) find_sub_dirs_mock = mocker.patch.object( - binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[site_dir] + binary_finder_module, "find_sub_dirs_all_sitepackages_ordered", return_value=[site_dir] ) monkeypatch.setenv("CONDA_PREFIX", conda_prefix) mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=cuda_home) @@ -143,7 +143,7 @@ def test_find_binary_windows_extension_and_search_dirs(monkeypatch, mocker): # The .exe extension is appended and the Windows-specific dirs are probed in order. assert result is None - find_sub_dirs_mock.assert_called_once_with(site_key.split(os.sep)) + find_sub_dirs_mock.assert_called_once_with((tuple(site_key.split(os.sep)),)) assert checked == [os.path.join(d, "nvcc.exe") for d in expected_dirs] @@ -219,7 +219,7 @@ def test_find_binary_windows_nsight_conda_precedes_registry(monkeypatch, mocker, expected = os.path.join(conda_bin, candidate_names[0]) mocker.patch.object(binary_finder_module, "IS_WINDOWS", new=True) - mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[site_dir]) + mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages_ordered", return_value=[site_dir]) monkeypatch.setenv("CONDA_PREFIX", conda_prefix) candidate_paths = mocker.patch.object(binary_finder_module.windows_nsight, f"{utility_name}_candidate_paths") get_cuda_path = mocker.patch.object(binary_finder_module, "get_cuda_path_or_home") @@ -269,7 +269,7 @@ def test_find_binary_windows_nsight_composes_registry_and_native_target( expected = os.path.join(install_root, target_rel) mocker.patch.object(binary_finder_module, "IS_WINDOWS", new=True) - mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[site_dir]) + mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages_ordered", return_value=[site_dir]) monkeypatch.setenv("CONDA_PREFIX", conda_prefix) registry_root = mocker.patch.object( binary_finder_module.windows_nsight, "_installed_product_root", return_value=install_root @@ -355,7 +355,9 @@ def test_find_windows_nsight_legacy_names_remain_literal_in_early_search(monkeyp "SITE_PACKAGES_BINDIRS", {utility_name: (site_key,)}, ) - find_sub_dirs = mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[site_dir]) + find_sub_dirs = mocker.patch.object( + binary_finder_module, "find_sub_dirs_all_sitepackages_ordered", return_value=[site_dir] + ) monkeypatch.setenv("CONDA_PREFIX", conda_prefix) get_cuda_path = mocker.patch.object(binary_finder_module, "get_cuda_path_or_home") nsys_candidates = mocker.patch.object(binary_finder_module.windows_nsight, "nsys_candidate_paths") @@ -364,7 +366,7 @@ def test_find_windows_nsight_legacy_names_remain_literal_in_early_search(monkeyp assert find_nvidia_binary_utility(utility_name) == os.path.abspath(expected) assert checked == [os.path.join(site_dir, f"{utility_name}.exe"), expected] - find_sub_dirs.assert_called_once_with(site_key.split(os.sep)) + find_sub_dirs.assert_called_once_with((tuple(site_key.split(os.sep)),)) get_cuda_path.assert_not_called() nsys_candidates.assert_not_called() ncu_candidates.assert_not_called() @@ -410,7 +412,7 @@ def test_find_binary_first_matching_dir_wins(monkeypatch, mocker): "SITE_PACKAGES_BINDIRS", {"nvcc": (site_key,)}, ) - mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[site_dir]) + mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages_ordered", return_value=[site_dir]) monkeypatch.setenv("CONDA_PREFIX", conda_prefix) mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=cuda_home) mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary", return_value=None) @@ -433,7 +435,7 @@ def test_find_binary_ctk_root_canary_fallback(monkeypatch, mocker): mocker.patch.object(binary_finder_module, "IS_WINDOWS", new=False) mocker.patch.object(binary_finder_module.supported_nvidia_binaries, "SITE_PACKAGES_BINDIRS", {}) - mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[]) + mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages_ordered", return_value=[]) monkeypatch.delenv("CONDA_PREFIX", raising=False) mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=None) canary_mock = mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary", return_value=ctk_root) @@ -454,7 +456,7 @@ def test_find_binary_canary_windows_bin_layout(monkeypatch, mocker): mocker.patch.object(binary_finder_module, "IS_WINDOWS", new=True) mocker.patch.object(binary_finder_module.supported_nvidia_binaries, "SITE_PACKAGES_BINDIRS", {}) - mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[]) + mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages_ordered", return_value=[]) monkeypatch.delenv("CONDA_PREFIX", raising=False) mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=None) mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary", return_value=ctk_root) @@ -478,7 +480,7 @@ def test_find_binary_canary_not_consulted_when_found_earlier(monkeypatch, mocker mocker.patch.object(binary_finder_module, "IS_WINDOWS", new=False) mocker.patch.object(binary_finder_module.supported_nvidia_binaries, "SITE_PACKAGES_BINDIRS", {}) - mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[]) + mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages_ordered", return_value=[]) monkeypatch.setenv("CONDA_PREFIX", conda_prefix) mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=None) canary_mock = mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary", return_value=None) @@ -501,7 +503,9 @@ def test_find_binary_returns_none_with_no_candidates(monkeypatch, mocker): "SITE_PACKAGES_BINDIRS", {"nvcc": (site_key,)}, ) - find_sub_dirs_mock = mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[]) + find_sub_dirs_mock = mocker.patch.object( + binary_finder_module, "find_sub_dirs_all_sitepackages_ordered", return_value=[] + ) monkeypatch.delenv("CONDA_PREFIX", raising=False) mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=None) mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary", return_value=None) @@ -510,7 +514,7 @@ def test_find_binary_returns_none_with_no_candidates(monkeypatch, mocker): result = find_nvidia_binary_utility("nvcc") assert result is None - find_sub_dirs_mock.assert_called_once_with(site_key.split(os.sep)) + find_sub_dirs_mock.assert_called_once_with((tuple(site_key.split(os.sep)),)) # No trusted dirs were assembled, so nothing is probed at all. assert checked == [] @@ -522,7 +526,9 @@ def test_find_binary_without_site_packages_entry(monkeypatch, mocker): mocker.patch.object(binary_finder_module, "IS_WINDOWS", new=False) mocker.patch.object(binary_finder_module.supported_nvidia_binaries, "SITE_PACKAGES_BINDIRS", {}) - find_sub_dirs_mock = mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[]) + find_sub_dirs_mock = mocker.patch.object( + binary_finder_module, "find_sub_dirs_all_sitepackages_ordered", return_value=[] + ) monkeypatch.setenv("CONDA_PREFIX", conda_prefix) mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=cuda_home) mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary", return_value=None) @@ -543,7 +549,7 @@ def test_find_binary_without_site_packages_entry(monkeypatch, mocker): def test_find_binary_cache_negative_result(monkeypatch, mocker): mocker.patch.object(binary_finder_module, "IS_WINDOWS", new=False) mocker.patch.object(binary_finder_module.supported_nvidia_binaries, "SITE_PACKAGES_BINDIRS", {}) - mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[]) + mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages_ordered", return_value=[]) monkeypatch.delenv("CONDA_PREFIX", raising=False) cuda_home_mock = mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=None) canary_mock = mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary", return_value=None) diff --git a/cuda_pathfinder/tests/test_find_nvidia_headers.py b/cuda_pathfinder/tests/test_find_nvidia_headers.py index ec114372b88..ac5d8052c96 100644 --- a/cuda_pathfinder/tests/test_find_nvidia_headers.py +++ b/cuda_pathfinder/tests/test_find_nvidia_headers.py @@ -208,7 +208,7 @@ def test_locate_non_ctk_headers_uses_product_root(tmp_path, monkeypatch, mocker, monkeypatch.setenv(env_var, str(product_root)) monkeypatch.setenv("CUDA_HOME", str(tmp_path / "unused-cuda-home")) monkeypatch.delenv("CUDA_PATH", raising=False) - mocker.patch.object(find_nvidia_headers_module, "find_sub_dirs_all_sitepackages", return_value=[]) + mocker.patch.object(find_nvidia_headers_module, "find_sub_dirs_all_sitepackages_ordered", return_value=[]) located_hdr_dir = locate_nvidia_header_directory(libname) @@ -350,7 +350,7 @@ def test_locate_ctk_headers_uses_canary_fallback_when_cuda_home_unset(tmp_path, monkeypatch.delenv("CONDA_PREFIX", raising=False) monkeypatch.delenv("CUDA_HOME", raising=False) monkeypatch.delenv("CUDA_PATH", raising=False) - mocker.patch.object(find_nvidia_headers_module, "find_sub_dirs_all_sitepackages", return_value=[]) + mocker.patch.object(find_nvidia_headers_module, "find_sub_dirs_all_sitepackages_ordered", return_value=[]) probe = mocker.patch.object( find_nvidia_headers_module, "_resolve_system_loaded_abs_path_in_subprocess", @@ -375,7 +375,7 @@ def test_locate_ctk_headers_cuda_home_takes_priority_over_canary(tmp_path, monke monkeypatch.delenv("CONDA_PREFIX", raising=False) monkeypatch.setenv("CUDA_HOME", str(cuda_home)) monkeypatch.delenv("CUDA_PATH", raising=False) - mocker.patch.object(find_nvidia_headers_module, "find_sub_dirs_all_sitepackages", return_value=[]) + mocker.patch.object(find_nvidia_headers_module, "find_sub_dirs_all_sitepackages_ordered", return_value=[]) probe = mocker.patch.object( find_nvidia_headers_module, "_resolve_system_loaded_abs_path_in_subprocess", @@ -395,7 +395,7 @@ def test_locate_ctk_headers_canary_miss_paths_are_non_fatal(monkeypatch, mocker) monkeypatch.delenv("CONDA_PREFIX", raising=False) monkeypatch.delenv("CUDA_HOME", raising=False) monkeypatch.delenv("CUDA_PATH", raising=False) - mocker.patch.object(find_nvidia_headers_module, "find_sub_dirs_all_sitepackages", return_value=[]) + mocker.patch.object(find_nvidia_headers_module, "find_sub_dirs_all_sitepackages_ordered", return_value=[]) mocker.patch.object( find_nvidia_headers_module, "_resolve_system_loaded_abs_path_in_subprocess", @@ -411,7 +411,7 @@ def test_locate_ctk_headers_canary_probe_errors_are_not_masked(monkeypatch, mock monkeypatch.delenv("CONDA_PREFIX", raising=False) monkeypatch.delenv("CUDA_HOME", raising=False) monkeypatch.delenv("CUDA_PATH", raising=False) - mocker.patch.object(find_nvidia_headers_module, "find_sub_dirs_all_sitepackages", return_value=[]) + mocker.patch.object(find_nvidia_headers_module, "find_sub_dirs_all_sitepackages_ordered", return_value=[]) mocker.patch.object( find_nvidia_headers_module, "_resolve_system_loaded_abs_path_in_subprocess", diff --git a/cuda_pathfinder/tests/test_find_static_lib.py b/cuda_pathfinder/tests/test_find_static_lib.py index cf0e62dc8a2..9b699ee0809 100644 --- a/cuda_pathfinder/tests/test_find_static_lib.py +++ b/cuda_pathfinder/tests/test_find_static_lib.py @@ -95,7 +95,7 @@ def test_locate_static_lib_search_order(monkeypatch, tmp_path): monkeypatch.setattr( find_static_lib_module, - "find_sub_dirs_all_sitepackages", + "find_sub_dirs_all_sitepackages_ordered", lambda _sub_dir: [str(site_packages_lib_dir)], ) monkeypatch.setenv("CONDA_PREFIX", str(conda_prefix)) @@ -131,7 +131,7 @@ def test_locate_static_lib_conda_rel_path_fallback(monkeypatch, tmp_path): monkeypatch.setattr( find_static_lib_module, - "find_sub_dirs_all_sitepackages", + "find_sub_dirs_all_sitepackages_ordered", lambda _sub_dir: [], ) monkeypatch.setenv("CONDA_PREFIX", str(conda_prefix)) @@ -191,7 +191,7 @@ def test_find_static_lib_not_found_error_includes_cuda_home_directory_listing(mo monkeypatch.setattr( find_static_lib_module, - "find_sub_dirs_all_sitepackages", + "find_sub_dirs_all_sitepackages_ordered", lambda _sub_dir: [], ) monkeypatch.delenv("CONDA_PREFIX", raising=False) @@ -214,7 +214,7 @@ def test_find_static_lib_not_found_error_without_cuda_home(monkeypatch): monkeypatch.setattr( find_static_lib_module, - "find_sub_dirs_all_sitepackages", + "find_sub_dirs_all_sitepackages_ordered", lambda _sub_dir: [], ) monkeypatch.delenv("CONDA_PREFIX", raising=False) diff --git a/cuda_pathfinder/tests/test_search_steps.py b/cuda_pathfinder/tests/test_search_steps.py index 5fa194d544a..77178c557c9 100644 --- a/cuda_pathfinder/tests/test_search_steps.py +++ b/cuda_pathfinder/tests/test_search_steps.py @@ -72,7 +72,11 @@ def _find_sub_dirs(sub_dirs): path = root.joinpath(*sub_dirs) return [str(path)] if path.is_dir() else [] - return mocker.patch(f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages", side_effect=_find_sub_dirs) + mocker.patch(f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages", side_effect=_find_sub_dirs) + return mocker.patch( + f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages_ordered", + side_effect=lambda patterns: [directory for pattern in patterns for directory in _find_sub_dirs(pattern)], + ) def _write_pe(path, machine): @@ -277,7 +281,7 @@ def test_found_linux(self, mocker, tmp_path): so_file.touch() mocker.patch( - f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages", + f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages_ordered", return_value=[str(lib_dir)], ) @@ -297,7 +301,7 @@ def test_nvvm_linux_wheel_accepts_declared_unversioned_filename(self, mocker, tm so_file.touch() mocker.patch( - f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages", + f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages_ordered", return_value=[str(lib_dir)], ) @@ -314,7 +318,7 @@ def test_found_windows(self, mocker, tmp_path): dll.touch() mocker.patch( - f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages", + f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages_ordered", return_value=[str(bin_dir)], ) mocker.patch(f"{_PLAT_MOD}.is_suppressed_dll_file", return_value=False) @@ -338,7 +342,7 @@ def test_windows_exact_names_reject_cudnn_sidecar(self, mocker, tmp_path): (bin_dir / "cudnn_adv64_9.dll").touch() mocker.patch( - f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages", + f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages_ordered", return_value=[str(bin_dir)], ) @@ -355,7 +359,7 @@ def test_windows_matching_rejects_unlisted_cupti_name(self, mocker, tmp_path): (bin_dir / "cupti64_14.dll").touch() mocker.patch( - f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages", + f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages_ordered", return_value=[str(bin_dir)], ) @@ -476,7 +480,7 @@ def test_not_found_appends_error(self, mocker, tmp_path): empty_dir.mkdir(parents=True) mocker.patch( - f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages", + f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages_ordered", return_value=[str(empty_dir)], ) @@ -493,7 +497,7 @@ def test_linux_declared_versioned_soname_does_not_need_unversioned_link(self, mo versioned.touch() mocker.patch( - f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages", + f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages_ordered", return_value=[str(lib_dir)], ) @@ -513,7 +517,7 @@ def test_linux_declared_sonames_follow_preference_order(self, mocker, tmp_path): fallback.touch() mocker.patch( - f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages", + f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages_ordered", return_value=[str(lib_dir)], ) @@ -534,7 +538,7 @@ def test_linux_rejects_undeclared_filename(self, mocker, tmp_path, undeclared_fi (lib_dir / undeclared_filename).touch() mocker.patch( - f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages", + f"{_PLAT_MOD}.find_sub_dirs_all_sitepackages_ordered", return_value=[str(lib_dir)], ) diff --git a/cuda_pathfinder/tests/test_site_packages_precedence.py b/cuda_pathfinder/tests/test_site_packages_precedence.py new file mode 100644 index 00000000000..f677442e234 --- /dev/null +++ b/cuda_pathfinder/tests/test_site_packages_precedence.py @@ -0,0 +1,133 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Site-package root priority must apply across alternative wheel layouts.""" + +from pathlib import Path + +import pytest + +from cuda.pathfinder import find_nvidia_binary_utility +from cuda.pathfinder._binaries import supported_nvidia_binaries +from cuda.pathfinder._dynamic_libs.lib_descriptor import LIB_DESCRIPTORS +from cuda.pathfinder._dynamic_libs.search_platform import LinuxSearchPlatform, WindowsSearchPlatform +from cuda.pathfinder._headers.find_nvidia_headers import find_in_site_packages +from cuda.pathfinder._headers.header_descriptor import HEADER_DESCRIPTORS +from cuda.pathfinder._static_libs.find_bitcode_lib import _FindBitcodeLib +from cuda.pathfinder._static_libs.find_static_lib import _FindStaticLib +from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_cached +from cuda.pathfinder._utils.platform_aware import IS_WINDOWS + + +@pytest.fixture +def site_roots(monkeypatch, tmp_path): + venv, user, system = (tmp_path / name for name in ("venv", "user", "system")) + for root in (venv, user, system): + root.mkdir() + monkeypatch.setattr("site.getsitepackages", lambda: [str(venv), str(system)]) + monkeypatch.setattr("site.getusersitepackages", lambda: str(user)) + monkeypatch.setattr("site.ENABLE_USER_SITE", True) + monkeypatch.setattr("sys.prefix", str(venv)) + monkeypatch.setattr("sys.base_prefix", str(system)) + for variable in ("CUDA_PATH", "CUDA_HOME", "CONDA_PREFIX"): + monkeypatch.delenv(variable, raising=False) + find_sub_dirs_cached.cache_clear() + find_nvidia_binary_utility.cache_clear() + yield venv, user, system + find_sub_dirs_cached.cache_clear() + find_nvidia_binary_utility.cache_clear() + + +def _case(kind): + if kind == "bitcode": + finder = _FindBitcodeLib("device") + return finder.site_packages_dirs, finder.filename, finder.filename, finder.try_site_packages + if kind == "static": + finder = _FindStaticLib("cudadevrt") + return finder.site_packages_dirs, finder.filename, finder.filename, finder.try_site_packages + if kind == "header": + desc = HEADER_DESCRIPTORS["cudart"] + + def lookup(): + result = find_in_site_packages(desc) + return None if result is None else Path(result.abs_path) / desc.header_basename + + return desc.site_packages_dirs, desc.header_basename, desc.header_basename, lookup + if kind == "binary": + filename = "nvcc.exe" if IS_WINDOWS else "nvcc" + return ( + supported_nvidia_binaries.SITE_PACKAGES_BINDIRS["nvcc"], + filename, + filename, + lambda: find_nvidia_binary_utility("nvcc"), + ) + desc = LIB_DESCRIPTORS["cudart"] + if kind == "linux_dynamic": + platform = LinuxSearchPlatform() + newer, older = desc.linux_sonames + else: + platform = WindowsSearchPlatform(target_arch="x64") + newer, older = desc.windows_dlls + dirs = platform.site_packages_rel_dirs(desc) + return dirs, newer, older, lambda: platform.find_in_site_packages(dirs, desc, [], []) + + +def _put(root, relative_dir, filename): + path = root / relative_dir / filename + path.parent.mkdir(parents=True, exist_ok=True) + path.touch() + path.chmod(0o755) + return path + + +@pytest.mark.parametrize("kind", ("bitcode", "static", "header", "binary", "linux_dynamic", "windows_dynamic")) +@pytest.mark.agent_authored(model="gpt-6") +def test_venv_layout_takes_priority_over_user_and_system(kind, site_roots): + venv, user, system = site_roots + layouts, newer, older, lookup = _case(kind) + if len(layouts) < 2: + pytest.skip("No alternative wheel layout for this platform") + expected = _put(venv, layouts[-1], older) + _put(user, layouts[0], newer) + _put(system, layouts[0], newer) + assert Path(lookup()) == expected + + +@pytest.mark.parametrize("kind", ("bitcode", "static", "header", "binary", "linux_dynamic", "windows_dynamic")) +@pytest.mark.agent_authored(model="gpt-6") +def test_layout_order_is_preserved_within_a_site_root(kind, site_roots): + venv, _, _ = site_roots + layouts, newer, older, lookup = _case(kind) + if len(layouts) < 2: + pytest.skip("No alternative wheel layout for this platform") + expected = _put(venv, layouts[0], newer) + _put(venv, layouts[-1], older) + assert Path(lookup()) == expected + + +@pytest.mark.parametrize("kind", ("bitcode", "static", "header", "binary", "linux_dynamic", "windows_dynamic")) +@pytest.mark.agent_authored(model="gpt-6") +def test_lower_priority_root_is_used_when_higher_root_has_no_file(kind, site_roots): + venv, user, _ = site_roots + layouts, newer, _, lookup = _case(kind) + if len(layouts) < 2: + pytest.skip("No alternative wheel layout for this platform") + (venv / layouts[-1]).mkdir(parents=True) + expected = _put(user, layouts[0], newer) + assert Path(lookup()) == expected + + +@pytest.mark.parametrize("in_venv", (True, False)) +@pytest.mark.parametrize("kind", ("bitcode", "static", "header", "binary", "linux_dynamic", "windows_dynamic")) +@pytest.mark.agent_authored(model="gpt-6") +def test_user_layout_takes_priority_over_system(kind, in_venv, site_roots, monkeypatch): + _, user, system = site_roots + if not in_venv: + monkeypatch.setattr("site.getsitepackages", lambda: [str(system)]) + monkeypatch.setattr("sys.prefix", str(system)) + layouts, newer, older, lookup = _case(kind) + if len(layouts) < 2: + pytest.skip("No alternative wheel layout for this platform") + expected = _put(user, layouts[-1], older) + _put(system, layouts[0], newer) + assert Path(lookup()) == expected From b361e245b02eaa3b68d5e06d5c6257f17688581f Mon Sep 17 00:00:00 2001 From: 0z5a Date: Wed, 4 Nov 2026 16:43:04 +0100 Subject: [PATCH 2/2] [pathfinder] Mark site-precedence tests thread-unsafe Signed-off-by: 0z5a --- cuda_pathfinder/tests/test_site_packages_precedence.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cuda_pathfinder/tests/test_site_packages_precedence.py b/cuda_pathfinder/tests/test_site_packages_precedence.py index f677442e234..da99b7bfd3f 100644 --- a/cuda_pathfinder/tests/test_site_packages_precedence.py +++ b/cuda_pathfinder/tests/test_site_packages_precedence.py @@ -81,6 +81,7 @@ def _put(root, relative_dir, filename): @pytest.mark.parametrize("kind", ("bitcode", "static", "header", "binary", "linux_dynamic", "windows_dynamic")) +@pytest.mark.thread_unsafe(reason="Patches process-wide site and sys state and clears caches.") @pytest.mark.agent_authored(model="gpt-6") def test_venv_layout_takes_priority_over_user_and_system(kind, site_roots): venv, user, system = site_roots @@ -94,6 +95,7 @@ def test_venv_layout_takes_priority_over_user_and_system(kind, site_roots): @pytest.mark.parametrize("kind", ("bitcode", "static", "header", "binary", "linux_dynamic", "windows_dynamic")) +@pytest.mark.thread_unsafe(reason="Patches process-wide site and sys state and clears caches.") @pytest.mark.agent_authored(model="gpt-6") def test_layout_order_is_preserved_within_a_site_root(kind, site_roots): venv, _, _ = site_roots @@ -106,6 +108,7 @@ def test_layout_order_is_preserved_within_a_site_root(kind, site_roots): @pytest.mark.parametrize("kind", ("bitcode", "static", "header", "binary", "linux_dynamic", "windows_dynamic")) +@pytest.mark.thread_unsafe(reason="Patches process-wide site and sys state and clears caches.") @pytest.mark.agent_authored(model="gpt-6") def test_lower_priority_root_is_used_when_higher_root_has_no_file(kind, site_roots): venv, user, _ = site_roots @@ -119,6 +122,7 @@ def test_lower_priority_root_is_used_when_higher_root_has_no_file(kind, site_roo @pytest.mark.parametrize("in_venv", (True, False)) @pytest.mark.parametrize("kind", ("bitcode", "static", "header", "binary", "linux_dynamic", "windows_dynamic")) +@pytest.mark.thread_unsafe(reason="Patches process-wide site and sys state and clears caches.") @pytest.mark.agent_authored(model="gpt-6") def test_user_layout_takes_priority_over_system(kind, in_venv, site_roots, monkeypatch): _, user, system = site_roots