Skip to content
Merged
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
12 changes: 12 additions & 0 deletions cmake/doxygen.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,18 @@ function(add_doxygen_target name)
"GENERATE_MAN = NO\n"
"GENERATE_RTF = NO\n"
"GENERATE_DOCBOOK = NO\n"
# Stage 2 parses the same inputs and reports every warning again, so
# stage 1's copy is pure duplication. Worse, stage 1 warns falsely: with
# TAGFILES cleared, every reference into a peer is "unresolved". Its
# warnings must therefore never fail the build either. WARNINGS alone is
# not enough: the WARN_IF_* switches warn independently of it.
"WARNINGS = NO\n"
"WARN_IF_UNDOCUMENTED = NO\n"
"WARN_IF_DOC_ERROR = NO\n"
"WARN_IF_INCOMPLETE_DOC = NO\n"
"WARN_NO_PARAMDOC = NO\n"
"WARN_IF_UNDOC_ENUM_VAL = NO\n"
"WARN_AS_ERROR = NO\n"
)

# Doxygen is invoked through run_doxygen.cmake (a `cmake -P` wrapper) so a
Expand Down
28 changes: 27 additions & 1 deletion cmake/registry.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ endmacro()
# with zero output formats (fail loudly at configure
# time, per this engine's own convention — see
# add_sphinx_target's identical BUILDERS check).
# With `doxygen_tag:`, also <id>-needstag: its needs
# as deploy/html/<id>/needs.tag, after <id>-index, in
# doc-index but not doc-tags.
# <group>-<builder> depends on every qualifying document's own
# <id>-<builder> target.
# <group>-<builder>-nodeps the same, but against each document's
Expand Down Expand Up @@ -118,7 +121,9 @@ endmacro()
- ``sphinx`` (or omitted) — :cmake:command:`add_sphinx_target`
``(<id> BUILDERS <builders...> REGISTRY <REGISTRY> [DOCDIR ...])``.
A configure-time ``FATAL_ERROR`` naming the document if ``builders:`` is
empty or missing.
empty or missing. A document with ``doxygen_tag:`` also gets
``<id>-needstag``, which writes its needs as a Doxygen tag file after its
stage-1 index (in ``doc-index``, not ``doc-tags``).
- ``doxygen`` — :cmake:command:`add_doxygen_target`
``(<id> REGISTRY <REGISTRY> [DOCDIR ...])``.
- ``external`` / ``sphinx-external`` — no CMake target of any kind.
Expand Down Expand Up @@ -306,6 +311,27 @@ function(add_docs_from_registry)
if(NOT _zdocs_testmodule_spec STREQUAL "")
add_dependencies(${_zdocs_id}-index ${_zdocs_testmodule_spec}-index)
endif()

# doxygen_tag: this document's needs, as a Doxygen tag file every
# doxygen peer lists in TAGFILES (docrefs.py tagfiles), so `\verifies`
# and `\satisfies` resolve against requirements authored in rst.
# Generated from the needs.json that <id>-index exports, so it runs
# after that. It joins doc-index but deliberately NOT doc-tags: every
# Sphinx index waits on doc-tags, so membership there would be a cycle
# (<id>-needstag -> <id>-index -> doc-tags -> <id>-needstag). Nothing
# needs it earlier, because stage-1 doxygen blanks its TAGFILES.
string(JSON _zdocs_doxygen_tag GET "${_zdocs_entry}" "doxygen_tag")
if(_zdocs_doxygen_tag)
add_custom_target(
${_zdocs_id}-needstag
COMMAND
${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../scripts/docrefs.py
needs-tag ${_zdocs_id} ${CMAKE_CURRENT_BINARY_DIR}/deploy --registry ${ARGS_REGISTRY}
COMMENT "Doxygen needs tag for ${_zdocs_id}..."
)
add_dependencies(${_zdocs_id}-needstag ${_zdocs_id}-index)
add_dependencies(doc-index ${_zdocs_id}-needstag)
endif()
endif()
endforeach()

Expand Down
10 changes: 10 additions & 0 deletions cmake/sphinx.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,16 @@ function(add_sphinx_target doc_name)
if(NOT ZDOCS_TWISTER_OUT STREQUAL "")
list(APPEND SPHINX_ENV ZDOCS_TWISTER_OUT=${ZDOCS_TWISTER_OUT})
endif()
# The Zephyr that find_package(Zephyr) found. zdocs_conf loads Zephyr's doc
# extensions from it, including external_content, which stages the sources
# into ${DOCS_SRC_DIR}. find_package sets only the CMake variable, never the
# environment, so without this the build depended on the user's shell
# exporting ZEPHYR_BASE. A shell without it staged nothing, and Sphinx failed
# with "unable to load the master document". Set explicitly, it also
# overrides a stale shell value that points at a different Zephyr.
if(NOT ZEPHYR_BASE STREQUAL "")
list(APPEND SPHINX_ENV ZEPHYR_BASE=${ZEPHYR_BASE})
endif()

if(ARGS_REGISTRY)
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${ARGS_REGISTRY})
Expand Down
59 changes: 59 additions & 0 deletions doc/manual/reference/registry-schema.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,24 @@ Top level
or empty — see :doc:`cli`. Optional; without it, ``doc-check`` simply skips
that one check.

``doc_check_accepted``
Optional list of ``doccheck`` findings to let through, for defects the
consumer cannot fix (e.g. one inside a generated upstream API):

.. code-block:: yaml

doc_check_accepted:
- finding: "html/api/structfoo.html: dead link -> structfoo_1_1_0d13.html"
reason: "Doxygen does not generate nested anonymous struct pages"

``finding`` is the finding's exact printed text, never a pattern, so an
acceptance cannot swallow a new, different finding. ``reason`` is required.
Accepted findings do not fail the check, but they are still printed under
``accepted (N)`` with their reason. An entry that no longer matches anything
is printed under ``accepted but no longer found`` without failing, so fixing
the defect never breaks the build and the list does not rot. A malformed
entry is a bad invocation (exit 2).

``doxygen_xml``
Project-scoped boolean, default off. When true, **every** ``kind: doxygen``
document in the registry generates XML into
Expand Down Expand Up @@ -182,6 +200,17 @@ Each entry in ``documents:``, keyed by its id:
Downloaded at build time and stored locally as ``doxygen.tag``, whatever
the remote file is actually called.

``crossref``
Boolean, default ``true``. ``false`` takes the document out of the
cross-reference graph in both directions: no peer gets an intersphinx,
doxylink, external-needs or Doxygen ``TAGFILES`` entry for it, and it gets
none for its peers. It is still built, deployed and listed in the
navigation. Meant for a large reference build that documents a superset of
its peers' symbols — a project's full API beside a scoped subset. Doxygen
projects that import each other's tag files leave shared symbols to the
other project, so neither generates their pages. A quoted ``"false"`` is a
configure-time error rather than a truthy string.

``needs``
Opt-in sub-block; presence is what makes this document importable as
external needs by every peer. Two shapes:
Expand Down Expand Up @@ -209,6 +238,36 @@ Each entry in ``documents:``, keyed by its id:
rejected at configure time as a ``spec:`` target, because ``testreport``
correlates against ``needs.json``, and the synthesized stub is not that.

``doxygen_tag``
Opt-in; publishes this document's needs as Doxygen requirements, so a
``\verifies`` or ``\satisfies`` in any ``kind: doxygen`` peer resolves
against requirements authored in reStructuredText, and links to their
Sphinx pages. Two shapes:

.. code-block:: yaml

doxygen_tag: true # every need this document defines

.. code-block:: yaml

doxygen_tag:
types: [requirement] # only needs of these types

Requires ``kind: sphinx`` and ``needs: {source: json}``, because the tag file
is generated from this document's own ``needs.json``. Anything else is a
configure-time error, as are an unknown key and an empty or non-list
``types``. The engine adds a ``<id>-needstag`` target that writes
``deploy/html/<id>/needs.tag`` after the document's stage-1 index, and lists
that file in every Doxygen peer's ``TAGFILES``. Imported (external) needs are
left out, because their own document publishes them.

Nothing but Sphinx parses the requirements, so no ``.dox`` is generated and
no Doxygen project is built for them. A ``\verifies`` naming an id the tag
does not declare warns ``Reference to unknown requirement``, which the
stage-2 warning gate (``ZDOCS_DOXYGEN_WARN_FAIL_PATTERNS``) fails on.
``crossref: false`` on either side removes the entry, as for any tag file.
Needs Doxygen 1.16 or newer.

``testmodule``
Opt-in sub-block (see :doc:`../explanation/testmodule-and-twister` and
:doc:`directives-and-roles`); its presence is the sole trigger that loads
Expand Down
56 changes: 52 additions & 4 deletions scripts/doccheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,29 @@ def check_deploy_links(deploy: Path, base_url: str) -> list[str]:
return findings


def _accepted_findings(entries) -> dict[str, str] | None:
"""The registry's ``doc_check_accepted:`` list as ``{finding: reason}``.

Each entry names one finding by its exact printed text, never a pattern,
so an acceptance cannot swallow a new, different finding. ``reason`` is
required: an exception nobody can explain is one nobody can retire.
Returns ``None`` (after printing why) for a malformed list.
"""
accepted: dict[str, str] = {}
for i, entry in enumerate(entries or []):
finding = entry.get("finding") if isinstance(entry, dict) else None
reason = entry.get("reason") if isinstance(entry, dict) else None
if not (isinstance(finding, str) and finding and isinstance(reason, str) and reason):
print(
f"doccheck: doc_check_accepted[{i}] needs a non-empty 'finding' "
"and 'reason'",
file=sys.stderr,
)
return None
accepted[finding] = reason
return accepted


def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--registry", type=Path, required=True)
Expand All @@ -223,6 +246,10 @@ def main() -> int:
print(f"doccheck: no deploy tree at {args.deploy}", file=sys.stderr)
return 2

accepted = _accepted_findings(raw.get("doc_check_accepted"))
if accepted is None:
return 2

groups: list[tuple[str, list[str]]] = []
if smoke_page:
groups.append(
Expand All @@ -231,13 +258,34 @@ def main() -> int:
groups.append(("dead deploy links", check_deploy_links(args.deploy, base_url)))

total = 0
matched: set[str] = set()
for title, findings in groups:
if findings:
total += len(findings)
print(f"\ndoccheck: {title} ({len(findings)}):", file=sys.stderr)
for f in findings:
failing = [f for f in findings if f not in accepted]
matched.update(f for f in findings if f in accepted)
if failing:
total += len(failing)
print(f"\ndoccheck: {title} ({len(failing)}):", file=sys.stderr)
for f in failing:
print(f" {f}", file=sys.stderr)

# Accepted findings are printed, never hidden: the build passes, but the
# log still says what is broken and why it was let through.
if matched:
print(f"\ndoccheck: accepted ({len(matched)}):", file=sys.stderr)
for f in sorted(matched):
print(f" {f}\n reason: {accepted[f]}", file=sys.stderr)
stale = sorted(set(accepted) - matched)
if stale:
# Not a failure — fixing the underlying problem must not break the
# build — but said out loud, so the list does not rot.
print(
f"\ndoccheck: accepted but no longer found ({len(stale)}) — "
"remove from doc_check_accepted:",
file=sys.stderr,
)
for f in stale:
print(f" {f}", file=sys.stderr)

if total:
print(f"\ndoccheck: FAILED with {total} finding(s)", file=sys.stderr)
return 1
Expand Down
Loading