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
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,38 @@ jobs:
with:
is-release: ${{ github.ref_type == 'tag' }}

precommit-windows:
Comment thread
mdboom marked this conversation as resolved.
name: Pre-commit on Windows
runs-on: windows-latest
if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.skip) }}
needs:
- should-skip
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 1
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: '3.13'

- name: Install pre-commit
shell: bash
run: |
set -euxo pipefail
python -m pip install --upgrade pip pre-commit

- name: Run pre-commit
shell: bash
run: |
set -euxo pipefail
SKIP=lychee pre-commit run --all-files

checks:
name: Check job status
if: always()
Expand All @@ -429,6 +461,7 @@ jobs:
- test-linux-aarch64
- test-windows
- doc
- precommit-windows
steps:
- name: Exit
run: |
Expand Down Expand Up @@ -461,6 +494,7 @@ jobs:
check_result "should-skip" "success" "${{ needs.should-skip.result }}"
check_result "detect-changes" "success" "${{ needs.detect-changes.result }}"
check_result "doc" "success" "${{ needs.doc.result }}"
check_result "precommit-windows" "success" "${{ needs.precommit-windows.result }}"

# [doc-only] flips these from 'success' to 'skipped'
if [[ "$doc_only" == "true" ]]; then expected="skipped"; else expected="success"; fi
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ repos:

- id: stubgen-pyx-cuda-core
name: Generate .pyi stubs for cuda_core
entry: stubgen-pyx cuda_core/cuda --continue-on-error --include-private
entry: python ./toolshed/run_stubgen_pyx.py
language: python
files: ^cuda_core/cuda/.*\.(pyx|pxd)$
pass_filenames: false
Expand Down Expand Up @@ -95,7 +95,7 @@ repos:
- id: check-yaml
- id: debug-statements
- id: end-of-file-fixer
exclude: &gen_exclude '^(?:cuda_python/README\.md|cuda_bindings/cuda/bindings/.*\.in?|cuda_bindings/docs/source/module/.*\.rst?|.*\.pyi)$'
exclude: &gen_exclude '^(?:cuda_python/README\.md|(?:.*/)?CLAUDE\.md|(?:.*/)?\.git_archival\.txt|cuda_bindings/cuda/bindings/.*\.in?|cuda_bindings/docs/source/module/.*\.rst?|.*\.pyi)$'
- id: mixed-line-ending
- id: trailing-whitespace
exclude: |
Expand Down
12 changes: 12 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Thank you for your interest in contributing to CUDA Python! Based on the type of
- [Table of Contents](#table-of-contents)
- [Type stubs for cuda.core](#type-stubs-for-cudacore)
- [Pre-commit](#pre-commit)
- [Pre-commit on Windows](#pre-commit-on-windows)
- [Signing Your Work](#signing-your-work)
- [Code signing](#code-signing)
- [Developer Certificate of Origin (DCO)](#developer-certificate-of-origin-dco)
Expand Down Expand Up @@ -74,6 +75,17 @@ between commits, leaving stale headers or out-of-date stubs in the history.
If the hook isn't installed, `pre-commit run` (and CI) will print a visible
warning reminding you to run `pre-commit install`.

### Pre-commit on Windows

For development on Windows (not WSL), the `lychee` pre-commit task will not work
when running `pre-commit run --all-files`. This problem does not occur if you
install the pre-commit hook and run it automatically as part of your `git
commit` workflow. To resolve this, you can either:

1. Run `pre-commit` in Git Bash, rather directly in PowerShell or cmd

2. Skip it by setting the environment variable `SKIP` to `lychee`. This would
be `$env:SKIP = "lychee"` in PowerShell or `set SKIP=lychee` in cmd.

## Signing Your Work

Expand Down
58 changes: 35 additions & 23 deletions cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_linux.py

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Touching this file seems unnecessary, why does pre-commit care on Windows?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because mypy is now checking this on Windows, where things like os.RTLD_NOW don't exist.

mypy doesn't/can't know that this is only imported on Linux. It does have special support for parsing if sys.platform == "linux" (which is why this works), but it can't follow that between modules. (And we use slightly different logic there anyway).

IME, this stuff is necessary to make type-checking complete, annoying as it is.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

from __future__ import annotations
Expand All @@ -7,14 +7,18 @@
import ctypes
import ctypes.util
import os
import sys
from typing import TYPE_CHECKING, cast

from cuda.pathfinder._dynamic_libs.load_dl_common import LoadedDL

if TYPE_CHECKING:
from cuda.pathfinder._dynamic_libs.lib_descriptor import LibDescriptor

CDLL_MODE = os.RTLD_NOW | os.RTLD_GLOBAL
if sys.platform == "linux":
CDLL_MODE = os.RTLD_NOW | os.RTLD_GLOBAL
else:
CDLL_MODE = 0


def _load_libdl() -> ctypes.CDLL:
Expand Down Expand Up @@ -132,27 +136,35 @@ def _candidate_sonames(desc: LibDescriptor) -> list[str]:
return candidates


def check_if_already_loaded_from_elsewhere(desc: LibDescriptor, _have_abs_path: bool) -> LoadedDL | None:
for soname in _candidate_sonames(desc):
try:
handle = ctypes.CDLL(soname, mode=os.RTLD_NOLOAD)
except OSError:
continue
else:
return LoadedDL(
abs_path_for_dynamic_library(desc.name, handle),
True,
handle._handle,
"was-already-loaded-from-elsewhere",
)
return None


def _load_lib(desc: LibDescriptor, filename: str) -> ctypes.CDLL:
cdll_mode = CDLL_MODE
if desc.requires_rtld_deepbind:
cdll_mode |= os.RTLD_DEEPBIND
return ctypes.CDLL(filename, cdll_mode)
if sys.platform == "linux":

def check_if_already_loaded_from_elsewhere(desc: LibDescriptor, _have_abs_path: bool) -> LoadedDL | None:
for soname in _candidate_sonames(desc):
try:
handle = ctypes.CDLL(soname, mode=os.RTLD_NOLOAD)
except OSError:
continue
else:
return LoadedDL(
abs_path_for_dynamic_library(desc.name, handle),
True,
handle._handle,
"was-already-loaded-from-elsewhere",
)
return None

def _load_lib(desc: LibDescriptor, filename: str) -> ctypes.CDLL:
cdll_mode = CDLL_MODE
if desc.requires_rtld_deepbind:
cdll_mode |= os.RTLD_DEEPBIND
return ctypes.CDLL(filename, cdll_mode)
else:

def check_if_already_loaded_from_elsewhere(_desc: LibDescriptor, _have_abs_path: bool) -> LoadedDL | None:
raise RuntimeError(f"check_if_already_loaded_from_elsewhere() is not supported on platform {sys.platform!r}")

def _load_lib(_desc: LibDescriptor, _filename: str) -> ctypes.CDLL:
raise RuntimeError(f"_load_lib() is not supported on platform {sys.platform!r}")


def load_with_system_search(desc: LibDescriptor) -> LoadedDL | None:
Expand Down
25 changes: 19 additions & 6 deletions cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_windows.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

from __future__ import annotations
Expand All @@ -7,6 +7,7 @@
import ctypes.wintypes
import os
import struct
import sys
import warnings
from typing import TYPE_CHECKING

Expand All @@ -22,7 +23,10 @@
POINTER_ADDRESS_SPACE = 2 ** (struct.calcsize("P") * 8)

# Set up kernel32 functions with proper types
kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
windll = getattr(ctypes, "windll", None)
if windll is None:
raise RuntimeError("ctypes.windll is required on Windows")
kernel32 = windll.kernel32

# GetModuleHandleW
kernel32.GetModuleHandleW.argtypes = [ctypes.wintypes.LPCWSTR]
Expand All @@ -44,6 +48,14 @@
]
kernel32.GetModuleFileNameW.restype = ctypes.wintypes.DWORD

# AddDllDirectory (Windows 7+)
kernel32.AddDllDirectory.argtypes = [ctypes.wintypes.LPCWSTR]
kernel32.AddDllDirectory.restype = ctypes.c_void_p # DLL_DIRECTORY_COOKIE

# GetLastError
kernel32.GetLastError.argtypes = []
kernel32.GetLastError.restype = ctypes.wintypes.DWORD


def ctypes_handle_to_unsigned_int(handle: ctypes.wintypes.HMODULE) -> int:
"""Convert ctypes HMODULE to unsigned int."""
Expand Down Expand Up @@ -73,7 +85,8 @@ def add_dll_directory(dll_abs_path: str) -> None:
# the directory must stay on the search path for the process lifetime, and
# the handle has no finalizer, so dropping it does not remove the directory.
try:
os.add_dll_directory(dirpath) # type: ignore[attr-defined]
if sys.platform == "win32":
os.add_dll_directory(dirpath)
except OSError as e:
# Warn instead of failing silently; the PATH update below is a weaker
# fallback that newer loaders may ignore.
Expand All @@ -96,15 +109,15 @@ def abs_path_for_dynamic_library(libname: str, handle: ctypes.wintypes.HMODULE)
length = kernel32.GetModuleFileNameW(handle, buffer, len(buffer))

if length == 0:
error_code = ctypes.GetLastError() # type: ignore[attr-defined]
error_code = kernel32.GetLastError()
raise RuntimeError(f"GetModuleFileNameW failed for {libname!r} (error code: {error_code})")

# If buffer was too small, try with larger buffer
if length == len(buffer):
buffer = ctypes.create_unicode_buffer(32768) # Extended path length
length = kernel32.GetModuleFileNameW(handle, buffer, len(buffer))
if length == 0:
error_code = ctypes.GetLastError() # type: ignore[attr-defined]
error_code = kernel32.GetLastError()
raise RuntimeError(f"GetModuleFileNameW failed for {libname!r} (error code: {error_code})")

return buffer.value
Expand Down Expand Up @@ -170,7 +183,7 @@ def load_with_abs_path(desc: LibDescriptor, found_path: str, found_via: str | No
handle = kernel32.LoadLibraryExW(found_path, None, flags)

if not handle:
error_code = ctypes.GetLastError() # type: ignore[attr-defined]
error_code = kernel32.GetLastError()
raise RuntimeError(f"Failed to load DLL at {found_path}: Windows error {error_code}")

return LoadedDL(found_path, False, ctypes_handle_to_unsigned_int(handle), found_via)
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Platform loader seam for OS-specific dynamic linking.
Expand All @@ -16,11 +16,11 @@

from __future__ import annotations

import sys
from typing import Protocol

from cuda.pathfinder._dynamic_libs.lib_descriptor import LibDescriptor
from cuda.pathfinder._dynamic_libs.load_dl_common import LoadedDL
from cuda.pathfinder._utils.platform_aware import IS_WINDOWS


class PlatformLoader(Protocol):
Expand All @@ -31,7 +31,7 @@ def load_with_system_search(self, desc: LibDescriptor) -> LoadedDL | None: ...
def load_with_abs_path(self, desc: LibDescriptor, found_path: str, found_via: str | None = None) -> LoadedDL: ...


if IS_WINDOWS:
if sys.platform == "win32":
from cuda.pathfinder._dynamic_libs import load_dl_windows as _impl
else:
from cuda.pathfinder._dynamic_libs import load_dl_linux as _impl
Expand Down
16 changes: 8 additions & 8 deletions cuda_pathfinder/cuda/pathfinder/_utils/driver_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@

import ctypes
import functools
import sys
from collections.abc import Callable
from dataclasses import dataclass

from cuda.pathfinder._dynamic_libs.load_nvidia_dynamic_lib import (
load_nvidia_dynamic_lib as _load_nvidia_dynamic_lib,
)
from cuda.pathfinder._utils.platform_aware import IS_WINDOWS


class QueryDriverCudaVersionError(RuntimeError):
Expand Down Expand Up @@ -60,16 +60,16 @@ def query_driver_cuda_version() -> DriverCudaVersion:
raise QueryDriverCudaVersionError("Failed to query the CUDA driver version.") from exc


if sys.platform == "win32":
_DRIVER_LIB_LOADER: Callable[[str], ctypes.CDLL] = ctypes.WinDLL
else:
_DRIVER_LIB_LOADER = ctypes.CDLL


def _query_driver_cuda_version_int() -> int:
"""Return the encoded CUDA driver version from ``cuDriverGetVersion()``."""
loaded_cuda = _load_nvidia_dynamic_lib("cuda")
if IS_WINDOWS:
# `ctypes.WinDLL` exists on Windows at runtime. The ignore is only for
# Linux mypy runs, where the platform stubs do not define that attribute.
loader_cls: Callable[[str], ctypes.CDLL] = ctypes.WinDLL # type: ignore[attr-defined]
else:
loader_cls = ctypes.CDLL
driver_lib = loader_cls(loaded_cuda.abs_path)
driver_lib = _DRIVER_LIB_LOADER(loaded_cuda.abs_path)
cu_driver_get_version = driver_lib.cuDriverGetVersion
cu_driver_get_version.argtypes = [ctypes.POINTER(ctypes.c_int)]
cu_driver_get_version.restype = ctypes.c_int
Expand Down
6 changes: 2 additions & 4 deletions cuda_pathfinder/tests/test_utils_driver_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ def test_query_driver_cuda_version_uses_windll_on_windows(monkeypatch):
fake_driver_lib = _FakeDriverLib(status=0, version=12080)
loaded_paths: list[str] = []

monkeypatch.setattr(driver_info, "IS_WINDOWS", True)
monkeypatch.setattr(
driver_info,
"_load_nvidia_dynamic_lib",
Expand All @@ -57,7 +56,7 @@ def fake_windll(abs_path: str):
loaded_paths.append(abs_path)
return fake_driver_lib

monkeypatch.setattr(driver_info.ctypes, "WinDLL", fake_windll, raising=False)
monkeypatch.setattr(driver_info, "_DRIVER_LIB_LOADER", fake_windll)

assert driver_info._query_driver_cuda_version_int() == 12080
assert loaded_paths == [r"C:\Windows\System32\nvcuda.dll"]
Expand Down Expand Up @@ -93,9 +92,8 @@ def fail_query_driver_cuda_version_int() -> int:
def test_query_driver_cuda_version_int_raises_when_cuda_call_fails(monkeypatch):
fake_driver_lib = _FakeDriverLib(status=1, version=0)

monkeypatch.setattr(driver_info, "IS_WINDOWS", False)
monkeypatch.setattr(driver_info, "_load_nvidia_dynamic_lib", lambda _libname: _loaded_cuda("/usr/lib/libcuda.so.1"))
monkeypatch.setattr(driver_info.ctypes, "CDLL", lambda _abs_path: fake_driver_lib)
monkeypatch.setattr(driver_info, "_DRIVER_LIB_LOADER", lambda _abs_path: fake_driver_lib)

with pytest.raises(RuntimeError, match=r"cuDriverGetVersion\(\) \(status=1\)"):
driver_info._query_driver_cuda_version_int()
Loading
Loading