CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
277 lines
8.6 KiB
Python
277 lines
8.6 KiB
Python
"""
|
|
Register custom Sphinx directives for rendering AMD SMI API documentation.
|
|
|
|
This extension reads a Doxygen tag file, collects API symbols and groups, and
|
|
emits Breathe directives to generate API reference content in Sphinx. It
|
|
supports Myst-flavoured Markdown sources via `myst-parser`.
|
|
"""
|
|
|
|
import xml.etree.ElementTree as XMLTree
|
|
from collections.abc import Sequence
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
from docutils import nodes
|
|
from docutils.parsers.rst import directives
|
|
from sphinx.application import Sphinx
|
|
from sphinx.config import Config
|
|
from sphinx.environment import BuildEnvironment
|
|
from sphinx.errors import ExtensionError
|
|
from sphinx.util.docutils import SphinxDirective
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class DoxygenTagData:
|
|
# groups are represented by `@defgroup` and their members by `@ingroup`
|
|
# in Doxygen comments
|
|
groups: list[tuple[str, str]] = field(default_factory=list)
|
|
enums: list[str] = field(default_factory=list)
|
|
defines: list[str] = field(default_factory=list)
|
|
structs: list[str] = field(default_factory=list)
|
|
unions: list[str] = field(default_factory=list)
|
|
typedefs: list[str] = field(default_factory=list)
|
|
|
|
|
|
def _render_myst_options(options: Sequence[str]) -> str:
|
|
return "".join(
|
|
f"{option if option.startswith(':') else f':{option}:'}\n"
|
|
for option in options
|
|
)
|
|
|
|
|
|
def _dedupe_preserve_order(items: Sequence[str]) -> list[str]:
|
|
return list(dict.fromkeys(items))
|
|
|
|
|
|
def render_groups_myst_markdown(
|
|
groups: Sequence[tuple[str, str]], heading_level: int
|
|
) -> str:
|
|
hashes = "#" * heading_level
|
|
return "\n\n".join(
|
|
f"({tag})=\n\n"
|
|
f"{hashes} {title}\n\n"
|
|
f"```{{doxygengroup}} {tag}\n"
|
|
f":content-only:\n"
|
|
f"```"
|
|
for tag, title in groups
|
|
)
|
|
|
|
|
|
def render_members_myst_markdown(
|
|
directive_name: str,
|
|
names: Sequence[str],
|
|
options: Sequence[str] = (),
|
|
) -> str:
|
|
breathe_options = _render_myst_options(options)
|
|
|
|
return "\n\n".join(
|
|
f"({name})=\n\n"
|
|
f"```{{{directive_name}}} {name}\n"
|
|
f"{breathe_options}"
|
|
f"```"
|
|
for name in names
|
|
)
|
|
|
|
|
|
class AmdsmiDoxygenDirective(SphinxDirective):
|
|
"""
|
|
Render AMD SMI API content from a Doxygen tagfile via Breathe.
|
|
|
|
Supported kinds:
|
|
- groups
|
|
- enums
|
|
- defines
|
|
- structs
|
|
- unions
|
|
- typedefs
|
|
|
|
Usage in .md files:
|
|
|
|
```{amdsmi-doxygen}
|
|
:kind: groups
|
|
:heading-level: 2
|
|
```
|
|
|
|
```{amdsmi-doxygen}
|
|
:kind: enums
|
|
```
|
|
"""
|
|
|
|
has_content = False
|
|
required_arguments = 0
|
|
optional_arguments = 0
|
|
final_argument_whitespace = False
|
|
option_spec = {
|
|
"kind": directives.unchanged_required,
|
|
"heading-level": directives.nonnegative_int,
|
|
}
|
|
|
|
def run(self) -> list[nodes.Node]:
|
|
kind = self.options["kind"].strip().lower()
|
|
|
|
if kind != "groups" and "heading-level" in self.options:
|
|
raise self.error(":heading-level: is only valid when :kind: is 'groups'.")
|
|
|
|
heading_level = self.options.get("heading-level", 2)
|
|
if kind == "groups" and not 1 <= heading_level <= 6:
|
|
raise self.error(":heading-level: must be between 1 and 6.")
|
|
|
|
data = self._get_doxygen_tag_data()
|
|
|
|
match kind:
|
|
case "groups":
|
|
return self._render_groups(data.groups, heading_level)
|
|
case "enums":
|
|
return self._render_members("doxygenenum", data.enums)
|
|
case "defines":
|
|
return self._render_members("doxygendefine", data.defines)
|
|
case "structs":
|
|
return self._render_members(
|
|
"doxygenstruct",
|
|
data.structs,
|
|
(":members:",),
|
|
)
|
|
case "unions":
|
|
return self._render_members(
|
|
"doxygenunion",
|
|
data.unions,
|
|
(":members:",),
|
|
)
|
|
case "typedefs":
|
|
return self._render_members("doxygentypedef", data.typedefs)
|
|
case _:
|
|
raise self.error(
|
|
f"Invalid :kind: {kind!r}. Expected one of: "
|
|
"groups, enums, defines, structs, unions, typedefs"
|
|
)
|
|
|
|
def _render_members(
|
|
self,
|
|
directive_name: str,
|
|
names: Sequence[str],
|
|
options: Sequence[str] = (),
|
|
) -> list[nodes.Node]:
|
|
if not names:
|
|
return []
|
|
|
|
text = render_members_myst_markdown(directive_name, names, options)
|
|
return self.parse_text_to_nodes(text)
|
|
|
|
def _render_groups(
|
|
self,
|
|
groups: Sequence[tuple[str, str]],
|
|
heading_level: int,
|
|
) -> list[nodes.Node]:
|
|
if not groups:
|
|
return []
|
|
|
|
text = render_groups_myst_markdown(groups, heading_level)
|
|
return self.parse_text_to_nodes(text, allow_section_headings=True)
|
|
|
|
def _get_doxygen_tag_data(self) -> DoxygenTagData:
|
|
tagfile = Path(self.config.amdsmi_doxygen_tagfile).resolve()
|
|
self.env.note_dependency(str(tagfile))
|
|
|
|
cache = getattr(self.env, "_amdsmi_doxygen_tag_cache", None)
|
|
if cache is None:
|
|
cache = {}
|
|
setattr(self.env, "_amdsmi_doxygen_tag_cache", cache)
|
|
|
|
key = str(tagfile)
|
|
data = cache.get(key)
|
|
if data is not None:
|
|
return data
|
|
|
|
if not tagfile.exists():
|
|
raise self.error(
|
|
f"Doxygen tagfile not found: {tagfile}. "
|
|
"Check the GENERATE_TAGFILE option in Doxyfile and "
|
|
"'amdsmi_doxygen_tagfile' in conf.py."
|
|
)
|
|
|
|
try:
|
|
root = XMLTree.parse(tagfile).getroot()
|
|
except XMLTree.ParseError as e:
|
|
raise self.error(f"Failed to parse Doxygen tagfile: {tagfile}") from e
|
|
|
|
data = DoxygenTagData()
|
|
|
|
for compound in root.findall("compound"):
|
|
match compound.get("kind"):
|
|
case "group":
|
|
group_tag = (compound.findtext("name") or "").strip()
|
|
group_title = (compound.findtext("title") or "").strip()
|
|
if group_tag and group_title:
|
|
data.groups.append((group_tag, group_title))
|
|
|
|
case "file":
|
|
for member in compound.findall("member"):
|
|
member_kind = member.get("kind")
|
|
member_name = (member.findtext("name") or "").strip()
|
|
if not member_name:
|
|
continue
|
|
|
|
if member_kind == "enumeration":
|
|
data.enums.append(member_name)
|
|
elif member_kind == "define":
|
|
data.defines.append(member_name)
|
|
elif member_kind == "typedef":
|
|
data.typedefs.append(member_name)
|
|
|
|
case "struct":
|
|
struct_name = (compound.findtext("name") or "").strip()
|
|
if struct_name:
|
|
data.structs.append(struct_name)
|
|
|
|
case "union":
|
|
union_name = (compound.findtext("name") or "").strip()
|
|
if union_name:
|
|
data.unions.append(union_name)
|
|
|
|
case _:
|
|
continue
|
|
|
|
data.groups = list(dict.fromkeys(data.groups))
|
|
data.enums = _dedupe_preserve_order(data.enums)
|
|
data.defines = _dedupe_preserve_order(data.defines)
|
|
data.structs = _dedupe_preserve_order(data.structs)
|
|
data.unions = _dedupe_preserve_order(data.unions)
|
|
data.typedefs = _dedupe_preserve_order(data.typedefs)
|
|
|
|
cache[key] = data
|
|
return data
|
|
|
|
|
|
def _validate_breathe_loaded(app: Sphinx, _config: Config) -> None:
|
|
if "breathe" not in app.extensions:
|
|
raise ExtensionError(
|
|
"The 'amdsmi-doxygen' extension requires the 'breathe' Sphinx "
|
|
"extension to be enabled in conf.py."
|
|
)
|
|
|
|
|
|
def _clear_tagfile_cache(
|
|
_app: Sphinx,
|
|
env: BuildEnvironment,
|
|
_docnames: list[str],
|
|
) -> None:
|
|
if hasattr(env, "_amdsmi_doxygen_tag_cache"):
|
|
delattr(env, "_amdsmi_doxygen_tag_cache")
|
|
|
|
|
|
def setup(app: Sphinx) -> dict[str, bool]:
|
|
app.add_config_value(
|
|
"amdsmi_doxygen_tagfile",
|
|
Path(app.confdir) / "doxygen" / "_out" / "tagfile.xml",
|
|
"env",
|
|
types=(str, Path),
|
|
)
|
|
app.add_directive("amdsmi-doxygen", AmdsmiDoxygenDirective)
|
|
app.connect("config-inited", _validate_breathe_loaded, priority=600)
|
|
app.connect("env-before-read-docs", _clear_tagfile_cache)
|
|
|
|
return {
|
|
"parallel_read_safe": True,
|
|
"parallel_write_safe": True,
|
|
}
|