Skip to content

Commit 7ea6afe

Browse files
committed
Use pathlib in cuda.core build hooks, tests and examples
Part of #2410. Replaces os.path with pathlib.Path in cuda_core/build_hooks.py, tests/helpers, the example test driver and the two examples that assemble CUDA include paths. ProgramOptions now accepts os.PathLike for every path-valued option (include_path, pre_include, create_pch, use_pch, pch_dir, fdevice_time_trace) and normalizes what it is given to pathlib.Path, so callers no longer have to convert back to str. Non-path values (False, range(...), ...) are left untouched, preserving the existing "silently ignored at compile time" behavior. name stays str (NVRTC uses it as a label and the program cache inspects it for a directory component); time stays str-or-bool because the same field is forwarded to LinkerOptions.time, which is a flag.
1 parent 4fe801c commit 7ea6afe

11 files changed

Lines changed: 181 additions & 70 deletions

File tree

cuda_core/build_hooks.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ def _import_get_cuda_path_or_home():
4646
cuda = None
4747

4848
for p in sys.path:
49-
sp_cuda = os.path.join(p, "cuda")
50-
if os.path.isdir(os.path.join(sp_cuda, "pathfinder")):
51-
cuda.__path__ = list(cuda.__path__) + [sp_cuda]
49+
sp_cuda = Path(p, "cuda")
50+
if (sp_cuda / "pathfinder").is_dir():
51+
cuda.__path__ = list(cuda.__path__) + [str(sp_cuda)]
5252
break
5353
else:
5454
raise ModuleNotFoundError(
@@ -93,7 +93,7 @@ def _determine_cuda_major_version() -> str:
9393

9494
# Derive from the CUDA headers (the authoritative source for what we compile against).
9595
cuda_path = _get_cuda_path()
96-
cuda_h = os.path.join(cuda_path, "include", "cuda.h")
96+
cuda_h = Path(cuda_path, "include", "cuda.h")
9797
try:
9898
with open(cuda_h, encoding="utf-8") as f:
9999
for line in f:
@@ -153,10 +153,11 @@ def _build_cuda_core(debug=False):
153153
# It seems setuptools' wildcard support has problems for namespace packages,
154154
# so we explicitly spell out all Extension instances.
155155
def module_names():
156-
root_path = os.path.sep.join(["cuda", "core", ""])
157-
for filename in glob.glob(f"{root_path}/**/*.pyx", recursive=True):
158-
mod = filename[len(root_path) : -4]
159-
if sys.platform == "win32" and mod.replace(os.path.sep, "/") in _posix_only_modules:
156+
root_path = Path("cuda", "core")
157+
for filename in glob.glob(str(root_path / "**" / "*.pyx"), recursive=True):
158+
# Module names are always spelled POSIX-style, on every platform.
159+
mod = Path(filename).relative_to(root_path).with_suffix("").as_posix()
160+
if sys.platform == "win32" and mod in _posix_only_modules:
160161
continue
161162
yield mod
162163

@@ -167,12 +168,12 @@ def get_sources(mod_name):
167168
# Add module-specific .cpp file from _cpp/ directory if it exists
168169
# Example: _resource_handles.pyx finds _cpp/resource_handles.cpp.
169170
cpp_file = f"cuda/core/_cpp/{mod_name.lstrip('_')}.cpp"
170-
if os.path.exists(cpp_file):
171+
if Path(cpp_file).exists():
171172
sources.append(cpp_file)
172173

173174
return sources
174175

175-
all_include_dirs = [os.path.join(_get_cuda_path(), "include")]
176+
all_include_dirs = [str(Path(_get_cuda_path(), "include"))]
176177
extra_compile_args = []
177178
extra_link_args = []
178179
extra_cythonize_kwargs = {}
@@ -196,7 +197,7 @@ def get_sources(mod_name):
196197

197198
ext_modules = tuple(
198199
Extension(
199-
f"cuda.core.{mod.replace(os.path.sep, '.')}",
200+
f"cuda.core.{mod.replace('/', '.')}",
200201
sources=get_sources(mod),
201202
include_dirs=[
202203
"cuda/core/_include",
@@ -230,7 +231,7 @@ def get_sources(mod_name):
230231
return
231232

232233

233-
def _add_cython_include_paths_to_pth(wheel_path: str) -> None:
234+
def _add_cython_include_paths_to_pth(wheel_path: Path) -> None:
234235
"""
235236
Modify the .pth file in an editable install wheel to add Cython include paths.
236237
@@ -261,7 +262,7 @@ def _add_cython_include_paths_to_pth(wheel_path: str) -> None:
261262
# Create a temporary directory for wheel manipulation
262263
with tempfile.TemporaryDirectory() as tmpdir:
263264
tmpdir_path = Path(tmpdir)
264-
wheel_file = Path(wheel_path)
265+
wheel_file = wheel_path
265266

266267
# Extract the wheel
267268
extract_dir = tmpdir_path / "extracted"
@@ -316,7 +317,7 @@ def build_editable(wheel_directory, config_settings=None, metadata_directory=Non
316317
wheel_name = _build_meta.build_editable(wheel_directory, config_settings, metadata_directory)
317318

318319
# Patch the .pth file to add Cython include paths
319-
wheel_path = os.path.join(wheel_directory, wheel_name)
320+
wheel_path = Path(wheel_directory, wheel_name)
320321
_add_cython_include_paths_to_pth(wheel_path)
321322

322323
return wheel_name

cuda_core/cuda/core/_program.pyi

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This module provides :class:`Program` for compiling source code into
88
from __future__ import annotations
99

1010
from dataclasses import dataclass
11+
from os import PathLike
1112

1213
from cuda.bindings import nvrtc
1314
from cuda.core._linker import LinkerHandleT
@@ -141,6 +142,12 @@ class Program:
141142
class ProgramOptions:
142143
"""Customizable options for configuring :class:`Program`.
143144
145+
Every path-valued option (``include_path``, ``pre_include``, ``create_pch``,
146+
``use_pch``, ``pch_dir``, ``fdevice_time_trace``) accepts either a :class:`str`
147+
or any :class:`os.PathLike`, and stores it as a :class:`pathlib.Path`. Callers
148+
building paths with :mod:`pathlib` therefore never need to convert back to
149+
``str``.
150+
144151
Attributes
145152
----------
146153
name : str, optional
@@ -206,11 +213,11 @@ class ProgramOptions:
206213
undefine_macro : Union[str, list[str]], optional
207214
Cancel any previous definition of a macro, or list of macros.
208215
Default: None
209-
include_path : Union[str, list[str]], optional
216+
include_path : Union[str, os.PathLike, list[Union[str, os.PathLike]]], optional
210217
Add the directory or directories to the list of directories to be searched for headers.
211218
Default: None
212-
pre_include : Union[str, list[str]], optional
213-
Preinclude one or more headers during preprocessing. Can be either a string or a list of strings.
219+
pre_include : Union[str, os.PathLike, list[Union[str, os.PathLike]]], optional
220+
Preinclude one or more headers during preprocessing. Can be either a single path or a list of paths.
214221
Default: None
215222
no_source_include : bool, optional
216223
Disable the default behavior of adding the directory of each input source to the include path.
@@ -269,7 +276,7 @@ class ProgramOptions:
269276
no_cache : bool, optional
270277
Disable compiler caching.
271278
Default: False
272-
fdevice_time_trace : str, optional
279+
fdevice_time_trace : Union[str, os.PathLike], optional
273280
Generate time trace JSON for profiling compilation (NVRTC only).
274281
Default: None
275282
device_float128 : bool, optional
@@ -284,13 +291,13 @@ class ProgramOptions:
284291
pch : bool, optional
285292
Use default precompiled header (NVRTC only, CUDA 12.8+).
286293
Default: False
287-
create_pch : str, optional
294+
create_pch : Union[str, os.PathLike], optional
288295
Create precompiled header file (NVRTC only, CUDA 12.8+).
289296
Default: None
290-
use_pch : str, optional
297+
use_pch : Union[str, os.PathLike], optional
291298
Use specific precompiled header file (NVRTC only, CUDA 12.8+).
292299
Default: None
293-
pch_dir : str, optional
300+
pch_dir : Union[str, os.PathLike], optional
294301
PCH directory location (NVRTC only, CUDA 12.8+).
295302
Default: None
296303
pch_verbose : bool, optional
@@ -332,8 +339,8 @@ class ProgramOptions:
332339
gen_opt_lto: bool | None = None
333340
define_macro: str | tuple[str, str] | list[str | tuple[str, str]] | tuple[str | tuple[str, str], ...] | None = None
334341
undefine_macro: str | list[str] | tuple[str] | None = None
335-
include_path: str | list[str] | tuple[str] | None = None
336-
pre_include: str | list[str] | tuple[str] | None = None
342+
include_path: str | PathLike[str] | list[str | PathLike[str]] | tuple[str | PathLike[str]] | None = None
343+
pre_include: str | PathLike[str] | list[str | PathLike[str]] | tuple[str | PathLike[str]] | None = None
337344
no_source_include: bool | None = None
338345
std: str | None = None
339346
builtin_move_forward: bool | None = None
@@ -353,14 +360,14 @@ class ProgramOptions:
353360
fdevice_syntax_only: bool | None = None
354361
minimal: bool | None = None
355362
no_cache: bool | None = None
356-
fdevice_time_trace: str | None = None
363+
fdevice_time_trace: str | PathLike[str] | None = None
357364
device_float128: bool | None = None
358365
frandom_seed: str | None = None
359366
ofast_compile: str | None = None
360367
pch: bool | None = None
361-
create_pch: str | None = None
362-
use_pch: str | None = None
363-
pch_dir: str | None = None
368+
create_pch: str | PathLike[str] | None = None
369+
use_pch: str | PathLike[str] | None = None
370+
pch_dir: str | PathLike[str] | None = None
364371
pch_verbose: bool | None = None
365372
pch_messages: bool | None = None
366373
instantiate_templates_in_pch: bool | None = None
@@ -417,12 +424,23 @@ class ProgramOptions:
417424
"""Convert extra_sources to bytes format for NVVM."""
418425
__all__ = ['Program', 'ProgramOptions']
419426
ProgramHandleT = nvrtc.nvrtcProgram | int | LinkerHandleT
427+
_PATH_OPTION_FIELDS = ('include_path', 'pre_include', 'create_pch', 'use_pch', 'pch_dir', 'fdevice_time_trace')
420428
_nvvm_module = None
421429
_nvvm_import_attempted = False
422430

423431
def _can_load_generated_ptx() -> bool:
424432
"""Check if the driver can load PTX generated by the current NVRTC version."""
425433

434+
def _coerce_path_option(value):
435+
"""Normalize a path-valued :class:`ProgramOptions` field.
436+
437+
``str`` / :class:`os.PathLike` becomes :class:`pathlib.Path`; a ``list``
438+
or ``tuple`` has its ``str`` / :class:`os.PathLike` items converted while
439+
keeping the container type. Anything else is returned unchanged, so the
440+
"silently ignored at compile time" behavior of non-path values (``False``,
441+
``range(...)``, ...) is unaffected.
442+
"""
443+
426444
def _program_compile_uncached(program, target_type, name_expressions, logs):
427445
"""Run ``Program_compile`` without the cache wrapper.
428446

cuda_core/cuda/core/_program.pyx

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ This module provides :class:`Program` for compiling source code into
1010
from __future__ import annotations
1111

1212
from dataclasses import dataclass
13+
from os import PathLike
14+
from pathlib import Path
1315
import threading
1416
from typing import TYPE_CHECKING
1517
from warnings import warn
@@ -294,6 +296,12 @@ cdef class Program:
294296
class ProgramOptions:
295297
"""Customizable options for configuring :class:`Program`.
296298

299+
Every path-valued option (``include_path``, ``pre_include``, ``create_pch``,
300+
``use_pch``, ``pch_dir``, ``fdevice_time_trace``) accepts either a :class:`str`
301+
or any :class:`os.PathLike`, and stores it as a :class:`pathlib.Path`. Callers
302+
building paths with :mod:`pathlib` therefore never need to convert back to
303+
``str``.
304+
297305
Attributes
298306
----------
299307
name : str, optional
@@ -359,11 +367,11 @@ class ProgramOptions:
359367
undefine_macro : Union[str, list[str]], optional
360368
Cancel any previous definition of a macro, or list of macros.
361369
Default: None
362-
include_path : Union[str, list[str]], optional
370+
include_path : Union[str, os.PathLike, list[Union[str, os.PathLike]]], optional
363371
Add the directory or directories to the list of directories to be searched for headers.
364372
Default: None
365-
pre_include : Union[str, list[str]], optional
366-
Preinclude one or more headers during preprocessing. Can be either a string or a list of strings.
373+
pre_include : Union[str, os.PathLike, list[Union[str, os.PathLike]]], optional
374+
Preinclude one or more headers during preprocessing. Can be either a single path or a list of paths.
367375
Default: None
368376
no_source_include : bool, optional
369377
Disable the default behavior of adding the directory of each input source to the include path.
@@ -422,7 +430,7 @@ class ProgramOptions:
422430
no_cache : bool, optional
423431
Disable compiler caching.
424432
Default: False
425-
fdevice_time_trace : str, optional
433+
fdevice_time_trace : Union[str, os.PathLike], optional
426434
Generate time trace JSON for profiling compilation (NVRTC only).
427435
Default: None
428436
device_float128 : bool, optional
@@ -437,13 +445,13 @@ class ProgramOptions:
437445
pch : bool, optional
438446
Use default precompiled header (NVRTC only, CUDA 12.8+).
439447
Default: False
440-
create_pch : str, optional
448+
create_pch : Union[str, os.PathLike], optional
441449
Create precompiled header file (NVRTC only, CUDA 12.8+).
442450
Default: None
443-
use_pch : str, optional
451+
use_pch : Union[str, os.PathLike], optional
444452
Use specific precompiled header file (NVRTC only, CUDA 12.8+).
445453
Default: None
446-
pch_dir : str, optional
454+
pch_dir : Union[str, os.PathLike], optional
447455
PCH directory location (NVRTC only, CUDA 12.8+).
448456
Default: None
449457
pch_verbose : bool, optional
@@ -486,8 +494,8 @@ class ProgramOptions:
486494
gen_opt_lto: bool | None = None
487495
define_macro: str | tuple[str, str] | list[str | tuple[str, str]] | tuple[str | tuple[str, str], ...] | None = None
488496
undefine_macro: str | list[str] | tuple[str] | None = None
489-
include_path: str | list[str] | tuple[str] | None = None
490-
pre_include: str | list[str] | tuple[str] | None = None
497+
include_path: str | PathLike[str] | list[str | PathLike[str]] | tuple[str | PathLike[str]] | None = None
498+
pre_include: str | PathLike[str] | list[str | PathLike[str]] | tuple[str | PathLike[str]] | None = None
491499
no_source_include: bool | None = None
492500
std: str | None = None
493501
builtin_move_forward: bool | None = None
@@ -507,14 +515,14 @@ class ProgramOptions:
507515
fdevice_syntax_only: bool | None = None
508516
minimal: bool | None = None
509517
no_cache: bool | None = None
510-
fdevice_time_trace: str | None = None
518+
fdevice_time_trace: str | PathLike[str] | None = None
511519
device_float128: bool | None = None
512520
frandom_seed: str | None = None
513521
ofast_compile: str | None = None
514522
pch: bool | None = None
515-
create_pch: str | None = None
516-
use_pch: str | None = None
517-
pch_dir: str | None = None
523+
create_pch: str | PathLike[str] | None = None
524+
use_pch: str | PathLike[str] | None = None
525+
pch_dir: str | PathLike[str] | None = None
518526
pch_verbose: bool | None = None
519527
pch_messages: bool | None = None
520528
instantiate_templates_in_pch: bool | None = None
@@ -524,6 +532,12 @@ class ProgramOptions:
524532

525533
def __post_init__(self) -> None:
526534
self._name = self.name.encode()
535+
# Path-valued options accept str or os.PathLike; normalize to Path so
536+
# callers never have to convert back to str just to build options.
537+
for _field in _PATH_OPTION_FIELDS:
538+
_value = getattr(self, _field)
539+
if _value is not None:
540+
setattr(self, _field, _coerce_path_option(_value))
527541
# Set arch to default if not provided
528542
if self.arch is None:
529543
self.arch = f"sm_{Device().arch}"
@@ -628,6 +642,42 @@ class ProgramOptions:
628642
# =============================================================================
629643

630644

645+
# ``ProgramOptions`` fields that name a filesystem path. ``str`` and
646+
# ``os.PathLike`` values for these are normalized to :class:`pathlib.Path` in
647+
# ``ProgramOptions.__post_init__`` so everything downstream sees one type.
648+
#
649+
# ``name`` is not here: NVRTC uses it as the source *filename*, but it is a
650+
# plain label (``ProgramOptions.__post_init__`` encodes it, and the program
651+
# cache inspects it for a directory component), so it stays ``str``. ``time``
652+
# is not here either: NVRTC treats it as an output filename, but the same
653+
# field is forwarded to ``LinkerOptions.time`` (a bool flag) for PTX inputs.
654+
_PATH_OPTION_FIELDS = (
655+
"include_path",
656+
"pre_include",
657+
"create_pch",
658+
"use_pch",
659+
"pch_dir",
660+
"fdevice_time_trace",
661+
)
662+
663+
664+
def _coerce_path_option(value):
665+
"""Normalize a path-valued :class:`ProgramOptions` field.
666+
667+
``str`` / :class:`os.PathLike` becomes :class:`pathlib.Path`; a ``list``
668+
or ``tuple`` has its ``str`` / :class:`os.PathLike` items converted while
669+
keeping the container type. Anything else is returned unchanged, so the
670+
"silently ignored at compile time" behavior of non-path values (``False``,
671+
``range(...)``, ...) is unaffected.
672+
"""
673+
if isinstance(value, (str, PathLike)):
674+
return Path(value)
675+
if isinstance(value, (list, tuple)):
676+
coerced = [Path(v) if isinstance(v, (str, PathLike)) else v for v in value]
677+
return tuple(coerced) if isinstance(value, tuple) else coerced
678+
return value
679+
680+
631681
def _program_compile_uncached(program, target_type, name_expressions, logs):
632682
"""Run ``Program_compile`` without the cache wrapper.
633683
@@ -1125,13 +1175,15 @@ cdef inline list _prepare_nvrtc_options_impl(object opts):
11251175
for macro in opts.undefine_macro:
11261176
options.append(f"--undefine-macro={macro}")
11271177
if opts.include_path is not None:
1128-
if isinstance(opts.include_path, str):
1178+
# ``__post_init__`` turns str/PathLike into Path, but the dataclass is
1179+
# mutable, so accept either form here.
1180+
if isinstance(opts.include_path, (str, PathLike)):
11291181
options.append(f"--include-path={opts.include_path}")
11301182
elif is_sequence(opts.include_path):
11311183
for path in opts.include_path:
11321184
options.append(f"--include-path={path}")
11331185
if opts.pre_include is not None:
1134-
if isinstance(opts.pre_include, str):
1186+
if isinstance(opts.pre_include, (str, PathLike)):
11351187
options.append(f"--pre-include={opts.pre_include}")
11361188
elif is_sequence(opts.pre_include):
11371189
for header in opts.pre_include:

0 commit comments

Comments
 (0)