Skip to content

Commit 219dc35

Browse files
switch to pyelftools so we dont require a new ctk component
1 parent ec60dcf commit 219dc35

3 files changed

Lines changed: 36 additions & 29 deletions

File tree

‎cuda_core/pixi.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pytest-rerunfailures = "*"
2323
cloudpickle = "*"
2424
docutils = "*"
2525
psutil = "*"
26+
pyelftools = "*"
2627
pyglet = "*"
2728

2829
[feature.test.pypi-dependencies]

‎cuda_core/pyproject.toml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ test = [
7070
"cloudpickle==3.1.2",
7171
"psutil==7.2.2",
7272
"docutils==0.23",
73+
# Reads the DWARF line table out of a compiled cubin; pure Python, so it
74+
# works on every platform and free-threaded build in the test matrix.
75+
"pyelftools==0.33",
7376
# TODO: remove the Python 3.15 guard once 3.15 is officially supported
7477
"cffi==2.0.0; python_version < '3.15'",
7578
]

‎cuda_core/tests/test_program.py‎

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
# SPDX-License-Identifier: Apache-2.0
44

55
import contextlib
6+
import io
67
import os
78
import re
8-
import shutil
9-
import subprocess
109
import warnings
1110

1211
import pytest
12+
from elftools.elf.elffile import ELFFile
1313

1414
from cuda.core import _linker
1515
from cuda.core._device import Device
@@ -370,9 +370,36 @@ def test_program_options_name_accepts_none(name):
370370
assert options._name == expected.encode()
371371

372372

373+
def _dwarf_source_path(cubin: bytes) -> str:
374+
"""The source path a debugger resolves for ``cubin``.
375+
376+
A cubin is an ELF64 image carrying ordinary DWARF, so the compilation
377+
unit's ``DW_AT_name`` is the path cuda-gdb opens to display source.
378+
"""
379+
with io.BytesIO(cubin) as stream:
380+
dwarf = ELFFile(stream).get_dwarf_info(relocate_dwarf_sections=False)
381+
units = list(dwarf.iter_CUs())
382+
assert len(units) == 1, f"expected one compilation unit, got {len(units)}"
383+
384+
top = units[0].get_top_DIE()
385+
path = top.attributes["DW_AT_name"].value.decode()
386+
comp_dir = top.attributes.get("DW_AT_comp_dir")
387+
if not os.path.isabs(path) and comp_dir is not None:
388+
path = os.path.join(comp_dir.value.decode(), path)
389+
390+
# The line table is what actually maps addresses to lines, so confirm it
391+
# names the same file instead of trusting DW_AT_name alone. Comparing
392+
# basenames avoids the dir_index encoding, which differs across DWARF
393+
# versions.
394+
listed = {entry.name.decode() for entry in dwarf.line_program_for_CU(units[0]).header["file_entry"]}
395+
assert os.path.basename(path) in listed, f"{path!r} absent from line table {sorted(listed)}"
396+
397+
return path
398+
399+
373400
@pytest.mark.human_reviewed
374401
@pytest.mark.parametrize("name", [None, "my_program"])
375-
def test_program_string_source_debug(name, tmp_path):
402+
def test_program_string_source_debug(name):
376403
# when a file doesn't exist on disk, nvrtc
377404
# still ends up referencing a file it thinks is there
378405
# in the dwarf table. As a WAR, we put the file there
@@ -382,33 +409,9 @@ def test_program_string_source_debug(name, tmp_path):
382409
program = Program(code, "c++", options={"name": name, "debug": True, "lineinfo": True})
383410
cubin = program.compile("cubin")
384411

385-
# read dwarf table
386-
nvdisasm = shutil.which("nvdisasm")
387-
388-
cubin_path = tmp_path / "program.cubin"
389-
cubin_path.write_bytes(bytes(cubin.code))
390-
391-
result = subprocess.run( # noqa: S603
392-
[nvdisasm, "-g", str(cubin_path)],
393-
capture_output=True,
394-
text=True,
395-
errors="replace",
396-
)
397-
if result.returncode != 0:
398-
pytest.fail(f"nvdisasm -g failed with exit code {result.returncode}\n{result.stderr}", pytrace=False)
399-
400-
# -g annotates each instruction with the line-table entry that covers it:
401-
# //## File "/abs/dir/name.cu", line 12
402-
# The directory comes from the line table's directory table, so this is the
403-
# fully resolved path cuda-gdb will try to open.
404-
paths = set(re.findall(r'//## File "([^"]+)", line \d+', result.stdout))
405-
assert len(paths) == 1, f"expected exactly one source file in the line table, got {sorted(paths)}"
406-
dwarf_path = paths.pop()
407-
408-
# cuda-gdb opens this path literally, so the source has to actually be
409-
# sitting there for source-level debugging to work.
410-
assert os.path.isfile(dwarf_path)
412+
dwarf_path = _dwarf_source_path(bytes(cubin.code))
411413

414+
assert os.path.isfile(dwarf_path), f"no source file on disk at the DWARF path {dwarf_path!r}"
412415
with open(dwarf_path, encoding="utf-8") as source_file:
413416
assert source_file.read().splitlines() == code.splitlines()
414417

0 commit comments

Comments
 (0)