diff --git a/.github/scripts/check_site.py b/.github/scripts/check_site.py new file mode 100644 index 0000000..4b9bb6c --- /dev/null +++ b/.github/scripts/check_site.py @@ -0,0 +1,89 @@ +# Copyright (c) 2026 inovex GmbH +# +# SPDX-License-Identifier: Apache-2.0 +"""Check the internal links of the assembled Pages site. + +Usage: python check_site.py + +The site is served under a path prefix (https://tiacsys.github.io/zdocs/). +A static server maps a URL to a file, so the check resolves every href and +src of every HTML page against the file system, relative to the page: + +- the target file must exist (a directory needs an index.html), +- the target must stay inside the site directory (a link that climbs out + of it leaves the prefix and fails on GitHub Pages), +- a #fragment on an HTML target must name an id or a name in that page. + +A ?query is ignored. Links with a scheme (https:, mailto:, ...) and +protocol-relative links are not checked. The exit status is 1 if there is +a broken link. +""" + +import sys +from html.parser import HTMLParser +from pathlib import Path +from urllib.parse import unquote, urldefrag, urlparse + + +class Page(HTMLParser): + def __init__(self): + super().__init__() + self.links, self.ids = [], set() + + def handle_starttag(self, tag, attrs): + a = dict(attrs) + for key in ("id", "name"): + if a.get(key): + self.ids.add(a[key]) + for key in ("href", "src"): + if a.get(key): + self.links.append(a[key]) + + +_PAGES = {} + + +def parse(path): + if path not in _PAGES: + page = Page() + page.feed(path.read_text(encoding="utf-8", errors="replace")) + _PAGES[path] = page + return _PAGES[path] + + +def main(root): + root = root.resolve() + pages = sorted(root.rglob("*.html")) + broken, checked = [], 0 + for page_path in pages: + for link in parse(page_path).links: + url = urlparse(link) + if url.scheme or url.netloc or link.startswith("#"): + continue + checked += 1 + target_ref, fragment = urldefrag(link) + target_ref = target_ref.split("?", 1)[0] + if target_ref.startswith("/"): + broken.append((page_path, link, "absolute path")) + continue + target = (page_path.parent / unquote(target_ref)).resolve() + if not target.is_relative_to(root): + broken.append((page_path, link, "outside the site")) + continue + if target.is_dir(): + target = target / "index.html" + if not target.is_file(): + broken.append((page_path, link, "no such file")) + continue + if fragment and target.suffix == ".html" and unquote(fragment) not in parse(target).ids: + broken.append((page_path, link, "no such #fragment")) + print(f"check_site: {len(pages)} pages, {checked} internal links, {len(broken)} broken") + for page_path, link, reason in broken: + print(f" {page_path.relative_to(root)}: {link} ({reason})") + return 1 if broken else 0 + + +if __name__ == "__main__": + if len(sys.argv) != 2: + sys.exit(__doc__) + sys.exit(main(Path(sys.argv[1]))) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..e361c60 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,194 @@ +# Copyright (c) 2026 inovex GmbH +# +# SPDX-License-Identifier: Apache-2.0 +# +# Builds the documentation of zdocs itself (doc/: the manual and the API +# reference) and publishes it to GitHub Pages +# (https://tiacsys.github.io/zdocs/). +# +# - Pull request: build and checks only. +# - Push to main, or a manual run on main: build, checks, deploy. +# +# Not in this workflow: the engine unit tests, the zdocs-tests acceptance +# suite and twister runs. This document set has no PDF and no Doxygen +# document. +# +# One-time repository setting: Settings > Pages > Source = "GitHub Actions". + +name: Documentation + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: docs-${{ github.ref }} + cancel-in-progress: true + +env: + # zdocs runs find_package(Doxygen REQUIRED) at configure time, also for a + # document set without a Doxygen document. The job uses the same Doxygen + # as the consumers (>= 1.16 for native \verifies and \satisfies): the + # official release binary, checked by hash. Ubuntu has older packages. + DOXYGEN_VERSION: "1.16.1" + DOXYGEN_SHA256: "a56f885d37e3aae08a99f638d17bbb381224c03a878d9e2dda4f9fa4baf1d8bd" + +jobs: + build: + name: Build documentation + runs-on: ubuntu-24.04 + env: + # find_package(Zephyr) in doc/CMakeLists.txt finds Zephyr through + # ZEPHYR_BASE. West does not set it. + ZEPHYR_BASE: ${{ github.workspace }}/zephyr + steps: + # The west workspace is the job's work directory: this repository is + # the manifest repository at zdocs/, doc/west.yml adds zephyr/. + - name: Check out + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: zdocs + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: zdocs/doc/requirements-ci.txt + + - name: Install Python packages + run: python -m pip install -r zdocs/doc/requirements-ci.txt + + # dot renders the graphviz diagrams of the architecture pages. + - name: Install Graphviz + run: | + sudo apt-get update + sudo apt-get install --yes --no-install-recommends graphviz + + - name: Restore Doxygen + id: doxygen-cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/doxygen + key: doxygen-${{ env.DOXYGEN_VERSION }}-${{ env.DOXYGEN_SHA256 }} + + - name: Download Doxygen + if: steps.doxygen-cache.outputs.cache-hit != 'true' + run: | + tag="Release_${DOXYGEN_VERSION//./_}" + curl -sSfL -o doxygen.tar.gz \ + "https://github.com/doxygen/doxygen/releases/download/${tag}/doxygen-${DOXYGEN_VERSION}.linux.bin.tar.gz" + echo "${DOXYGEN_SHA256} doxygen.tar.gz" | sha256sum --check --strict + mkdir -p ~/doxygen + tar -xzf doxygen.tar.gz -C ~/doxygen --strip-components=1 + rm doxygen.tar.gz + + - name: Put Doxygen on the path + run: echo "${HOME}/doxygen/bin" >> "${GITHUB_PATH}" + + - name: Show tool versions + run: | + doxygen --version + dot -V + cmake --version | head -n 1 + python --version + west --version + python -m pip freeze + + # Zephyr is needed only as a CMake package (find_package(Zephyr + # COMPONENTS doc)) and for its doc/_extensions: no toolchain, no SDK, + # no module. A narrow, shallow update fetches Zephyr only, at depth 1. + - name: Set up the west workspace + run: | + west init -l --mf doc/west.yml zdocs + west update --narrow --fetch-opt=--depth=1 + west list + + - name: Configure + run: cmake -S zdocs/doc -B build/doc + + # Stage two of the build is all-or-nothing, so the stage-one indexes + # of every document come first. all-docs runs doc-check as its last + # step. + - name: Build the documents + run: | + cmake --build build/doc --target doc-index + cmake --build build/doc --target all-docs + + - name: Check cross-document links + run: cmake --build build/doc --target doc-check + + # The stage-two logs (html.log) hold only the warnings of the final + # build. The baseline is zero warnings. + - name: Check for warnings + run: | + if grep -nE 'WARNING|ERROR' build/doc/*/build/html.log; then + echo "::error::the documentation build has warnings" + exit 1 + fi + + # The site root is deploy/html/, as doc/manual/explanation/ + # deploy-layout.rst describes: the documents are at /. The + # landing page sits at the root, next to them. This document set has + # no Doxygen document, so the Doxygen cross-document navigation (it + # expects the deploy/ level as the root) is not in the site. + - name: Assemble the site + run: | + cp -a build/doc/deploy/html build/site + cp zdocs/doc/site/index.html build/site/index.html + + - name: Check the links of the site + run: python zdocs/.github/scripts/check_site.py build/site + + # Nothing private goes public: no local paths, no addresses, no tokens. + # The reference pages use jane@example.com and john@example.com as + # sample values, so example.com, .org and .net are accepted. + - name: Audit the site + working-directory: build/site + run: | + status=0 + refuse() { + if grep -rlIE -- "$2" .; then + echo "::error::the site contains $1" + status=1 + fi + } + refuse "a local absolute path" '/home/[^/ ]+/|/Users/|/tmp/|/wrk/' + refuse "a local server address" 'localhost:[0-9]|127\.0\.0\.1' + refuse "a token or a private key" 'ghp_|gho_|github_pat_|glpat-|AKIA[0-9A-Z]{16}|xox[baprs]-|-----BEGIN [A-Z ]*PRIVATE KEY' + if grep -rhoIE '[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}' . \ + | grep -vE '@example\.(com|org|net)$'; then + echo "::error::the site contains an e-mail address" + status=1 + fi + exit "${status}" + + - name: Upload the site + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + with: + path: build/site + + deploy: + name: Deploy to GitHub Pages + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' + needs: build + runs-on: ubuntu-24.04 + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + concurrency: + group: pages + cancel-in-progress: false + steps: + - name: Deploy + id: deployment + uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1 diff --git a/doc/requirements-ci.txt b/doc/requirements-ci.txt new file mode 100644 index 0000000..9e43226 --- /dev/null +++ b/doc/requirements-ci.txt @@ -0,0 +1,76 @@ +# Copyright (c) 2026 inovex GmbH +# +# SPDX-License-Identifier: Apache-2.0 +# +# Python packages for the documentation workflow +# (.github/workflows/docs.yml). Every package is pinned, also the indirect +# ones, so that a new release cannot change the CI build. +# +# pip install -r doc/requirements-ci.txt +# +# The engine versions are the ones in sphinx/requirements-doc.txt, and the +# document-set version is the one in doc/requirements.txt. Change a version +# here only together with the same change in those files, and with a full +# docs build. sphinx-autobuild and sphobjinv are not here: the workflow does +# not use the html-live targets or `needs: source: inventory`. + +# Workspace and Zephyr module discovery (find_package(Zephyr) runs +# zephyr_module.py, which validates module.yml files with jsonschema). +west==1.4.0 +jsonschema==4.26.0 +PyYAML==6.0.3 + +# The zdocs engine (sphinx/requirements-doc.txt). +Sphinx==9.1.0 +sphinx_rtd_theme==3.1.0 +sphinx-needs==8.3.0 +sphinxcontrib-doxylink==1.13.0 + +# This document set only (doc/requirements.txt). +sphinxcontrib-moderncmakedomain==3.29.0 + +# Pygments 2.21.0 writes quotes in highlighted code without escaping. Then +# doccheck reads the HREF_RE pattern in the viewcode page of doccheck.py as a +# dead link ("html/api/_modules/doccheck.html: dead link -> ([^"). Remove this +# pin when doccheck's HREF_RE ignores code (a separate engine fix). +Pygments==2.20.0 + +# Indirect packages (Python 3.12). Each version is the one that the zdocs +# test suites ran with, if that environment has the package. +alabaster==1.0.0 +attrs==26.1.0 +babel==2.18.0 +certifi==2026.7.22 +charset-normalizer==3.4.9 +colorama==0.4.6 +docopt==0.6.2 +docutils==0.22.4 +idna==3.18 +imagesize==2.0.0 +Jinja2==3.1.6 +jsonschema-specifications==2025.9.1 +jsonschema_rs==0.37.4 +MarkupSafe==3.0.3 +minijinja==2.22.0 +packaging==26.3 +pykwalify==1.8.0 +pyparsing==3.3.2 +python-dateutil==2.9.0.post0 +referencing==0.37.0 +requests==2.34.2 +requests-file==2.1.0 +roman-numerals==4.1.0 +rpds-py==0.30.0 +ruamel.yaml==0.19.1 +six==1.17.0 +snowballstemmer==3.1.1 +sphinx-data-viewer==0.1.5 +sphinxcontrib-applehelp==2.0.0 +sphinxcontrib-devhelp==2.0.0 +sphinxcontrib-htmlhelp==2.1.0 +sphinxcontrib-jquery==4.1 +sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-qthelp==2.0.0 +sphinxcontrib-serializinghtml==2.0.0 +typing_extensions==4.16.0 +urllib3==2.7.0 diff --git a/doc/site/index.html b/doc/site/index.html new file mode 100644 index 0000000..4b992fd --- /dev/null +++ b/doc/site/index.html @@ -0,0 +1,46 @@ + + + + + + + zdocs — Documentation + + + +
+

zdocs — Documentation

+

A documentation engine for Zephyr projects. It builds Sphinx and Doxygen documents as one set, with cross-references in both directions.

+
    +
  • zdocs Manual — tutorials, how-to guides, reference and explanation.
  • +
  • zdocs API Reference — the Python modules and the CMake commands of the engine.
  • +
+ +
+ + diff --git a/doc/west.yml b/doc/west.yml new file mode 100644 index 0000000..0c68829 --- /dev/null +++ b/doc/west.yml @@ -0,0 +1,29 @@ +# Copyright (c) 2026 inovex GmbH +# +# SPDX-License-Identifier: Apache-2.0 +# +# West manifest for the documentation workflow (.github/workflows/docs.yml). +# It is not a manifest for consumers: they add zdocs as a project of their +# own manifest. +# +# west init -l --mf doc/west.yml zdocs +# west update --narrow --fetch-opt=--depth=1 +# +# The build of doc/ needs Zephyr only as a CMake package +# (find_package(Zephyr COMPONENTS doc)) and its doc/_extensions. No +# toolchain, no SDK, no HAL. So the manifest has no `import:` key. + +manifest: + version: "0.13" + + remotes: + - name: zephyrproject + url-base: https://github.com/zephyrproject-rtos + + self: + path: zdocs + + projects: + - name: zephyr + remote: zephyrproject + revision: v4.4.1 diff --git a/sphinx/requirements-doc.txt b/sphinx/requirements-doc.txt index bb35db4..1121579 100644 --- a/sphinx/requirements-doc.txt +++ b/sphinx/requirements-doc.txt @@ -24,7 +24,7 @@ sphinx_rtd_theme==3.1.0 # -- Loaded unconditionally by zdocs_conf.py's engine_extensions --------------- # sphinx-needs: structured requirements/specifications and the traceability # graph; also the needs.json export the registry's cross-document import reads. -sphinx-needs==8.1.1 +sphinx-needs==8.3.0 # sphinxcontrib-doxylink: ::`symbol` references into the doxygen # documents. The map is derived from documents.yaml (docrefs.Refs.doxylink). sphinxcontrib-doxylink==1.13.0