summaryrefslogtreecommitdiff
path: root/rtemsspec
diff options
context:
space:
mode:
authorSebastian Huber <sebastian.huber@embedded-brains.de>2023-11-21 11:13:16 +0100
committerSebastian Huber <sebastian.huber@embedded-brains.de>2023-11-21 11:15:25 +0100
commit2ddd68a9db4dc56f1d35a4a30691bc1dc987266c (patch)
treed344fbdde826734559945f604e58f743043ec169 /rtemsspec
parenta7234d1fcee12e0eca245b4e4d34707d2f321cf7 (diff)
sphinxbuilder: New
Diffstat (limited to 'rtemsspec')
-rw-r--r--rtemsspec/packagebuildfactory.py3
-rw-r--r--rtemsspec/sphinxbuilder.py466
-rw-r--r--rtemsspec/tests/spec-packagebuild/glossary/term.yml12
-rw-r--r--rtemsspec/tests/spec-packagebuild/qdp/build/doc-2.yml13
-rw-r--r--rtemsspec/tests/spec-packagebuild/qdp/build/doc.yml13
-rw-r--r--rtemsspec/tests/spec-packagebuild/qdp/deployment/doc-2.yml13
-rw-r--r--rtemsspec/tests/spec-packagebuild/qdp/deployment/doc.yml13
-rw-r--r--rtemsspec/tests/spec-packagebuild/qdp/package-build.yml4
-rw-r--r--rtemsspec/tests/spec-packagebuild/qdp/source/doc-section.yml13
-rw-r--r--rtemsspec/tests/spec-packagebuild/qdp/source/doc-subsection.yml15
-rw-r--r--rtemsspec/tests/spec-packagebuild/qdp/source/doc.yml19
-rw-r--r--rtemsspec/tests/spec-packagebuild/qdp/steps/doc-2.yml36
-rw-r--r--rtemsspec/tests/spec-packagebuild/qdp/steps/doc.yml102
-rw-r--r--rtemsspec/tests/spec-packagebuild/rtems/if.yml3
-rw-r--r--rtemsspec/tests/test-files/pkg/build/src/doc/copy-and-substitute.rst21
-rw-r--r--rtemsspec/tests/test-files/pkg/build/src/doc/copy.rst5
-rw-r--r--rtemsspec/tests/test-files/pkg/build/src/doc/index.rst19
-rw-r--r--rtemsspec/tests/test-files/pkg/build/src/doc/some.txt1
-rw-r--r--rtemsspec/tests/test_packagebuild.py215
19 files changed, 985 insertions, 1 deletions
diff --git a/rtemsspec/packagebuildfactory.py b/rtemsspec/packagebuildfactory.py
index 0d959f91..4e7567e0 100644
--- a/rtemsspec/packagebuildfactory.py
+++ b/rtemsspec/packagebuildfactory.py
@@ -33,6 +33,7 @@ from rtemsspec.reposubset import RepositorySubset
from rtemsspec.rtems import RTEMSItemCache
from rtemsspec.runactions import RunActions
from rtemsspec.runtests import RunTests, TestLog
+from rtemsspec.sphinxbuilder import SphinxBuilder, SphinxSection
from rtemsspec.testrunner import DummyTestRunner, GRMONManualTestRunner, \
SubprocessTestRunner
@@ -49,11 +50,13 @@ def create_build_item_factory() -> BuildItemFactory:
factory.add_constructor("qdp/build-step/rtems-item-cache", RTEMSItemCache)
factory.add_constructor("qdp/build-step/run-actions", RunActions)
factory.add_constructor("qdp/build-step/run-tests", RunTests)
+ factory.add_constructor("qdp/build-step/sphinx/generic", SphinxBuilder)
factory.add_constructor("qdp/directory-state/generic", DirectoryState)
factory.add_constructor("qdp/directory-state/repository", DirectoryState)
factory.add_constructor("qdp/directory-state/test-log", TestLog)
factory.add_constructor("qdp/directory-state/unpacked-archive",
DirectoryState)
+ factory.add_constructor("qdp/sphinx-section", SphinxSection)
factory.add_constructor("qdp/test-runner/dummy", DummyTestRunner)
factory.add_constructor("qdp/test-runner/grmon-manual",
GRMONManualTestRunner)
diff --git a/rtemsspec/sphinxbuilder.py b/rtemsspec/sphinxbuilder.py
new file mode 100644
index 00000000..298bce26
--- /dev/null
+++ b/rtemsspec/sphinxbuilder.py
@@ -0,0 +1,466 @@
+# SPDX-License-Identifier: BSD-2-Clause
+""" Contains the SphinxBuilder class. """
+
+# Copyright (C) 2020, 2023 embedded brains GmbH & Co. KG
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from contextlib import contextmanager
+import logging
+import os
+import re
+import shutil
+from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple
+import yaml
+
+from rtemsspec.content import BSD_2_CLAUSE_LICENSE, Copyrights
+from rtemsspec.directorystate import DirectoryState
+from rtemsspec.packagebuild import BuildItem, BuildItemFactory, \
+ PackageBuildDirector
+from rtemsspec.items import is_enabled, Item, ItemGetValueContext, Link
+from rtemsspec import glossary
+from rtemsspec.sphinxcontent import SphinxContent
+from rtemsspec.util import run_command
+
+_BREAK = "\\break"
+
+_PUSH_ENABLED_BY = re.compile(r"^\${\.:/push-enabled-by:(.+)}$")
+
+_POP_ENABLED_BY = re.compile(r"^\${\.:/pop-enabled-by")
+
+_RST_HEADERS = re.compile(
+ r"^\.\. SPDX-License-Identifier: (.+)\n\n((\.\. Copyright \(C\).*\n)+)\n",
+ flags=re.MULTILINE)
+
+
+def _get_normal_title(ctx: ItemGetValueContext) -> Any:
+ return ctx.item["document-title"].replace(_BREAK, " ")
+
+
+def _get_sphinx_title(ctx: ItemGetValueContext) -> Any:
+ content = SphinxContent()
+ content.add_header(_get_normal_title(ctx), level=1)
+ return "\n".join(content.lines)
+
+
+def _get_release(ctx: ItemGetValueContext) -> Any:
+ return str(len(ctx.item["document-releases"]))
+
+
+def _no_action(_component: Dict[str, Any]) -> None:
+ pass
+
+
+def _sep(seps: Tuple[str, ...], maxi: Tuple[int, ...]) -> str:
+ return "+" + "+".join(f"{sep * (val + 2)}"
+ for sep, val in zip(seps, maxi)) + "+"
+
+
+def _row(row: Tuple[str, ...], maxi: Tuple[int, ...]) -> str:
+ return "|" + "|".join(f" {cell:{width}} "
+ for cell, width in zip(row, maxi)) + "|"
+
+
+def _get_contributors(ctx: ItemGetValueContext) -> Any:
+ rows = [("Action", "Name", "Organization", "Signature")]
+ maxi = tuple(map(len, rows[0]))
+ for action in ctx.item["document-contributors"]:
+ for contributor in action["contributors"]:
+ row = (action["action"], contributor["name"],
+ contributor["organization"], "")
+ rows.append(row)
+ row_lengths = tuple(map(len, row))
+ maxi = tuple(map(max, zip(maxi, row_lengths)))
+ sep_0 = _sep(("-", "-", "-", "-"), maxi)
+ sep_1 = _sep(("=", "=", "=", "="), maxi)
+ sep_2 = _sep((" ", "-", "-", "-"), maxi)
+ lines = [sep_0, _row(rows[0], maxi)]
+ last_action = rows[0][0]
+ for row in rows[1:]:
+ if last_action == "Action":
+ lines.append(sep_1)
+ last_action = row[0]
+ elif last_action == row[0]:
+ lines.append(sep_2)
+ row = ("", row[1], row[2], row[3])
+ else:
+ lines.append(sep_0)
+ last_action = row[0]
+ lines.append(_row(row, maxi))
+ lines.append(sep_0)
+ content = SphinxContent()
+ with content.directive(
+ "table", options=[":class: longtable", ":widths: 16 26 30 28"]):
+ content.add(lines)
+ return "\n".join(content.lines)
+
+
+def _latex_escape(value: str) -> str:
+ return value.replace("_", "\\_").replace("&", "\\&")
+
+
+_COPYRIGHT = re.compile(r"^\s*Copyright\s+\(C\)\s+", re.IGNORECASE)
+_YEARS = re.compile(r"^[0-9, ]*")
+
+
+def _get_document_copyright(ctx: ItemGetValueContext) -> Any:
+ copyrights = ctx.item["document-copyrights"]
+ main = _COPYRIGHT.sub("", copyrights[0])
+ if len(copyrights) == 1:
+ return main
+ return f"{main} and contributors"
+
+
+def _get_document_author(ctx: ItemGetValueContext) -> Any:
+ return _YEARS.sub("", _get_document_copyright(ctx))
+
+
+def _get_document_year(ctx: ItemGetValueContext) -> Any:
+ match = _YEARS.search(_get_document_copyright(ctx))
+ assert match
+ return match.group(0).split(",")[-1]
+
+
+class SphinxBuilder(BuildItem):
+ """ Base class for Sphinx document builds. """
+
+ # pylint: disable=too-many-instance-attributes
+ @classmethod
+ def prepare_factory(cls, factory: BuildItemFactory,
+ type_name: str) -> None:
+ BuildItem.prepare_factory(factory, type_name)
+ factory.add_get_value(f"{type_name}:/document-release", _get_release)
+ factory.add_get_value(f"{type_name}:/document-year",
+ _get_document_year)
+
+ def __init__(self, director: PackageBuildDirector, item: Item):
+ super().__init__(director, item)
+ source = self.input("source")
+ assert isinstance(source, DirectoryState)
+
+ build = self.output("build")
+ assert isinstance(build, DirectoryState)
+
+ self._index: List[str] = []
+ self._section_level = 2
+ self.source_dir = source.directory
+ self.build_dir = build.directory
+ self.file_path = self.build_dir
+ my_type = self.item.type
+ self.mapper.add_get_value(f"{my_type}:/document-author",
+ _get_document_author)
+ self.mapper.add_get_value(f"{my_type}:/document-contract-html",
+ self._get_contract_html)
+ self.mapper.add_get_value(f"{my_type}:/document-contract-latex",
+ self._get_contract_latex)
+ self.mapper.add_get_value(f"{my_type}:/document-copyright",
+ _get_document_copyright)
+ self.mapper.add_get_value(f"{my_type}:/document-copyrights",
+ self._get_document_copyrights)
+ self.mapper.add_get_value(
+ f"{my_type}:/document-bsd-2-clause-copyrights",
+ self._get_document_bsd_2_clause_copyrights)
+ self.mapper.add_get_value(f"{my_type}:/document-normal-title",
+ _get_normal_title)
+ self.mapper.add_get_value(f"{my_type}:/document-latex-copyright",
+ self._get_latex_copyright)
+ self.mapper.add_get_value(f"{my_type}:/document-latex-title",
+ self._get_latex_title)
+ self.mapper.add_get_value(f"{my_type}:/document-title-page-title",
+ self._get_title_page_title)
+ self.mapper.add_get_value(f"{my_type}:/document-sphinx-title",
+ _get_sphinx_title)
+ self.mapper.add_get_value(f"{my_type}:/document-index",
+ self._get_index)
+ self.mapper.add_get_value(f"{my_type}:/document-releases",
+ self._get_releases)
+ self.mapper.add_get_value(f"{my_type}:/document-contributors",
+ _get_contributors)
+ for name in [my_type, "qdp/sphinx-section"]:
+ self.mapper.add_get_value(f"{name}:/sections", self._get_sections)
+ self._actions = {
+ "add-to-index": _no_action,
+ "copy": self._copy,
+ "copy-and-substitute": self._copy_and_substitute,
+ "copy-files": self._copy_files,
+ "glossary": self._glossary
+ }
+
+ def run(self) -> None:
+ self.mapper.copyrights_by_license.clear()
+
+ destination = self.output("destination")
+ assert isinstance(destination, DirectoryState)
+ destination.clear()
+
+ os.makedirs(os.path.join(self.build_dir, "source"), exist_ok=True)
+ status = run_command(
+ ["python3", "-msphinx", "-M", "clean", "source", "build"],
+ self.build_dir)
+ assert status == 0
+ enabled_set = self.enabled_set
+ for component in self["document-components"]:
+ if is_enabled(enabled_set, component.get("enabled-by", True)):
+ self.file_path = os.path.join(
+ self.build_dir, component.get("destination", "."))
+ self._actions[component["action"]](component)
+ self._add_to_index(component)
+ output = self["output-pdf"]
+ if output:
+ status = run_command(
+ ["python3", "-msphinx", "-M", "latexpdf", "source", "build"],
+ self.build_dir)
+ assert status == 0
+ src_path = os.path.join(self.build_dir, "build", "latex",
+ "document.pdf")
+ destination.copy_file(src_path, output)
+ output = self["output-html"]
+ if output:
+ status = run_command(
+ ["python3", "-msphinx", "-M", "html", "source", "build"],
+ self.build_dir)
+ assert status == 0
+ src_path = os.path.join(self.build_dir, "build", "html")
+ destination.copy_tree(src_path, output)
+
+ my_license = self["document-license"]
+ destination["copyrights-by-license"] = dict(
+ (key, value.get_statements())
+ for key, value in self._get_copyrights().items()
+ if key != my_license)
+
+ def add_component_action(self, name: str,
+ action: Callable[[Dict[str, Any]], None]) -> None:
+ """ Adds a component action. """
+ self._actions[name] = action
+
+ def _get_releases(self, ctx: ItemGetValueContext) -> Any:
+ content = SphinxContent()
+ releases = ctx.item["document-releases"]
+ count = len(releases)
+ for idx, release in enumerate(reversed(releases)):
+ date = release["date"]
+ status = release["status"]
+ line = f"Release: {count - idx}, Date: {date}, Status: {status}"
+ with content.directive("topic", line):
+ lines = self._push_pop_enabled_by(
+ release["changes"].splitlines())
+ content.add(self.mapper.substitute("\n".join(lines), ctx.item))
+ return "\n".join(content.lines)
+
+ def _add_to_index(self, component: Dict[str, Any]) -> None:
+ if component.get("add-to-index", False):
+ self._index.append(
+ os.path.basename(component["destination"]).replace(".rst", ""))
+
+ def _get_index(self, ctx: ItemGetValueContext) -> Any:
+ content = SphinxContent()
+ maxdepth = f":maxdepth: {ctx.item['document-toctree-maxdepth']}"
+ with content.directive("toctree", options=[maxdepth, ":numbered:"]):
+ content.add(self._index)
+ return "\n".join(content.lines)
+
+ def _get_contract_html(self, ctx: ItemGetValueContext) -> Any:
+ contract = self.mapper.substitute(ctx.item["document-contract"])
+ dot = ". " if contract else ""
+ return f"{dot}{contract.replace(_BREAK, ' ')}"
+
+ def _get_contract_latex(self, ctx: ItemGetValueContext) -> Any:
+ return _latex_escape(
+ self.mapper.substitute(ctx.item["document-contract"]).replace(
+ _BREAK, " \\vspace{-4pt} \\break "))
+
+ def _get_latex_copyright(self, ctx: ItemGetValueContext) -> Any:
+ return _latex_escape(
+ self.mapper.substitute(_get_document_copyright(ctx)))
+
+ def _get_copyrights(self) -> Dict[str, Copyrights]:
+ my_license = self["document-license"]
+ copyrights: Dict[str, Copyrights] = {}
+ copyrights.setdefault(my_license, Copyrights()).register(
+ self["document-copyrights"])
+ license_map = self["document-license-map"]
+ for key, value in self.mapper.copyrights_by_license.items():
+ the_license = license_map.get(key, key)
+ copyrights.setdefault(the_license, Copyrights()).register(value)
+ return copyrights
+
+ def _get_document_copyrights(self, ctx: ItemGetValueContext) -> Any:
+ my_license = self["document-license"]
+ assert " OR " not in my_license
+ copyrights = self._get_copyrights()
+ prefix = ctx.args if ctx.args else ""
+ return "\n".join(copyrights[my_license].get_statements(f"{prefix}| ©"))
+
+ def _get_document_bsd_2_clause_copyrights(
+ self, _ctx: ItemGetValueContext) -> Any:
+ copyrights = self._get_copyrights()
+ the_license = "BSD-2-Clause"
+ if the_license not in copyrights:
+ return ""
+ statements = "\n".join(copyrights[the_license].get_statements("| ©"))
+ return f"""{statements}
+
+{BSD_2_CLAUSE_LICENSE}"""
+
+ def _get_latex_title(self, ctx: ItemGetValueContext) -> Any:
+ return _latex_escape(
+ self.mapper.substitute(ctx.item["document-title"].replace(
+ _BREAK, " ")))
+
+ def _get_title_page_title(self, ctx: ItemGetValueContext) -> Any:
+ return _latex_escape(
+ self.mapper.substitute(ctx.item["document-title"].replace(
+ _BREAK, " \\break \\break ")))
+
+ @contextmanager
+ def section_level(
+ self,
+ ctx: ItemGetValueContext) -> Iterator[Tuple[int, Optional[str]]]:
+ """ Opens a section level with optional additional arguments. """
+ if ctx.args:
+ colon = ctx.args.find(":")
+ if colon >= 0:
+ level_change = int(ctx.args[:colon])
+ args: Optional[str] = ctx.args[colon + 1:]
+ else:
+ level_change = int(ctx.args)
+ args = None
+ else:
+ level_change = 1
+ args = None
+ section_level = self._section_level
+ new_section_level = section_level + level_change
+ self._section_level = new_section_level
+ yield new_section_level, args
+ self._section_level = section_level
+
+ def _get_sections(self, ctx: ItemGetValueContext) -> Any:
+ with self.section_level(ctx) as (section_level, args):
+ assert args
+ content = SphinxContent(section_level)
+ build_item = self.director[ctx.item.uid]
+ for section in build_item.inputs(args):
+ with content.section(section.item["header"],
+ label=section.item["label"]):
+ content.add(section.item["content"].strip())
+ return "\n".join(content.lines)
+
+ def _register_text_copyrights(self, text: str) -> None:
+ match = _RST_HEADERS.match(text)
+ assert match
+ the_license = match.group(1)
+ statements = [
+ statement[3:] for statement in match.group(2).split("\n")[:-1]
+ ]
+ logging.info("%s: register license %s with copyrights %s", self.uid,
+ the_license, statements)
+ self.mapper.copyrights_by_license.setdefault(the_license,
+ set()).update(statements)
+
+ def _do_copy(self, src_file: str, dst_file: str) -> None:
+ os.makedirs(os.path.dirname(dst_file), exist_ok=True)
+ if dst_file.endswith(".rst"):
+ logging.info("%s: read: %s", self.uid, src_file)
+ with open(src_file, "r", encoding="utf-8") as src:
+ text = src.read()
+ self._register_text_copyrights(text)
+ logging.info("%s: write: %s", self.uid, dst_file)
+ with open(dst_file, "w+", encoding="utf-8") as dst:
+ dst.write(text)
+ else:
+ logging.info("%s: copy '%s' to '%s'", self.uid, src_file, dst_file)
+ shutil.copy2(src_file, dst_file)
+
+ def _copy(self, component: Dict[str, Any]) -> None:
+ self._do_copy(os.path.join(self.source_dir, component["source"]),
+ os.path.join(self.build_dir, component["destination"]))
+
+ def _push_pop_enabled_by(self, lines: List[str]) -> List[str]:
+ filtered_lines: List[str] = []
+ enabled: List[bool] = [True]
+ for line in lines:
+ push_match = _PUSH_ENABLED_BY.search(line)
+ if push_match is not None:
+ data = push_match.group(1)
+ enabled_by = yaml.load(data, yaml.SafeLoader)
+ enabled.append(enabled[-1]
+ and is_enabled(self.enabled_set, enabled_by))
+ continue
+ if _POP_ENABLED_BY.search(line) is not None:
+ enabled.pop()
+ elif enabled[-1]:
+ filtered_lines.append(line)
+ return filtered_lines
+
+ def _copy_and_substitute(self, component: Dict[str, Any]) -> None:
+ src_path = os.path.join(self.source_dir, component["source"])
+ dst_path = os.path.join(self.build_dir, component["destination"])
+ logging.info("%s: read: %s", self.uid, src_path)
+ with open(src_path, "r", encoding="utf-8") as src:
+ logging.info("%s: process push/pop enabled by", self.uid)
+ text = "".join(self._push_pop_enabled_by(src.readlines()))
+ if dst_path.endswith(".rst"):
+ self._register_text_copyrights(text)
+ logging.info("%s: substitute", self.uid)
+ text = self.mapper.substitute(text)
+ logging.info("%s: write: %s", self.uid, dst_path)
+ os.makedirs(os.path.dirname(dst_path), exist_ok=True)
+ with open(dst_path, "w+", encoding="utf-8") as dst:
+ dst.write(text)
+
+ def _copy_files(self, component: Dict[str, Any]) -> None:
+ src_dir = os.path.join(self.source_dir, component["source"])
+ dst_dir = os.path.join(self.build_dir, component["destination"])
+ for a_file in component["files"]:
+ self._do_copy(os.path.join(src_dir, a_file),
+ os.path.join(dst_dir, a_file))
+
+ def _glossary(self, component: Dict[str, Any]) -> None:
+ config = {
+ "project-groups":
+ component["glossary-groups"],
+ "project-header":
+ None,
+ "project-target":
+ None,
+ "documents": [{
+ "header":
+ "Terms, definitions and abbreviated terms",
+ "rest-source-paths": [
+ str(os.path.join(self.build_dir, "source")),
+ str(os.path.join(self.build_dir, "include"))
+ ],
+ "target":
+ str(os.path.join(self.build_dir, component["destination"])),
+ }]
+ }
+ glossary.generate(config, self.item.cache, self.mapper)
+
+
+class SphinxSection(BuildItem):
+ """ This class represents a Sphinx section. """
+
+ def has_changed(self, link: Link) -> bool:
+ if self.is_build_necessary():
+ return True
+ return super().has_changed(link)
diff --git a/rtemsspec/tests/spec-packagebuild/glossary/term.yml b/rtemsspec/tests/spec-packagebuild/glossary/term.yml
new file mode 100644
index 00000000..aa4e1432
--- /dev/null
+++ b/rtemsspec/tests/spec-packagebuild/glossary/term.yml
@@ -0,0 +1,12 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2023 Alice
+- Copyright (C) 2020 embedded brains GmbH & Co. KG
+enabled-by: true
+glossary-type: term
+links:
+- role: glossary-member
+ uid: ../glossary-general
+term: Term
+text: This is the term.
+type: glossary
diff --git a/rtemsspec/tests/spec-packagebuild/qdp/build/doc-2.yml b/rtemsspec/tests/spec-packagebuild/qdp/build/doc-2.yml
new file mode 100644
index 00000000..87634f3f
--- /dev/null
+++ b/rtemsspec/tests/spec-packagebuild/qdp/build/doc-2.yml
@@ -0,0 +1,13 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2023 embedded brains GmbH & Co. KG
+copyrights-by-license: {}
+directory: ${../variant:/build-directory}/doc-2
+directory-state-type: generic
+enabled-by: true
+files: []
+hash: null
+links: []
+patterns: []
+qdp-type: directory-state
+type: qdp
diff --git a/rtemsspec/tests/spec-packagebuild/qdp/build/doc.yml b/rtemsspec/tests/spec-packagebuild/qdp/build/doc.yml
new file mode 100644
index 00000000..ec7d11dd
--- /dev/null
+++ b/rtemsspec/tests/spec-packagebuild/qdp/build/doc.yml
@@ -0,0 +1,13 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2023 embedded brains GmbH & Co. KG
+copyrights-by-license: {}
+directory: ${../variant:/build-directory}/doc
+directory-state-type: generic
+enabled-by: true
+files: []
+hash: null
+links: []
+patterns: []
+qdp-type: directory-state
+type: qdp
diff --git a/rtemsspec/tests/spec-packagebuild/qdp/deployment/doc-2.yml b/rtemsspec/tests/spec-packagebuild/qdp/deployment/doc-2.yml
new file mode 100644
index 00000000..a252bf6e
--- /dev/null
+++ b/rtemsspec/tests/spec-packagebuild/qdp/deployment/doc-2.yml
@@ -0,0 +1,13 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2023 embedded brains GmbH & Co. KG
+copyrights-by-license: {}
+directory: ${../variant:/deployment-directory}/doc-2
+directory-state-type: generic
+enabled-by: true
+files: []
+hash: null
+links: []
+patterns: []
+qdp-type: directory-state
+type: qdp
diff --git a/rtemsspec/tests/spec-packagebuild/qdp/deployment/doc.yml b/rtemsspec/tests/spec-packagebuild/qdp/deployment/doc.yml
new file mode 100644
index 00000000..ca375bca
--- /dev/null
+++ b/rtemsspec/tests/spec-packagebuild/qdp/deployment/doc.yml
@@ -0,0 +1,13 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2023 embedded brains GmbH & Co. KG
+copyrights-by-license: {}
+directory: ${../variant:/deployment-directory}/doc
+directory-state-type: generic
+enabled-by: true
+files: []
+hash: null
+links: []
+patterns: []
+qdp-type: directory-state
+type: qdp
diff --git a/rtemsspec/tests/spec-packagebuild/qdp/package-build.yml b/rtemsspec/tests/spec-packagebuild/qdp/package-build.yml
index 4686460e..4147fd6f 100644
--- a/rtemsspec/tests/spec-packagebuild/qdp/package-build.yml
+++ b/rtemsspec/tests/spec-packagebuild/qdp/package-build.yml
@@ -22,6 +22,10 @@ links:
- role: build-step
uid: steps/membench
- role: build-step
+ uid: steps/doc
+- role: build-step
+ uid: steps/doc-2
+- role: build-step
uid: steps/archive
qdp-type: package-build
type: qdp
diff --git a/rtemsspec/tests/spec-packagebuild/qdp/source/doc-section.yml b/rtemsspec/tests/spec-packagebuild/qdp/source/doc-section.yml
new file mode 100644
index 00000000..8dc66d45
--- /dev/null
+++ b/rtemsspec/tests/spec-packagebuild/qdp/source/doc-section.yml
@@ -0,0 +1,13 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+content: |
+ Section content:
+
+ ${.:/sections:2:subsection}
+copyrights:
+- Copyright (C) 2023 embedded brains GmbH & Co. KG
+enabled-by: true
+header: Section Header
+label: SectionHeader
+links: []
+qdp-type: sphinx-section
+type: qdp
diff --git a/rtemsspec/tests/spec-packagebuild/qdp/source/doc-subsection.yml b/rtemsspec/tests/spec-packagebuild/qdp/source/doc-subsection.yml
new file mode 100644
index 00000000..0636dada
--- /dev/null
+++ b/rtemsspec/tests/spec-packagebuild/qdp/source/doc-subsection.yml
@@ -0,0 +1,15 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+content: |
+ Subsection content.
+copyrights:
+- Copyright (C) 2023 embedded brains GmbH & Co. KG
+enabled-by: true
+header: Subsection Header
+label: SubsectionHeader
+links:
+- hash: null
+ name: spec
+ role: input
+ uid: ../steps/rtems-item-cache
+qdp-type: sphinx-section
+type: qdp
diff --git a/rtemsspec/tests/spec-packagebuild/qdp/source/doc.yml b/rtemsspec/tests/spec-packagebuild/qdp/source/doc.yml
new file mode 100644
index 00000000..ca2a620b
--- /dev/null
+++ b/rtemsspec/tests/spec-packagebuild/qdp/source/doc.yml
@@ -0,0 +1,19 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2023 embedded brains GmbH & Co. KG
+copyrights-by-license: {}
+directory: ${../variant:/build-directory}/src/doc
+directory-state-type: generic
+enabled-by: true
+files:
+- file: copy-and-substitute.rst
+ hash: null
+- file: copy.rst
+ hash: null
+- file: index.rst
+ hash: null
+hash: null
+links: []
+patterns: []
+qdp-type: directory-state
+type: qdp
diff --git a/rtemsspec/tests/spec-packagebuild/qdp/steps/doc-2.yml b/rtemsspec/tests/spec-packagebuild/qdp/steps/doc-2.yml
new file mode 100644
index 00000000..910cb2fd
--- /dev/null
+++ b/rtemsspec/tests/spec-packagebuild/qdp/steps/doc-2.yml
@@ -0,0 +1,36 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+build-step-type: sphinx
+copyrights:
+- Copyright (C) 2023 embedded brains GmbH & Co. KG
+description: |
+ Builds a Sphinx document 2.
+document-title: The Title
+document-toctree-maxdepth: 4
+document-html-help-base-name: Puh
+document-components: []
+document-contract: ''
+document-copyrights:
+- Copyright (C) 2023 embedded brains GmbH & Co. KG
+document-key: bla
+document-releases: []
+document-contributors: []
+document-license: CC-BY-SA-4.0
+document-license-map:
+ BSD-2-Clause: CC-BY-SA-4.0 OR BSD-2-Clause
+document-type: generic
+enabled-by: sphinx-builder-2
+links:
+- hash: null
+ name: source
+ role: input
+ uid: ../source/doc
+- name: build
+ role: output
+ uid: ../build/doc-2
+- name: destination
+ role: output
+ uid: ../deployment/doc-2
+output-pdf: null
+output-html: null
+qdp-type: build-step
+type: qdp
diff --git a/rtemsspec/tests/spec-packagebuild/qdp/steps/doc.yml b/rtemsspec/tests/spec-packagebuild/qdp/steps/doc.yml
new file mode 100644
index 00000000..7a6c1b17
--- /dev/null
+++ b/rtemsspec/tests/spec-packagebuild/qdp/steps/doc.yml
@@ -0,0 +1,102 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+build-step-type: sphinx
+copyrights:
+- Copyright (C) 2020 embedded brains GmbH & Co. KG
+description: |
+ Builds a Sphinx document.
+document-title: The\breakTitle
+document-toctree-maxdepth: 4
+document-html-help-base-name: Base
+document-components:
+- action: copy-and-substitute
+ add-to-index: true
+ source: copy-and-substitute.rst
+ destination: source/copy-and-substitute.rst
+- action: copy
+ add-to-index: false
+ source: copy.rst
+ destination: source/copy.rst
+- action: copy
+ add-to-index: false
+ enabled-by: false
+ source: nil.rst
+ destination: source/nada.rst
+- action: copy
+ add-to-index: false
+ source: some.txt
+ destination: source/some.txt
+- action: copy-and-substitute
+ add-to-index: false
+ source: some.txt
+ destination: source/some.txt
+- action: copy-files
+ add-to-index: false
+ source: .
+ destination: other
+ files:
+ - copy.rst
+- action: add-to-index
+ add-to-index: true
+ destination: source/glossary.rst
+- action: copy-and-substitute
+ add-to-index: false
+ source: index.rst
+ destination: source/index.rst
+- action: glossary
+ add-to-index: false
+ destination: source/glossary.rst
+ glossary-groups:
+ - /glossary-general
+document-contract: Contract
+document-copyrights:
+- Copyright (C) 2020 embedded brains GmbH & Co. KG
+document-key: blub
+document-releases:
+- date: '1970-01-01'
+ status: Replaced
+ changes: Initial release.
+- date: '2020-10-26'
+ status: Draft
+ changes: |
+ * ${.:/document-copyright}
+
+ * e
+document-contributors:
+- action: Written by
+ contributors:
+ - name: John Doe
+ organization: Some Organization
+ - name: Foo
+ organization: Bár Organization
+- action: Super Action
+ contributors:
+ - name: This is a Long Name
+ organization: Short
+document-license: CC-BY-SA-4.0
+document-license-map:
+ CC-BY-SA-4.0 OR BSD-2-Clause: CC-BY-SA-4.0
+document-type: generic
+enabled-by: sphinx-builder
+links:
+- hash: null
+ name: source
+ role: input
+ uid: ../source/doc
+- hash: null
+ name: section
+ role: input
+ uid: ../source/doc-section
+- hash: null
+ name: subsection
+ role: input
+ uid: ../source/doc-subsection
+- name: build
+ role: output
+ uid: ../build/doc
+- name: destination
+ role: output
+ uid: ../deployment/doc
+output-pdf: doc.pdf
+output-html: html
+qdp-type: build-step
+type: qdp
diff --git a/rtemsspec/tests/spec-packagebuild/rtems/if.yml b/rtemsspec/tests/spec-packagebuild/rtems/if.yml
index 0e1d67fe..3e0c69d6 100644
--- a/rtemsspec/tests/spec-packagebuild/rtems/if.yml
+++ b/rtemsspec/tests/spec-packagebuild/rtems/if.yml
@@ -1,7 +1,8 @@
-SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+SPDX-License-Identifier: BSD-2-Clause
brief: |
Brief.
copyrights:
+- Copyright (C) 2023 Bob
- Copyright (C) 2023 embedded brains GmbH & Co. KG
definition:
default:
diff --git a/rtemsspec/tests/test-files/pkg/build/src/doc/copy-and-substitute.rst b/rtemsspec/tests/test-files/pkg/build/src/doc/copy-and-substitute.rst
new file mode 100644
index 00000000..916b6389
--- /dev/null
+++ b/rtemsspec/tests/test-files/pkg/build/src/doc/copy-and-substitute.rst
@@ -0,0 +1,21 @@
+.. SPDX-License-Identifier: CC-BY-SA-4.0
+
+.. Copyright (C) 2023 embedded brains GmbH & Co. KG
+
+${.:/push-enabled-by:and: [sphinx-builder, {not: {not: sphinx-builder}}]}
+${.:/push-enabled-by:not: sphinx-builder}
+This is disabled.
+${.:/pop-enabled-by}
+${.:/document-contract-html}
+${.:/document-contract-latex}
+${.:/document-normal-title}
+${.:/document-title-page-title}
+${.:/document-release}
+${.:/document-latex-copyright}
+${.:/document-latex-title}
+${/glossary/term:/term}
+${/glossary/term:/plural}
+${/rtems/if:/name}
+${.:/spec}
+${.:/sections:1:section}
+${.:/pop-enabled-by}
diff --git a/rtemsspec/tests/test-files/pkg/build/src/doc/copy.rst b/rtemsspec/tests/test-files/pkg/build/src/doc/copy.rst
new file mode 100644
index 00000000..f76dec5e
--- /dev/null
+++ b/rtemsspec/tests/test-files/pkg/build/src/doc/copy.rst
@@ -0,0 +1,5 @@
+.. SPDX-License-Identifier: CC-BY-SA-4.0
+
+.. Copyright (C) 2023 embedded brains GmbH & Co. KG
+
+Only copy, no ${/variable:/substitute}.
diff --git a/rtemsspec/tests/test-files/pkg/build/src/doc/index.rst b/rtemsspec/tests/test-files/pkg/build/src/doc/index.rst
new file mode 100644
index 00000000..edd05e6a
--- /dev/null
+++ b/rtemsspec/tests/test-files/pkg/build/src/doc/index.rst
@@ -0,0 +1,19 @@
+.. SPDX-License-Identifier: CC-BY-SA-4.0
+
+.. Copyright (C) 2023 embedded brains GmbH & Co. KG
+
+${.:/document-copyright}
+
+${.:/document-author}
+
+${.:/document-copyrights}
+
+${.:/document-bsd-2-clause-copyrights}
+
+${.:/document-sphinx-title}
+
+${.:/document-releases}
+
+${.:/document-contributors}
+
+${.:/document-index}
diff --git a/rtemsspec/tests/test-files/pkg/build/src/doc/some.txt b/rtemsspec/tests/test-files/pkg/build/src/doc/some.txt
new file mode 100644
index 00000000..3e715502
--- /dev/null
+++ b/rtemsspec/tests/test-files/pkg/build/src/doc/some.txt
@@ -0,0 +1 @@
+Some text.
diff --git a/rtemsspec/tests/test_packagebuild.py b/rtemsspec/tests/test_packagebuild.py
index 5ddbff41..5ad614d8 100644
--- a/rtemsspec/tests/test_packagebuild.py
+++ b/rtemsspec/tests/test_packagebuild.py
@@ -42,6 +42,7 @@ from rtemsspec.packagebuild import BuildItem, BuildItemMapper, \
from rtemsspec.packagebuildfactory import create_build_item_factory
from rtemsspec.rtems import RTEMSItemCache
from rtemsspec.specverify import verify
+import rtemsspec.sphinxbuilder
import rtemsspec.testrunner
from rtemsspec.testrunner import Executable, Report, TestRunner
from rtemsspec.tests.util import get_and_clear_log
@@ -130,6 +131,31 @@ def _gather_sections(item_cache, path, objdump, gdb):
return {}
+def _sphinx_builder_run_command(args, cwd=None, stdout=None):
+ if args == ["python3", "-msphinx", "-M", "clean", "source", "build"]:
+ return 0
+ if args == ["python3", "-msphinx", "-M", "latexpdf", "source", "build"]:
+ os.makedirs(os.path.join(cwd, "build/latex"))
+ open(os.path.join(cwd, "build/latex/document.pdf"), "w+").close()
+ return 0
+ if args == ["python3", "-msphinx", "-M", "html", "source", "build"]:
+ os.makedirs(os.path.join(cwd, "build/html"))
+ open(os.path.join(cwd, "build/html/index.html"), "w+").close()
+ return 0
+ if args == ["make", "super", "clean"]:
+ return 0
+ if args == ["make"]:
+ stdout.append("example")
+ return 0
+ if "pkg-config" in args:
+ stdout.append(" ".join(args))
+ return 0
+ if args[0].endswith("verify"):
+ stdout.append("verify")
+ return 0
+ return 1
+
+
def test_packagebuild(caplog, tmpdir, monkeypatch):
tmp_dir = Path(tmpdir)
item_cache = _create_item_cache(tmp_dir, Path("spec-packagebuild"))
@@ -444,3 +470,192 @@ def test_packagebuild(caplog, tmpdir, monkeypatch):
monkeypatch.undo()
log = get_and_clear_log(caplog)
assert f"/qdp/steps/membench: get memory benchmarks for build-label from: arch/bsp" in log
+
+ # Test SphinxBuilder
+ variant["enabled"] = ["sphinx-builder"]
+ doc = director["/qdp/steps/doc"]
+ doc.substitute("${.:/document-author}") == "embedded brains GmbH & Co. KG"
+ doc.substitute("${.:/document-year}") == "2020"
+ doc.substitute(
+ "${.:/document-copyright}") == "2020 embedded brains GmbH & Co. KG"
+ doc.item["document-copyrights"].append("Copyright (C) 2023 John Doe")
+ doc.substitute("${.:/document-author}"
+ ) == "embedded brains GmbH & Co. KG and contributors"
+ doc.substitute("${.:/document-copyright}"
+ ) == "2020 embedded brains GmbH & Co. KG and contributors"
+ doc.item["document-copyrights"].pop()
+ doc_src = director["/qdp/source/doc"]
+ doc_src.load()
+ doc_build = Path(director["/qdp/build/doc"].directory)
+ assert not (doc_build / "source" / "copy.rst").exists()
+ assert not (doc_build / "other" / "copy.rst").exists()
+ monkeypatch.setattr(rtemsspec.sphinxbuilder, "run_command",
+ _sphinx_builder_run_command)
+ director.build_package(None, None)
+ monkeypatch.undo()
+ assert (doc_build / "source" / "copy.rst").is_file()
+ assert (doc_build / "other" / "copy.rst").is_file()
+ doc_result = doc_build / "source" / "copy-and-substitute.rst"
+ with open(doc_result, "r", encoding="utf-8") as src:
+ assert src.read() == """.. SPDX-License-Identifier: CC-BY-SA-4.0
+
+.. Copyright (C) 2023 embedded brains GmbH & Co. KG
+
+. Contract
+Contract
+The Title
+The \\break \\break Title
+2
+2020 embedded brains GmbH \\& Co. KG
+The Title
+:term:`Term`
+:term:`Terms <Term>`
+:c:func:`identity`
+spec:/qdp/steps/doc
+.. _SectionHeader:
+
+Section Header
+--------------
+
+Section content:
+
+.. _SubsectionHeader:
+
+Subsection Header
+^^^^^^^^^^^^^^^^^
+
+Subsection content.
+"""
+ doc_index = doc_build / "source" / "index.rst"
+ with open(doc_index, "r", encoding="utf-8") as src:
+ assert src.read() == """.. SPDX-License-Identifier: CC-BY-SA-4.0
+
+.. Copyright (C) 2023 embedded brains GmbH & Co. KG
+
+2020 embedded brains GmbH & Co. KG
+
+embedded brains GmbH & Co. KG
+
+| © 2023 Alice
+| © 2020, 2023 embedded brains GmbH & Co. KG
+
+| © 2023 Bob
+| © 2023 embedded brains GmbH & Co. KG
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+
+The Title
+*********
+
+.. topic:: Release: 2, Date: 2020-10-26, Status: Draft
+
+ * 2020 embedded brains GmbH & Co. KG
+
+ * e
+
+.. topic:: Release: 1, Date: 1970-01-01, Status: Replaced
+
+ Initial release.
+
+.. table::
+ :class: longtable
+ :widths: 16 26 30 28
+
+ +--------------+---------------------+-------------------+-----------+
+ | Action | Name | Organization | Signature |
+ +==============+=====================+===================+===========+
+ | Written by | John Doe | Some Organization | |
+ + +---------------------+-------------------+-----------+
+ | | Foo | Bár Organization | |
+ +--------------+---------------------+-------------------+-----------+
+ | Super Action | This is a Long Name | Short | |
+ +--------------+---------------------+-------------------+-----------+
+
+.. toctree::
+ :maxdepth: 4
+ :numbered:
+
+ copy-and-substitute
+ glossary
+"""
+ doc_glossary = doc_build / "source" / "glossary.rst"
+ with open(doc_glossary, "r", encoding="utf-8") as src:
+ assert src.read() == """.. SPDX-License-Identifier: CC-BY-SA-4.0
+
+.. Copyright (C) 2023 Alice
+.. Copyright (C) 2020 embedded brains GmbH & Co. KG
+
+Terms, definitions and abbreviated terms
+****************************************
+
+.. glossary::
+ :sorted:
+
+ Term
+ This is the term.
+"""
+ doc_deployment = director["/qdp/deployment/doc"]
+ assert doc_deployment["copyrights-by-license"] == {
+ "BSD-2-Clause": [
+ "Copyright (C) 2023 Bob",
+ "Copyright (C) 2023 embedded brains GmbH & Co. KG"
+ ]
+ }
+
+ variant["enabled"] = ["sphinx-builder-2"]
+ doc_2 = director["/qdp/steps/doc-2"]
+ doc_2.substitute("${.:/document-bsd-2-clause-copyrights}") == "\n"
+
+ with pytest.raises(NotImplementedError):
+ doc_2.mapper.get_link(doc_2.mapper.item)
+
+ with doc_2.section_level(
+ ItemGetValueContext(doc_2.item, "", "", "", -1,
+ "")) as (section_level, args):
+ assert section_level == 3
+ assert args == None
+ with doc_2.section_level(
+ ItemGetValueContext(doc_2.item, "", "", "", -1,
+ "-1")) as (section_level, args):
+ assert section_level == 1
+ assert args == None
+ with doc_2.section_level(
+ ItemGetValueContext(doc_2.item, "", "", "", -1,
+ "2:mo:re")) as (section_level, args):
+ assert section_level == 4
+ assert args == "mo:re"
+
+ doc_2.item["document-components"].append({
+ "action": "foobar",
+ "add-to-index": False,
+ "value": 123
+ })
+ action_run = 0
+
+ def action(component):
+ nonlocal action_run
+ action_run += 1
+ assert component["value"] == 123
+
+ doc_2.add_component_action("foobar", action)
+ director.build_package(None, None)
+ assert action_run == 1