aboutsummaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/about/deprecated.rst17
-rw-r--r--docs/about/removed-features.rst7
-rw-r--r--docs/conf.py18
-rw-r--r--docs/devel/codebase.rst6
-rw-r--r--docs/devel/index-build.rst1
-rw-r--r--docs/devel/qapi-domain.rst670
-rw-r--r--docs/glossary.rst10
-rw-r--r--docs/interop/qemu-qmp-ref.rst1
-rw-r--r--docs/sphinx-static/theme_overrides.css98
-rw-r--r--docs/sphinx/compat.py230
-rw-r--r--docs/sphinx/qapi_domain.py931
-rw-r--r--docs/sphinx/qapidoc.py913
-rw-r--r--docs/sphinx/qapidoc_legacy.py440
-rw-r--r--docs/system/ppc/amigang.rst17
-rw-r--r--docs/system/ppc/embedded.rst1
-rw-r--r--docs/system/ppc/powernv.rst7
16 files changed, 2921 insertions, 446 deletions
diff --git a/docs/about/deprecated.rst b/docs/about/deprecated.rst
index 589951b..e2b4f07 100644
--- a/docs/about/deprecated.rst
+++ b/docs/about/deprecated.rst
@@ -266,6 +266,15 @@ in the QEMU object model anymore. ``Sun-UltraSparc-IIIi+`` and
but for consistency these will get removed in a future release, too.
Use ``Sun-UltraSparc-IIIi-plus`` and ``Sun-UltraSparc-IV-plus`` instead.
+PPC 405 CPUs (since 10.0)
+'''''''''''''''''''''''''
+
+The PPC 405 CPU has no known users and the ``ref405ep`` machine was
+removed in QEMU 10.0. Since the IBM POWER [8-11] processors uses an
+embedded 405 for power management (OCC) and other internal tasks, it
+is theoretically possible to use QEMU to model them. Let's keep the
+CPU implementation for a while before removing all support.
+
System emulator machines
------------------------
@@ -277,14 +286,6 @@ deprecated; use the new name ``dtb-randomness`` instead. The new name
better reflects the way this property affects all random data within
the device tree blob, not just the ``kaslr-seed`` node.
-PPC 405 ``ref405ep`` machine (since 9.1)
-''''''''''''''''''''''''''''''''''''''''
-
-The ``ref405ep`` machine and PPC 405 CPU have no known users, firmware
-images are not available, OpenWRT dropped support in 2019, U-Boot in
-2017, Linux also is dropping support in 2024. It is time to let go of
-this ancient hardware and focus on newer CPUs and platforms.
-
Big-Endian variants of MicroBlaze ``petalogix-ml605`` and ``xlnx-zynqmp-pmu`` machines (since 9.2)
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
diff --git a/docs/about/removed-features.rst b/docs/about/removed-features.rst
index 156c0c2..2527a91 100644
--- a/docs/about/removed-features.rst
+++ b/docs/about/removed-features.rst
@@ -1064,6 +1064,13 @@ for all machine types using the PXA2xx and OMAP2 SoCs. We are also
dropping the ``cheetah`` OMAP1 board, because we don't have any
test images for it and don't know of anybody who does.
+ppc ``ref405ep`` machine (removed in 10.0)
+''''''''''''''''''''''''''''''''''''''''''
+
+This machine was removed because PPC 405 CPU have no known users,
+firmware images are not available, OpenWRT dropped support in 2019,
+U-Boot in 2017, and Linux in 2024.
+
linux-user mode CPUs
--------------------
diff --git a/docs/conf.py b/docs/conf.py
index 31bb9a3..a3f9fa6 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -60,7 +60,14 @@ needs_sphinx = '3.4.3'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
-extensions = ['kerneldoc', 'qmp_lexer', 'hxtool', 'depfile', 'qapidoc']
+extensions = [
+ 'depfile',
+ 'hxtool',
+ 'kerneldoc',
+ 'qapi_domain',
+ 'qapidoc',
+ 'qmp_lexer',
+]
if sphinx.version_info[:3] > (4, 0, 0):
tags.add('sphinx4')
@@ -146,6 +153,15 @@ rst_epilog = ".. |CONFDIR| replace:: ``" + confdir + "``\n"
with open(os.path.join(qemu_docdir, 'defs.rst.inc')) as f:
rst_epilog += f.read()
+
+# Normally, the QAPI domain is picky about what field lists you use to
+# describe a QAPI entity. If you'd like to use arbitrary additional
+# fields in source documentation, add them here.
+qapi_allowed_fields = {
+ "see also",
+}
+
+
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
diff --git a/docs/devel/codebase.rst b/docs/devel/codebase.rst
index 4039875..1b09953 100644
--- a/docs/devel/codebase.rst
+++ b/docs/devel/codebase.rst
@@ -23,7 +23,7 @@ Some of the main QEMU subsystems are:
- `Devices<device-emulation>` & Board models
- `Documentation <documentation-root>`
- `GDB support<GDB usage>`
-- `Migration<migration>`
+- :ref:`Migration<migration>`
- `Monitor<QEMU monitor>`
- :ref:`QOM (QEMU Object Model)<qom>`
- `System mode<System emulation>`
@@ -112,7 +112,7 @@ yet, so sometimes the source code is all you have.
* `libdecnumber <https://gitlab.com/qemu-project/qemu/-/tree/master/libdecnumber>`_:
Import of gcc library, used to implement decimal number arithmetic.
* `migration <https://gitlab.com/qemu-project/qemu/-/tree/master/migration>`__:
- `Migration framework <migration>`.
+ :ref:`Migration framework <migration>`.
* `monitor <https://gitlab.com/qemu-project/qemu/-/tree/master/monitor>`_:
`Monitor <QEMU monitor>` implementation (HMP & QMP).
* `nbd <https://gitlab.com/qemu-project/qemu/-/tree/master/nbd>`_:
@@ -193,7 +193,7 @@ yet, so sometimes the source code is all you have.
- `lcitool <https://gitlab.com/qemu-project/qemu/-/tree/master/tests/lcitool>`_:
Generate dockerfiles for CI containers.
- `migration <https://gitlab.com/qemu-project/qemu/-/tree/master/tests/migration>`_:
- Test scripts and data for `Migration framework <migration>`.
+ Test scripts and data for :ref:`Migration framework <migration>`.
- `multiboot <https://gitlab.com/qemu-project/qemu/-/tree/master/tests/multiboot>`_:
Test multiboot functionality for x86_64/i386.
- `qapi-schema <https://gitlab.com/qemu-project/qemu/-/tree/master/tests/qapi-schema>`_:
diff --git a/docs/devel/index-build.rst b/docs/devel/index-build.rst
index 0745c81..3f3cb21 100644
--- a/docs/devel/index-build.rst
+++ b/docs/devel/index-build.rst
@@ -12,4 +12,5 @@ some of the basics if you are adding new files and targets to the build.
kconfig
docs
qapi-code-gen
+ qapi-domain
control-flow-integrity
diff --git a/docs/devel/qapi-domain.rst b/docs/devel/qapi-domain.rst
new file mode 100644
index 0000000..1475870
--- /dev/null
+++ b/docs/devel/qapi-domain.rst
@@ -0,0 +1,670 @@
+======================
+The Sphinx QAPI Domain
+======================
+
+An extension to the `rST syntax
+<https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html>`_
+in Sphinx is provided by the QAPI Domain, located in
+``docs/sphinx/qapi_domain.py``. This extension is analogous to the
+`Python Domain
+<https://www.sphinx-doc.org/en/master/usage/domains/python.html>`_
+included with Sphinx, but provides special directives and roles
+speciically for annotating and documenting QAPI definitions
+specifically.
+
+A `Domain
+<https://www.sphinx-doc.org/en/master/usage/domains/index.html>`_
+provides a set of special rST directives and cross-referencing roles to
+Sphinx for understanding rST markup written to document a specific
+language. By itself, this QAPI extension is only sufficient to parse rST
+markup written by hand; the `autodoc
+<https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html>`_
+functionality is provided elsewhere, in ``docs/sphinx/qapidoc.py``, by
+the "Transmogrifier".
+
+It is not expected that any developer nor documentation writer would
+never need to write *nor* read these special rST forms. However, in the
+event that something needs to be debugged, knowing the syntax of the
+domain is quite handy. This reference may also be useful as a guide for
+understanding the QAPI Domain extension code itself. Although most of
+these forms will not be needed for documentation writing purposes,
+understanding the cross-referencing syntax *will* be helpful when
+writing rST documentation elsewhere, or for enriching the body of
+QAPIDoc blocks themselves.
+
+
+Concepts
+========
+
+The QAPI Domain itself provides no mechanisms for reading the QAPI
+Schema or generating documentation from code that exists. It is merely
+the rST syntax used to describe things. For instance, the Sphinx Python
+domain adds syntax like ``:py:func:`` for describing Python functions in
+documentation, but it's the autodoc module that is responsible for
+reading python code and generating such syntax. QAPI is analagous here:
+qapidoc.py is responsible for reading the QAPI Schema and generating rST
+syntax, and qapi_domain.py is responsible for translating that special
+syntax and providing APIs for Sphinx internals.
+
+In other words:
+
+qapi_domain.py adds syntax like ``.. qapi:command::`` to Sphinx, and
+qapidoc.py transforms the documentation in ``qapi/*.json`` into rST
+using directives defined by the domain.
+
+Or even shorter:
+
+``:py:`` is to ``:qapi:`` as *autodoc* is to *qapidoc*.
+
+
+Info Field Lists
+================
+
+`Field lists
+<https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#field-lists>`_
+are a standard syntax in reStructuredText. Sphinx `extends that syntax
+<https://www.sphinx-doc.org/en/master/usage/domains/python.html#info-field-lists>`_
+to give certain field list entries special meaning and parsing to, for
+example, add cross-references. The QAPI Domain takes advantage of this
+field list extension to document things like Arguments, Members, Values,
+and so on.
+
+The special parsing and handling of info field lists in Sphinx is provided by
+three main classes; Field, GroupedField, and TypedField. The behavior
+and formatting for each configured field list entry in the domain
+changes depending on which class is used.
+
+Field:
+ * Creates an ungrouped field: i.e., each entry will create its own
+ section and they will not be combined.
+ * May *optionally* support an argument.
+ * May apply cross-reference roles to *either* the argument *or* the
+ content body, both, or neither.
+
+This is used primarily for entries which are not expected to be
+repeated, i.e., items that may only show up at most once. The QAPI
+domain uses this class for "Errors" section.
+
+GroupedField:
+ * Creates a grouped field: i.e. multiple adjacent entries will be
+ merged into one section, and the content will form a bulleted list.
+ * *Must* take an argument.
+ * May optionally apply a cross-reference role to the argument, but not
+ the body.
+ * Can be configured to remove the bulleted list if there is only a
+ single entry.
+ * All items will be generated with the form: "argument -- body"
+
+This is used for entries which are expected to be repeated, but aren't
+expected to have two arguments, i.e. types without names, or names
+without types. The QAPI domain uses this class for features, returns,
+and enum values.
+
+TypedField:
+ * Creates a grouped, typed field. Multiple adjacent entres will be
+ merged into one section, and the content will form a bulleted list.
+ * *Must* take at least one argument, but supports up to two -
+ nominally, a name and a type.
+ * May optionally apply a cross-reference role to the type or the name
+ argument, but not the body.
+ * Can be configured to remove the bulleted list if there is only a
+ single entry.
+ * All items will be generated with the form "name (type) -- body"
+
+This is used for entries that are expected to be repeated and will have
+a name, a type, and a description. The QAPI domain uses this class for
+arguments, alternatives, and members. Wherever type names are referenced
+below, They must be a valid, documented type that will be
+cross-referenced in the HTML output; or one of the built-in JSON types
+(string, number, int, boolean, null, value, q_empty).
+
+
+``:feat:``
+----------
+
+Document a feature attached to a QAPI definition.
+
+:availability: This field list is available in the body of Command,
+ Event, Enum, Object and Alternate directives.
+:syntax: ``:feat name: Lorem ipsum, dolor sit amet...``
+:type: `sphinx.util.docfields.GroupedField
+ <https://pydoc.dev/sphinx/latest/sphinx.util.docfields.GroupedField.html?private=1>`_
+
+Example::
+
+ .. qapi:object:: BlockdevOptionsVirtioBlkVhostVdpa
+ :since: 7.2
+ :ifcond: CONFIG_BLKIO
+
+ Driver specific block device options for the virtio-blk-vhost-vdpa
+ backend.
+
+ :memb string path: path to the vhost-vdpa character device.
+ :feat fdset: Member ``path`` supports the special "/dev/fdset/N" path
+ (since 8.1)
+
+
+``:arg:``
+---------
+
+Document an argument to a QAPI command.
+
+:availability: This field list is only available in the body of the
+ Command directive.
+:syntax: ``:arg type name: description``
+:type: `sphinx.util.docfields.TypedField
+ <https://pydoc.dev/sphinx/latest/sphinx.util.docfields.TypedField.html?private=1>`_
+
+
+Example::
+
+ .. qapi:command:: job-pause
+ :since: 3.0
+
+ Pause an active job.
+
+ This command returns immediately after marking the active job for
+ pausing. Pausing an already paused job is an error.
+
+ The job will pause as soon as possible, which means transitioning
+ into the PAUSED state if it was RUNNING, or into STANDBY if it was
+ READY. The corresponding JOB_STATUS_CHANGE event will be emitted.
+
+ Cancelling a paused job automatically resumes it.
+
+ :arg string id: The job identifier.
+
+
+``:error:``
+-----------
+
+Document the error condition(s) of a QAPI command.
+
+:availability: This field list is only available in the body of the
+ Command directive.
+:syntax: ``:error: Lorem ipsum dolor sit amet ...``
+:type: `sphinx.util.docfields.Field
+ <https://pydoc.dev/sphinx/latest/sphinx.util.docfields.Field.html?private=1>`_
+
+The format of the :errors: field list description is free-form rST. The
+alternative spelling ":errors:" is also permitted, but strictly
+analogous.
+
+Example::
+
+ .. qapi:command:: block-job-set-speed
+ :since: 1.1
+
+ Set maximum speed for a background block operation.
+
+ This command can only be issued when there is an active block job.
+
+ Throttling can be disabled by setting the speed to 0.
+
+ :arg string device: The job identifier. This used to be a device
+ name (hence the name of the parameter), but since QEMU 2.7 it
+ can have other values.
+ :arg int speed: the maximum speed, in bytes per second, or 0 for
+ unlimited. Defaults to 0.
+ :error:
+ - If no background operation is active on this device,
+ DeviceNotActive
+
+
+``:return:``
+-------------
+
+Document the return type(s) and value(s) of a QAPI command.
+
+:availability: This field list is only available in the body of the
+ Command directive.
+:syntax: ``:return type: Lorem ipsum dolor sit amet ...``
+:type: `sphinx.util.docfields.GroupedField
+ <https://pydoc.dev/sphinx/latest/sphinx.util.docfields.GroupedField.html?private=1>`_
+
+
+Example::
+
+ .. qapi:command:: query-replay
+ :since: 5.2
+
+ Retrieve the record/replay information. It includes current
+ instruction count which may be used for ``replay-break`` and
+ ``replay-seek`` commands.
+
+ :return ReplayInfo: record/replay information.
+
+ .. qmp-example::
+
+ -> { "execute": "query-replay" }
+ <- { "return": {
+ "mode": "play", "filename": "log.rr", "icount": 220414 }
+ }
+
+
+``:value:``
+-----------
+
+Document a possible value for a QAPI enum.
+
+:availability: This field list is only available in the body of the Enum
+ directive.
+:syntax: ``:value name: Lorem ipsum, dolor sit amet ...``
+:type: `sphinx.util.docfields.GroupedField
+ <https://pydoc.dev/sphinx/latest/sphinx.util.docfields.GroupedField.html?private=1>`_
+
+Example::
+
+ .. qapi:enum:: QapiErrorClass
+ :since: 1.2
+
+ QEMU error classes
+
+ :value GenericError: this is used for errors that don't require a specific
+ error class. This should be the default case for most errors
+ :value CommandNotFound: the requested command has not been found
+ :value DeviceNotActive: a device has failed to be become active
+ :value DeviceNotFound: the requested device has not been found
+ :value KVMMissingCap: the requested operation can't be fulfilled because a
+ required KVM capability is missing
+
+
+``:alt:``
+------------
+
+Document a possible branch for a QAPI alternate.
+
+:availability: This field list is only available in the body of the
+ Alternate directive.
+:syntax: ``:alt type name: Lorem ipsum, dolor sit amet ...``
+:type: `sphinx.util.docfields.TypedField
+ <https://pydoc.dev/sphinx/latest/sphinx.util.docfields.TypedField.html?private=1>`_
+
+As a limitation of Sphinx, we must document the "name" of the branch in
+addition to the type, even though this information is not visible on the
+wire in the QMP protocol format. This limitation *may* be lifted at a
+future date.
+
+Example::
+
+ .. qapi:alternate:: StrOrNull
+ :since: 2.10
+
+ This is a string value or the explicit lack of a string (null
+ pointer in C). Intended for cases when 'optional absent' already
+ has a different meaning.
+
+ :alt string s: the string value
+ :alt null n: no string value
+
+
+``:memb:``
+----------
+
+Document a member of an Event or Object.
+
+:availability: This field list is available in the body of Event or
+ Object directives.
+:syntax: ``:memb type name: Lorem ipsum, dolor sit amet ...``
+:type: `sphinx.util.docfields.TypedField
+ <https://pydoc.dev/sphinx/latest/sphinx.util.docfields.TypedField.html?private=1>`_
+
+This is fundamentally the same as ``:arg:`` and ``:alt:``, but uses the
+"Members" phrasing for Events and Objects (Structs and Unions).
+
+Example::
+
+ .. qapi:event:: JOB_STATUS_CHANGE
+ :since: 3.0
+
+ Emitted when a job transitions to a different status.
+
+ :memb string id: The job identifier
+ :memb JobStatus status: The new job status
+
+
+Arbitrary field lists
+---------------------
+
+Other field list names, while valid rST syntax, are prohibited inside of
+QAPI directives to help prevent accidental misspellings of info field
+list names. If you want to add a new arbitrary "non-value-added" field
+list to QAPI documentation, you must add the field name to the allow
+list in ``docs/conf.py``
+
+For example::
+
+ qapi_allowed_fields = {
+ "see also",
+ }
+
+Will allow you to add arbitrary field lists in QAPI directives::
+
+ .. qapi:command:: x-fake-command
+
+ :see also: Lorem ipsum, dolor sit amet ...
+
+
+Cross-references
+================
+
+Cross-reference `roles
+<https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html>`_
+in the QAPI domain are modeled closely after the `Python
+cross-referencing syntax
+<https://www.sphinx-doc.org/en/master/usage/domains/python.html#cross-referencing-python-objects>`_.
+
+QAPI definitions can be referenced using the standard `any
+<https://www.sphinx-doc.org/en/master/usage/referencing.html#role-any>`_
+role cross-reference syntax, such as with ```query-blockstats```. In
+the event that disambiguation is needed, cross-references can also be
+written using a number of explicit cross-reference roles:
+
+* ``:qapi:mod:`block-core``` -- Reference a QAPI module. The link will
+ take you to the beginning of that section in the documentation.
+* ``:qapi:cmd:`query-block``` -- Reference a QAPI command.
+* ``:qapi:event:`JOB_STATUS_CHANGE``` -- Reference a QAPI event.
+* ``:qapi:enum:`QapiErrorClass``` -- Reference a QAPI enum.
+* ``:qapi:obj:`BlockdevOptionsVirtioBlkVhostVdpa`` -- Reference a QAPI
+ object (struct or union)
+* ``:qapi:alt:`StrOrNull``` -- Reference a QAPI alternate.
+* ``:qapi:type:`BlockDirtyInfo``` -- Reference *any* QAPI type; this
+ excludes modules, commands, and events.
+* ``:qapi:any:`block-job-set-speed``` -- Reference absolutely any QAPI entity.
+
+Type arguments in info field lists are converted into references as if
+you had used the ``:qapi:type:`` role. All of the special syntax below
+applies to both info field lists and standalone explicit
+cross-references.
+
+
+Type decorations
+----------------
+
+Type names in references can be surrounded by brackets, like
+``[typename]``, to indicate an array of that type. The cross-reference
+will apply only to the type name between the brackets. For example;
+``:qapi:type:`[Qcow2BitmapInfoFlags]``` renders to:
+:qapi:type:`[Qcow2BitmapInfoFlags]`
+
+To indicate an optional argument/member in a field list, the type name
+can be suffixed with ``?``. The cross-reference will be transformed to
+"type, Optional" with the link applying only to the type name. For
+example; ``:qapi:type:`BitmapSyncMode?``` renders to:
+:qapi:type:`BitmapSyncMode?`
+
+
+Namespaces
+----------
+
+Mimicking the `Python domain target specification syntax
+<https://www.sphinx-doc.org/en/master/usage/domains/python.html#target-specification>`_,
+QAPI allows you to specify the fully qualified path for a data
+type. QAPI enforces globally unique names, so it's unlikely you'll need
+this specific feature, but it may be extended in the near future to
+allow referencing identically named commands and data types from
+different utilities; i.e. QEMU Storage Daemon vs QMP.
+
+* A module can be explicitly provided;
+ ``:qapi:type:`block-core.BitmapSyncMode``` will render to:
+ :qapi:type:`block-core.BitmapSyncMode`
+* If you don't want to display the "fully qualified" name, it can be
+ prefixed with a tilde; ``:qapi:type:`~block-core.BitmapSyncMode```
+ will render to: :qapi:type:`~block-core.BitmapSyncMode`
+
+
+Custom link text
+----------------
+
+The name of a cross-reference link can be explicitly overridden like
+`most stock Sphinx references
+<https://www.sphinx-doc.org/en/master/usage/referencing.html#syntax>`_
+using the ``custom text <target>`` syntax.
+
+For example, ``:qapi:cmd:`Merge dirty bitmaps
+<block-dirty-bitmap-merge>``` will render as: :qapi:cmd:`Merge dirty
+bitmaps <block-dirty-bitmap-merge>`
+
+
+Directives
+==========
+
+The QAPI domain adds a number of custom directives for documenting
+various QAPI/QMP entities. The syntax is plain rST, and follows this
+general format::
+
+ .. qapi:directive:: argument
+ :option:
+ :another-option: with an argument
+
+ Content body, arbitrary rST is allowed here.
+
+
+Sphinx standard options
+-----------------------
+
+All QAPI directives inherit a number of `standard options
+<https://www.sphinx-doc.org/en/master/usage/domains/index.html#basic-markup>`_
+from Sphinx's ObjectDescription class.
+
+The dashed spellings of the below options were added in Sphinx 7.2, the
+undashed spellings are currently retained as aliases, but will be
+removed in a future version.
+
+* ``:no-index:`` and ``:noindex:`` -- Do not add this item into the
+ Index, and do not make it available for cross-referencing.
+* ``no-index-entry:`` and ``:noindexentry:`` -- Do not add this item
+ into the Index, but allow it to be cross-referenced.
+* ``no-contents-entry`` and ``:nocontentsentry:`` -- Exclude this item
+ from the Table of Contents.
+* ``no-typesetting`` -- Create TOC, Index and cross-referencing
+ entities, but don't actually display the content.
+
+
+QAPI standard options
+---------------------
+
+All QAPI directives -- *except* for module -- support these common options.
+
+* ``:module: modname`` -- Borrowed from the Python domain, this option allows
+ you to override the module association of a given definition.
+* ``:since: x.y`` -- Allows the documenting of "Since" information, which is
+ displayed in the signature bar.
+* ``:ifcond: CONDITION`` -- Allows the documenting of conditional availability
+ information, which is displayed in an eyecatch just below the
+ signature bar.
+* ``:deprecated:`` -- Adds an eyecatch just below the signature bar that
+ advertises that this definition is deprecated and should be avoided.
+* ``:unstable:`` -- Adds an eyecatch just below the signature bar that
+ advertises that this definition is unstable and should not be used in
+ production code.
+
+
+qapi:module
+-----------
+
+The ``qapi:module`` directive marks the start of a QAPI module. It may have
+a content body, but it can be omitted. All subsequent QAPI directives
+are associated with the most recent module; this effects their "fully
+qualified" name, but has no other effect.
+
+Example::
+
+ .. qapi:module:: block-core
+
+ Welcome to the block-core module!
+
+Will be rendered as:
+
+.. qapi:module:: block-core
+ :noindex:
+
+ Welcome to the block-core module!
+
+
+qapi:command
+------------
+
+This directive documents a QMP command. It may use any of the standard
+Sphinx or QAPI options, and the documentation body may contain
+``:arg:``, ``:feat:``, ``:error:``, or ``:return:`` info field list
+entries.
+
+Example::
+
+ .. qapi:command:: x-fake-command
+ :since: 42.0
+ :unstable:
+
+ This command is fake, so it can't hurt you!
+
+ :arg int foo: Your favorite number.
+ :arg string? bar: Your favorite season.
+ :return [string]: A lovely computer-written poem for you.
+
+
+Will be rendered as:
+
+ .. qapi:command:: x-fake-command
+ :noindex:
+ :since: 42.0
+ :unstable:
+
+ This command is fake, so it can't hurt you!
+
+ :arg int foo: Your favorite number.
+ :arg string? bar: Your favorite season.
+ :return [string]: A lovely computer-written poem for you.
+
+
+qapi:event
+----------
+
+This directive documents a QMP event. It may use any of the standard
+Sphinx or QAPI options, and the documentation body may contain
+``:memb:`` or ``:feat:`` info field list entries.
+
+Example::
+
+ .. qapi:event:: COMPUTER_IS_RUINED
+ :since: 0.1
+ :deprecated:
+
+ This event is emitted when your computer is *extremely* ruined.
+
+ :memb string reason: Diagnostics as to what caused your computer to
+ be ruined.
+ :feat sadness: When present, the diagnostic message will also
+ explain how sad the computer is as a result of your wrongdoings.
+
+Will be rendered as:
+
+.. qapi:event:: COMPUTER_IS_RUINED
+ :noindex:
+ :since: 0.1
+ :deprecated:
+
+ This event is emitted when your computer is *extremely* ruined.
+
+ :memb string reason: Diagnostics as to what caused your computer to
+ be ruined.
+ :feat sadness: When present, the diagnostic message will also explain
+ how sad the computer is as a result of your wrongdoings.
+
+
+qapi:enum
+---------
+
+This directive documents a QAPI enum. It may use any of the standard
+Sphinx or QAPI options, and the documentation body may contain
+``:value:`` or ``:feat:`` info field list entries.
+
+Example::
+
+ .. qapi:enum:: Mood
+ :ifcond: LIB_PERSONALITY
+
+ This enum represents your virtual machine's current mood!
+
+ :value Happy: Your VM is content and well-fed.
+ :value Hungry: Your VM needs food.
+ :value Melancholic: Your VM is experiencing existential angst.
+ :value Petulant: Your VM is throwing a temper tantrum.
+
+Will be rendered as:
+
+.. qapi:enum:: Mood
+ :noindex:
+ :ifcond: LIB_PERSONALITY
+
+ This enum represents your virtual machine's current mood!
+
+ :value Happy: Your VM is content and well-fed.
+ :value Hungry: Your VM needs food.
+ :value Melancholic: Your VM is experiencing existential angst.
+ :value Petulant: Your VM is throwing a temper tantrum.
+
+
+qapi:object
+-----------
+
+This directive documents a QAPI structure or union and represents a QMP
+object. It may use any of the standard Sphinx or QAPI options, and the
+documentation body may contain ``:memb:`` or ``:feat:`` info field list
+entries.
+
+Example::
+
+ .. qapi:object:: BigBlobOfStuff
+
+ This object has a bunch of disparate and unrelated things in it.
+
+ :memb int Birthday: Your birthday, represented in seconds since the
+ UNIX epoch.
+ :memb [string] Fav-Foods: A list of your favorite foods.
+ :memb boolean? Bizarre-Docs: True if the documentation reference
+ should be strange.
+
+Will be rendered as:
+
+.. qapi:object:: BigBlobOfStuff
+ :noindex:
+
+ This object has a bunch of disparate and unrelated things in it.
+
+ :memb int Birthday: Your birthday, represented in seconds since the
+ UNIX epoch.
+ :memb [string] Fav-Foods: A list of your favorite foods.
+ :memb boolean? Bizarre-Docs: True if the documentation reference
+ should be strange.
+
+
+qapi:alternate
+--------------
+
+This directive documents a QAPI alternate. It may use any of the
+standard Sphinx or QAPI options, and the documentation body may contain
+``:alt:`` or ``:feat:`` info field list entries.
+
+Example::
+
+ .. qapi:alternate:: ErrorCode
+
+ This alternate represents an Error Code from the VM.
+
+ :alt int ec: An error code, like the type you're used to.
+ :alt string em: An expletive-laced error message, if your
+ computer is feeling particularly cranky and tired of your
+ antics.
+
+Will be rendered as:
+
+.. qapi:alternate:: ErrorCode
+ :noindex:
+
+ This alternate represents an Error Code from the VM.
+
+ :alt int ec: An error code, like the type you're used to.
+ :alt string em: An expletive-laced error message, if your
+ computer is feeling particularly cranky and tired of your
+ antics.
diff --git a/docs/glossary.rst b/docs/glossary.rst
index 693d985..4fa044b 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -120,7 +120,7 @@ Migration
---------
QEMU can save and restore the execution of a virtual machine between different
-host systems. This is provided by the `Migration framework<migration>`.
+host systems. This is provided by the :ref:`Migration framework<migration>`.
NBD
---
@@ -212,14 +212,14 @@ machine emulator and virtualizer.
QOM
---
-`QEMU Object Model <qom>` is an object oriented API used to define various
-devices and hardware in the QEMU codebase.
+:ref:`QEMU Object Model <qom>` is an object oriented API used to define
+various devices and hardware in the QEMU codebase.
Record/replay
-------------
-`Record/replay <replay>` is a feature of QEMU allowing to have a deterministic
-and reproducible execution of a virtual machine.
+:ref:`Record/replay <replay>` is a feature of QEMU allowing to have a
+deterministic and reproducible execution of a virtual machine.
Rust
----
diff --git a/docs/interop/qemu-qmp-ref.rst b/docs/interop/qemu-qmp-ref.rst
index f94614a..e95eeac 100644
--- a/docs/interop/qemu-qmp-ref.rst
+++ b/docs/interop/qemu-qmp-ref.rst
@@ -7,3 +7,4 @@ QEMU QMP Reference Manual
:depth: 3
.. qapi-doc:: qapi/qapi-schema.json
+ :transmogrify:
diff --git a/docs/sphinx-static/theme_overrides.css b/docs/sphinx-static/theme_overrides.css
index 965ecac..b225bf7 100644
--- a/docs/sphinx-static/theme_overrides.css
+++ b/docs/sphinx-static/theme_overrides.css
@@ -18,8 +18,8 @@ h1, h2, .rst-content .toctree-wrapper p.caption, h3, h4, h5, h6, legend {
.rst-content dl:not(.docutils) dt {
border-top: none;
- border-left: solid 3px #ccc;
- background-color: #f0f0f0;
+ border-left: solid 5px #bcc6d2;
+ background-color: #eaedf1;
color: black;
}
@@ -208,3 +208,97 @@ div[class^="highlight"] pre {
color: inherit;
}
}
+
+/* QAPI domain theming */
+
+/* most content in a QAPI object definition should not eclipse about
+ 80ch, but nested field lists are explicitly exempt due to their
+ two-column nature */
+.qapi dd *:not(dl) {
+ max-width: 80ch;
+}
+
+/* but the content column itself should still be less than ~80ch. */
+.qapi .field-list dd {
+ max-width: 80ch;
+}
+
+.qapi-infopips {
+ margin-bottom: 1em;
+}
+
+.qapi-infopip {
+ display: inline-block;
+ padding: 0em 0.5em 0em 0.5em;
+ margin: 0.25em;
+}
+
+.qapi-deprecated,.qapi-unstable {
+ background-color: #fffef5;
+ border: solid #fff176 6px;
+ font-weight: bold;
+ padding: 8px;
+ border-radius: 15px;
+ margin: 5px;
+}
+
+.qapi-unstable::before {
+ content: '🚧 ';
+}
+
+.qapi-deprecated::before {
+ content: '⚠️ ';
+}
+
+.qapi-ifcond::before {
+ /* gaze ye into the crystal ball to determine feature availability */
+ content: '🔮 ';
+}
+
+.qapi-ifcond {
+ background-color: #f9f5ff;
+ border: solid #dac2ff 6px;
+ padding: 8px;
+ border-radius: 15px;
+ margin: 5px;
+}
+
+/* code blocks */
+.qapi div[class^="highlight"] {
+ width: fit-content;
+ background-color: #fffafd;
+ border: 2px solid #ffe1f3;
+}
+
+/* note, warning, etc. */
+.qapi .admonition {
+ width: fit-content;
+}
+
+/* pad the top of the field-list so the text doesn't start directly at
+ the top border; primarily for the field list labels, but adjust the
+ field bodies as well for parity. */
+dl.field-list > dt:first-of-type, dl.field-list > dd:first-of-type {
+ padding-top: 0.3em;
+}
+
+dl.field-list > dt:last-of-type, dl.field-list > dd:last-of-type {
+ padding-bottom: 0.3em;
+}
+
+/* pad the field list labels so they don't crash into the border */
+dl.field-list > dt {
+ padding-left: 0.5em;
+ padding-right: 0.5em;
+}
+
+/* Add a little padding between field list sections */
+dl.field-list > dd:not(:last-child) {
+ padding-bottom: 1em;
+}
+
+/* Sphinx 3.x: unresolved xrefs */
+.rst-content *:not(a) > code.xref {
+ font-weight: 400;
+ color: #333333;
+}
diff --git a/docs/sphinx/compat.py b/docs/sphinx/compat.py
new file mode 100644
index 0000000..9cf7fe0
--- /dev/null
+++ b/docs/sphinx/compat.py
@@ -0,0 +1,230 @@
+"""
+Sphinx cross-version compatibility goop
+"""
+
+import re
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Callable,
+ Optional,
+ Type,
+)
+
+from docutils import nodes
+from docutils.nodes import Element, Node, Text
+from docutils.statemachine import StringList
+
+import sphinx
+from sphinx import addnodes, util
+from sphinx.directives import ObjectDescription
+from sphinx.environment import BuildEnvironment
+from sphinx.roles import XRefRole
+from sphinx.util import docfields
+from sphinx.util.docutils import (
+ ReferenceRole,
+ SphinxDirective,
+ switch_source_input,
+)
+from sphinx.util.typing import TextlikeNode
+
+
+MAKE_XREF_WORKAROUND = sphinx.version_info[:3] < (4, 1, 0)
+
+
+SpaceNode: Callable[[str], Node]
+KeywordNode: Callable[[str, str], Node]
+
+if sphinx.version_info[:3] >= (4, 0, 0):
+ SpaceNode = addnodes.desc_sig_space
+ KeywordNode = addnodes.desc_sig_keyword
+else:
+ SpaceNode = Text
+ KeywordNode = addnodes.desc_annotation
+
+
+def nested_parse_with_titles(
+ directive: SphinxDirective, content_node: Element
+) -> None:
+ """
+ This helper preserves error parsing context across sphinx versions.
+ """
+
+ # necessary so that the child nodes get the right source/line set
+ content_node.document = directive.state.document
+
+ try:
+ # Modern sphinx (6.2.0+) supports proper offsetting for
+ # nested parse error context management
+ util.nodes.nested_parse_with_titles(
+ directive.state,
+ directive.content,
+ content_node,
+ content_offset=directive.content_offset,
+ )
+ except TypeError:
+ # No content_offset argument. Fall back to SSI method.
+ with switch_source_input(directive.state, directive.content):
+ util.nodes.nested_parse_with_titles(
+ directive.state, directive.content, content_node
+ )
+
+
+# ###########################################
+# xref compatibility hacks for Sphinx < 4.1 #
+# ###########################################
+
+# When we require >= Sphinx 4.1, the following function and the
+# subsequent 3 compatibility classes can be removed. Anywhere in
+# qapi_domain that uses one of these Compat* types can be switched to
+# using the garden-variety lib-provided classes with no trickery.
+
+
+def _compat_make_xref( # pylint: disable=unused-argument
+ self: sphinx.util.docfields.Field,
+ rolename: str,
+ domain: str,
+ target: str,
+ innernode: Type[TextlikeNode] = addnodes.literal_emphasis,
+ contnode: Optional[Node] = None,
+ env: Optional[BuildEnvironment] = None,
+ inliner: Any = None,
+ location: Any = None,
+) -> Node:
+ """
+ Compatibility workaround for Sphinx versions prior to 4.1.0.
+
+ Older sphinx versions do not use the domain's XRefRole for parsing
+ and formatting cross-references, so we need to perform this magick
+ ourselves to avoid needing to write the parser/formatter in two
+ separate places.
+
+ This workaround isn't brick-for-brick compatible with modern Sphinx
+ versions, because we do not have access to the parent directive's
+ state during this parsing like we do in more modern versions.
+
+ It's no worse than what pre-Sphinx 4.1.0 does, so... oh well!
+ """
+
+ # Yes, this function is gross. Pre-4.1 support is a miracle.
+ # pylint: disable=too-many-locals
+
+ assert env
+ # Note: Sphinx's own code ignores the type warning here, too.
+ if not rolename:
+ return contnode or innernode(target, target) # type: ignore[call-arg]
+
+ # Get the role instance, but don't *execute it* - we lack the
+ # correct state to do so. Instead, we'll just use its public
+ # methods to do our reference formatting, and emulate the rest.
+ role = env.get_domain(domain).roles[rolename]
+ assert isinstance(role, XRefRole)
+
+ # XRefRole features not supported by this compatibility shim;
+ # these were not supported in Sphinx 3.x either, so nothing of
+ # value is really lost.
+ assert not target.startswith("!")
+ assert not re.match(ReferenceRole.explicit_title_re, target)
+ assert not role.lowercase
+ assert not role.fix_parens
+
+ # Code below based mostly on sphinx.roles.XRefRole; run() and
+ # create_xref_node()
+ options = {
+ "refdoc": env.docname,
+ "refdomain": domain,
+ "reftype": rolename,
+ "refexplicit": False,
+ "refwarn": role.warn_dangling,
+ }
+ refnode = role.nodeclass(target, **options)
+ title, target = role.process_link(env, refnode, False, target, target)
+ refnode["reftarget"] = target
+ classes = ["xref", domain, f"{domain}-{rolename}"]
+ refnode += role.innernodeclass(target, title, classes=classes)
+
+ # This is the very gross part of the hack. Normally,
+ # result_nodes takes a document object to which we would pass
+ # self.inliner.document. Prior to Sphinx 4.1, we don't *have* an
+ # inliner to pass, so we have nothing to pass here. However, the
+ # actual implementation of role.result_nodes in this case
+ # doesn't actually use that argument, so this winds up being
+ # ... fine. Rest easy at night knowing this code only runs under
+ # old versions of Sphinx, so at least it won't change in the
+ # future on us and lead to surprising new failures.
+ # Gross, I know.
+ result_nodes, _messages = role.result_nodes(
+ None, # type: ignore
+ env,
+ refnode,
+ is_ref=True,
+ )
+ return nodes.inline(target, "", *result_nodes)
+
+
+class CompatField(docfields.Field):
+ if MAKE_XREF_WORKAROUND:
+ make_xref = _compat_make_xref
+
+
+class CompatGroupedField(docfields.GroupedField):
+ if MAKE_XREF_WORKAROUND:
+ make_xref = _compat_make_xref
+
+
+class CompatTypedField(docfields.TypedField):
+ if MAKE_XREF_WORKAROUND:
+ make_xref = _compat_make_xref
+
+
+# ################################################################
+# Nested parsing error location fix for Sphinx 5.3.0 < x < 6.2.0 #
+# ################################################################
+
+# When we require Sphinx 4.x, the TYPE_CHECKING hack where we avoid
+# subscripting ObjectDescription at runtime can be removed in favor of
+# just always subscripting the class.
+
+# When we require Sphinx > 6.2.0, the rest of this compatibility hack
+# can be dropped and QAPIObject can just inherit directly from
+# ObjectDescription[Signature].
+
+SOURCE_LOCATION_FIX = (5, 3, 0) <= sphinx.version_info[:3] < (6, 2, 0)
+
+Signature = str
+
+
+if TYPE_CHECKING:
+ _BaseClass = ObjectDescription[Signature]
+else:
+ _BaseClass = ObjectDescription
+
+
+class ParserFix(_BaseClass):
+
+ _temp_content: StringList
+ _temp_offset: int
+ _temp_node: Optional[addnodes.desc_content]
+
+ def before_content(self) -> None:
+ # Work around a sphinx bug and parse the content ourselves.
+ self._temp_content = self.content
+ self._temp_offset = self.content_offset
+ self._temp_node = None
+
+ if SOURCE_LOCATION_FIX:
+ self._temp_node = addnodes.desc_content()
+ self.state.nested_parse(
+ self.content, self.content_offset, self._temp_node
+ )
+ # Sphinx will try to parse the content block itself,
+ # Give it nothingness to parse instead.
+ self.content = StringList()
+ self.content_offset = 0
+
+ def transform_content(self, content_node: addnodes.desc_content) -> None:
+ # Sphinx workaround: Inject our parsed content and restore state.
+ if self._temp_node:
+ content_node += self._temp_node.children
+ self.content = self._temp_content
+ self.content_offset = self._temp_offset
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
new file mode 100644
index 0000000..7ff618d
--- /dev/null
+++ b/docs/sphinx/qapi_domain.py
@@ -0,0 +1,931 @@
+"""
+QAPI domain extension.
+"""
+
+# The best laid plans of mice and men, ...
+# pylint: disable=too-many-lines
+
+from __future__ import annotations
+
+from typing import (
+ TYPE_CHECKING,
+ AbstractSet,
+ Any,
+ Dict,
+ Iterable,
+ List,
+ NamedTuple,
+ Optional,
+ Tuple,
+ Union,
+ cast,
+)
+
+from docutils import nodes
+from docutils.parsers.rst import directives
+
+from compat import (
+ CompatField,
+ CompatGroupedField,
+ CompatTypedField,
+ KeywordNode,
+ ParserFix,
+ Signature,
+ SpaceNode,
+)
+from sphinx import addnodes
+from sphinx.addnodes import desc_signature, pending_xref
+from sphinx.directives import ObjectDescription
+from sphinx.domains import (
+ Domain,
+ Index,
+ IndexEntry,
+ ObjType,
+)
+from sphinx.locale import _, __
+from sphinx.roles import XRefRole
+from sphinx.util import logging
+from sphinx.util.nodes import make_id, make_refnode
+
+
+if TYPE_CHECKING:
+ from docutils.nodes import Element, Node
+
+ from sphinx.application import Sphinx
+ from sphinx.builders import Builder
+ from sphinx.environment import BuildEnvironment
+ from sphinx.util.typing import OptionSpec
+
+logger = logging.getLogger(__name__)
+
+
+def _unpack_field(
+ field: nodes.Node,
+) -> Tuple[nodes.field_name, nodes.field_body]:
+ """
+ docutils helper: unpack a field node in a type-safe manner.
+ """
+ assert isinstance(field, nodes.field)
+ assert len(field.children) == 2
+ assert isinstance(field.children[0], nodes.field_name)
+ assert isinstance(field.children[1], nodes.field_body)
+ return (field.children[0], field.children[1])
+
+
+class ObjectEntry(NamedTuple):
+ docname: str
+ node_id: str
+ objtype: str
+ aliased: bool
+
+
+class QAPIXRefRole(XRefRole):
+
+ def process_link(
+ self,
+ env: BuildEnvironment,
+ refnode: Element,
+ has_explicit_title: bool,
+ title: str,
+ target: str,
+ ) -> tuple[str, str]:
+ refnode["qapi:module"] = env.ref_context.get("qapi:module")
+
+ # Cross-references that begin with a tilde adjust the title to
+ # only show the reference without a leading module, even if one
+ # was provided. This is a Sphinx-standard syntax; give it
+ # priority over QAPI-specific type markup below.
+ hide_module = False
+ if target.startswith("~"):
+ hide_module = True
+ target = target[1:]
+
+ # Type names that end with "?" are considered optional
+ # arguments and should be documented as such, but it's not
+ # part of the xref itself.
+ if target.endswith("?"):
+ refnode["qapi:optional"] = True
+ target = target[:-1]
+
+ # Type names wrapped in brackets denote lists. strip the
+ # brackets and remember to add them back later.
+ if target.startswith("[") and target.endswith("]"):
+ refnode["qapi:array"] = True
+ target = target[1:-1]
+
+ if has_explicit_title:
+ # Don't mess with the title at all if it was explicitly set.
+ # Explicit title syntax for references is e.g.
+ # :qapi:type:`target <explicit title>`
+ # and this explicit title overrides everything else here.
+ return title, target
+
+ title = target
+ if hide_module:
+ title = target.split(".")[-1]
+
+ return title, target
+
+ def result_nodes(
+ self,
+ document: nodes.document,
+ env: BuildEnvironment,
+ node: Element,
+ is_ref: bool,
+ ) -> Tuple[List[nodes.Node], List[nodes.system_message]]:
+
+ # node here is the pending_xref node (or whatever nodeclass was
+ # configured at XRefRole class instantiation time).
+ results: List[nodes.Node] = [node]
+
+ if node.get("qapi:array"):
+ results.insert(0, nodes.literal("[", "["))
+ results.append(nodes.literal("]", "]"))
+
+ if node.get("qapi:optional"):
+ results.append(nodes.Text(", "))
+ results.append(nodes.emphasis("?", "optional"))
+
+ return results, []
+
+
+class QAPIDescription(ParserFix):
+ """
+ Generic QAPI description.
+
+ This is meant to be an abstract class, not instantiated
+ directly. This class handles the abstract details of indexing, the
+ TOC, and reference targets for QAPI descriptions.
+ """
+
+ def handle_signature(self, sig: str, signode: desc_signature) -> Signature:
+ # Do nothing. The return value here is the "name" of the entity
+ # being documented; for QAPI, this is the same as the
+ # "signature", which is just a name.
+
+ # Normally this method must also populate signode with nodes to
+ # render the signature; here we do nothing instead - the
+ # subclasses will handle this.
+ return sig
+
+ def get_index_text(self, name: Signature) -> Tuple[str, str]:
+ """Return the text for the index entry of the object."""
+
+ # NB: this is used for the global index, not the QAPI index.
+ return ("single", f"{name} (QMP {self.objtype})")
+
+ def add_target_and_index(
+ self, name: Signature, sig: str, signode: desc_signature
+ ) -> None:
+ # name is the return value of handle_signature.
+ # sig is the original, raw text argument to handle_signature.
+ # For QAPI, these are identical, currently.
+
+ assert self.objtype
+
+ # If we're documenting a module, don't include the module as
+ # part of the FQN.
+ modname = ""
+ if self.objtype != "module":
+ modname = self.options.get(
+ "module", self.env.ref_context.get("qapi:module")
+ )
+ fullname = (modname + "." if modname else "") + name
+
+ node_id = make_id(
+ self.env, self.state.document, self.objtype, fullname
+ )
+ signode["ids"].append(node_id)
+
+ self.state.document.note_explicit_target(signode)
+ domain = cast(QAPIDomain, self.env.get_domain("qapi"))
+ domain.note_object(fullname, self.objtype, node_id, location=signode)
+
+ if "no-index-entry" not in self.options:
+ arity, indextext = self.get_index_text(name)
+ assert self.indexnode is not None
+ if indextext:
+ self.indexnode["entries"].append(
+ (arity, indextext, node_id, "", None)
+ )
+
+ def _object_hierarchy_parts(
+ self, sig_node: desc_signature
+ ) -> Tuple[str, ...]:
+ if "fullname" not in sig_node:
+ return ()
+ modname = sig_node.get("module")
+ fullname = sig_node["fullname"]
+
+ if modname:
+ return (modname, *fullname.split("."))
+
+ return tuple(fullname.split("."))
+
+ def _toc_entry_name(self, sig_node: desc_signature) -> str:
+ # This controls the name in the TOC and on the sidebar.
+
+ # This is the return type of _object_hierarchy_parts().
+ toc_parts = cast(Tuple[str, ...], sig_node.get("_toc_parts", ()))
+ if not toc_parts:
+ return ""
+
+ config = self.env.app.config
+ *parents, name = toc_parts
+ if config.toc_object_entries_show_parents == "domain":
+ return sig_node.get("fullname", name)
+ if config.toc_object_entries_show_parents == "hide":
+ return name
+ if config.toc_object_entries_show_parents == "all":
+ return ".".join(parents + [name])
+ return ""
+
+
+class QAPIObject(QAPIDescription):
+ """
+ Description of a generic QAPI object.
+
+ It's not used directly, but is instead subclassed by specific directives.
+ """
+
+ # Inherit some standard options from Sphinx's ObjectDescription
+ option_spec: OptionSpec = ( # type:ignore[misc]
+ ObjectDescription.option_spec.copy()
+ )
+ option_spec.update(
+ {
+ # Borrowed from the Python domain:
+ "module": directives.unchanged, # Override contextual module name
+ # These are QAPI originals:
+ "since": directives.unchanged,
+ "ifcond": directives.unchanged,
+ "deprecated": directives.flag,
+ "unstable": directives.flag,
+ }
+ )
+
+ doc_field_types = [
+ # :feat name: descr
+ CompatGroupedField(
+ "feature",
+ label=_("Features"),
+ names=("feat",),
+ can_collapse=False,
+ ),
+ ]
+
+ def get_signature_prefix(self) -> List[nodes.Node]:
+ """Return a prefix to put before the object name in the signature."""
+ assert self.objtype
+ return [
+ KeywordNode("", self.objtype.title()),
+ SpaceNode(" "),
+ ]
+
+ def get_signature_suffix(self) -> List[nodes.Node]:
+ """Return a suffix to put after the object name in the signature."""
+ ret: List[nodes.Node] = []
+
+ if "since" in self.options:
+ ret += [
+ SpaceNode(" "),
+ addnodes.desc_sig_element(
+ "", f"(Since: {self.options['since']})"
+ ),
+ ]
+
+ return ret
+
+ def handle_signature(self, sig: str, signode: desc_signature) -> Signature:
+ """
+ Transform a QAPI definition name into RST nodes.
+
+ This method was originally intended for handling function
+ signatures. In the QAPI domain, however, we only pass the
+ definition name as the directive argument and handle everything
+ else in the content body with field lists.
+
+ As such, the only argument here is "sig", which is just the QAPI
+ definition name.
+ """
+ modname = self.options.get(
+ "module", self.env.ref_context.get("qapi:module")
+ )
+
+ signode["fullname"] = sig
+ signode["module"] = modname
+ sig_prefix = self.get_signature_prefix()
+ if sig_prefix:
+ signode += addnodes.desc_annotation(
+ str(sig_prefix), "", *sig_prefix
+ )
+ signode += addnodes.desc_name(sig, sig)
+ signode += self.get_signature_suffix()
+
+ return sig
+
+ def _add_infopips(self, contentnode: addnodes.desc_content) -> None:
+ # Add various eye-catches and things that go below the signature
+ # bar, but precede the user-defined content.
+ infopips = nodes.container()
+ infopips.attributes["classes"].append("qapi-infopips")
+
+ def _add_pip(
+ source: str, content: Union[str, List[nodes.Node]], classname: str
+ ) -> None:
+ node = nodes.container(source)
+ if isinstance(content, str):
+ node.append(nodes.Text(content))
+ else:
+ node.extend(content)
+ node.attributes["classes"].extend(["qapi-infopip", classname])
+ infopips.append(node)
+
+ if "deprecated" in self.options:
+ _add_pip(
+ ":deprecated:",
+ f"This {self.objtype} is deprecated.",
+ "qapi-deprecated",
+ )
+
+ if "unstable" in self.options:
+ _add_pip(
+ ":unstable:",
+ f"This {self.objtype} is unstable/experimental.",
+ "qapi-unstable",
+ )
+
+ if self.options.get("ifcond", ""):
+ ifcond = self.options["ifcond"]
+ _add_pip(
+ f":ifcond: {ifcond}",
+ [
+ nodes.emphasis("", "Availability"),
+ nodes.Text(": "),
+ nodes.literal(ifcond, ifcond),
+ ],
+ "qapi-ifcond",
+ )
+
+ if infopips.children:
+ contentnode.insert(0, infopips)
+
+ def _validate_field(self, field: nodes.field) -> None:
+ """Validate field lists in this QAPI Object Description."""
+ name, _ = _unpack_field(field)
+ allowed_fields = set(self.env.app.config.qapi_allowed_fields)
+
+ field_label = name.astext()
+ if field_label in allowed_fields:
+ # Explicitly allowed field list name, OK.
+ return
+
+ try:
+ # split into field type and argument (if provided)
+ # e.g. `:arg type name: descr` is
+ # field_type = "arg", field_arg = "type name".
+ field_type, field_arg = field_label.split(None, 1)
+ except ValueError:
+ # No arguments provided
+ field_type = field_label
+ field_arg = ""
+
+ typemap = self.get_field_type_map()
+ if field_type in typemap:
+ # This is a special docfield, yet-to-be-processed. Catch
+ # correct names, but incorrect arguments. This mismatch WILL
+ # cause Sphinx to render this field incorrectly (without a
+ # warning), which is never what we want.
+ typedesc = typemap[field_type][0]
+ if typedesc.has_arg != bool(field_arg):
+ msg = f"docfield field list type {field_type!r} "
+ if typedesc.has_arg:
+ msg += "requires an argument."
+ else:
+ msg += "takes no arguments."
+ logger.warning(msg, location=field)
+ else:
+ # This is unrecognized entirely. It's valid rST to use
+ # arbitrary fields, but let's ensure the documentation
+ # writer has done this intentionally.
+ valid = ", ".join(sorted(set(typemap) | allowed_fields))
+ msg = (
+ f"Unrecognized field list name {field_label!r}.\n"
+ f"Valid fields for qapi:{self.objtype} are: {valid}\n"
+ "\n"
+ "If this usage is intentional, please add it to "
+ "'qapi_allowed_fields' in docs/conf.py."
+ )
+ logger.warning(msg, location=field)
+
+ def transform_content(self, content_node: addnodes.desc_content) -> None:
+ # This hook runs after before_content and the nested parse, but
+ # before the DocFieldTransformer is executed.
+ super().transform_content(content_node)
+
+ self._add_infopips(content_node)
+
+ # Validate field lists.
+ for child in content_node:
+ if isinstance(child, nodes.field_list):
+ for field in child.children:
+ assert isinstance(field, nodes.field)
+ self._validate_field(field)
+
+
+class SpecialTypedField(CompatTypedField):
+ def make_field(self, *args: Any, **kwargs: Any) -> nodes.field:
+ ret = super().make_field(*args, **kwargs)
+
+ # Look for the characteristic " -- " text node that Sphinx
+ # inserts for each TypedField entry ...
+ for node in ret.traverse(lambda n: str(n) == " -- "):
+ par = node.parent
+ if par.children[0].astext() != "q_dummy":
+ continue
+
+ # If the first node's text is q_dummy, this is a dummy
+ # field we want to strip down to just its contents.
+ del par.children[:-1]
+
+ return ret
+
+
+class QAPICommand(QAPIObject):
+ """Description of a QAPI Command."""
+
+ doc_field_types = QAPIObject.doc_field_types.copy()
+ doc_field_types.extend(
+ [
+ # :arg TypeName ArgName: descr
+ SpecialTypedField(
+ "argument",
+ label=_("Arguments"),
+ names=("arg",),
+ typerolename="type",
+ can_collapse=False,
+ ),
+ # :error: descr
+ CompatField(
+ "error",
+ label=_("Errors"),
+ names=("error", "errors"),
+ has_arg=False,
+ ),
+ # :return TypeName: descr
+ CompatGroupedField(
+ "returnvalue",
+ label=_("Return"),
+ rolename="type",
+ names=("return",),
+ can_collapse=True,
+ ),
+ ]
+ )
+
+
+class QAPIEnum(QAPIObject):
+ """Description of a QAPI Enum."""
+
+ doc_field_types = QAPIObject.doc_field_types.copy()
+ doc_field_types.extend(
+ [
+ # :value name: descr
+ CompatGroupedField(
+ "value",
+ label=_("Values"),
+ names=("value",),
+ can_collapse=False,
+ )
+ ]
+ )
+
+
+class QAPIAlternate(QAPIObject):
+ """Description of a QAPI Alternate."""
+
+ doc_field_types = QAPIObject.doc_field_types.copy()
+ doc_field_types.extend(
+ [
+ # :alt type name: descr
+ CompatTypedField(
+ "alternative",
+ label=_("Alternatives"),
+ names=("alt",),
+ typerolename="type",
+ can_collapse=False,
+ ),
+ ]
+ )
+
+
+class QAPIObjectWithMembers(QAPIObject):
+ """Base class for Events/Structs/Unions"""
+
+ doc_field_types = QAPIObject.doc_field_types.copy()
+ doc_field_types.extend(
+ [
+ # :member type name: descr
+ SpecialTypedField(
+ "member",
+ label=_("Members"),
+ names=("memb",),
+ typerolename="type",
+ can_collapse=False,
+ ),
+ ]
+ )
+
+
+class QAPIEvent(QAPIObjectWithMembers):
+ # pylint: disable=too-many-ancestors
+ """Description of a QAPI Event."""
+
+
+class QAPIJSONObject(QAPIObjectWithMembers):
+ # pylint: disable=too-many-ancestors
+ """Description of a QAPI Object: structs and unions."""
+
+
+class QAPIModule(QAPIDescription):
+ """
+ Directive to mark description of a new module.
+
+ This directive doesn't generate any special formatting, and is just
+ a pass-through for the content body. Named section titles are
+ allowed in the content body.
+
+ Use this directive to create entries for the QAPI module in the
+ global index and the QAPI index; as well as to associate subsequent
+ definitions with the module they are defined in for purposes of
+ search and QAPI index organization.
+
+ :arg: The name of the module.
+ :opt no-index: Don't add cross-reference targets or index entries.
+ :opt no-typesetting: Don't render the content body (but preserve any
+ cross-reference target IDs in the squelched output.)
+
+ Example::
+
+ .. qapi:module:: block-core
+ :no-index:
+ :no-typesetting:
+
+ Lorem ipsum, dolor sit amet ...
+ """
+
+ def run(self) -> List[Node]:
+ modname = self.arguments[0].strip()
+ self.env.ref_context["qapi:module"] = modname
+ ret = super().run()
+
+ # ObjectDescription always creates a visible signature bar. We
+ # want module items to be "invisible", however.
+
+ # Extract the content body of the directive:
+ assert isinstance(ret[-1], addnodes.desc)
+ desc_node = ret.pop(-1)
+ assert isinstance(desc_node.children[1], addnodes.desc_content)
+ ret.extend(desc_node.children[1].children)
+
+ # Re-home node_ids so anchor refs still work:
+ node_ids: List[str]
+ if node_ids := [
+ node_id
+ for el in desc_node.children[0].traverse(nodes.Element)
+ for node_id in cast(List[str], el.get("ids", ()))
+ ]:
+ target_node = nodes.target(ids=node_ids)
+ ret.insert(1, target_node)
+
+ return ret
+
+
+class QAPIIndex(Index):
+ """
+ Index subclass to provide the QAPI definition index.
+ """
+
+ # pylint: disable=too-few-public-methods
+
+ name = "index"
+ localname = _("QAPI Index")
+ shortname = _("QAPI Index")
+
+ def generate(
+ self,
+ docnames: Optional[Iterable[str]] = None,
+ ) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]:
+ assert isinstance(self.domain, QAPIDomain)
+ content: Dict[str, List[IndexEntry]] = {}
+ collapse = False
+
+ # list of all object (name, ObjectEntry) pairs, sorted by name
+ # (ignoring the module)
+ objects = sorted(
+ self.domain.objects.items(),
+ key=lambda x: x[0].split(".")[-1].lower(),
+ )
+
+ for objname, obj in objects:
+ if docnames and obj.docname not in docnames:
+ continue
+
+ # Strip the module name out:
+ objname = objname.split(".")[-1]
+
+ # Add an alphabetical entry:
+ entries = content.setdefault(objname[0].upper(), [])
+ entries.append(
+ IndexEntry(
+ objname, 0, obj.docname, obj.node_id, obj.objtype, "", ""
+ )
+ )
+
+ # Add a categorical entry:
+ category = obj.objtype.title() + "s"
+ entries = content.setdefault(category, [])
+ entries.append(
+ IndexEntry(objname, 0, obj.docname, obj.node_id, "", "", "")
+ )
+
+ # alphabetically sort categories; type names first, ABC entries last.
+ sorted_content = sorted(
+ content.items(),
+ key=lambda x: (len(x[0]) == 1, x[0]),
+ )
+ return sorted_content, collapse
+
+
+class QAPIDomain(Domain):
+ """QAPI language domain."""
+
+ name = "qapi"
+ label = "QAPI"
+
+ # This table associates cross-reference object types (key) with an
+ # ObjType instance, which defines the valid cross-reference roles
+ # for each object type.
+ #
+ # e.g., the :qapi:type: cross-reference role can refer to enum,
+ # struct, union, or alternate objects; but :qapi:obj: can refer to
+ # anything. Each object also gets its own targeted cross-reference role.
+ object_types: Dict[str, ObjType] = {
+ "module": ObjType(_("module"), "mod", "any"),
+ "command": ObjType(_("command"), "cmd", "any"),
+ "event": ObjType(_("event"), "event", "any"),
+ "enum": ObjType(_("enum"), "enum", "type", "any"),
+ "object": ObjType(_("object"), "obj", "type", "any"),
+ "alternate": ObjType(_("alternate"), "alt", "type", "any"),
+ }
+
+ # Each of these provides a rST directive,
+ # e.g. .. qapi:module:: block-core
+ directives = {
+ "module": QAPIModule,
+ "command": QAPICommand,
+ "event": QAPIEvent,
+ "enum": QAPIEnum,
+ "object": QAPIJSONObject,
+ "alternate": QAPIAlternate,
+ }
+
+ # These are all cross-reference roles; e.g.
+ # :qapi:cmd:`query-block`. The keys correlate to the names used in
+ # the object_types table values above.
+ roles = {
+ "mod": QAPIXRefRole(),
+ "cmd": QAPIXRefRole(),
+ "event": QAPIXRefRole(),
+ "enum": QAPIXRefRole(),
+ "obj": QAPIXRefRole(), # specifically structs and unions.
+ "alt": QAPIXRefRole(),
+ # reference any data type (excludes modules, commands, events)
+ "type": QAPIXRefRole(),
+ "any": QAPIXRefRole(), # reference *any* type of QAPI object.
+ }
+
+ # Moved into the data property at runtime;
+ # this is the internal index of reference-able objects.
+ initial_data: Dict[str, Dict[str, Tuple[Any]]] = {
+ "objects": {}, # fullname -> ObjectEntry
+ }
+
+ # Index pages to generate; each entry is an Index class.
+ indices = [
+ QAPIIndex,
+ ]
+
+ @property
+ def objects(self) -> Dict[str, ObjectEntry]:
+ ret = self.data.setdefault("objects", {})
+ return ret # type: ignore[no-any-return]
+
+ def note_object(
+ self,
+ name: str,
+ objtype: str,
+ node_id: str,
+ aliased: bool = False,
+ location: Any = None,
+ ) -> None:
+ """Note a QAPI object for cross reference."""
+ if name in self.objects:
+ other = self.objects[name]
+ if other.aliased and aliased is False:
+ # The original definition found. Override it!
+ pass
+ elif other.aliased is False and aliased:
+ # The original definition is already registered.
+ return
+ else:
+ # duplicated
+ logger.warning(
+ __(
+ "duplicate object description of %s, "
+ "other instance in %s, use :no-index: for one of them"
+ ),
+ name,
+ other.docname,
+ location=location,
+ )
+ self.objects[name] = ObjectEntry(
+ self.env.docname, node_id, objtype, aliased
+ )
+
+ def clear_doc(self, docname: str) -> None:
+ for fullname, obj in list(self.objects.items()):
+ if obj.docname == docname:
+ del self.objects[fullname]
+
+ def merge_domaindata(
+ self, docnames: AbstractSet[str], otherdata: Dict[str, Any]
+ ) -> None:
+ for fullname, obj in otherdata["objects"].items():
+ if obj.docname in docnames:
+ # Sphinx's own python domain doesn't appear to bother to
+ # check for collisions. Assert they don't happen and
+ # we'll fix it if/when the case arises.
+ assert fullname not in self.objects, (
+ "bug - collision on merge?"
+ f" {fullname=} {obj=} {self.objects[fullname]=}"
+ )
+ self.objects[fullname] = obj
+
+ def find_obj(
+ self, modname: str, name: str, typ: Optional[str]
+ ) -> list[tuple[str, ObjectEntry]]:
+ """
+ Find a QAPI object for "name", perhaps using the given module.
+
+ Returns a list of (name, object entry) tuples.
+
+ :param modname: The current module context (if any!)
+ under which we are searching.
+ :param name: The name of the x-ref to resolve;
+ may or may not include a leading module.
+ :param type: The role name of the x-ref we're resolving, if provided.
+ (This is absent for "any" lookups.)
+ """
+ if not name:
+ return []
+
+ names: list[str] = []
+ matches: list[tuple[str, ObjectEntry]] = []
+
+ fullname = name
+ if "." in fullname:
+ # We're searching for a fully qualified reference;
+ # ignore the contextual module.
+ pass
+ elif modname:
+ # We're searching for something from somewhere;
+ # try searching the current module first.
+ # e.g. :qapi:cmd:`query-block` or `query-block` is being searched.
+ fullname = f"{modname}.{name}"
+
+ if typ is None:
+ # type isn't specified, this is a generic xref.
+ # search *all* qapi-specific object types.
+ objtypes: List[str] = list(self.object_types)
+ else:
+ # type is specified and will be a role (e.g. obj, mod, cmd)
+ # convert this to eligible object types (e.g. command, module)
+ # using the QAPIDomain.object_types table.
+ objtypes = self.objtypes_for_role(typ, [])
+
+ if name in self.objects and self.objects[name].objtype in objtypes:
+ names = [name]
+ elif (
+ fullname in self.objects
+ and self.objects[fullname].objtype in objtypes
+ ):
+ names = [fullname]
+ else:
+ # exact match wasn't found; e.g. we are searching for
+ # `query-block` from a different (or no) module.
+ searchname = "." + name
+ names = [
+ oname
+ for oname in self.objects
+ if oname.endswith(searchname)
+ and self.objects[oname].objtype in objtypes
+ ]
+
+ matches = [(oname, self.objects[oname]) for oname in names]
+ if len(matches) > 1:
+ matches = [m for m in matches if not m[1].aliased]
+ return matches
+
+ def resolve_xref(
+ self,
+ env: BuildEnvironment,
+ fromdocname: str,
+ builder: Builder,
+ typ: str,
+ target: str,
+ node: pending_xref,
+ contnode: Element,
+ ) -> nodes.reference | None:
+ modname = node.get("qapi:module")
+ matches = self.find_obj(modname, target, typ)
+
+ if not matches:
+ # Normally, we could pass warn_dangling=True to QAPIXRefRole(),
+ # but that will trigger on references to these built-in types,
+ # which we'd like to ignore instead.
+
+ # Take care of that warning here instead, so long as the
+ # reference isn't to one of our built-in core types.
+ if target not in (
+ "string",
+ "number",
+ "int",
+ "boolean",
+ "null",
+ "value",
+ "q_empty",
+ ):
+ logger.warning(
+ __("qapi:%s reference target not found: %r"),
+ typ,
+ target,
+ type="ref",
+ subtype="qapi",
+ location=node,
+ )
+ return None
+
+ if len(matches) > 1:
+ logger.warning(
+ __("more than one target found for cross-reference %r: %s"),
+ target,
+ ", ".join(match[0] for match in matches),
+ type="ref",
+ subtype="qapi",
+ location=node,
+ )
+
+ name, obj = matches[0]
+ return make_refnode(
+ builder, fromdocname, obj.docname, obj.node_id, contnode, name
+ )
+
+ def resolve_any_xref(
+ self,
+ env: BuildEnvironment,
+ fromdocname: str,
+ builder: Builder,
+ target: str,
+ node: pending_xref,
+ contnode: Element,
+ ) -> List[Tuple[str, nodes.reference]]:
+ results: List[Tuple[str, nodes.reference]] = []
+ matches = self.find_obj(node.get("qapi:module"), target, None)
+ for name, obj in matches:
+ rolename = self.role_for_objtype(obj.objtype)
+ assert rolename is not None
+ role = f"qapi:{rolename}"
+ refnode = make_refnode(
+ builder, fromdocname, obj.docname, obj.node_id, contnode, name
+ )
+ results.append((role, refnode))
+ return results
+
+
+def setup(app: Sphinx) -> Dict[str, Any]:
+ app.setup_extension("sphinx.directives")
+ app.add_config_value(
+ "qapi_allowed_fields",
+ set(),
+ "env", # Setting impacts parsing phase
+ types=set,
+ )
+ app.add_domain(QAPIDomain)
+
+ return {
+ "version": "1.0",
+ "env_version": 1,
+ "parallel_read_safe": True,
+ "parallel_write_safe": True,
+ }
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index 61997fd..432fef0 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -2,6 +2,7 @@
#
# QEMU qapidoc QAPI file parsing extension
#
+# Copyright (c) 2024-2025 Red Hat
# Copyright (c) 2020 Linaro
#
# This work is licensed under the terms of the GNU GPLv2 or later.
@@ -24,440 +25,431 @@ The Sphinx documentation on writing extensions is at:
https://www.sphinx-doc.org/en/master/development/index.html
"""
+from __future__ import annotations
+
+__version__ = "2.0"
+
+from contextlib import contextmanager
import os
+from pathlib import Path
import re
import sys
-import textwrap
-from typing import List
+from typing import TYPE_CHECKING
from docutils import nodes
-from docutils.parsers.rst import Directive, directives
-from docutils.statemachine import ViewList
-from qapi.error import QAPIError, QAPISemError
-from qapi.gen import QAPISchemaVisitor
-from qapi.schema import QAPISchema
-
+from docutils.parsers.rst import directives
+from docutils.statemachine import StringList
+from qapi.error import QAPIError
+from qapi.parser import QAPIDoc
+from qapi.schema import (
+ QAPISchema,
+ QAPISchemaArrayType,
+ QAPISchemaCommand,
+ QAPISchemaDefinition,
+ QAPISchemaEnumMember,
+ QAPISchemaEvent,
+ QAPISchemaFeature,
+ QAPISchemaMember,
+ QAPISchemaObjectType,
+ QAPISchemaObjectTypeMember,
+ QAPISchemaType,
+ QAPISchemaVisitor,
+)
+from qapi.source import QAPISourceInfo
+
+from qapidoc_legacy import QAPISchemaGenRSTVisitor # type: ignore
from sphinx import addnodes
from sphinx.directives.code import CodeBlock
from sphinx.errors import ExtensionError
-from sphinx.util.docutils import switch_source_input
+from sphinx.util import logging
+from sphinx.util.docutils import SphinxDirective, switch_source_input
from sphinx.util.nodes import nested_parse_with_titles
-__version__ = "1.0"
+if TYPE_CHECKING:
+ from typing import (
+ Any,
+ Generator,
+ List,
+ Optional,
+ Sequence,
+ Union,
+ )
+ from sphinx.application import Sphinx
+ from sphinx.util.typing import ExtensionMetadata
-def dedent(text: str) -> str:
- # Adjust indentation to make description text parse as paragraph.
- lines = text.splitlines(True)
- if re.match(r"\s+", lines[0]):
- # First line is indented; description started on the line after
- # the name. dedent the whole block.
- return textwrap.dedent(text)
+logger = logging.getLogger(__name__)
- # Descr started on same line. Dedent line 2+.
- return lines[0] + textwrap.dedent("".join(lines[1:]))
+class Transmogrifier:
+ # pylint: disable=too-many-public-methods
-# Disable black auto-formatter until re-enabled:
-# fmt: off
+ # Field names used for different entity types:
+ field_types = {
+ "enum": "value",
+ "struct": "memb",
+ "union": "memb",
+ "event": "memb",
+ "command": "arg",
+ "alternate": "alt",
+ }
+ def __init__(self) -> None:
+ self._curr_ent: Optional[QAPISchemaDefinition] = None
+ self._result = StringList()
+ self.indent = 0
-class QAPISchemaGenRSTVisitor(QAPISchemaVisitor):
- """A QAPI schema visitor which generates docutils/Sphinx nodes
+ @property
+ def result(self) -> StringList:
+ return self._result
- This class builds up a tree of docutils/Sphinx nodes corresponding
- to documentation for the various QAPI objects. To use it, first
- create a QAPISchemaGenRSTVisitor object, and call its
- visit_begin() method. Then you can call one of the two methods
- 'freeform' (to add documentation for a freeform documentation
- chunk) or 'symbol' (to add documentation for a QAPI symbol). These
- will cause the visitor to build up the tree of document
- nodes. Once you've added all the documentation via 'freeform' and
- 'symbol' method calls, you can call 'get_document_nodes' to get
- the final list of document nodes (in a form suitable for returning
- from a Sphinx directive's 'run' method).
- """
- def __init__(self, sphinx_directive):
- self._cur_doc = None
- self._sphinx_directive = sphinx_directive
- self._top_node = nodes.section()
- self._active_headings = [self._top_node]
-
- def _make_dlitem(self, term, defn):
- """Return a dlitem node with the specified term and definition.
-
- term should be a list of Text and literal nodes.
- defn should be one of:
- - a string, which will be handed to _parse_text_into_node
- - a list of Text and literal nodes, which will be put into
- a paragraph node
- """
- dlitem = nodes.definition_list_item()
- dlterm = nodes.term('', '', *term)
- dlitem += dlterm
- if defn:
- dldef = nodes.definition()
- if isinstance(defn, list):
- dldef += nodes.paragraph('', '', *defn)
- else:
- self._parse_text_into_node(defn, dldef)
- dlitem += dldef
- return dlitem
-
- def _make_section(self, title):
- """Return a section node with optional title"""
- section = nodes.section(ids=[self._sphinx_directive.new_serialno()])
- if title:
- section += nodes.title(title, title)
- return section
-
- def _nodes_for_ifcond(self, ifcond, with_if=True):
- """Return list of Text, literal nodes for the ifcond
-
- Return a list which gives text like ' (If: condition)'.
- If with_if is False, we don't return the "(If: " and ")".
- """
+ @property
+ def entity(self) -> QAPISchemaDefinition:
+ assert self._curr_ent is not None
+ return self._curr_ent
- doc = ifcond.docgen()
- if not doc:
- return []
- doc = nodes.literal('', doc)
- if not with_if:
- return [doc]
+ @property
+ def member_field_type(self) -> str:
+ return self.field_types[self.entity.meta]
- nodelist = [nodes.Text(' ('), nodes.strong('', 'If: ')]
- nodelist.append(doc)
- nodelist.append(nodes.Text(')'))
- return nodelist
+ # General-purpose rST generation functions
- def _nodes_for_one_member(self, member):
- """Return list of Text, literal nodes for this member
+ def get_indent(self) -> str:
+ return " " * self.indent
- Return a list of doctree nodes which give text like
- 'name: type (optional) (If: ...)' suitable for use as the
- 'term' part of a definition list item.
- """
- term = [nodes.literal('', member.name)]
- if member.type.doc_type():
- term.append(nodes.Text(': '))
- term.append(nodes.literal('', member.type.doc_type()))
- if member.optional:
- term.append(nodes.Text(' (optional)'))
- if member.ifcond.is_present():
- term.extend(self._nodes_for_ifcond(member.ifcond))
- return term
-
- def _nodes_for_variant_when(self, branches, variant):
- """Return list of Text, literal nodes for variant 'when' clause
-
- Return a list of doctree nodes which give text like
- 'when tagname is variant (If: ...)' suitable for use in
- the 'branches' part of a definition list.
- """
- term = [nodes.Text(' when '),
- nodes.literal('', branches.tag_member.name),
- nodes.Text(' is '),
- nodes.literal('', '"%s"' % variant.name)]
- if variant.ifcond.is_present():
- term.extend(self._nodes_for_ifcond(variant.ifcond))
- return term
-
- def _nodes_for_members(self, doc, what, base=None, branches=None):
- """Return list of doctree nodes for the table of members"""
- dlnode = nodes.definition_list()
- for section in doc.args.values():
- term = self._nodes_for_one_member(section.member)
- # TODO drop fallbacks when undocumented members are outlawed
- if section.text:
- defn = dedent(section.text)
- else:
- defn = [nodes.Text('Not documented')]
+ @contextmanager
+ def indented(self) -> Generator[None]:
+ self.indent += 1
+ try:
+ yield
+ finally:
+ self.indent -= 1
- dlnode += self._make_dlitem(term, defn)
+ def add_line_raw(self, line: str, source: str, *lineno: int) -> None:
+ """Append one line of generated reST to the output."""
- if base:
- dlnode += self._make_dlitem([nodes.Text('The members of '),
- nodes.literal('', base.doc_type())],
- None)
+ # NB: Sphinx uses zero-indexed lines; subtract one.
+ lineno = tuple((n - 1 for n in lineno))
- if branches:
- for v in branches.variants:
- if v.type.name == 'q_empty':
- continue
- assert not v.type.is_implicit()
- term = [nodes.Text('The members of '),
- nodes.literal('', v.type.doc_type())]
- term.extend(self._nodes_for_variant_when(branches, v))
- dlnode += self._make_dlitem(term, None)
-
- if not dlnode.children:
- return []
-
- section = self._make_section(what)
- section += dlnode
- return [section]
-
- def _nodes_for_enum_values(self, doc):
- """Return list of doctree nodes for the table of enum values"""
- seen_item = False
- dlnode = nodes.definition_list()
- for section in doc.args.values():
- termtext = [nodes.literal('', section.member.name)]
- if section.member.ifcond.is_present():
- termtext.extend(self._nodes_for_ifcond(section.member.ifcond))
- # TODO drop fallbacks when undocumented members are outlawed
- if section.text:
- defn = dedent(section.text)
- else:
- defn = [nodes.Text('Not documented')]
-
- dlnode += self._make_dlitem(termtext, defn)
- seen_item = True
-
- if not seen_item:
- return []
-
- section = self._make_section('Values')
- section += dlnode
- return [section]
-
- def _nodes_for_arguments(self, doc, arg_type):
- """Return list of doctree nodes for the arguments section"""
- if arg_type and not arg_type.is_implicit():
- assert not doc.args
- section = self._make_section('Arguments')
- dlnode = nodes.definition_list()
- dlnode += self._make_dlitem(
- [nodes.Text('The members of '),
- nodes.literal('', arg_type.name)],
- None)
- section += dlnode
- return [section]
-
- return self._nodes_for_members(doc, 'Arguments')
-
- def _nodes_for_features(self, doc):
- """Return list of doctree nodes for the table of features"""
- seen_item = False
- dlnode = nodes.definition_list()
- for section in doc.features.values():
- dlnode += self._make_dlitem(
- [nodes.literal('', section.member.name)], dedent(section.text))
- seen_item = True
-
- if not seen_item:
- return []
-
- section = self._make_section('Features')
- section += dlnode
- return [section]
-
- def _nodes_for_sections(self, doc):
- """Return list of doctree nodes for additional sections"""
- nodelist = []
- for section in doc.sections:
- if section.tag and section.tag == 'TODO':
- # Hide TODO: sections
- continue
-
- if not section.tag:
- # Sphinx cannot handle sectionless titles;
- # Instead, just append the results to the prior section.
- container = nodes.container()
- self._parse_text_into_node(section.text, container)
- nodelist += container.children
- continue
-
- snode = self._make_section(section.tag)
- self._parse_text_into_node(dedent(section.text), snode)
- nodelist.append(snode)
- return nodelist
-
- def _nodes_for_if_section(self, ifcond):
- """Return list of doctree nodes for the "If" section"""
- nodelist = []
- if ifcond.is_present():
- snode = self._make_section('If')
- snode += nodes.paragraph(
- '', '', *self._nodes_for_ifcond(ifcond, with_if=False)
+ if line.strip():
+ # not a blank line
+ self._result.append(
+ self.get_indent() + line.rstrip("\n"), source, *lineno
)
- nodelist.append(snode)
- return nodelist
-
- def _add_doc(self, typ, sections):
- """Add documentation for a command/object/enum...
-
- We assume we're documenting the thing defined in self._cur_doc.
- typ is the type of thing being added ("Command", "Object", etc)
-
- sections is a list of nodes for sections to add to the definition.
- """
-
- doc = self._cur_doc
- snode = nodes.section(ids=[self._sphinx_directive.new_serialno()])
- snode += nodes.title('', '', *[nodes.literal(doc.symbol, doc.symbol),
- nodes.Text(' (' + typ + ')')])
- self._parse_text_into_node(doc.body.text, snode)
- for s in sections:
- if s is not None:
- snode += s
- self._add_node_to_current_heading(snode)
-
- def visit_enum_type(self, name, info, ifcond, features, members, prefix):
- doc = self._cur_doc
- self._add_doc('Enum',
- self._nodes_for_enum_values(doc)
- + self._nodes_for_features(doc)
- + self._nodes_for_sections(doc)
- + self._nodes_for_if_section(ifcond))
-
- def visit_object_type(self, name, info, ifcond, features,
- base, members, branches):
- doc = self._cur_doc
- if base and base.is_implicit():
- base = None
- self._add_doc('Object',
- self._nodes_for_members(doc, 'Members', base, branches)
- + self._nodes_for_features(doc)
- + self._nodes_for_sections(doc)
- + self._nodes_for_if_section(ifcond))
-
- def visit_alternate_type(self, name, info, ifcond, features,
- alternatives):
- doc = self._cur_doc
- self._add_doc('Alternate',
- self._nodes_for_members(doc, 'Members')
- + self._nodes_for_features(doc)
- + self._nodes_for_sections(doc)
- + self._nodes_for_if_section(ifcond))
-
- def visit_command(self, name, info, ifcond, features, arg_type,
- ret_type, gen, success_response, boxed, allow_oob,
- allow_preconfig, coroutine):
- doc = self._cur_doc
- self._add_doc('Command',
- self._nodes_for_arguments(doc, arg_type)
- + self._nodes_for_features(doc)
- + self._nodes_for_sections(doc)
- + self._nodes_for_if_section(ifcond))
-
- def visit_event(self, name, info, ifcond, features, arg_type, boxed):
- doc = self._cur_doc
- self._add_doc('Event',
- self._nodes_for_arguments(doc, arg_type)
- + self._nodes_for_features(doc)
- + self._nodes_for_sections(doc)
- + self._nodes_for_if_section(ifcond))
-
- def symbol(self, doc, entity):
- """Add documentation for one symbol to the document tree
-
- This is the main entry point which causes us to add documentation
- nodes for a symbol (which could be a 'command', 'object', 'event',
- etc). We do this by calling 'visit' on the schema entity, which
- will then call back into one of our visit_* methods, depending
- on what kind of thing this symbol is.
- """
- self._cur_doc = doc
- entity.visit(self)
- self._cur_doc = None
-
- def _start_new_heading(self, heading, level):
- """Start a new heading at the specified heading level
+ else:
+ self._result.append("", source, *lineno)
+
+ def add_line(self, content: str, info: QAPISourceInfo) -> None:
+ # NB: We *require* an info object; this works out OK because we
+ # don't document built-in objects that don't have
+ # one. Everything else should.
+ self.add_line_raw(content, info.fname, info.line)
+
+ def add_lines(
+ self,
+ content: str,
+ info: QAPISourceInfo,
+ ) -> None:
+ lines = content.splitlines(True)
+ for i, line in enumerate(lines):
+ self.add_line_raw(line, info.fname, info.line + i)
+
+ def ensure_blank_line(self) -> None:
+ # Empty document -- no blank line required.
+ if not self._result:
+ return
+
+ # Last line isn't blank, add one.
+ if self._result[-1].strip(): # pylint: disable=no-member
+ fname, line = self._result.info(-1)
+ assert isinstance(line, int)
+ # New blank line is credited to one-after the current last line.
+ # +2: correct for zero/one index, then increment by one.
+ self.add_line_raw("", fname, line + 2)
+
+ def add_field(
+ self,
+ kind: str,
+ name: str,
+ body: str,
+ info: QAPISourceInfo,
+ typ: Optional[str] = None,
+ ) -> None:
+ if typ:
+ text = f":{kind} {typ} {name}: {body}"
+ else:
+ text = f":{kind} {name}: {body}"
+ self.add_lines(text, info)
+
+ def format_type(
+ self, ent: Union[QAPISchemaDefinition | QAPISchemaMember]
+ ) -> Optional[str]:
+ if isinstance(ent, (QAPISchemaEnumMember, QAPISchemaFeature)):
+ return None
+
+ qapi_type = ent
+ optional = False
+ if isinstance(ent, QAPISchemaObjectTypeMember):
+ qapi_type = ent.type
+ optional = ent.optional
+
+ if isinstance(qapi_type, QAPISchemaArrayType):
+ ret = f"[{qapi_type.element_type.doc_type()}]"
+ else:
+ assert isinstance(qapi_type, QAPISchemaType)
+ tmp = qapi_type.doc_type()
+ assert tmp
+ ret = tmp
+ if optional:
+ ret += "?"
+
+ return ret
+
+ def generate_field(
+ self,
+ kind: str,
+ member: QAPISchemaMember,
+ body: str,
+ info: QAPISourceInfo,
+ ) -> None:
+ typ = self.format_type(member)
+ self.add_field(kind, member.name, body, info, typ)
+
+ # Transmogrification helpers
+
+ def visit_paragraph(self, section: QAPIDoc.Section) -> None:
+ # Squelch empty paragraphs.
+ if not section.text:
+ return
+
+ self.ensure_blank_line()
+ self.add_lines(section.text, section.info)
+ self.ensure_blank_line()
+
+ def visit_member(self, section: QAPIDoc.ArgSection) -> None:
+ # FIXME: ifcond for members
+ # TODO: features for members (documented at entity-level,
+ # but sometimes defined per-member. Should we add such
+ # information to member descriptions when we can?)
+ assert section.member
+ self.generate_field(
+ self.member_field_type,
+ section.member,
+ # TODO drop fallbacks when undocumented members are outlawed
+ section.text if section.text else "Not documented",
+ section.info,
+ )
- Create a new section whose title is 'heading' and which is placed
- in the docutils node tree as a child of the most recent level-1
- heading. Subsequent document sections (commands, freeform doc chunks,
- etc) will be placed as children of this new heading section.
+ def visit_feature(self, section: QAPIDoc.ArgSection) -> None:
+ # FIXME - ifcond for features is not handled at all yet!
+ # Proposal: decorate the right-hand column with some graphical
+ # element to indicate conditional availability?
+ assert section.text # Guaranteed by parser.py
+ assert section.member
+
+ self.generate_field("feat", section.member, section.text, section.info)
+
+ def visit_returns(self, section: QAPIDoc.Section) -> None:
+ assert isinstance(self.entity, QAPISchemaCommand)
+ rtype = self.entity.ret_type
+ # q_empty can produce None, but we won't be documenting anything
+ # without an explicit return statement in the doc block, and we
+ # should not have any such explicit statements when there is no
+ # return value.
+ assert rtype
+
+ typ = self.format_type(rtype)
+ assert typ
+ assert section.text
+ self.add_field("return", typ, section.text, section.info)
+
+ def visit_errors(self, section: QAPIDoc.Section) -> None:
+ # FIXME: the formatting for errors may be inconsistent and may
+ # or may not require different newline placement to ensure
+ # proper rendering as a nested list.
+ self.add_lines(f":error:\n{section.text}", section.info)
+
+ def preamble(self, ent: QAPISchemaDefinition) -> None:
"""
- if len(self._active_headings) < level:
- raise QAPISemError(self._cur_doc.info,
- 'Level %d subheading found outside a '
- 'level %d heading'
- % (level, level - 1))
- snode = self._make_section(heading)
- self._active_headings[level - 1] += snode
- self._active_headings = self._active_headings[:level]
- self._active_headings.append(snode)
- return snode
-
- def _add_node_to_current_heading(self, node):
- """Add the node to whatever the current active heading is"""
- self._active_headings[-1] += node
-
- def freeform(self, doc):
- """Add a piece of 'freeform' documentation to the document tree
-
- A 'freeform' document chunk doesn't relate to any particular
- symbol (for instance, it could be an introduction).
-
- If the freeform document starts with a line of the form
- '= Heading text', this is a section or subsection heading, with
- the heading level indicated by the number of '=' signs.
+ Generate option lines for QAPI entity directives.
"""
+ if ent.doc and ent.doc.since:
+ assert ent.doc.since.kind == QAPIDoc.Kind.SINCE
+ # Generated from the entity's docblock; info location is exact.
+ self.add_line(f":since: {ent.doc.since.text}", ent.doc.since.info)
+
+ if ent.ifcond.is_present():
+ doc = ent.ifcond.docgen()
+ assert ent.info
+ # Generated from entity definition; info location is approximate.
+ self.add_line(f":ifcond: {doc}", ent.info)
+
+ # Hoist special features such as :deprecated: and :unstable:
+ # into the options block for the entity. If, in the future, new
+ # special features are added, qapi-domain will chirp about
+ # unrecognized options and fail until they are handled in
+ # qapi-domain.
+ for feat in ent.features:
+ if feat.is_special():
+ # FIXME: handle ifcond if present. How to display that
+ # information is TBD.
+ # Generated from entity def; info location is approximate.
+ assert feat.info
+ self.add_line(f":{feat.name}:", feat.info)
+
+ self.ensure_blank_line()
+
+ def _insert_member_pointer(self, ent: QAPISchemaDefinition) -> None:
+
+ def _get_target(
+ ent: QAPISchemaDefinition,
+ ) -> Optional[QAPISchemaDefinition]:
+ if isinstance(ent, (QAPISchemaCommand, QAPISchemaEvent)):
+ return ent.arg_type
+ if isinstance(ent, QAPISchemaObjectType):
+ return ent.base
+ return None
+
+ target = _get_target(ent)
+ if target is not None and not target.is_implicit():
+ assert ent.info
+ self.add_field(
+ self.member_field_type,
+ "q_dummy",
+ f"The members of :qapi:type:`{target.name}`.",
+ ent.info,
+ "q_dummy",
+ )
- # QAPIDoc documentation says free-form documentation blocks
- # must have only a body section, nothing else.
- assert not doc.sections
- assert not doc.args
- assert not doc.features
- self._cur_doc = doc
-
- text = doc.body.text
- if re.match(r'=+ ', text):
- # Section/subsection heading (if present, will always be
- # the first line of the block)
- (heading, _, text) = text.partition('\n')
- (leader, _, heading) = heading.partition(' ')
- node = self._start_new_heading(heading, len(leader))
- if text == '':
- return
- else:
- node = nodes.container()
-
- self._parse_text_into_node(text, node)
- self._cur_doc = None
+ if isinstance(ent, QAPISchemaObjectType) and ent.branches is not None:
+ for variant in ent.branches.variants:
+ if variant.type.name == "q_empty":
+ continue
+ assert ent.info
+ self.add_field(
+ self.member_field_type,
+ "q_dummy",
+ f" When ``{ent.branches.tag_member.name}`` is "
+ f"``{variant.name}``: "
+ f"The members of :qapi:type:`{variant.type.name}`.",
+ ent.info,
+ "q_dummy",
+ )
+
+ def visit_sections(self, ent: QAPISchemaDefinition) -> None:
+ sections = ent.doc.all_sections if ent.doc else []
+
+ # Determine the index location at which we should generate
+ # documentation for "The members of ..." pointers. This should
+ # go at the end of the members section(s) if any. Note that
+ # index 0 is assumed to be a plain intro section, even if it is
+ # empty; and that a members section if present will always
+ # immediately follow the opening PLAIN section.
+ gen_index = 1
+ if len(sections) > 1:
+ while sections[gen_index].kind == QAPIDoc.Kind.MEMBER:
+ gen_index += 1
+ if gen_index >= len(sections):
+ break
+
+ # Add sections in source order:
+ for i, section in enumerate(sections):
+ # @var is translated to ``var``:
+ section.text = re.sub(r"@([\w-]+)", r"``\1``", section.text)
+
+ if section.kind == QAPIDoc.Kind.PLAIN:
+ self.visit_paragraph(section)
+ elif section.kind == QAPIDoc.Kind.MEMBER:
+ assert isinstance(section, QAPIDoc.ArgSection)
+ self.visit_member(section)
+ elif section.kind == QAPIDoc.Kind.FEATURE:
+ assert isinstance(section, QAPIDoc.ArgSection)
+ self.visit_feature(section)
+ elif section.kind in (QAPIDoc.Kind.SINCE, QAPIDoc.Kind.TODO):
+ # Since is handled in preamble, TODO is skipped intentionally.
+ pass
+ elif section.kind == QAPIDoc.Kind.RETURNS:
+ self.visit_returns(section)
+ elif section.kind == QAPIDoc.Kind.ERRORS:
+ self.visit_errors(section)
+ else:
+ assert False
+
+ # Generate "The members of ..." entries if necessary:
+ if i == gen_index - 1:
+ self._insert_member_pointer(ent)
+
+ self.ensure_blank_line()
+
+ # Transmogrification core methods
+
+ def visit_module(self, path: str) -> None:
+ name = Path(path).stem
+ # module directives are credited to the first line of a module file.
+ self.add_line_raw(f".. qapi:module:: {name}", path, 1)
+ self.ensure_blank_line()
+
+ def visit_freeform(self, doc: QAPIDoc) -> None:
+ # TODO: Once the old qapidoc transformer is deprecated, freeform
+ # sections can be updated to pure rST, and this transformed removed.
+ #
+ # For now, translate our micro-format into rST. Code adapted
+ # from Peter Maydell's freeform().
+
+ assert len(doc.all_sections) == 1, doc.all_sections
+ body = doc.all_sections[0]
+ text = body.text
+ info = doc.info
+
+ if re.match(r"=+ ", text):
+ # Section/subsection heading (if present, will always be the
+ # first line of the block)
+ (heading, _, text) = text.partition("\n")
+ (leader, _, heading) = heading.partition(" ")
+ # Implicit +1 for heading in the containing .rst doc
+ level = len(leader) + 1
+
+ # https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#sections
+ markers = ' #*=_^"'
+ overline = level <= 2
+ marker = markers[level]
+
+ self.ensure_blank_line()
+ # This credits all 2 or 3 lines to the single source line.
+ if overline:
+ self.add_line(marker * len(heading), info)
+ self.add_line(heading, info)
+ self.add_line(marker * len(heading), info)
+ self.ensure_blank_line()
+
+ # Eat blank line(s) and advance info
+ trimmed = text.lstrip("\n")
+ text = trimmed
+ info = info.next_line(len(text) - len(trimmed) + 1)
+
+ self.add_lines(text, info)
+ self.ensure_blank_line()
+
+ def visit_entity(self, ent: QAPISchemaDefinition) -> None:
+ assert ent.info
- def _parse_text_into_node(self, doctext, node):
- """Parse a chunk of QAPI-doc-format text into the node
+ try:
+ self._curr_ent = ent
- The doc comment can contain most inline rST markup, including
- bulleted and enumerated lists.
- As an extra permitted piece of markup, @var will be turned
- into ``var``.
- """
+ # Squish structs and unions together into an "object" directive.
+ meta = ent.meta
+ if meta in ("struct", "union"):
+ meta = "object"
- # Handle the "@var means ``var`` case
- doctext = re.sub(r'@([\w-]+)', r'``\1``', doctext)
-
- rstlist = ViewList()
- for line in doctext.splitlines():
- # The reported line number will always be that of the start line
- # of the doc comment, rather than the actual location of the error.
- # Being more precise would require overhaul of the QAPIDoc class
- # to track lines more exactly within all the sub-parts of the doc
- # comment, as well as counting lines here.
- rstlist.append(line, self._cur_doc.info.fname,
- self._cur_doc.info.line)
- # Append a blank line -- in some cases rST syntax errors get
- # attributed to the line after one with actual text, and if there
- # isn't anything in the ViewList corresponding to that then Sphinx
- # 1.6's AutodocReporter will then misidentify the source/line location
- # in the error message (usually attributing it to the top-level
- # .rst file rather than the offending .json file). The extra blank
- # line won't affect the rendered output.
- rstlist.append("", self._cur_doc.info.fname, self._cur_doc.info.line)
- self._sphinx_directive.do_parse(rstlist, node)
-
- def get_document_nodes(self):
- """Return the list of docutils nodes which make up the document"""
- return self._top_node.children
-
-
-# Turn the black formatter on for the rest of the file.
-# fmt: on
+ # This line gets credited to the start of the /definition/.
+ self.add_line(f".. qapi:{meta}:: {ent.name}", ent.info)
+ with self.indented():
+ self.preamble(ent)
+ self.visit_sections(ent)
+ finally:
+ self._curr_ent = None
class QAPISchemaGenDepVisitor(QAPISchemaVisitor):
@@ -468,22 +460,22 @@ class QAPISchemaGenDepVisitor(QAPISchemaVisitor):
schema file associated with each module in the QAPI input.
"""
- def __init__(self, env, qapidir):
+ def __init__(self, env: Any, qapidir: str) -> None:
self._env = env
self._qapidir = qapidir
- def visit_module(self, name):
+ def visit_module(self, name: str) -> None:
if name != "./builtin":
qapifile = self._qapidir + "/" + name
self._env.note_dependency(os.path.abspath(qapifile))
super().visit_module(name)
-class NestedDirective(Directive):
- def run(self):
+class NestedDirective(SphinxDirective):
+ def run(self) -> Sequence[nodes.Node]:
raise NotImplementedError
- def do_parse(self, rstlist, node):
+ def do_parse(self, rstlist: StringList, node: nodes.Node) -> None:
"""
Parse rST source lines and add them to the specified node
@@ -502,18 +494,104 @@ class QAPIDocDirective(NestedDirective):
required_argument = 1
optional_arguments = 1
- option_spec = {"qapifile": directives.unchanged_required}
+ option_spec = {
+ "qapifile": directives.unchanged_required,
+ "transmogrify": directives.flag,
+ }
has_content = False
- def new_serialno(self):
+ def new_serialno(self) -> str:
"""Return a unique new ID string suitable for use as a node's ID"""
env = self.state.document.settings.env
return "qapidoc-%d" % env.new_serialno("qapidoc")
- def run(self):
+ def transmogrify(self, schema: QAPISchema) -> nodes.Element:
+ logger.info("Transmogrifying QAPI to rST ...")
+ vis = Transmogrifier()
+ modules = set()
+
+ for doc in schema.docs:
+ module_source = doc.info.fname
+ if module_source not in modules:
+ vis.visit_module(module_source)
+ modules.add(module_source)
+
+ if doc.symbol:
+ ent = schema.lookup_entity(doc.symbol)
+ assert isinstance(ent, QAPISchemaDefinition)
+ vis.visit_entity(ent)
+ else:
+ vis.visit_freeform(doc)
+
+ logger.info("Transmogrification complete.")
+
+ contentnode = nodes.section()
+ content = vis.result
+ titles_allowed = True
+
+ logger.info("Transmogrifier running nested parse ...")
+ with switch_source_input(self.state, content):
+ if titles_allowed:
+ node: nodes.Element = nodes.section()
+ node.document = self.state.document
+ nested_parse_with_titles(self.state, content, contentnode)
+ else:
+ node = nodes.paragraph()
+ node.document = self.state.document
+ self.state.nested_parse(content, 0, contentnode)
+ logger.info("Transmogrifier's nested parse completed.")
+
+ if self.env.app.verbosity >= 2 or os.environ.get("DEBUG"):
+ argname = "_".join(Path(self.arguments[0]).parts)
+ name = Path(argname).stem + ".ir"
+ self.write_intermediate(content, name)
+
+ sys.stdout.flush()
+ return contentnode
+
+ def write_intermediate(self, content: StringList, filename: str) -> None:
+ logger.info(
+ "writing intermediate rST for '%s' to '%s'",
+ self.arguments[0],
+ filename,
+ )
+
+ srctree = Path(self.env.app.config.qapidoc_srctree).resolve()
+ outlines = []
+ lcol_width = 0
+
+ for i, line in enumerate(content):
+ src, lineno = content.info(i)
+ srcpath = Path(src).resolve()
+ srcpath = srcpath.relative_to(srctree)
+
+ lcol = f"{srcpath}:{lineno:04d}"
+ lcol_width = max(lcol_width, len(lcol))
+ outlines.append((lcol, line))
+
+ with open(filename, "w", encoding="UTF-8") as outfile:
+ for lcol, rcol in outlines:
+ outfile.write(lcol.rjust(lcol_width))
+ outfile.write(" |")
+ if rcol:
+ outfile.write(f" {rcol}")
+ outfile.write("\n")
+
+ def legacy(self, schema: QAPISchema) -> nodes.Element:
+ vis = QAPISchemaGenRSTVisitor(self)
+ vis.visit_begin(schema)
+ for doc in schema.docs:
+ if doc.symbol:
+ vis.symbol(doc, schema.lookup_entity(doc.symbol))
+ else:
+ vis.freeform(doc)
+ return vis.get_document_node() # type: ignore
+
+ def run(self) -> Sequence[nodes.Node]:
env = self.state.document.settings.env
qapifile = env.config.qapidoc_srctree + "/" + self.arguments[0]
qapidir = os.path.dirname(qapifile)
+ transmogrify = "transmogrify" in self.options
try:
schema = QAPISchema(qapifile)
@@ -521,20 +599,18 @@ class QAPIDocDirective(NestedDirective):
# First tell Sphinx about all the schema files that the
# output documentation depends on (including 'qapifile' itself)
schema.visit(QAPISchemaGenDepVisitor(env, qapidir))
-
- vis = QAPISchemaGenRSTVisitor(self)
- vis.visit_begin(schema)
- for doc in schema.docs:
- if doc.symbol:
- vis.symbol(doc, schema.lookup_entity(doc.symbol))
- else:
- vis.freeform(doc)
- return vis.get_document_nodes()
except QAPIError as err:
# Launder QAPI parse errors into Sphinx extension errors
# so they are displayed nicely to the user
raise ExtensionError(str(err)) from err
+ if transmogrify:
+ contentnode = self.transmogrify(schema)
+ else:
+ contentnode = self.legacy(schema)
+
+ return contentnode.children
+
class QMPExample(CodeBlock, NestedDirective):
"""
@@ -585,7 +661,7 @@ class QMPExample(CodeBlock, NestedDirective):
)
return node
- def admonition_wrap(self, *content) -> List[nodes.Node]:
+ def admonition_wrap(self, *content: nodes.Node) -> List[nodes.Node]:
title = "Example:"
if "title" in self.options:
title = f"{title} {self.options['title']}"
@@ -631,8 +707,9 @@ class QMPExample(CodeBlock, NestedDirective):
return self.admonition_wrap(*content_nodes)
-def setup(app):
+def setup(app: Sphinx) -> ExtensionMetadata:
"""Register qapi-doc directive with Sphinx"""
+ app.setup_extension("qapi_domain")
app.add_config_value("qapidoc_srctree", None, "env")
app.add_directive("qapi-doc", QAPIDocDirective)
app.add_directive("qmp-example", QMPExample)
diff --git a/docs/sphinx/qapidoc_legacy.py b/docs/sphinx/qapidoc_legacy.py
new file mode 100644
index 0000000..13520f4
--- /dev/null
+++ b/docs/sphinx/qapidoc_legacy.py
@@ -0,0 +1,440 @@
+# coding=utf-8
+# type: ignore
+#
+# QEMU qapidoc QAPI file parsing extension
+#
+# Copyright (c) 2020 Linaro
+#
+# This work is licensed under the terms of the GNU GPLv2 or later.
+# See the COPYING file in the top-level directory.
+
+"""
+qapidoc is a Sphinx extension that implements the qapi-doc directive
+
+The purpose of this extension is to read the documentation comments
+in QAPI schema files, and insert them all into the current document.
+
+It implements one new rST directive, "qapi-doc::".
+Each qapi-doc:: directive takes one argument, which is the
+pathname of the schema file to process, relative to the source tree.
+
+The docs/conf.py file must set the qapidoc_srctree config value to
+the root of the QEMU source tree.
+
+The Sphinx documentation on writing extensions is at:
+https://www.sphinx-doc.org/en/master/development/index.html
+"""
+
+import re
+import textwrap
+
+from docutils import nodes
+from docutils.statemachine import ViewList
+from qapi.error import QAPISemError
+from qapi.gen import QAPISchemaVisitor
+from qapi.parser import QAPIDoc
+
+
+def dedent(text: str) -> str:
+ # Adjust indentation to make description text parse as paragraph.
+
+ lines = text.splitlines(True)
+ if re.match(r"\s+", lines[0]):
+ # First line is indented; description started on the line after
+ # the name. dedent the whole block.
+ return textwrap.dedent(text)
+
+ # Descr started on same line. Dedent line 2+.
+ return lines[0] + textwrap.dedent("".join(lines[1:]))
+
+
+class QAPISchemaGenRSTVisitor(QAPISchemaVisitor):
+ """A QAPI schema visitor which generates docutils/Sphinx nodes
+
+ This class builds up a tree of docutils/Sphinx nodes corresponding
+ to documentation for the various QAPI objects. To use it, first
+ create a QAPISchemaGenRSTVisitor object, and call its
+ visit_begin() method. Then you can call one of the two methods
+ 'freeform' (to add documentation for a freeform documentation
+ chunk) or 'symbol' (to add documentation for a QAPI symbol). These
+ will cause the visitor to build up the tree of document
+ nodes. Once you've added all the documentation via 'freeform' and
+ 'symbol' method calls, you can call 'get_document_nodes' to get
+ the final list of document nodes (in a form suitable for returning
+ from a Sphinx directive's 'run' method).
+ """
+ def __init__(self, sphinx_directive):
+ self._cur_doc = None
+ self._sphinx_directive = sphinx_directive
+ self._top_node = nodes.section()
+ self._active_headings = [self._top_node]
+
+ def _make_dlitem(self, term, defn):
+ """Return a dlitem node with the specified term and definition.
+
+ term should be a list of Text and literal nodes.
+ defn should be one of:
+ - a string, which will be handed to _parse_text_into_node
+ - a list of Text and literal nodes, which will be put into
+ a paragraph node
+ """
+ dlitem = nodes.definition_list_item()
+ dlterm = nodes.term('', '', *term)
+ dlitem += dlterm
+ if defn:
+ dldef = nodes.definition()
+ if isinstance(defn, list):
+ dldef += nodes.paragraph('', '', *defn)
+ else:
+ self._parse_text_into_node(defn, dldef)
+ dlitem += dldef
+ return dlitem
+
+ def _make_section(self, title):
+ """Return a section node with optional title"""
+ section = nodes.section(ids=[self._sphinx_directive.new_serialno()])
+ if title:
+ section += nodes.title(title, title)
+ return section
+
+ def _nodes_for_ifcond(self, ifcond, with_if=True):
+ """Return list of Text, literal nodes for the ifcond
+
+ Return a list which gives text like ' (If: condition)'.
+ If with_if is False, we don't return the "(If: " and ")".
+ """
+
+ doc = ifcond.docgen()
+ if not doc:
+ return []
+ doc = nodes.literal('', doc)
+ if not with_if:
+ return [doc]
+
+ nodelist = [nodes.Text(' ('), nodes.strong('', 'If: ')]
+ nodelist.append(doc)
+ nodelist.append(nodes.Text(')'))
+ return nodelist
+
+ def _nodes_for_one_member(self, member):
+ """Return list of Text, literal nodes for this member
+
+ Return a list of doctree nodes which give text like
+ 'name: type (optional) (If: ...)' suitable for use as the
+ 'term' part of a definition list item.
+ """
+ term = [nodes.literal('', member.name)]
+ if member.type.doc_type():
+ term.append(nodes.Text(': '))
+ term.append(nodes.literal('', member.type.doc_type()))
+ if member.optional:
+ term.append(nodes.Text(' (optional)'))
+ if member.ifcond.is_present():
+ term.extend(self._nodes_for_ifcond(member.ifcond))
+ return term
+
+ def _nodes_for_variant_when(self, branches, variant):
+ """Return list of Text, literal nodes for variant 'when' clause
+
+ Return a list of doctree nodes which give text like
+ 'when tagname is variant (If: ...)' suitable for use in
+ the 'branches' part of a definition list.
+ """
+ term = [nodes.Text(' when '),
+ nodes.literal('', branches.tag_member.name),
+ nodes.Text(' is '),
+ nodes.literal('', '"%s"' % variant.name)]
+ if variant.ifcond.is_present():
+ term.extend(self._nodes_for_ifcond(variant.ifcond))
+ return term
+
+ def _nodes_for_members(self, doc, what, base=None, branches=None):
+ """Return list of doctree nodes for the table of members"""
+ dlnode = nodes.definition_list()
+ for section in doc.args.values():
+ term = self._nodes_for_one_member(section.member)
+ # TODO drop fallbacks when undocumented members are outlawed
+ if section.text:
+ defn = dedent(section.text)
+ else:
+ defn = [nodes.Text('Not documented')]
+
+ dlnode += self._make_dlitem(term, defn)
+
+ if base:
+ dlnode += self._make_dlitem([nodes.Text('The members of '),
+ nodes.literal('', base.doc_type())],
+ None)
+
+ if branches:
+ for v in branches.variants:
+ if v.type.name == 'q_empty':
+ continue
+ assert not v.type.is_implicit()
+ term = [nodes.Text('The members of '),
+ nodes.literal('', v.type.doc_type())]
+ term.extend(self._nodes_for_variant_when(branches, v))
+ dlnode += self._make_dlitem(term, None)
+
+ if not dlnode.children:
+ return []
+
+ section = self._make_section(what)
+ section += dlnode
+ return [section]
+
+ def _nodes_for_enum_values(self, doc):
+ """Return list of doctree nodes for the table of enum values"""
+ seen_item = False
+ dlnode = nodes.definition_list()
+ for section in doc.args.values():
+ termtext = [nodes.literal('', section.member.name)]
+ if section.member.ifcond.is_present():
+ termtext.extend(self._nodes_for_ifcond(section.member.ifcond))
+ # TODO drop fallbacks when undocumented members are outlawed
+ if section.text:
+ defn = dedent(section.text)
+ else:
+ defn = [nodes.Text('Not documented')]
+
+ dlnode += self._make_dlitem(termtext, defn)
+ seen_item = True
+
+ if not seen_item:
+ return []
+
+ section = self._make_section('Values')
+ section += dlnode
+ return [section]
+
+ def _nodes_for_arguments(self, doc, arg_type):
+ """Return list of doctree nodes for the arguments section"""
+ if arg_type and not arg_type.is_implicit():
+ assert not doc.args
+ section = self._make_section('Arguments')
+ dlnode = nodes.definition_list()
+ dlnode += self._make_dlitem(
+ [nodes.Text('The members of '),
+ nodes.literal('', arg_type.name)],
+ None)
+ section += dlnode
+ return [section]
+
+ return self._nodes_for_members(doc, 'Arguments')
+
+ def _nodes_for_features(self, doc):
+ """Return list of doctree nodes for the table of features"""
+ seen_item = False
+ dlnode = nodes.definition_list()
+ for section in doc.features.values():
+ dlnode += self._make_dlitem(
+ [nodes.literal('', section.member.name)], dedent(section.text))
+ seen_item = True
+
+ if not seen_item:
+ return []
+
+ section = self._make_section('Features')
+ section += dlnode
+ return [section]
+
+ def _nodes_for_sections(self, doc):
+ """Return list of doctree nodes for additional sections"""
+ nodelist = []
+ for section in doc.sections:
+ if section.kind == QAPIDoc.Kind.TODO:
+ # Hide TODO: sections
+ continue
+
+ if section.kind == QAPIDoc.Kind.PLAIN:
+ # Sphinx cannot handle sectionless titles;
+ # Instead, just append the results to the prior section.
+ container = nodes.container()
+ self._parse_text_into_node(section.text, container)
+ nodelist += container.children
+ continue
+
+ snode = self._make_section(section.kind.name.title())
+ self._parse_text_into_node(dedent(section.text), snode)
+ nodelist.append(snode)
+ return nodelist
+
+ def _nodes_for_if_section(self, ifcond):
+ """Return list of doctree nodes for the "If" section"""
+ nodelist = []
+ if ifcond.is_present():
+ snode = self._make_section('If')
+ snode += nodes.paragraph(
+ '', '', *self._nodes_for_ifcond(ifcond, with_if=False)
+ )
+ nodelist.append(snode)
+ return nodelist
+
+ def _add_doc(self, typ, sections):
+ """Add documentation for a command/object/enum...
+
+ We assume we're documenting the thing defined in self._cur_doc.
+ typ is the type of thing being added ("Command", "Object", etc)
+
+ sections is a list of nodes for sections to add to the definition.
+ """
+
+ doc = self._cur_doc
+ snode = nodes.section(ids=[self._sphinx_directive.new_serialno()])
+ snode += nodes.title('', '', *[nodes.literal(doc.symbol, doc.symbol),
+ nodes.Text(' (' + typ + ')')])
+ self._parse_text_into_node(doc.body.text, snode)
+ for s in sections:
+ if s is not None:
+ snode += s
+ self._add_node_to_current_heading(snode)
+
+ def visit_enum_type(self, name, info, ifcond, features, members, prefix):
+ doc = self._cur_doc
+ self._add_doc('Enum',
+ self._nodes_for_enum_values(doc)
+ + self._nodes_for_features(doc)
+ + self._nodes_for_sections(doc)
+ + self._nodes_for_if_section(ifcond))
+
+ def visit_object_type(self, name, info, ifcond, features,
+ base, members, branches):
+ doc = self._cur_doc
+ if base and base.is_implicit():
+ base = None
+ self._add_doc('Object',
+ self._nodes_for_members(doc, 'Members', base, branches)
+ + self._nodes_for_features(doc)
+ + self._nodes_for_sections(doc)
+ + self._nodes_for_if_section(ifcond))
+
+ def visit_alternate_type(self, name, info, ifcond, features,
+ alternatives):
+ doc = self._cur_doc
+ self._add_doc('Alternate',
+ self._nodes_for_members(doc, 'Members')
+ + self._nodes_for_features(doc)
+ + self._nodes_for_sections(doc)
+ + self._nodes_for_if_section(ifcond))
+
+ def visit_command(self, name, info, ifcond, features, arg_type,
+ ret_type, gen, success_response, boxed, allow_oob,
+ allow_preconfig, coroutine):
+ doc = self._cur_doc
+ self._add_doc('Command',
+ self._nodes_for_arguments(doc, arg_type)
+ + self._nodes_for_features(doc)
+ + self._nodes_for_sections(doc)
+ + self._nodes_for_if_section(ifcond))
+
+ def visit_event(self, name, info, ifcond, features, arg_type, boxed):
+ doc = self._cur_doc
+ self._add_doc('Event',
+ self._nodes_for_arguments(doc, arg_type)
+ + self._nodes_for_features(doc)
+ + self._nodes_for_sections(doc)
+ + self._nodes_for_if_section(ifcond))
+
+ def symbol(self, doc, entity):
+ """Add documentation for one symbol to the document tree
+
+ This is the main entry point which causes us to add documentation
+ nodes for a symbol (which could be a 'command', 'object', 'event',
+ etc). We do this by calling 'visit' on the schema entity, which
+ will then call back into one of our visit_* methods, depending
+ on what kind of thing this symbol is.
+ """
+ self._cur_doc = doc
+ entity.visit(self)
+ self._cur_doc = None
+
+ def _start_new_heading(self, heading, level):
+ """Start a new heading at the specified heading level
+
+ Create a new section whose title is 'heading' and which is placed
+ in the docutils node tree as a child of the most recent level-1
+ heading. Subsequent document sections (commands, freeform doc chunks,
+ etc) will be placed as children of this new heading section.
+ """
+ if len(self._active_headings) < level:
+ raise QAPISemError(self._cur_doc.info,
+ 'Level %d subheading found outside a '
+ 'level %d heading'
+ % (level, level - 1))
+ snode = self._make_section(heading)
+ self._active_headings[level - 1] += snode
+ self._active_headings = self._active_headings[:level]
+ self._active_headings.append(snode)
+ return snode
+
+ def _add_node_to_current_heading(self, node):
+ """Add the node to whatever the current active heading is"""
+ self._active_headings[-1] += node
+
+ def freeform(self, doc):
+ """Add a piece of 'freeform' documentation to the document tree
+
+ A 'freeform' document chunk doesn't relate to any particular
+ symbol (for instance, it could be an introduction).
+
+ If the freeform document starts with a line of the form
+ '= Heading text', this is a section or subsection heading, with
+ the heading level indicated by the number of '=' signs.
+ """
+
+ # QAPIDoc documentation says free-form documentation blocks
+ # must have only a body section, nothing else.
+ assert not doc.sections
+ assert not doc.args
+ assert not doc.features
+ self._cur_doc = doc
+
+ text = doc.body.text
+ if re.match(r'=+ ', text):
+ # Section/subsection heading (if present, will always be
+ # the first line of the block)
+ (heading, _, text) = text.partition('\n')
+ (leader, _, heading) = heading.partition(' ')
+ node = self._start_new_heading(heading, len(leader))
+ if text == '':
+ return
+ else:
+ node = nodes.container()
+
+ self._parse_text_into_node(text, node)
+ self._cur_doc = None
+
+ def _parse_text_into_node(self, doctext, node):
+ """Parse a chunk of QAPI-doc-format text into the node
+
+ The doc comment can contain most inline rST markup, including
+ bulleted and enumerated lists.
+ As an extra permitted piece of markup, @var will be turned
+ into ``var``.
+ """
+
+ # Handle the "@var means ``var`` case
+ doctext = re.sub(r'@([\w-]+)', r'``\1``', doctext)
+
+ rstlist = ViewList()
+ for line in doctext.splitlines():
+ # The reported line number will always be that of the start line
+ # of the doc comment, rather than the actual location of the error.
+ # Being more precise would require overhaul of the QAPIDoc class
+ # to track lines more exactly within all the sub-parts of the doc
+ # comment, as well as counting lines here.
+ rstlist.append(line, self._cur_doc.info.fname,
+ self._cur_doc.info.line)
+ # Append a blank line -- in some cases rST syntax errors get
+ # attributed to the line after one with actual text, and if there
+ # isn't anything in the ViewList corresponding to that then Sphinx
+ # 1.6's AutodocReporter will then misidentify the source/line location
+ # in the error message (usually attributing it to the top-level
+ # .rst file rather than the offending .json file). The extra blank
+ # line won't affect the rendered output.
+ rstlist.append("", self._cur_doc.info.fname, self._cur_doc.info.line)
+ self._sphinx_directive.do_parse(rstlist, node)
+
+ def get_document_node(self):
+ """Return the root docutils node which makes up the document"""
+ return self._top_node
diff --git a/docs/system/ppc/amigang.rst b/docs/system/ppc/amigang.rst
index e2c9cb7..21bb14e 100644
--- a/docs/system/ppc/amigang.rst
+++ b/docs/system/ppc/amigang.rst
@@ -21,6 +21,7 @@ Emulated devices
* VIA VT82C686B south bridge
* PCI VGA compatible card (guests may need other card instead)
* PS/2 keyboard and mouse
+ * 4 KiB NVRAM (use ``-drive if=mtd,format=raw,file=nvram.bin`` to keep contents persistent)
Firmware
--------
@@ -54,14 +55,14 @@ To boot the system run:
-cdrom "A1 Linux Net Installer.iso" \
-device ati-vga,model=rv100,romfile=VGABIOS-lgpl-latest.bin
-From the firmware menu that appears select ``Boot sequence`` →
-``Amiga Multiboot Options`` and set ``Boot device 1`` to
-``Onboard VIA IDE CDROM``. Then hit escape until the main screen appears again,
-hit escape once more and from the exit menu that appears select either
-``Save settings and exit`` or ``Use settings for this session only``. It may
-take a long time loading the kernel into memory but eventually it boots and the
-installer becomes visible. The ``ati-vga`` RV100 emulation is not
-complete yet so only frame buffer works, DRM and 3D is not available.
+If a firmware menu appears, select ``Boot sequence`` → ``Amiga Multiboot Options``
+and set ``Boot device 1`` to ``Onboard VIA IDE CDROM``. Then hit escape until
+the main screen appears again, hit escape once more and from the exit menu that
+appears select either ``Save settings and exit`` or ``Use settings for this
+session only``. It may take a long time loading the kernel into memory but
+eventually it boots and the installer becomes visible. The ``ati-vga`` RV100
+emulation is not complete yet so only frame buffer works, DRM and 3D is not
+available.
Genesi/bPlan Pegasos II (``pegasos2``)
======================================
diff --git a/docs/system/ppc/embedded.rst b/docs/system/ppc/embedded.rst
index af3b3d9..5cb7d98 100644
--- a/docs/system/ppc/embedded.rst
+++ b/docs/system/ppc/embedded.rst
@@ -4,6 +4,5 @@ Embedded family boards
- ``bamboo`` bamboo
- ``mpc8544ds`` mpc8544ds
- ``ppce500`` generic paravirt e500 platform
-- ``ref405ep`` ref405ep
- ``sam460ex`` aCube Sam460ex
- ``virtex-ml507`` Xilinx Virtex ML507 reference design
diff --git a/docs/system/ppc/powernv.rst b/docs/system/ppc/powernv.rst
index de7a807..f3ec2cc 100644
--- a/docs/system/ppc/powernv.rst
+++ b/docs/system/ppc/powernv.rst
@@ -195,6 +195,13 @@ Use a MTD drive to add a PNOR to the machine, and get a NVRAM :
-drive file=./witherspoon.pnor,format=raw,if=mtd
+If no mtd drive is provided, the powernv platform will create a default
+PNOR device using a tiny formatted PNOR in pc-bios/pnv-pnor.bin opened
+read-only (PNOR changes will be persistent across reboots but not across
+invocations of QEMU). If no defaults are used, an erased 128MB PNOR is
+provided (which skiboot will probably not recognize since it is not
+formatted).
+
Maintainer contact information
------------------------------