diff --git a/.gitattributes b/.gitattributes
index dae59aa844a..9cd3992297e 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -2,6 +2,7 @@
*.gpg binary generated
*.bmp binary
*.base64 generated
+*.rst conflict-marker-size=100
# Mark files as "generated", i.e. no license applies to them.
# This includes output from programs, directive lists generated by grepping
diff --git a/.gitignore b/.gitignore
index 08510b0ec2e..01477579293 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,3 +40,6 @@ mkosi.local.conf
.dir-locals-2.el
.vscode/
/pkg/
+/doc-migration/.venv
+/doc-migration/build
+.venv
diff --git a/doc-migration/Makefile b/doc-migration/Makefile
new file mode 100644
index 00000000000..d0c3cbf1020
--- /dev/null
+++ b/doc-migration/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS ?=
+SPHINXBUILD ?= sphinx-build
+SOURCEDIR = source
+BUILDDIR = build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/doc-migration/README.md b/doc-migration/README.md
new file mode 100644
index 00000000000..34bad8f1f51
--- /dev/null
+++ b/doc-migration/README.md
@@ -0,0 +1,172 @@
+# Migration of Documentation from Docbook to Sphinx
+
+- [Migration of Documentation from Docbook to Sphinx](#migration-of-documentation-from-docbook-to-sphinx)
+ - [Prerequisites](#prerequisites)
+ - [Transformation Process](#transformation-process)
+ - [1. Docbook to `rst`](#1-docbook-to-rst)
+ - [2. `rst` to Sphinx](#2-rst-to-sphinx)
+ - [Sphinx Extensions](#sphinx-extensions)
+ - [sphinxcontrib-globalsubs](#sphinxcontrib-globalsubs)
+ - [Custom Sphinx Extensions](#custom-sphinx-extensions)
+ - [directive_roles.py (90% done)](#directive_rolespy-90-done)
+ - [external_man_links.py](#external_man_linkspy)
+ - [Includes](#includes)
+ - [Todo:](#todo)
+
+## Prerequisites
+
+Python dependencies for parsing docbook files and generating `rst`:
+
+- `lxml`
+
+Python dependencies for generating `html` and `man` pages from `rst`:
+
+- `sphinx`
+- `sphinxcontrib-globalsubs`
+- `furo` (The Sphinx theme)
+
+To install these (see [Sphinx Docs](https://www.sphinx-doc.org/en/master/tutorial/getting-started.html#setting-up-your-project-and-development-environment)):
+
+```sh
+# Generate a Python env:
+$ python3 -m venv .venv
+$ source .venv/bin/activate
+# Install deps
+$ python3 -m pip install -U lxml
+$ python3 -m pip install -U sphinx
+$ python3 -m pip install -U sphinxcontrib-globalsubs
+$ python3 -m pip install -U furo
+$ cd doc-migration && ./convert.sh
+```
+
+## Transformation Process
+
+You can run the entire process with `./convert.sh` in the `doc-migration` folder. The individual steps are:
+
+### 1. Docbook to `rst`
+
+Use the `main.py` script to convert a single Docbook file to `rst`:
+
+```sh
+# in the `doc-migration` folder:
+$ python3 main.py --file ../man/busctl.xml --output 'in-progress'
+```
+
+This file calls `db2rst.py` that parses Docbook elements on each file, does some string transformation to the contents of each, and glues them all back together again. It will also output info on unhandled elements, so we know whether our converter is feature complete and can achieve parity with the old docs.
+
+To run the script against all files you can use :
+
+```sh
+# in the `doc-migration` folder:
+$ python3 main.py --dir ../man --output 'in-progress'
+```
+
+> When using the script to convert all files at once in our man folder we recommend using "in-progress" folder name as our output dir so we don't end up replacing some the files that were converted and been marked as finished inside the source folder.
+
+After using the above script at least once you will get two files(`errors.json`,`successes_with_unhandled_tags.json`) in the output dir.
+
+`errors.json` will have all the files that failed to convert to rst with the respective error message for each file.
+running : `python3 main.py --errored` will only process the files that had an error and present in `errors.json`
+
+`successes_with_unhandled_tags.json` will have all the files that were converted but there were still some tags that are not defined in `db2rst.py` yet.
+
+running : `python3 main.py --unhandled-only` will only process the files that are present in `successes_with_unhandled_tags.json`
+
+This is to avoid running all files at once when we only need to work on files that are not completely processed.
+
+### 2. `rst` to Sphinx
+
+```sh
+# in the `/doc-migration` folder
+$ rm -rf build
+# ☝️ if you already have a build
+$ make html man
+```
+
+- The `html` files end up in `/doc-migration/build/html`. Open the `index.html` there to browse the docs.
+- The `man` files end up in `/doc-migration/build/man`. Preview an individual file with `$ mandoc -l build/man/busctl.1`
+
+#### Sphinx Extensions
+
+We use the following Sphinx extensions to achieve parity with the old docs:
+
+##### sphinxcontrib-globalsubs
+
+Allows referencing variables in the `global_substitutions` object in `/doc-migrations/source/conf.py` (the Sphinx config file).
+
+#### Custom Sphinx Extensions
+
+##### directive_roles.py (90% done)
+
+This is used to add custom Sphinx directives and roles to generate systemD directive lists page.
+
+To achieve the functionality exiting in `tools/make-directive-index.py` by building the Directive Index page from custom Sphinx role, here is an example:
+
+The formula for those sphinx roles is like this: `:directive:{directive_id}:{type}`
+
+For example we can use an inline Sphinx role like this:
+
+```
+ :directive:environment-variables:var:`SYSEXT_SCOPE=`
+```
+
+This will be then inserted in the SystemD directive page on build under the group `environment-variables`
+we can use the `{type}` to have more control over how this will be treated inside the Directive Index page.
+
+##### external_man_links.py
+
+This is used to create custom sphinx roles to handle external links for man pages to avoid having full urls in rst for example:
+
+`:die-net:`refentrytitle(manvolnum)` will lead to 'http://linux.die.net/man/{manvolnum}/{refentrytitle}'
+a full list of these roles can be found in [external_man_links](source/_ext/external_man_links.py).
+
+#### Includes
+
+1. Versions
+ In the Docbook files you may find lines like these: `` which would render into `Added in version 205` in the docs. This is now archived with the existing [sphinx directive ".. versionadded::"](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-versionadded) and represented as `.. versionadded:: 205` in the rst file
+
+2. Code Snippets
+ These can be included with the [literalinclude directive](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-literalinclude) when living in their own file.
+
+ Example:
+
+ ```rst
+ .. literalinclude:: ./check-os-release-simple.py
+ :language: python
+ ```
+
+ There is also the option to include a [code block](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block) directly in the rst file.
+
+ Example:
+
+ ```rst
+ .. code-block:: sh
+
+ a{sv} 3 One s Eins Two u 2 Yes b true
+
+ ```
+
+3. Text Snippets
+
+ There are a few xml files were sections of these files are reused in multiple other files. While it is no problem to include a whole other rst file the concept of only including a part of that file is a bit more tricky. You can choose to include text partial that starts after a specific text and also to stop before reaching another text. So we decided it would be best to add start and stop markers to define the section in these source files. These markers are: `.. inclusion-marker-do-not-remove` / ``So that a`` turns into:
+
+ ```rst
+ .. include:: ./standard-options.rst
+ :start-after: .. inclusion-marker-do-not-remove no-pager
+ :end-before: .. inclusion-end-marker-do-not-remove no-pager
+ ```
+
+## Todo
+
+An incomplete list.
+
+- [ ] Custom Link transformations:
+ - [ ] `custom-man.xsl`
+ - [x] `custom-html.xsl`
+- [ ] See whether `tools/tools/xml_helper.py` does anything we don’t do, this also contains useful code for:
+ - [ ] Build a man index, as in `tools/make-man-index.py`
+ - [x] Build a directives index, as in `tools/make-directive-index.py`
+ - [ ] DBUS doc generation `tools/update-dbus-docs.py`
+- [ ] See whether `tools/update-man-rules.py` does anything we don’t do
+- [ ] Make sure the `man_pages` we generate for Sphinx’s `conf.py` match the Meson rules in `man/rules/meson.build`
+- [ ] Re-implement check-api-docs
diff --git a/doc-migration/convert.sh b/doc-migration/convert.sh
new file mode 100755
index 00000000000..879ddadb6e7
--- /dev/null
+++ b/doc-migration/convert.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+# Array of XML filenames
+files=("sd_journal_get_data" "busctl" "systemd" "journalctl" "os-release")
+
+# Directory paths
+input_dir="../man"
+output_dir="source/docs"
+
+echo "---------------------"
+echo "Converting xml to rst"
+echo ""
+# Iterate over the filenames
+for file in "${files[@]}"; do
+ echo "------------------"
+ python3 main.py --dir ${input_dir} --output ${output_dir} --file "${file}.xml"
+done
+
+# Clean and build
+rm -rf build
+
+echo "--------------------"
+echo "Building Sphinx Docs"
+echo "--------------------"
+make html
diff --git a/doc-migration/db2rst.py b/doc-migration/db2rst.py
new file mode 100644
index 00000000000..5efb40478ee
--- /dev/null
+++ b/doc-migration/db2rst.py
@@ -0,0 +1,830 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# SPDX-License-Identifier: LGPL-2.1-or-later
+"""
+ DocBook to ReST converter
+ =========================
+ This script may not work out of the box, but is easy to extend.
+ If you extend it, please send me a patch: wojdyr at gmail.
+
+ Docbook has >400 elements, most of them are not supported (yet).
+ ``pydoc db2rst`` shows the list of supported elements.
+
+ In reST, inline markup can not be nested (major deficiency of reST).
+ Since it is not clear what to do with, say,
+ x
+ the script outputs incorrect (nested) reST (:sub:`*x*`)
+ and it is up to user to decide how to change it.
+
+ Usage: db2rst.py file.xml > file.rst
+
+ Ported to Python3 in 2024 by neighbourhood.ie
+
+ :copyright: 2009 by Marcin Wojdyr.
+ :license: BSD.
+"""
+
+# If this option is True, XML comment are discarded. Otherwise, they are
+# converted to ReST comments.
+# Note that ReST doesn't support inline comments. XML comments
+# are converted to ReST comment blocks, what may break paragraphs.
+from source import conf
+import lxml.etree as ET
+import re
+import sys
+import os
+from pathlib import Path
+REMOVE_COMMENTS = False
+
+# id attributes of DocBook elements are translated to ReST labels.
+# If this option is False, only labels that are used in links are generated.
+WRITE_UNUSED_LABELS = False
+
+
+# The Files have sections that are used as includes in other files
+FILES_USED_FOR_INCLUDES = ['sd_journal_get_data.xml', 'standard-options.xml',
+ 'user-system-options.xml', 'common-variables.xml', 'standard-conf.xml',
+ 'libsystemd-pkgconfig.xml', 'threads-aware.xml']
+
+# to avoid dupliate error reports
+_not_handled_tags = set()
+
+# to remember which id/labels are really needed
+_linked_ids = set()
+
+# buffer that is flushed after the end of paragraph,
+# used for ReST substitutions
+_buffer = ""
+
+_indent_next_listItem_by = 0
+
+
+def _run(input_file, output_dir):
+ sys.stderr.write("Parsing XML file `%s'...\n" % input_file)
+
+ parser = ET.XMLParser(remove_comments=REMOVE_COMMENTS, no_network=False)
+ tree = ET.parse(input_file, parser=parser)
+
+ for elem in tree.iter():
+ if elem.tag in ("xref", "link"):
+ _linked_ids.add(elem.get("linkend"))
+
+ output_file = os.path.join(output_dir, os.path.basename(
+ input_file).replace('.xml', '.rst'))
+
+ with open(output_file, 'w') as file:
+ file.write(TreeRoot(tree.getroot()).encode('utf-8').decode('utf-8'))
+
+
+def _warn(s):
+ sys.stderr.write("WARNING: %s\n" % s)
+
+
+def _supports_only(el, tags):
+ "print warning if there are unexpected children"
+ for i in el:
+ if i.tag not in tags:
+ _warn("%s/%s skipped." % (el.tag, i.tag))
+
+
+def _what(el):
+ "returns string describing the element, such as or Comment"
+ if isinstance(el.tag, str):
+ return "<%s>" % el.tag
+ elif isinstance(el, ET._Comment):
+ return "Comment"
+ else:
+ return str(el)
+
+
+def _has_only_text(el):
+ "print warning if there are any children"
+ if list(el):
+ _warn("children of %s are skipped: %s" % (_get_path(el),
+ ", ".join(_what(i) for i in el)))
+
+
+def _has_no_text(el):
+ "print warning if there is any non-blank text"
+ if el.text is not None and not el.text.isspace():
+ _warn("skipping text of <%s>: %s" % (_get_path(el), el.text))
+ for i in el:
+ if i.tail is not None and not i.tail.isspace():
+ _warn("skipping tail of <%s>: %s" % (_get_path(i), i.tail))
+
+
+def _includes(el):
+ file_path_pathlib = Path(el.get('href'))
+ file_extension = file_path_pathlib.suffix
+ include_files = FILES_USED_FOR_INCLUDES
+ if file_extension == '.xml':
+ if el.get('href') == 'version-info.xml':
+ versionString = conf.global_substitutions.get(
+ el.get("xpointer"))
+ # `\n\n \n\n ` forces a newline and subsequent indent.
+ # The empty spaces are stripped later
+ return f".. only:: html\n\n \n\n .. versionadded:: {versionString}\n\n "
+ elif not el.get("xpointer"):
+ return f".. include:: ../includes/{el.get('href').replace('xml', 'rst')}"
+ elif el.get('href') in include_files:
+ return f""".. include:: ../includes/{el.get('href').replace('xml', 'rst')}
+ :start-after: .. inclusion-marker-do-not-remove {el.get("xpointer")}
+ :end-before: .. inclusion-end-marker-do-not-remove {el.get("xpointer")}
+ """
+
+ elif file_extension == '.c':
+ return f""".. literalinclude:: /code-examples/c/{el.get('href')}
+ :language: c
+ """
+ elif file_extension == '.py':
+ return f""".. literalinclude:: /code-examples/py/{el.get('href')}
+ :language: python
+ """
+ elif file_extension == '.sh':
+ return f""".. literalinclude:: /code-examples/sh/{el.get('href')}
+ :language: shell
+ """
+
+
+def _conv(el):
+ "element to string conversion; usually calls element_name() to do the job"
+ if el.tag in globals():
+ s = globals()[el.tag](el)
+ assert s, "Error: %s -> None\n" % _get_path(el)
+ return s
+ elif isinstance(el, ET._Comment):
+ return Comment(el) if (el.text and not el.text.isspace()) else ""
+ else:
+ if el.tag not in _not_handled_tags:
+ # Convert version references to `versionAdded` directives
+ if el.tag == "{http://www.w3.org/2001/XInclude}include":
+ return _includes(el)
+ else:
+ _warn("Don't know how to handle <%s>" % el.tag)
+ _warn(" ... from path: %s" % _get_path(el))
+ _not_handled_tags.add(el.tag)
+ return _concat(el)
+
+
+def _no_special_markup(el):
+ return _concat(el)
+
+
+def _remove_indent_and_escape(s, tag):
+ if tag == "programlisting":
+ return s
+ "remove indentation from the string s, escape some of the special chars"
+ s = "\n".join(i.lstrip().replace("\\", "\\\\") for i in s.splitlines())
+ # escape inline mark-up start-string characters (even if there is no
+ # end-string, docutils show warning if the start-string is not escaped)
+ # TODO: handle also Unicode: ‘ “ ’ « ¡ ¿ as preceding chars
+ s = re.sub(r"([\s'\"([{ 0 and not s[-1].isspace() and i.tail[0] in " \t":
+ s += i.tail[0]
+ s += _remove_indent_and_escape(i.tail, el.tag)
+ return s.strip()
+
+
+def _original_xml(el):
+ return ET.tostring(el, with_tail=False).decode('utf-8')
+
+
+def _no_markup(el):
+ s = ET.tostring(el, with_tail=False).decode('utf-8')
+ s = re.sub(r"<.+?>", " ", s) # remove tags
+ s = re.sub(r"\s+", " ", s) # replace all blanks with single space
+ return s
+
+
+def _get_level(el):
+ "return number of ancestors"
+ return sum(1 for i in el.iterancestors())
+
+
+def _get_path(el):
+ t = [el] + list(el.iterancestors())
+ return "/".join(str(i.tag) for i in reversed(t))
+
+
+def _make_title(t, level, indentLevel=0):
+ t = t.replace('\n', ' ').strip()
+
+ if level == 1:
+ return "\n\n" + "=" * len(t) + "\n" + t + "\n" + "=" * len(t)
+
+ char = ["#", "=", "-", "~", "^", "."]
+ underline = char[level-2] * len(t)
+ indentation = " "*indentLevel
+ return f"\n\n{indentation}{t}\n{indentation}{underline}"
+
+
+def _join_children(el, sep):
+ _has_no_text(el)
+ return sep.join(_conv(i) for i in el)
+
+
+def _block_separated_with_blank_line(el):
+ s = ""
+ id = el.get("id")
+ if id is not None:
+ s += "\n\n.. inclusion-marker-do-not-remove %s\n\n" % id
+ s += "\n\n" + _concat(el)
+ if id is not None:
+ s += "\n\n.. inclusion-end-marker-do-not-remove %s\n\n" % id
+ return s
+
+
+def _indent(el, indent, first_line=None, suppress_blank_line=False):
+ "returns indented block with exactly one blank line at the beginning"
+ start = "\n\n"
+ if suppress_blank_line:
+ start = ""
+
+ # lines = [" "*indent + i for i in _concat(el).splitlines()
+ # if i and not i.isspace()]
+ # TODO: This variant above strips empty lines within elements. We don’t want that to happen, at least not always
+ lines = [" "*indent + i for i in _concat(el).splitlines()
+ if i]
+ if first_line is not None:
+ # replace indentation of the first line with prefix `first_line'
+ lines[0] = first_line + lines[0][indent:]
+ return start + "\n".join(lines)
+
+
+def _normalize_whitespace(s):
+ return " ".join(s.split())
+
+################### DocBook elements #####################
+
+# special "elements"
+
+
+def TreeRoot(el):
+ output = _conv(el)
+ # add .. SPDX-License-Identifier: LGPL-2.1-or-later:
+ output = '\n\n'.join(
+ ['.. SPDX-License-Identifier: LGPL-2.1-or-later:', output])
+ # remove trailing whitespace
+ output = re.sub(r"[ \t]+\n", "\n", output)
+ # leave only one blank line
+ output = re.sub(r"\n{3,}", "\n\n", output)
+ return output
+
+
+def Comment(el):
+ return _indent(el, 12, ".. COMMENT: ")
+
+# Meta refs
+
+
+def refentry(el):
+ return _concat(el)
+
+# FIXME: how to ignore/delete a tag???
+
+
+def refentryinfo(el):
+ # ignore
+ return ' '
+
+
+def refnamediv(el):
+ # return '**Name** \n\n' + _make_title(_join_children(el, ' — '), 2)
+ return '.. only:: html\n\n' + _make_title(_join_children(el, ' — '), 2, 3)
+
+
+def refsynopsisdiv(el):
+ # return '**Synopsis** \n\n' + _make_title(_join_children(el, ' '), 3)
+ s = ""
+ s += _make_title('Synopsis', 2, 3)
+ s += '\n\n'
+ s += _join_children(el, ', ')
+ return s
+
+
+def refname(el):
+ _has_only_text(el)
+ return "%s" % el.text
+
+
+def refpurpose(el):
+ _has_only_text(el)
+ return "%s" % el.text
+
+
+def cmdsynopsis(el):
+ return _join_children(el, ' ')
+
+
+def arg(el):
+ text = el.text
+ if text is None:
+ text = _join_children(el, '')
+ # choice: req, opt, plain
+ choice = el.get("choice")
+ if choice == 'opt':
+ return f"[%s{'...' if el.get('rep') == 'repeat' else ''}]" % text
+ elif choice == 'req':
+ return "{%s}" % text
+ elif choice == 'plain':
+ return "%s" % text
+ else:
+ "print warning if there another choice"
+ _warn("skipping arg with choice of: %s" % (choice))
+
+
+# general inline elements
+
+def emphasis(el):
+ return "*%s*" % _concat(el).strip()
+
+
+phrase = emphasis
+citetitle = emphasis
+
+
+acronym = _no_special_markup
+
+
+def command(el):
+ # Only enclose in backticks if it’s not part of a term
+ # (which is already enclosed in backticks)
+ isInsideTerm = False
+ for term in el.iterancestors(tag='term'):
+ isInsideTerm = True
+
+ if isInsideTerm:
+ return _concat(el).strip()
+ return "``%s``" % _concat(el).strip()
+
+
+def literal(el):
+ return "\"%s\"" % _concat(el).strip()
+
+
+def varname(el):
+ isInsideTerm = False
+ for term in el.iterancestors(tag='term'):
+ isInsideTerm = True
+
+ if isInsideTerm:
+ return _concat(el).strip()
+
+ classname = ''
+ for varlist in el.iterancestors(tag='variablelist'):
+ if varlist.attrib.get('class', '') != '':
+ classname = varlist.attrib['class']
+ if len(classname) > 0:
+ return f":directive:{classname}:var:`%s`" % _concat(el).strip()
+ return "``%s``" % _concat(el).strip()
+
+
+def option(el):
+ isInsideTerm = False
+ for term in el.iterancestors(tag='term'):
+ isInsideTerm = True
+
+ if isInsideTerm:
+ return _concat(el).strip()
+
+ classname = ''
+ for varlist in el.iterancestors(tag='variablelist'):
+ if varlist.attrib.get('class', '') != '':
+ classname = varlist.attrib['class']
+ if len(classname) > 0:
+ return f":directive:{classname}:option:`%s`" % _concat(el).strip()
+ return "``%s``" % _concat(el).strip()
+
+
+def constant(el):
+ isInsideTerm = False
+ for term in el.iterancestors(tag='term'):
+ isInsideTerm = True
+
+ if isInsideTerm:
+ return _concat(el).strip()
+
+ classname = ''
+ for varlist in el.iterancestors(tag='variablelist'):
+ if varlist.attrib.get('class', '') != '':
+ classname = varlist.attrib['class']
+ if len(classname) > 0:
+ return f":directive:{classname}:constant:`%s`" % _concat(el).strip()
+ return "``%s``" % _concat(el).strip()
+
+
+filename = command
+
+
+def optional(el):
+ return "[%s]" % _concat(el).strip()
+
+
+def replaceable(el):
+ return "<%s>" % _concat(el).strip()
+
+
+def term(el):
+ if el.getparent().index(el) != 0:
+ return ' '
+
+ level = _get_level(el)
+ if level > 5:
+ level = 5
+ # Sometimes, there are multiple terms for one entry. We want those displayed in a single line, so we gather them all up and parse them together
+ hasMultipleTerms = False
+ titleStrings = [_concat(el).strip()]
+ title = ''
+ for term in el.itersiblings(tag='term'):
+ # We only arrive here if there is more than one `` in the `el`
+ hasMultipleTerms = True
+ titleStrings.append(_concat(term).strip())
+
+ if hasMultipleTerms:
+ title = ', '.join(titleStrings)
+ # return _make_title(f"``{titleString}``", 4)
+ else:
+ title = _concat(el).strip()
+
+ if level >= 5:
+ global _indent_next_listItem_by
+ _indent_next_listItem_by += 3
+ return f".. option:: {title}\n\n \n\n "
+ return _make_title(f"``{title}``", level) + '\n\n'
+
+# links
+
+
+def ulink(el):
+ url = el.get("url")
+ text = _concat(el).strip()
+ if text.startswith(".. image::"):
+ return "%s\n :target: %s\n\n" % (text, url)
+ elif url == text:
+ return text
+ elif not text:
+ return "`<%s>`_" % (url)
+ else:
+ return "`%s <%s>`_" % (text, url)
+
+# TODO: other elements are ignored
+
+
+def xref(el):
+ _has_no_text(el)
+ id = el.get("linkend")
+ return ":ref:`%s`" % id if id in _linked_ids else ":ref:`%s <%s>`" % (id, id)
+
+
+def link(el):
+ _has_no_text(el)
+ return "`%s`_" % el.get("linkend")
+
+
+# lists
+
+def itemizedlist(el):
+ return _indent(el, 2, "* ", True)
+
+
+def orderedlist(el):
+ return _indent(el, 2, "1. ", True)
+
+
+def simplelist(el):
+ type = el.get("type")
+ if type == "inline":
+ return _join_children(el, ', ')
+ else:
+ return _concat(el)
+
+
+def member(el):
+ return _concat(el)
+
+# varlists
+
+
+def variablelist(el):
+ return _concat(el)
+
+
+def varlistentry(el):
+ s = ""
+ id = el.get("id")
+ if id is not None:
+ s += "\n\n.. inclusion-marker-do-not-remove %s\n\n" % id
+ for i in el:
+ if i.tag == 'term':
+ s += _conv(i) + '\n\n'
+ else:
+ # Handle nested list items, this is mainly for
+ # options that have options
+ if i.tag == 'listitem':
+ global _indent_next_listItem_by
+ s += _indent(i, _indent_next_listItem_by, None, True)
+ _indent_next_listItem_by = 0
+ else:
+ s += _indent(i, 0, None, True)
+ if id is not None:
+ s += "\n\n.. inclusion-end-marker-do-not-remove %s\n\n" % id
+ return s
+
+
+def listitem(el):
+ _supports_only(
+ el, ["para", "simpara", "{http://www.w3.org/2001/XInclude}include"])
+ return _block_separated_with_blank_line(el)
+
+# sections
+
+
+def example(el):
+ # FIXME: too hacky?
+ elements = [i for i in el]
+ first, rest = elements[0], elements[1:]
+
+ return _make_title(_concat(first), 4) + "\n\n" + "".join(_conv(i) for i in rest)
+
+
+def sect1(el):
+ return _block_separated_with_blank_line(el)
+
+
+def sect2(el):
+ return _block_separated_with_blank_line(el)
+
+
+def sect3(el):
+ return _block_separated_with_blank_line(el)
+
+
+def sect4(el):
+ return _block_separated_with_blank_line(el)
+
+
+def section(el):
+ return _block_separated_with_blank_line(el)
+
+
+def title(el):
+ return _make_title(_concat(el).strip(), _get_level(el) + 1)
+
+# bibliographic elements
+
+
+def author(el):
+ _has_only_text(el)
+ return "\n\n.. _author:\n\n**%s**" % el.text
+
+
+def date(el):
+ _has_only_text(el)
+ return "\n\n.. _date:\n\n%s" % el.text
+
+# references
+
+
+def citerefentry(el):
+ project = el.get("project")
+ refentrytitle = el.xpath("refentrytitle")[0].text
+ manvolnum = el.xpath("manvolnum")[0].text
+
+ extlink_formats = {
+ 'man-pages': f':man-pages:`{refentrytitle}({manvolnum})`',
+ 'die-net': f':die-net:`{refentrytitle}({manvolnum})`',
+ 'mankier': f':mankier:`{refentrytitle}({manvolnum})`',
+ 'archlinux': f':archlinux:`{refentrytitle}({manvolnum})`',
+ 'debian': f':debian:`{refentrytitle}({manvolnum})`',
+ 'freebsd': f':freebsd:`{refentrytitle}({manvolnum})`',
+ 'dbus': f':dbus:`{refentrytitle}({manvolnum})`',
+ }
+
+ if project in extlink_formats:
+ return extlink_formats[project]
+
+ if project == 'url':
+ url = el.get("url")
+ return f"`{refentrytitle}({manvolnum}) <{url}>`_"
+
+ return f":ref:`{refentrytitle}({manvolnum})`"
+
+
+def refmeta(el):
+ refentrytitle = el.find('refentrytitle').text
+ manvolnum = el.find('manvolnum').text
+
+ meta_title = f":title: {refentrytitle}"
+
+ meta_manvolnum = f":manvolnum: {manvolnum}"
+
+ doc_title = ".. _%s:" % _join_children(
+ el, '') + '\n\n' + _make_title(_join_children(el, ''), 1)
+
+ return '\n\n'.join([meta_title, meta_manvolnum, doc_title])
+
+
+def refentrytitle(el):
+ if el.get("url"):
+ return ulink(el)
+ else:
+ return _concat(el)
+
+
+def manvolnum(el):
+ return "(%s)" % el.text
+
+# media objects
+
+
+def imageobject(el):
+ return _indent(el, 3, ".. image:: ", True)
+
+
+def imagedata(el):
+ _has_no_text(el)
+ return el.get("fileref")
+
+
+def videoobject(el):
+ return _indent(el, 3, ".. raw:: html\n\n", True)
+
+
+def videodata(el):
+ _has_no_text(el)
+ src = el.get("fileref")
+ return ' '
+
+
+def programlisting(el):
+ xi_include = el.find('.//{http://www.w3.org/2001/XInclude}include')
+ if xi_include is not None:
+ return _includes(xi_include)
+ else:
+ return f"\n\n.. code-block:: sh\n\n \n\n{_indent(el, 3)}\n\n"
+
+
+def screen(el):
+ return _indent(el, 3, "::\n\n", False) + "\n\n"
+
+
+def synopsis(el):
+ return _indent(el, 3, "::\n\n", False) + "\n\n"
+
+
+def funcsynopsis(el):
+ return _concat(el)
+
+
+def funcsynopsisinfo(el):
+ return "``%s``" % _concat(el)
+
+
+def funcprototype(el):
+ funcdef = ''.join(el.find('.//funcdef').itertext())
+ params = el.findall('.//paramdef')
+ param_list = [''.join(param.itertext()) for param in params]
+ s = ".. code-block:: \n\n "
+ s += f"{funcdef}("
+ s += ",\n\t".join(param_list)
+ s += ");"
+ return s
+
+
+def paramdef(el):
+ return el
+
+
+def funcdef(el):
+ return el
+
+
+def function(el):
+ return _concat(el).strip()
+
+
+def parameter(el):
+ return el
+
+
+def table(el):
+ title = _concat(el.find('title'))
+ headers = el.findall('.//thead/row/entry')
+ rows = el.findall('.//tbody/row')
+
+ # Collect header names
+ header_texts = [_concat(header) for header in headers]
+
+ # Collect row data
+ row_data = []
+ for row in rows:
+ entries = row.findall('entry')
+ row_data.append([_concat(entry) for entry in entries])
+
+ # Create the table in reST list-table format
+ rst_table = []
+ rst_table.append(f".. list-table:: {title}")
+ rst_table.append(" :header-rows: 1")
+ rst_table.append("")
+
+ # Add header row
+ header_line = " * - " + "\n - ".join(header_texts)
+ rst_table.append(header_line)
+
+ # Add rows
+ for row in row_data:
+ row_line = " * - " + "\n - ".join(row)
+ rst_table.append(row_line)
+
+ return '\n'.join(rst_table)
+
+
+def userinput(el):
+ return _indent(el, 3, "\n\n")
+
+
+def computeroutput(el):
+ return _indent(el, 3, "\n\n")
+
+
+# miscellaneous
+def keycombo(el):
+ return _join_children(el, ' + ')
+
+
+def keycap(el):
+ return ":kbd:`%s`" % el.text
+
+
+def warning(el):
+ return ".. warning::`%s`" % el.text
+
+
+def para(el):
+ return _block_separated_with_blank_line(el) + '\n\n \n\n'
+
+
+def simpara(el):
+ return _block_separated_with_blank_line(el)
+
+
+def important(el):
+ return _indent(el, 3, ".. note:: ", True)
+
+
+def itemizedlist(el):
+ return _indent(el, 2, "* ", True)
+
+
+def orderedlist(el):
+ return _indent(el, 2, "1. ", True)
+
+
+def refsect1(el):
+ return _block_separated_with_blank_line(el)
+
+
+def refsect2(el):
+ return _block_separated_with_blank_line(el)
+
+
+def refsect3(el):
+ return _block_separated_with_blank_line(el)
+
+
+def refsect4(el):
+ return _block_separated_with_blank_line(el)
+
+
+def refsect5(el):
+ return _block_separated_with_blank_line(el)
+
+
+def convert_xml_to_rst(xml_file_path, output_dir):
+ try:
+ _run(xml_file_path, output_dir)
+ return list(_not_handled_tags), ''
+ except Exception as e:
+ _warn('Failed to convert file %s' % xml_file_path)
+ return [], str(e)
diff --git a/doc-migration/main.py b/doc-migration/main.py
new file mode 100644
index 00000000000..fbd289293f7
--- /dev/null
+++ b/doc-migration/main.py
@@ -0,0 +1,179 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+import os
+import json
+import argparse
+from typing import List
+from db2rst import convert_xml_to_rst
+
+FILES_USED_FOR_INCLUDES = [
+ 'sd_journal_get_data.xml', 'standard-options.xml', 'user-system-options.xml',
+ 'common-variables.xml', 'standard-conf.xml', 'libsystemd-pkgconfig.xml', 'threads-aware.xml'
+]
+
+INCLUDES_DIR = "includes"
+
+
+def load_files_from_json(json_path: str) -> List[str]:
+ """
+ Loads a list of filenames from a JSON file.
+
+ Parameters:
+ json_path (str): Path to the JSON file.
+
+ Returns:
+ List[str]: List of filenames.
+ """
+ if not os.path.isfile(json_path):
+ print(f"Error: The file '{json_path}' does not exist.")
+ return []
+
+ with open(json_path, 'r') as json_file:
+ data = json.load(json_file)
+
+ return [entry['file'] for entry in data]
+
+
+def update_json_file(json_path: str, updated_entries: List[dict]) -> None:
+ """
+ Updates a JSON file with new entries.
+
+ Parameters:
+ json_path (str): Path to the JSON file.
+ updated_entries (List[dict]): List of updated entries to write to the JSON file.
+ """
+ with open(json_path, 'w') as json_file:
+ json.dump(updated_entries, json_file, indent=4)
+
+
+def process_xml_files_in_directory(dir: str, output_dir: str, specific_file: str = None, errored: bool = False, unhandled_only: bool = False) -> None:
+ """
+ Processes all XML files in a specified directory, logs results to a JSON file.
+
+ Parameters:
+ dir (str): Path to the directory containing XML files.
+ output_dir (str): Path to the JSON file for logging results.
+ specific_file (str, optional): Specific XML file to process. Defaults to None.
+ errored (bool, optional): Flag to process only files listed in errors.json. Defaults to False.
+ unhandled_only (bool, optional): Flag to process only files listed in successes_with_unhandled_tags.json. Defaults to False.
+ """
+ files_output_dir = os.path.join(output_dir, "files")
+ includes_output_dir = os.path.join(output_dir, INCLUDES_DIR)
+ os.makedirs(files_output_dir, exist_ok=True)
+ os.makedirs(includes_output_dir, exist_ok=True)
+
+ files_to_process = []
+
+ if errored:
+ errors_json_path = os.path.join(output_dir, "errors.json")
+ files_to_process = load_files_from_json(errors_json_path)
+ if not files_to_process:
+ print("No files to process from errors.json. Exiting.")
+ return
+ elif unhandled_only:
+ unhandled_json_path = os.path.join(
+ output_dir, "successes_with_unhandled_tags.json")
+ files_to_process = load_files_from_json(unhandled_json_path)
+ if not files_to_process:
+ print("No files to process from successes_with_unhandled_tags.json. Exiting.")
+ return
+ elif specific_file:
+ specific_file_path = os.path.join(dir, specific_file)
+ if os.path.isfile(specific_file_path):
+ files_to_process = [specific_file]
+ else:
+ print(f"Error: The file '{
+ specific_file}' does not exist in the directory '{dir}'.")
+ return
+ else:
+ files_to_process = [f for f in os.listdir(dir) if f.endswith(".xml")]
+
+ errors_json_path = os.path.join(output_dir, "errors.json")
+ unhandled_json_path = os.path.join(
+ output_dir, "successes_with_unhandled_tags.json")
+
+ existing_errors = []
+ existing_unhandled = []
+
+ if os.path.exists(errors_json_path):
+ with open(errors_json_path, 'r') as json_file:
+ existing_errors = json.load(json_file)
+
+ if os.path.exists(unhandled_json_path):
+ with open(unhandled_json_path, 'r') as json_file:
+ existing_unhandled = json.load(json_file)
+
+ updated_errors = []
+ updated_successes_with_unhandled_tags = []
+
+ for filename in files_to_process:
+ filepath = os.path.join(dir, filename)
+ output_subdir = includes_output_dir if filename in FILES_USED_FOR_INCLUDES else files_output_dir
+ print('converting file: ', filename)
+ try:
+ unhandled_tags, error = convert_xml_to_rst(filepath, output_subdir)
+ if error:
+ result = {
+ "file": filename,
+ "status": "error",
+ "unhandled_tags": unhandled_tags,
+ "error": error
+ }
+ updated_errors.append(result)
+ else:
+ result = {
+ "file": filename,
+ "status": "success",
+ "unhandled_tags": unhandled_tags,
+ "error": error
+ }
+ if len(unhandled_tags) > 0:
+ updated_successes_with_unhandled_tags.append(result)
+
+ existing_errors = [
+ entry for entry in existing_errors if entry['file'] != filename]
+ existing_unhandled = [
+ entry for entry in existing_unhandled if entry['file'] != filename]
+
+ except Exception as e:
+ result = {
+ "file": filename,
+ "status": "error",
+ "unhandled_tags": [],
+ "error": str(e)
+ }
+ updated_errors.append(result)
+
+ if not errored:
+ updated_errors += existing_errors
+
+ if not unhandled_only:
+ updated_successes_with_unhandled_tags += existing_unhandled
+
+ update_json_file(errors_json_path, updated_errors)
+ update_json_file(unhandled_json_path,
+ updated_successes_with_unhandled_tags)
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Process XML files and save results to a directory.")
+ parser.add_argument(
+ "--dir", type=str, help="Path to the directory containing XML files.", default="../man")
+ parser.add_argument(
+ "--output", type=str, help="Path to the output directory for results and log files.", default="in-progress")
+ parser.add_argument(
+ "--file", type=str, help="If provided, the script will only process the specified file.", default=None)
+ parser.add_argument("--errored", action='store_true',
+ help="Process only files listed in errors.json.")
+ parser.add_argument("--unhandled-only", action='store_true',
+ help="Process only files listed in successes_with_unhandled_tags.json.")
+
+ args = parser.parse_args()
+
+ process_xml_files_in_directory(
+ args.dir, args.output, args.file, args.errored, args.unhandled_only)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/doc-migration/source/_ext/autogen_index.py b/doc-migration/source/_ext/autogen_index.py
new file mode 100644
index 00000000000..80d26bfb92b
--- /dev/null
+++ b/doc-migration/source/_ext/autogen_index.py
@@ -0,0 +1,63 @@
+import os
+from sphinx.application import Sphinx
+from sphinx.util.console import bold
+from sphinx.util.typing import ExtensionMetadata
+
+
+def generate_toctree(app: Sphinx):
+ root_dir = app.srcdir
+
+ index_path = os.path.join(root_dir, 'index.rst')
+ if not os.path.exists(index_path):
+ app.logger.warning(
+ f"{index_path} does not exist, skipping generation.")
+ return
+
+ with open(index_path, 'w') as index_file:
+ index_file.write(""".. SPDX-License-Identifier: LGPL-2.1-or-later
+.. systemd documentation master file, created by
+ sphinx-quickstart on Wed Jun 26 16:24:13 2024.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+systemd — System and Service Manager
+===================================
+
+.. manual reference to a doc by its reference label
+ see: https://www.sphinx-doc.org/en/master/usage/referencing.html#cross-referencing-arbitrary-locations
+.. Manual links
+.. ------------
+.. :ref:`busctl(1)`
+.. :ref:`systemd(1)`
+.. OR using the toctree to pull in files
+ https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-toctree
+.. This only works if we restructure our headings to match
+ https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#sections
+ and then only have single top-level heading with the command name
+
+.. toctree::
+ :maxdepth: 1\n
+""")
+
+ for subdir, _, files in os.walk(root_dir + '/docs'):
+ if subdir == root_dir:
+ continue
+ for file in files:
+ if file.endswith('.rst'):
+ file_path = os.path.relpath(
+ os.path.join(subdir, file), root_dir)
+ # remove the .rst extension
+ index_file.write(f" {file_path[:-4]}\n")
+
+ index_file.write("""
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search` """)
+
+
+def setup(app: Sphinx) -> ExtensionMetadata:
+ app.connect('builder-inited', generate_toctree)
+ return {'version': '0.1', 'parallel_read_safe': True, 'parallel_write_safe': True, }
diff --git a/doc-migration/source/_ext/directive_roles.py b/doc-migration/source/_ext/directive_roles.py
new file mode 100644
index 00000000000..012ad19747d
--- /dev/null
+++ b/doc-migration/source/_ext/directive_roles.py
@@ -0,0 +1,196 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+from __future__ import annotations
+from typing import List, Dict, Any
+from docutils import nodes
+
+from sphinx.locale import _
+from sphinx.application import Sphinx
+from sphinx.util.docutils import SphinxRole, SphinxDirective
+from sphinx.util.typing import ExtensionMetadata
+
+
+class directive_list(nodes.General, nodes.Element):
+ pass
+
+
+class InlineDirectiveRole(SphinxRole):
+ def run(self) -> tuple[List[nodes.Node], List[nodes.system_message]]:
+ target_id = f'directive-{self.env.new_serialno("directive")}-{
+ self.text}'
+
+ target_node = nodes.target('', self.text, ids=[target_id])
+
+ if not hasattr(self.env, 'directives'):
+ self.env.directives = []
+
+ self.env.directives.append({
+ 'name': self.name,
+ 'text': self.text,
+ 'docname': self.env.docname,
+ 'lineno': self.lineno,
+ 'target_id': target_id,
+ })
+
+ return [target_node], []
+
+
+class ListDirectiveRoles(SphinxDirective):
+ def run(self) -> List[nodes.Node]:
+ return [directive_list('')]
+
+
+def register_directive_roles(app: Sphinx) -> None:
+ directives_data: List[Dict[str, Any]] = app.config.directives_data
+ role_types: List[str] = app.config.role_types
+
+ for directive in directives_data:
+ dir_id: str = directive['id']
+ for role_type in role_types:
+ role_name = f'directive:{dir_id}:{role_type}'
+ app.add_role(role_name, InlineDirectiveRole())
+
+
+def get_directive_metadata(app: Sphinx) -> Dict[str, Dict[str, Any]]:
+ directives_data: List[Dict[str, Any]] = app.config.directives_data
+ return {directive['id']: directive for directive in directives_data}
+
+
+def group_directives_by_id(env) -> Dict[str, List[Dict[str, Any]]]:
+ grouped_directives: Dict[str, List[Dict[str, Any]]] = {}
+ for dir_info in getattr(env, 'directives', []):
+ dir_id = dir_info['name'].split(':')[1]
+ if dir_id not in grouped_directives:
+ grouped_directives[dir_id] = []
+ grouped_directives[dir_id].append(dir_info)
+ return grouped_directives
+
+
+def create_reference_node(app: Sphinx, dir_info: Dict[str, Any], from_doc_name: str) -> nodes.reference:
+ ref_node = nodes.reference('', '')
+ ref_node['refdocname'] = dir_info['docname']
+ ref_node['refuri'] = app.builder.get_relative_uri(
+ from_doc_name, dir_info['docname']) + '#' + dir_info['target_id']
+
+ metadata: Dict[str, Any] = app.builder.env.metadata.get(
+ dir_info['docname'], {})
+ title: str = metadata.get('title', 'Unknown Title')
+ manvolnum: str = metadata.get('manvolnum', 'Unknown Volume')
+
+ ref_node.append(nodes.Text(f'{title}({manvolnum})'))
+ return ref_node
+
+
+def render_reference_node(references: List[nodes.reference]) -> nodes.paragraph:
+ para = nodes.inline()
+
+ for i, ref_node in enumerate(references):
+ para += ref_node
+ if i < len(references) - 1:
+ para += nodes.Text(", ")
+
+ return para
+
+
+def render_option(directive_text: str, references: List[nodes.reference]) -> nodes.section:
+ section = nodes.section()
+
+ title = nodes.title(text=directive_text, classes=['directive-header'])
+ title_id = nodes.make_id(directive_text)
+ title['ids'] = [title_id]
+ title['names'] = [directive_text]
+ section['ids'] = [title_id]
+ section += title
+
+ node = render_reference_node(references)
+ section += node
+
+ return section
+
+
+def render_variable(directive_text: str, references: List[nodes.reference]) -> nodes.section:
+ section = nodes.section()
+
+ title = nodes.title(text=directive_text, classes=['directive-header'])
+ title_id = nodes.make_id(directive_text)
+ title['ids'] = [title_id]
+ title['names'] = [directive_text]
+ section['ids'] = [title_id]
+ section += title
+
+ node = render_reference_node(references)
+ section += node
+
+ return section
+
+
+def render_constant(directive_text: str, references: List[nodes.reference]) -> nodes.section:
+ section = nodes.section()
+
+ title = nodes.title(text=directive_text, classes=['directive-header'])
+ title_id = nodes.make_id(directive_text)
+ title['ids'] = [title_id]
+ title['names'] = [directive_text]
+ section['ids'] = [title_id]
+ section += title
+
+ node = render_reference_node(references)
+ section += node
+
+ return section
+
+
+def process_items(app: Sphinx, doctree: nodes.document, from_doc_name: str) -> None:
+ env = app.builder.env
+ directive_lookup: Dict[str, Dict[str, Any]] = get_directive_metadata(app)
+ grouped_directives: Dict[str, List[Dict[str, Any]]
+ ] = group_directives_by_id(env)
+
+ render_map = {
+ 'option': render_option,
+ 'var': render_variable,
+ 'constant': render_constant,
+ }
+
+ for node in doctree.findall(directive_list):
+ content: List[nodes.section] = []
+
+ for dir_id, directives in grouped_directives.items():
+ directive_meta = directive_lookup.get(
+ dir_id, {'title': 'Unknown', 'description': 'No description available.'})
+ section = nodes.section(ids=[dir_id])
+ section += nodes.title(text=directive_meta['title'])
+ section += nodes.paragraph(text=directive_meta['description'])
+
+ directive_references: Dict[str, List[nodes.reference]] = {}
+
+ for dir_info in directives:
+ directive_text: str = dir_info['text']
+ role_type: str = dir_info['name'].split(':')[-1]
+
+ if directive_text not in directive_references:
+ directive_references[directive_text] = []
+
+ ref_node = create_reference_node(app, dir_info, from_doc_name)
+ directive_references[directive_text].append(ref_node)
+
+ for directive_text, references in directive_references.items():
+ render_fn = render_map.get(role_type, render_option)
+ rendered_section = render_fn(directive_text, references)
+ section += rendered_section
+
+ content.append(section)
+ node.replace_self(content)
+
+
+def setup(app: Sphinx) -> ExtensionMetadata:
+ app.add_config_value('directives_data', [], 'env')
+ app.add_config_value('role_types', [], 'env')
+
+ register_directive_roles(app)
+ app.add_directive('list_directive_roles', ListDirectiveRoles)
+ app.connect('doctree-resolved', process_items)
+ return {
+ 'version': '0.1',
+ 'parallel_read_safe': True,
+ 'parallel_write_safe': True,
+ }
diff --git a/doc-migration/source/_ext/external_man_links.py b/doc-migration/source/_ext/external_man_links.py
new file mode 100644
index 00000000000..4bc2104268d
--- /dev/null
+++ b/doc-migration/source/_ext/external_man_links.py
@@ -0,0 +1,59 @@
+from typing import List, Dict, Tuple, Any
+from docutils import nodes
+from docutils.parsers.rst import roles, states
+import re
+
+# Define the extlink_formats dictionary with type annotations
+extlink_formats: Dict[str, str] = {
+ 'man-pages': 'https://man7.org/linux/man-pages/man{manvolnum}/{refentrytitle}.{manvolnum}.html',
+ 'die-net': 'http://linux.die.net/man/{manvolnum}/{refentrytitle}',
+ 'mankier': 'https://www.mankier.com/{manvolnum}/{refentrytitle}',
+ 'archlinux': 'https://man.archlinux.org/man/{refentrytitle}.{manvolnum}.en.html',
+ 'debian': 'https://manpages.debian.org/unstable/{refentrytitle}/{refentrytitle}.{manvolnum}.en.html',
+ 'freebsd': 'https://www.freebsd.org/cgi/man.cgi?query={refentrytitle}&sektion={manvolnum}',
+ 'dbus': 'https://dbus.freedesktop.org/doc/dbus-specification.html#{refentrytitle}',
+}
+
+
+def man_role(
+ name: str,
+ rawtext: str,
+ text: str,
+ lineno: int,
+ inliner: states.Inliner,
+ options: Dict[str, Any] = {}
+) -> Tuple[List[nodes.reference], List[nodes.system_message]]:
+ # Regex to match text like 'locale(7)'
+ pattern = re.compile(r'(.+)\((\d+)\)')
+ match = pattern.match(text)
+ if not match:
+ msg = inliner.reporter.error(
+ f'Invalid man page format {text}, expected format "name(section)"',
+ nodes.literal_block(rawtext, rawtext),
+ line=lineno
+ )
+ return [inliner.problematic(rawtext, rawtext, msg)], [msg]
+
+ refentrytitle, manvolnum = match.groups()
+
+ if name not in extlink_formats:
+ msg = inliner.reporter.error(
+ f'Unknown man page role {name}',
+ nodes.literal_block(rawtext, rawtext),
+ line=lineno
+ )
+ return [inliner.problematic(rawtext, rawtext, msg)], [msg]
+
+ url = extlink_formats[name].format(
+ manvolnum=manvolnum, refentrytitle=refentrytitle
+ )
+ node = nodes.reference(
+ rawtext, f'{refentrytitle}({manvolnum})', refuri=url, **options
+ )
+ return [node], []
+
+
+def setup(app: Any) -> Dict[str, bool]:
+ for role in extlink_formats.keys():
+ roles.register_local_role(role, man_role)
+ return {'parallel_read_safe': True, 'parallel_write_safe': True}
diff --git a/doc-migration/source/_static/css/custom.css b/doc-migration/source/_static/css/custom.css
new file mode 100644
index 00000000000..c2ad3fc7060
--- /dev/null
+++ b/doc-migration/source/_static/css/custom.css
@@ -0,0 +1,28 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+.sidebar-logo {
+ margin-inline: 0;
+}
+
+section {
+ margin-block-end: 2em;
+}
+
+/* Make right sidebar wider to accomodate long titles */
+.toc-drawer {
+ width: 100%;
+}
+
+/* Make Toc section headers bold */
+.toc-tree li a:has(+ ul) {
+ font-weight: 600;
+}
+
+.sig-name,
+.sig-prename {
+ color: var(--color-content-foreground);
+}
+
+.std.option {
+ margin-left: 2rem;
+}
diff --git a/doc-migration/source/_static/systemd-logo.svg b/doc-migration/source/_static/systemd-logo.svg
new file mode 100644
index 00000000000..a8af438dca7
--- /dev/null
+++ b/doc-migration/source/_static/systemd-logo.svg
@@ -0,0 +1,7 @@
+
diff --git a/doc-migration/source/code-examples/c/event-quick-child.c b/doc-migration/source/code-examples/c/event-quick-child.c
new file mode 100644
index 00000000000..828f0cd6f4b
--- /dev/null
+++ b/doc-migration/source/code-examples/c/event-quick-child.c
@@ -0,0 +1,43 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+#define _GNU_SOURCE 1
+#include
+#include
+#include
+#include
+
+int main(int argc, char **argv) {
+ pid_t pid = fork();
+ assert(pid >= 0);
+
+ /* SIGCHLD signal must be blocked for sd_event_add_child to work */
+ sigset_t ss;
+ sigemptyset(&ss);
+ sigaddset(&ss, SIGCHLD);
+ sigprocmask(SIG_BLOCK, &ss, NULL);
+
+ if (pid == 0) /* child */
+ sleep(1);
+
+ else { /* parent */
+ sd_event *e = NULL;
+ int r;
+
+ /* Create the default event loop */
+ sd_event_default(&e);
+ assert(e);
+
+ /* We create a floating child event source (attached to 'e').
+ * The default handler will be called with 666 as userdata, which
+ * will become the exit value of the loop. */
+ r = sd_event_add_child(e, NULL, pid, WEXITED, NULL, (void*) 666);
+ assert(r >= 0);
+
+ r = sd_event_loop(e);
+ assert(r == 666);
+
+ sd_event_unref(e);
+ }
+
+ return 0;
+}
diff --git a/doc-migration/source/code-examples/c/glib-event-glue.c b/doc-migration/source/code-examples/c/glib-event-glue.c
new file mode 100644
index 00000000000..61e8bf6463d
--- /dev/null
+++ b/doc-migration/source/code-examples/c/glib-event-glue.c
@@ -0,0 +1,48 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+#include
+#include
+#include
+
+typedef struct SDEventSource {
+ GSource source;
+ GPollFD pollfd;
+ sd_event *event;
+} SDEventSource;
+
+static gboolean event_prepare(GSource *source, gint *timeout_) {
+ return sd_event_prepare(((SDEventSource *)source)->event) > 0;
+}
+
+static gboolean event_check(GSource *source) {
+ return sd_event_wait(((SDEventSource *)source)->event, 0) > 0;
+}
+
+static gboolean event_dispatch(GSource *source, GSourceFunc callback, gpointer user_data) {
+ return sd_event_dispatch(((SDEventSource *)source)->event) > 0;
+}
+
+static void event_finalize(GSource *source) {
+ sd_event_unref(((SDEventSource *)source)->event);
+}
+
+static GSourceFuncs event_funcs = {
+ .prepare = event_prepare,
+ .check = event_check,
+ .dispatch = event_dispatch,
+ .finalize = event_finalize,
+};
+
+GSource *g_sd_event_create_source(sd_event *event) {
+ SDEventSource *source;
+
+ source = (SDEventSource *)g_source_new(&event_funcs, sizeof(SDEventSource));
+
+ source->event = sd_event_ref(event);
+ source->pollfd.fd = sd_event_get_fd(event);
+ source->pollfd.events = G_IO_IN | G_IO_HUP | G_IO_ERR;
+
+ g_source_add_poll((GSource *)source, &source->pollfd);
+
+ return (GSource *)source;
+}
diff --git a/doc-migration/source/code-examples/c/hwdb-usb-device.c b/doc-migration/source/code-examples/c/hwdb-usb-device.c
new file mode 100644
index 00000000000..3ce3ccd87f5
--- /dev/null
+++ b/doc-migration/source/code-examples/c/hwdb-usb-device.c
@@ -0,0 +1,31 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+#define _GNU_SOURCE 1
+#include
+#include
+#include
+
+int print_usb_properties(uint16_t vid, uint16_t pid) {
+ char match[128];
+ sd_hwdb *hwdb;
+ const char *key, *value;
+ int r;
+
+ /* Match this USB vendor and product ID combination */
+ snprintf(match, sizeof match, "usb:v%04Xp%04X", vid, pid);
+
+ r = sd_hwdb_new(&hwdb);
+ if (r < 0)
+ return r;
+
+ SD_HWDB_FOREACH_PROPERTY(hwdb, match, key, value)
+ printf("%s: \"%s\" → \"%s\"\n", match, key, value);
+
+ sd_hwdb_unref(hwdb);
+ return 0;
+}
+
+int main(int argc, char **argv) {
+ print_usb_properties(0x046D, 0xC534);
+ return 0;
+}
diff --git a/doc-migration/source/code-examples/c/id128-app-specific.c b/doc-migration/source/code-examples/c/id128-app-specific.c
new file mode 100644
index 00000000000..b8982c75f85
--- /dev/null
+++ b/doc-migration/source/code-examples/c/id128-app-specific.c
@@ -0,0 +1,13 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+#include
+#include
+
+#define OUR_APPLICATION_ID SD_ID128_MAKE(c2,73,27,73,23,db,45,4e,a6,3b,b9,6e,79,b5,3e,97)
+
+int main(int argc, char *argv[]) {
+ sd_id128_t id;
+ sd_id128_get_machine_app_specific(OUR_APPLICATION_ID, &id);
+ printf("Our application ID: " SD_ID128_FORMAT_STR "\n", SD_ID128_FORMAT_VAL(id));
+ return 0;
+}
diff --git a/doc-migration/source/code-examples/c/inotify-watch-tmp.c b/doc-migration/source/code-examples/c/inotify-watch-tmp.c
new file mode 100644
index 00000000000..07ee8f6754b
--- /dev/null
+++ b/doc-migration/source/code-examples/c/inotify-watch-tmp.c
@@ -0,0 +1,58 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+#include
+#include
+#include
+
+#include
+
+#define _cleanup_(f) __attribute__((cleanup(f)))
+
+static int inotify_handler(sd_event_source *source,
+ const struct inotify_event *event,
+ void *userdata) {
+
+ const char *desc = NULL;
+
+ sd_event_source_get_description(source, &desc);
+
+ if (event->mask & IN_Q_OVERFLOW)
+ printf("inotify-handler <%s>: overflow\n", desc);
+ else if (event->mask & IN_CREATE)
+ printf("inotify-handler <%s>: create on %s\n", desc, event->name);
+ else if (event->mask & IN_DELETE)
+ printf("inotify-handler <%s>: delete on %s\n", desc, event->name);
+ else if (event->mask & IN_MOVED_TO)
+ printf("inotify-handler <%s>: moved-to on %s\n", desc, event->name);
+
+ /* Terminate the program if an "exit" file appears */
+ if ((event->mask & (IN_CREATE|IN_MOVED_TO)) &&
+ strcmp(event->name, "exit") == 0)
+ sd_event_exit(sd_event_source_get_event(source), 0);
+
+ return 1;
+}
+
+int main(int argc, char **argv) {
+ _cleanup_(sd_event_unrefp) sd_event *event = NULL;
+ _cleanup_(sd_event_source_unrefp) sd_event_source *source1 = NULL, *source2 = NULL;
+
+ const char *path1 = argc > 1 ? argv[1] : "/tmp";
+ const char *path2 = argc > 2 ? argv[2] : NULL;
+
+ /* Note: failure handling is omitted for brevity */
+
+ sd_event_default(&event);
+
+ sd_event_add_inotify(event, &source1, path1,
+ IN_CREATE | IN_DELETE | IN_MODIFY | IN_MOVED_TO,
+ inotify_handler, NULL);
+ if (path2)
+ sd_event_add_inotify(event, &source2, path2,
+ IN_CREATE | IN_DELETE | IN_MODIFY | IN_MOVED_TO,
+ inotify_handler, NULL);
+
+ sd_event_loop(event);
+
+ return 0;
+}
diff --git a/doc-migration/source/code-examples/c/journal-enumerate-fields.c b/doc-migration/source/code-examples/c/journal-enumerate-fields.c
new file mode 100644
index 00000000000..3d35b001786
--- /dev/null
+++ b/doc-migration/source/code-examples/c/journal-enumerate-fields.c
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+#include
+#include
+#include
+
+int main(int argc, char *argv[]) {
+ sd_journal *j;
+ const char *field;
+ int r;
+
+ r = sd_journal_open(&j, SD_JOURNAL_LOCAL_ONLY);
+ if (r < 0) {
+ fprintf(stderr, "Failed to open journal: %s\n", strerror(-r));
+ return 1;
+ }
+ SD_JOURNAL_FOREACH_FIELD(j, field)
+ printf("%s\n", field);
+ sd_journal_close(j);
+ return 0;
+}
diff --git a/doc-migration/source/code-examples/c/journal-iterate-foreach.c b/doc-migration/source/code-examples/c/journal-iterate-foreach.c
new file mode 100644
index 00000000000..9c0fa0eaf13
--- /dev/null
+++ b/doc-migration/source/code-examples/c/journal-iterate-foreach.c
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+#include
+#include
+#include
+
+int main(int argc, char *argv[]) {
+ int r;
+ sd_journal *j;
+
+ r = sd_journal_open(&j, SD_JOURNAL_LOCAL_ONLY);
+ if (r < 0) {
+ fprintf(stderr, "Failed to open journal: %s\n", strerror(-r));
+ return 1;
+ }
+ SD_JOURNAL_FOREACH(j) {
+ const char *d;
+ size_t l;
+
+ r = sd_journal_get_data(j, "MESSAGE", (const void **)&d, &l);
+ if (r < 0) {
+ fprintf(stderr, "Failed to read message field: %s\n", strerror(-r));
+ continue;
+ }
+
+ printf("%.*s\n", (int) l, d);
+ }
+ sd_journal_close(j);
+ return 0;
+}
diff --git a/doc-migration/source/code-examples/c/journal-iterate-poll.c b/doc-migration/source/code-examples/c/journal-iterate-poll.c
new file mode 100644
index 00000000000..6b78296267a
--- /dev/null
+++ b/doc-migration/source/code-examples/c/journal-iterate-poll.c
@@ -0,0 +1,28 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+#define _GNU_SOURCE 1
+#include
+#include
+#include
+
+int wait_for_changes(sd_journal *j) {
+ uint64_t t;
+ int msec;
+ struct pollfd pollfd;
+
+ sd_journal_get_timeout(j, &t);
+ if (t == (uint64_t) -1)
+ msec = -1;
+ else {
+ struct timespec ts;
+ uint64_t n;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ n = (uint64_t) ts.tv_sec * 1000000 + ts.tv_nsec / 1000;
+ msec = t > n ? (int) ((t - n + 999) / 1000) : 0;
+ }
+
+ pollfd.fd = sd_journal_get_fd(j);
+ pollfd.events = sd_journal_get_events(j);
+ poll(&pollfd, 1, msec);
+ return sd_journal_process(j);
+}
diff --git a/doc-migration/source/code-examples/c/journal-iterate-unique.c b/doc-migration/source/code-examples/c/journal-iterate-unique.c
new file mode 100644
index 00000000000..f44303d75cd
--- /dev/null
+++ b/doc-migration/source/code-examples/c/journal-iterate-unique.c
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+#include
+#include
+#include
+
+int main(int argc, char *argv[]) {
+ sd_journal *j;
+ const void *d;
+ size_t l;
+ int r;
+
+ r = sd_journal_open(&j, SD_JOURNAL_LOCAL_ONLY);
+ if (r < 0) {
+ fprintf(stderr, "Failed to open journal: %s\n", strerror(-r));
+ return 1;
+ }
+ r = sd_journal_query_unique(j, "_SYSTEMD_UNIT");
+ if (r < 0) {
+ fprintf(stderr, "Failed to query journal: %s\n", strerror(-r));
+ return 1;
+ }
+ SD_JOURNAL_FOREACH_UNIQUE(j, d, l)
+ printf("%.*s\n", (int) l, (const char*) d);
+ sd_journal_close(j);
+ return 0;
+}
diff --git a/doc-migration/source/code-examples/c/journal-iterate-wait.c b/doc-migration/source/code-examples/c/journal-iterate-wait.c
new file mode 100644
index 00000000000..69d3cccb34a
--- /dev/null
+++ b/doc-migration/source/code-examples/c/journal-iterate-wait.c
@@ -0,0 +1,44 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+#include
+#include
+#include
+
+int main(int argc, char *argv[]) {
+ int r;
+ sd_journal *j;
+
+ r = sd_journal_open(&j, SD_JOURNAL_LOCAL_ONLY);
+ if (r < 0) {
+ fprintf(stderr, "Failed to open journal: %s\n", strerror(-r));
+ return 1;
+ }
+
+ for (;;) {
+ const void *d;
+ size_t l;
+ r = sd_journal_next(j);
+ if (r < 0) {
+ fprintf(stderr, "Failed to iterate to next entry: %s\n", strerror(-r));
+ break;
+ }
+ if (r == 0) {
+ /* Reached the end, let's wait for changes, and try again */
+ r = sd_journal_wait(j, (uint64_t) -1);
+ if (r < 0) {
+ fprintf(stderr, "Failed to wait for changes: %s\n", strerror(-r));
+ break;
+ }
+ continue;
+ }
+ r = sd_journal_get_data(j, "MESSAGE", &d, &l);
+ if (r < 0) {
+ fprintf(stderr, "Failed to read message field: %s\n", strerror(-r));
+ continue;
+ }
+ printf("%.*s\n", (int) l, (const char*) d);
+ }
+
+ sd_journal_close(j);
+ return 0;
+}
diff --git a/doc-migration/source/code-examples/c/journal-stream-fd.c b/doc-migration/source/code-examples/c/journal-stream-fd.c
new file mode 100644
index 00000000000..595091af810
--- /dev/null
+++ b/doc-migration/source/code-examples/c/journal-stream-fd.c
@@ -0,0 +1,31 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+#define _GNU_SOURCE 1
+#include
+#include
+#include
+#include
+#include
+#include
+
+int main(int argc, char *argv[]) {
+ int fd;
+ FILE *log;
+
+ fd = sd_journal_stream_fd("test", LOG_INFO, 1);
+ if (fd < 0) {
+ fprintf(stderr, "Failed to create stream fd: %s\n", strerror(-fd));
+ return 1;
+ }
+
+ log = fdopen(fd, "w");
+ if (!log) {
+ fprintf(stderr, "Failed to create file object: %s\n", strerror(errno));
+ close(fd);
+ return 1;
+ }
+ fprintf(log, "Hello World!\n");
+ fprintf(log, SD_WARNING "This is a warning!\n");
+ fclose(log);
+ return 0;
+}
diff --git a/doc-migration/source/code-examples/c/logcontrol-example.c b/doc-migration/source/code-examples/c/logcontrol-example.c
new file mode 100644
index 00000000000..23e73846cdb
--- /dev/null
+++ b/doc-migration/source/code-examples/c/logcontrol-example.c
@@ -0,0 +1,251 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+/* Implements the LogControl1 interface as per specification:
+ * https://www.freedesktop.org/software/systemd/man/org.freedesktop.LogControl1.html
+ *
+ * Compile with 'cc logcontrol-example.c $(pkg-config --libs --cflags libsystemd)'
+ *
+ * To get and set properties via busctl:
+ *
+ * $ busctl --user get-property org.freedesktop.Example \
+ * /org/freedesktop/LogControl1 \
+ * org.freedesktop.LogControl1 \
+ * SyslogIdentifier
+ * s "example"
+ * $ busctl --user get-property org.freedesktop.Example \
+ * /org/freedesktop/LogControl1 \
+ * org.freedesktop.LogControl1 \
+ * LogTarget
+ * s "journal"
+ * $ busctl --user get-property org.freedesktop.Example \
+ * /org/freedesktop/LogControl1 \
+ * org.freedesktop.LogControl1 \
+ * LogLevel
+ * s "info"
+ * $ busctl --user set-property org.freedesktop.Example \
+ * /org/freedesktop/LogControl1 \
+ * org.freedesktop.LogControl1 \
+ * LogLevel \
+ * "s" debug
+ * $ busctl --user get-property org.freedesktop.Example \
+ * /org/freedesktop/LogControl1 \
+ * org.freedesktop.LogControl1 \
+ * LogLevel
+ * s "debug"
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define _cleanup_(f) __attribute__((cleanup(f)))
+
+static int log_error(int log_level, int error, const char *str) {
+ sd_journal_print(log_level, "%s failed: %s", str, strerror(-error));
+ return error;
+}
+
+typedef enum LogTarget {
+ LOG_TARGET_JOURNAL,
+ LOG_TARGET_KMSG,
+ LOG_TARGET_SYSLOG,
+ LOG_TARGET_CONSOLE,
+ _LOG_TARGET_MAX,
+} LogTarget;
+
+static const char* const log_target_table[_LOG_TARGET_MAX] = {
+ [LOG_TARGET_JOURNAL] = "journal",
+ [LOG_TARGET_KMSG] = "kmsg",
+ [LOG_TARGET_SYSLOG] = "syslog",
+ [LOG_TARGET_CONSOLE] = "console",
+};
+
+static const char* const log_level_table[LOG_DEBUG + 1] = {
+ [LOG_EMERG] = "emerg",
+ [LOG_ALERT] = "alert",
+ [LOG_CRIT] = "crit",
+ [LOG_ERR] = "err",
+ [LOG_WARNING] = "warning",
+ [LOG_NOTICE] = "notice",
+ [LOG_INFO] = "info",
+ [LOG_DEBUG] = "debug",
+};
+
+typedef struct object {
+ const char *syslog_identifier;
+ LogTarget log_target;
+ int log_level;
+} object;
+
+static int property_get(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *reply,
+ void *userdata,
+ sd_bus_error *error) {
+
+ object *o = userdata;
+
+ if (strcmp(property, "LogLevel") == 0)
+ return sd_bus_message_append(reply, "s", log_level_table[o->log_level]);
+
+ if (strcmp(property, "LogTarget") == 0)
+ return sd_bus_message_append(reply, "s", log_target_table[o->log_target]);
+
+ if (strcmp(property, "SyslogIdentifier") == 0)
+ return sd_bus_message_append(reply, "s", o->syslog_identifier);
+
+ return sd_bus_error_setf(error,
+ SD_BUS_ERROR_UNKNOWN_PROPERTY,
+ "Unknown property '%s'",
+ property);
+}
+
+static int property_set(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *error) {
+
+ object *o = userdata;
+ const char *value;
+ int r;
+
+ r = sd_bus_message_read(message, "s", &value);
+ if (r < 0)
+ return r;
+
+ if (strcmp(property, "LogLevel") == 0) {
+ int i;
+ for (i = 0; i < LOG_DEBUG + 1; i++)
+ if (strcmp(value, log_level_table[i]) == 0) {
+ o->log_level = i;
+ setlogmask(LOG_UPTO(i));
+ return 0;
+ }
+
+ return sd_bus_error_setf(error,
+ SD_BUS_ERROR_INVALID_ARGS,
+ "Invalid value for LogLevel: '%s'",
+ value);
+ }
+
+ if (strcmp(property, "LogTarget") == 0) {
+ LogTarget i;
+ for (i = 0; i < _LOG_TARGET_MAX; i++)
+ if (strcmp(value, log_target_table[i]) == 0) {
+ o->log_target = i;
+ return 0;
+ }
+
+ return sd_bus_error_setf(error,
+ SD_BUS_ERROR_INVALID_ARGS,
+ "Invalid value for LogTarget: '%s'",
+ value);
+ }
+
+ return sd_bus_error_setf(error,
+ SD_BUS_ERROR_UNKNOWN_PROPERTY,
+ "Unknown property '%s'",
+ property);
+}
+
+/* https://www.freedesktop.org/software/systemd/man/sd_bus_add_object.html
+ */
+static const sd_bus_vtable vtable[] = {
+ SD_BUS_VTABLE_START(0),
+ SD_BUS_WRITABLE_PROPERTY(
+ "LogLevel", "s",
+ property_get, property_set,
+ 0,
+ 0),
+ SD_BUS_WRITABLE_PROPERTY(
+ "LogTarget", "s",
+ property_get, property_set,
+ 0,
+ 0),
+ SD_BUS_PROPERTY(
+ "SyslogIdentifier", "s",
+ property_get,
+ 0,
+ SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_VTABLE_END
+};
+
+int main(int argc, char **argv) {
+ /* The bus should be relinquished before the program terminates. The cleanup
+ * attribute allows us to do it nicely and cleanly whenever we exit the
+ * block.
+ */
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+
+ object o = {
+ .log_level = LOG_INFO,
+ .log_target = LOG_TARGET_JOURNAL,
+ .syslog_identifier = "example",
+ };
+ int r;
+
+ /* https://man7.org/linux/man-pages/man3/setlogmask.3.html
+ * Programs using syslog() instead of sd_journal can use this API to cut logs
+ * emission at the source.
+ */
+ setlogmask(LOG_UPTO(o.log_level));
+
+ /* Acquire a connection to the bus, letting the library work out the details.
+ * https://www.freedesktop.org/software/systemd/man/sd_bus_default.html
+ */
+ r = sd_bus_default(&bus);
+ if (r < 0)
+ return log_error(o.log_level, r, "sd_bus_default()");
+
+ /* Publish an interface on the bus, specifying our well-known object access
+ * path and public interface name.
+ * https://www.freedesktop.org/software/systemd/man/sd_bus_add_object.html
+ * https://dbus.freedesktop.org/doc/dbus-tutorial.html
+ */
+ r = sd_bus_add_object_vtable(bus, NULL,
+ "/org/freedesktop/LogControl1",
+ "org.freedesktop.LogControl1",
+ vtable,
+ &o);
+ if (r < 0)
+ return log_error(o.log_level, r, "sd_bus_add_object_vtable()");
+
+ /* By default the service is assigned an ephemeral name. Also add a fixed
+ * one, so that clients know whom to call.
+ * https://www.freedesktop.org/software/systemd/man/sd_bus_request_name.html
+ */
+ r = sd_bus_request_name(bus, "org.freedesktop.Example", 0);
+ if (r < 0)
+ return log_error(o.log_level, r, "sd_bus_request_name()");
+
+ for (;;) {
+ /* https://www.freedesktop.org/software/systemd/man/sd_bus_wait.html
+ */
+ r = sd_bus_wait(bus, UINT64_MAX);
+ if (r < 0)
+ return log_error(o.log_level, r, "sd_bus_wait()");
+ /* https://www.freedesktop.org/software/systemd/man/sd_bus_process.html
+ */
+ r = sd_bus_process(bus, NULL);
+ if (r < 0)
+ return log_error(o.log_level, r, "sd_bus_process()");
+ }
+
+ /* https://www.freedesktop.org/software/systemd/man/sd_bus_release_name.html
+ */
+ r = sd_bus_release_name(bus, "org.freedesktop.Example");
+ if (r < 0)
+ return log_error(o.log_level, r, "sd_bus_release_name()");
+
+ return 0;
+}
diff --git a/doc-migration/source/code-examples/c/notify-selfcontained-example.c b/doc-migration/source/code-examples/c/notify-selfcontained-example.c
new file mode 100644
index 00000000000..6bbe4f2e3ba
--- /dev/null
+++ b/doc-migration/source/code-examples/c/notify-selfcontained-example.c
@@ -0,0 +1,188 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+/* Implement the systemd notify protocol without external dependencies.
+ * Supports both readiness notification on startup and on reloading,
+ * according to the protocol defined at:
+ * https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html
+ * This protocol is guaranteed to be stable as per:
+ * https://systemd.io/PORTABILITY_AND_STABILITY/ */
+
+#define _GNU_SOURCE 1
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define _cleanup_(f) __attribute__((cleanup(f)))
+
+static void closep(int *fd) {
+ if (!fd || *fd < 0)
+ return;
+
+ close(*fd);
+ *fd = -1;
+}
+
+static int notify(const char *message) {
+ union sockaddr_union {
+ struct sockaddr sa;
+ struct sockaddr_un sun;
+ } socket_addr = {
+ .sun.sun_family = AF_UNIX,
+ };
+ size_t path_length, message_length;
+ _cleanup_(closep) int fd = -1;
+ const char *socket_path;
+
+ /* Verify the argument first */
+ if (!message)
+ return -EINVAL;
+
+ message_length = strlen(message);
+ if (message_length == 0)
+ return -EINVAL;
+
+ /* If the variable is not set, the protocol is a noop */
+ socket_path = getenv("NOTIFY_SOCKET");
+ if (!socket_path)
+ return 0; /* Not set? Nothing to do */
+
+ /* Only AF_UNIX is supported, with path or abstract sockets */
+ if (socket_path[0] != '/' && socket_path[0] != '@')
+ return -EAFNOSUPPORT;
+
+ path_length = strlen(socket_path);
+ /* Ensure there is room for NUL byte */
+ if (path_length >= sizeof(socket_addr.sun.sun_path))
+ return -E2BIG;
+
+ memcpy(socket_addr.sun.sun_path, socket_path, path_length);
+
+ /* Support for abstract socket */
+ if (socket_addr.sun.sun_path[0] == '@')
+ socket_addr.sun.sun_path[0] = 0;
+
+ fd = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0);
+ if (fd < 0)
+ return -errno;
+
+ if (connect(fd, &socket_addr.sa, offsetof(struct sockaddr_un, sun_path) + path_length) != 0)
+ return -errno;
+
+ ssize_t written = write(fd, message, message_length);
+ if (written != (ssize_t) message_length)
+ return written < 0 ? -errno : -EPROTO;
+
+ return 1; /* Notified! */
+}
+
+static int notify_ready(void) {
+ return notify("READY=1");
+}
+
+static int notify_reloading(void) {
+ /* A buffer with length sufficient to format the maximum UINT64 value. */
+ char reload_message[sizeof("RELOADING=1\nMONOTONIC_USEC=18446744073709551615")];
+ struct timespec ts;
+ uint64_t now;
+
+ /* Notify systemd that we are reloading, including a CLOCK_MONOTONIC timestamp in usec
+ * so that the program is compatible with a Type=notify-reload service. */
+
+ if (clock_gettime(CLOCK_MONOTONIC, &ts) < 0)
+ return -errno;
+
+ if (ts.tv_sec < 0 || ts.tv_nsec < 0 ||
+ (uint64_t) ts.tv_sec > (UINT64_MAX - (ts.tv_nsec / 1000ULL)) / 1000000ULL)
+ return -EINVAL;
+
+ now = (uint64_t) ts.tv_sec * 1000000ULL + (uint64_t) ts.tv_nsec / 1000ULL;
+
+ if (snprintf(reload_message, sizeof(reload_message), "RELOADING=1\nMONOTONIC_USEC=%" PRIu64, now) < 0)
+ return -EINVAL;
+
+ return notify(reload_message);
+}
+
+static int notify_stopping(void) {
+ return notify("STOPPING=1");
+}
+
+static volatile sig_atomic_t reloading = 0;
+static volatile sig_atomic_t terminating = 0;
+
+static void signal_handler(int sig) {
+ if (sig == SIGHUP)
+ reloading = 1;
+ else if (sig == SIGINT || sig == SIGTERM)
+ terminating = 1;
+}
+
+int main(int argc, char **argv) {
+ struct sigaction sa = {
+ .sa_handler = signal_handler,
+ .sa_flags = SA_RESTART,
+ };
+ int r;
+
+ /* Setup signal handlers */
+ sigemptyset(&sa.sa_mask);
+ sigaction(SIGHUP, &sa, NULL);
+ sigaction(SIGINT, &sa, NULL);
+ sigaction(SIGTERM, &sa, NULL);
+
+ /* Do more service initialization work here … */
+
+ /* Now that all the preparations steps are done, signal readiness */
+
+ r = notify_ready();
+ if (r < 0) {
+ fprintf(stderr, "Failed to notify readiness to $NOTIFY_SOCKET: %s\n", strerror(-r));
+ return EXIT_FAILURE;
+ }
+
+ while (!terminating) {
+ if (reloading) {
+ reloading = false;
+
+ /* As a separate but related feature, we can also notify the manager
+ * when reloading configuration. This allows accurate state-tracking,
+ * and also automated hook-in of 'systemctl reload' without having to
+ * specify manually an ExecReload= line in the unit file. */
+
+ r = notify_reloading();
+ if (r < 0) {
+ fprintf(stderr, "Failed to notify reloading to $NOTIFY_SOCKET: %s\n", strerror(-r));
+ return EXIT_FAILURE;
+ }
+
+ /* Do some reconfiguration work here … */
+
+ r = notify_ready();
+ if (r < 0) {
+ fprintf(stderr, "Failed to notify readiness to $NOTIFY_SOCKET: %s\n", strerror(-r));
+ return EXIT_FAILURE;
+ }
+ }
+
+ /* Do some daemon work here … */
+ sleep(5);
+ }
+
+ r = notify_stopping();
+ if (r < 0) {
+ fprintf(stderr, "Failed to report termination to $NOTIFY_SOCKET: %s\n", strerror(-r));
+ return EXIT_FAILURE;
+ }
+
+ /* Do some shutdown work here … */
+
+ return EXIT_SUCCESS;
+}
diff --git a/doc-migration/source/code-examples/c/path-documents.c b/doc-migration/source/code-examples/c/path-documents.c
new file mode 100644
index 00000000000..994f20bcf4e
--- /dev/null
+++ b/doc-migration/source/code-examples/c/path-documents.c
@@ -0,0 +1,19 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+#include
+#include
+#include
+
+int main(void) {
+ int r;
+ char *t;
+
+ r = sd_path_lookup(SD_PATH_USER_DOCUMENTS, NULL, &t);
+ if (r < 0)
+ return EXIT_FAILURE;
+
+ printf("~/Documents: %s\n", t);
+ free(t);
+
+ return EXIT_SUCCESS;
+}
diff --git a/doc-migration/source/code-examples/c/print-unit-path-call-method.c b/doc-migration/source/code-examples/c/print-unit-path-call-method.c
new file mode 100644
index 00000000000..15e8d3f51b2
--- /dev/null
+++ b/doc-migration/source/code-examples/c/print-unit-path-call-method.c
@@ -0,0 +1,50 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+/* This is equivalent to:
+ * busctl call org.freedesktop.systemd1 /org/freedesktop/systemd1 \
+ * org.freedesktop.systemd1.Manager GetUnitByPID $$
+ *
+ * Compile with 'cc print-unit-path-call-method.c -lsystemd'
+ */
+
+#include
+#include
+#include
+#include
+
+#include
+
+#define _cleanup_(f) __attribute__((cleanup(f)))
+#define DESTINATION "org.freedesktop.systemd1"
+#define PATH "/org/freedesktop/systemd1"
+#define INTERFACE "org.freedesktop.systemd1.Manager"
+#define MEMBER "GetUnitByPID"
+
+static int log_error(int error, const char *message) {
+ fprintf(stderr, "%s: %s\n", message, strerror(-error));
+ return error;
+}
+
+int main(int argc, char **argv) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ int r;
+
+ r = sd_bus_open_system(&bus);
+ if (r < 0)
+ return log_error(r, "Failed to acquire bus");
+
+ r = sd_bus_call_method(bus, DESTINATION, PATH, INTERFACE, MEMBER, &error, &reply, "u", (unsigned) getpid());
+ if (r < 0)
+ return log_error(r, MEMBER " call failed");
+
+ const char *ans;
+ r = sd_bus_message_read(reply, "o", &ans);
+ if (r < 0)
+ return log_error(r, "Failed to read reply");
+
+ printf("Unit path is \"%s\".\n", ans);
+
+ return 0;
+}
diff --git a/doc-migration/source/code-examples/c/print-unit-path.c b/doc-migration/source/code-examples/c/print-unit-path.c
new file mode 100644
index 00000000000..737244feb0d
--- /dev/null
+++ b/doc-migration/source/code-examples/c/print-unit-path.c
@@ -0,0 +1,59 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+/* This is equivalent to:
+ * busctl call org.freedesktop.systemd1 /org/freedesktop/systemd1 \
+ * org.freedesktop.systemd1.Manager GetUnitByPID $$
+ *
+ * Compile with 'cc print-unit-path.c -lsystemd'
+ */
+
+#include
+#include
+#include
+#include
+
+#include
+
+#define _cleanup_(f) __attribute__((cleanup(f)))
+#define DESTINATION "org.freedesktop.systemd1"
+#define PATH "/org/freedesktop/systemd1"
+#define INTERFACE "org.freedesktop.systemd1.Manager"
+#define MEMBER "GetUnitByPID"
+
+static int log_error(int error, const char *message) {
+ fprintf(stderr, "%s: %s\n", message, strerror(-error));
+ return error;
+}
+
+int main(int argc, char **argv) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL, *m = NULL;
+ int r;
+
+ r = sd_bus_open_system(&bus);
+ if (r < 0)
+ return log_error(r, "Failed to acquire bus");
+
+ r = sd_bus_message_new_method_call(bus, &m,
+ DESTINATION, PATH, INTERFACE, MEMBER);
+ if (r < 0)
+ return log_error(r, "Failed to create bus message");
+
+ r = sd_bus_message_append(m, "u", (unsigned) getpid());
+ if (r < 0)
+ return log_error(r, "Failed to append to bus message");
+
+ r = sd_bus_call(bus, m, -1, &error, &reply);
+ if (r < 0)
+ return log_error(r, MEMBER " call failed");
+
+ const char *ans;
+ r = sd_bus_message_read(reply, "o", &ans);
+ if (r < 0)
+ return log_error(r, "Failed to read reply");
+
+ printf("Unit path is \"%s\".\n", ans);
+
+ return 0;
+}
diff --git a/doc-migration/source/code-examples/c/sd-bus-container-append.c b/doc-migration/source/code-examples/c/sd-bus-container-append.c
new file mode 100644
index 00000000000..07a24f24cc6
--- /dev/null
+++ b/doc-migration/source/code-examples/c/sd-bus-container-append.c
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+#include
+
+int append_strings_to_message(sd_bus_message *m, const char *const *arr) {
+ const char *s;
+ int r;
+
+ r = sd_bus_message_open_container(m, 'a', "s");
+ if (r < 0)
+ return r;
+
+ for (s = *arr; *s; s++) {
+ r = sd_bus_message_append(m, "s", s);
+ if (r < 0)
+ return r;
+ }
+
+ return sd_bus_message_close_container(m);
+}
diff --git a/doc-migration/source/code-examples/c/sd-bus-container-read.c b/doc-migration/source/code-examples/c/sd-bus-container-read.c
new file mode 100644
index 00000000000..5ede316c03b
--- /dev/null
+++ b/doc-migration/source/code-examples/c/sd-bus-container-read.c
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+#include
+
+#include
+
+int read_strings_from_message(sd_bus_message *m) {
+ int r;
+
+ r = sd_bus_message_enter_container(m, 'a', "s");
+ if (r < 0)
+ return r;
+
+ for (;;) {
+ const char *s;
+
+ r = sd_bus_message_read(m, "s", &s);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ break;
+
+ printf("%s\n", s);
+ }
+
+ return sd_bus_message_exit_container(m);
+}
diff --git a/doc-migration/source/code-examples/c/sd_bus_error-example.c b/doc-migration/source/code-examples/c/sd_bus_error-example.c
new file mode 100644
index 00000000000..3836f5e642a
--- /dev/null
+++ b/doc-migration/source/code-examples/c/sd_bus_error-example.c
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+#include
+#include
+#include
+#include
+
+int writer_with_negative_errno_return(int fd, sd_bus_error *error) {
+ const char *message = "Hello, World!\n";
+
+ ssize_t n = write(fd, message, strlen(message));
+ if (n >= 0)
+ return n; /* On success, return the number of bytes written, possibly 0. */
+
+ /* On error, initialize the error structure, and also propagate the errno
+ * value that write(2) set for us. */
+ return sd_bus_error_set_errnof(error, errno, "Failed to write to fd %i: %s", fd, strerror(errno));
+}
diff --git a/doc-migration/source/code-examples/c/vtable-example.c b/doc-migration/source/code-examples/c/vtable-example.c
new file mode 100644
index 00000000000..2e8994471a0
--- /dev/null
+++ b/doc-migration/source/code-examples/c/vtable-example.c
@@ -0,0 +1,143 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+#define _GNU_SOURCE 1
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define _cleanup_(f) __attribute__((cleanup(f)))
+
+typedef struct object {
+ char *name;
+ uint32_t number;
+} object;
+
+static int method(sd_bus_message *m, void *userdata, sd_bus_error *error) {
+ int r;
+
+ printf("Got called with userdata=%p\n", userdata);
+
+ if (sd_bus_message_is_method_call(m,
+ "org.freedesktop.systemd.VtableExample",
+ "Method4"))
+ return 1;
+
+ const char *string;
+ r = sd_bus_message_read(m, "s", &string);
+ if (r < 0) {
+ fprintf(stderr, "sd_bus_message_read() failed: %s\n", strerror(-r));
+ return 0;
+ }
+
+ r = sd_bus_reply_method_return(m, "s", string);
+ if (r < 0) {
+ fprintf(stderr, "sd_bus_reply_method_return() failed: %s\n", strerror(-r));
+ return 0;
+ }
+
+ return 1;
+}
+
+static const sd_bus_vtable vtable[] = {
+ SD_BUS_VTABLE_START(0),
+ SD_BUS_METHOD(
+ "Method1", "s", "s", method, 0),
+ SD_BUS_METHOD_WITH_NAMES_OFFSET(
+ "Method2",
+ "so", SD_BUS_PARAM(string) SD_BUS_PARAM(path),
+ "s", SD_BUS_PARAM(returnstring),
+ method, offsetof(object, number),
+ SD_BUS_VTABLE_DEPRECATED),
+ SD_BUS_METHOD_WITH_ARGS_OFFSET(
+ "Method3",
+ SD_BUS_ARGS("s", string, "o", path),
+ SD_BUS_RESULT("s", returnstring),
+ method, offsetof(object, number),
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS(
+ "Method4",
+ SD_BUS_NO_ARGS,
+ SD_BUS_NO_RESULT,
+ method,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_SIGNAL(
+ "Signal1",
+ "so",
+ 0),
+ SD_BUS_SIGNAL_WITH_NAMES(
+ "Signal2",
+ "so", SD_BUS_PARAM(string) SD_BUS_PARAM(path),
+ 0),
+ SD_BUS_SIGNAL_WITH_ARGS(
+ "Signal3",
+ SD_BUS_ARGS("s", string, "o", path),
+ 0),
+ SD_BUS_WRITABLE_PROPERTY(
+ "AutomaticStringProperty", "s", NULL, NULL,
+ offsetof(object, name),
+ SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+ SD_BUS_WRITABLE_PROPERTY(
+ "AutomaticIntegerProperty", "u", NULL, NULL,
+ offsetof(object, number),
+ SD_BUS_VTABLE_PROPERTY_EMITS_INVALIDATION),
+ SD_BUS_VTABLE_END
+};
+
+int main(int argc, char **argv) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ int r;
+
+ sd_bus_default(&bus);
+
+ object object = { .number = 666 };
+ object.name = strdup("name");
+ if (!object.name) {
+ fprintf(stderr, "OOM\n");
+ return EXIT_FAILURE;
+ }
+
+ r = sd_bus_add_object_vtable(bus, NULL,
+ "/org/freedesktop/systemd/VtableExample",
+ "org.freedesktop.systemd.VtableExample",
+ vtable,
+ &object);
+ if (r < 0) {
+ fprintf(stderr, "sd_bus_add_object_vtable() failed: %s\n", strerror(-r));
+ return EXIT_FAILURE;
+ }
+
+ r = sd_bus_request_name(bus,
+ "org.freedesktop.systemd.VtableExample",
+ 0);
+ if (r < 0) {
+ fprintf(stderr, "sd_bus_request_name() failed: %s\n", strerror(-r));
+ return EXIT_FAILURE;
+ }
+
+ for (;;) {
+ r = sd_bus_wait(bus, UINT64_MAX);
+ if (r < 0) {
+ fprintf(stderr, "sd_bus_wait() failed: %s\n", strerror(-r));
+ return EXIT_FAILURE;
+ }
+
+ r = sd_bus_process(bus, NULL);
+ if (r < 0) {
+ fprintf(stderr, "sd_bus_process() failed: %s\n", strerror(-r));
+ return EXIT_FAILURE;
+ }
+ }
+
+ r = sd_bus_release_name(bus, "org.freedesktop.systemd.VtableExample");
+ if (r < 0) {
+ fprintf(stderr, "sd_bus_release_name() failed: %s\n", strerror(-r));
+ return EXIT_FAILURE;
+ }
+
+ free(object.name);
+
+ return 0;
+}
diff --git a/doc-migration/source/code-examples/py/90-rearrange-path.py b/doc-migration/source/code-examples/py/90-rearrange-path.py
new file mode 100755
index 00000000000..5c727e411ec
--- /dev/null
+++ b/doc-migration/source/code-examples/py/90-rearrange-path.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: MIT-0
+
+"""
+
+Proof-of-concept systemd environment generator that makes sure that bin dirs
+are always after matching sbin dirs in the path.
+(Changes /sbin:/bin:/foo/bar to /bin:/sbin:/foo/bar.)
+
+This generator shows how to override the configuration possibly created by
+earlier generators. It would be easier to write in bash, but let's have it
+in Python just to prove that we can, and to serve as a template for more
+interesting generators.
+
+"""
+
+import os
+import pathlib
+
+def rearrange_bin_sbin(path):
+ """Make sure any pair of …/bin, …/sbin directories is in this order
+
+ >>> rearrange_bin_sbin('/bin:/sbin:/usr/sbin:/usr/bin')
+ '/bin:/sbin:/usr/bin:/usr/sbin'
+ """
+ items = [pathlib.Path(p) for p in path.split(':')]
+ for i in range(len(items)):
+ if 'sbin' in items[i].parts:
+ ind = items[i].parts.index('sbin')
+ bin = pathlib.Path(*items[i].parts[:ind], 'bin', *items[i].parts[ind+1:])
+ if bin in items[i+1:]:
+ j = i + 1 + items[i+1:].index(bin)
+ items[i], items[j] = items[j], items[i]
+ return ':'.join(p.as_posix() for p in items)
+
+if __name__ == '__main__':
+ path = os.environ['PATH'] # This should be always set.
+ # If it's not, we'll just crash, which is OK too.
+ new = rearrange_bin_sbin(path)
+ if new != path:
+ print('PATH={}'.format(new))
diff --git a/doc-migration/source/code-examples/py/check-os-release-simple.py b/doc-migration/source/code-examples/py/check-os-release-simple.py
new file mode 100644
index 00000000000..ce73c77b14a
--- /dev/null
+++ b/doc-migration/source/code-examples/py/check-os-release-simple.py
@@ -0,0 +1,12 @@
+#!/usr/bin/python
+# SPDX-License-Identifier: MIT-0
+
+import platform
+os_release = platform.freedesktop_os_release()
+
+pretty_name = os_release.get('PRETTY_NAME', 'Linux')
+print(f'Running on {pretty_name!r}')
+
+if 'fedora' in [os_release.get('ID', 'linux'),
+ *os_release.get('ID_LIKE', '').split()]:
+ print('Looks like Fedora!')
diff --git a/doc-migration/source/code-examples/py/check-os-release.py b/doc-migration/source/code-examples/py/check-os-release.py
new file mode 100644
index 00000000000..19b193ec76a
--- /dev/null
+++ b/doc-migration/source/code-examples/py/check-os-release.py
@@ -0,0 +1,37 @@
+#!/usr/bin/python
+# SPDX-License-Identifier: MIT-0
+
+import ast
+import re
+import sys
+
+def read_os_release():
+ try:
+ filename = '/etc/os-release'
+ f = open(filename)
+ except FileNotFoundError:
+ filename = '/usr/lib/os-release'
+ f = open(filename)
+
+ for line_number, line in enumerate(f, start=1):
+ line = line.rstrip()
+ if not line or line.startswith('#'):
+ continue
+ m = re.match(r'([A-Z][A-Z_0-9]+)=(.*)', line)
+ if m:
+ name, val = m.groups()
+ if val and val[0] in '"\'':
+ val = ast.literal_eval(val)
+ yield name, val
+ else:
+ print(f'{filename}:{line_number}: bad line {line!r}',
+ file=sys.stderr)
+
+os_release = dict(read_os_release())
+
+pretty_name = os_release.get('PRETTY_NAME', 'Linux')
+print(f'Running on {pretty_name!r}')
+
+if 'debian' in [os_release.get('ID', 'linux'),
+ *os_release.get('ID_LIKE', '').split()]:
+ print('Looks like Debian!')
diff --git a/doc-migration/source/code-examples/py/notify-selfcontained-example.py b/doc-migration/source/code-examples/py/notify-selfcontained-example.py
new file mode 100644
index 00000000000..a1efb419ced
--- /dev/null
+++ b/doc-migration/source/code-examples/py/notify-selfcontained-example.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: MIT-0
+#
+# Implement the systemd notify protocol without external dependencies.
+# Supports both readiness notification on startup and on reloading,
+# according to the protocol defined at:
+# https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html
+# This protocol is guaranteed to be stable as per:
+# https://systemd.io/PORTABILITY_AND_STABILITY/
+
+import errno
+import os
+import signal
+import socket
+import sys
+import time
+
+reloading = False
+terminating = False
+
+def notify(message):
+ if not message:
+ raise ValueError("notify() requires a message")
+
+ socket_path = os.environ.get("NOTIFY_SOCKET")
+ if not socket_path:
+ return
+
+ if socket_path[0] not in ("/", "@"):
+ raise OSError(errno.EAFNOSUPPORT, "Unsupported socket type")
+
+ # Handle abstract socket.
+ if socket_path[0] == "@":
+ socket_path = "\0" + socket_path[1:]
+
+ with socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM | socket.SOCK_CLOEXEC) as sock:
+ sock.connect(socket_path)
+ sock.sendall(message)
+
+def notify_ready():
+ notify(b"READY=1")
+
+def notify_reloading():
+ microsecs = time.clock_gettime_ns(time.CLOCK_MONOTONIC) // 1000
+ notify(f"RELOADING=1\nMONOTONIC_USEC={microsecs}".encode())
+
+def notify_stopping():
+ notify(b"STOPPING=1")
+
+def reload(signum, frame):
+ global reloading
+ reloading = True
+
+def terminate(signum, frame):
+ global terminating
+ terminating = True
+
+def main():
+ print("Doing initial setup")
+ global reloading, terminating
+
+ # Set up signal handlers.
+ print("Setting up signal handlers")
+ signal.signal(signal.SIGHUP, reload)
+ signal.signal(signal.SIGINT, terminate)
+ signal.signal(signal.SIGTERM, terminate)
+
+ # Do any other setup work here.
+
+ # Once all setup is done, signal readiness.
+ print("Done setting up")
+ notify_ready()
+
+ print("Starting loop")
+ while not terminating:
+ if reloading:
+ print("Reloading")
+ reloading = False
+
+ # Support notifying the manager when reloading configuration.
+ # This allows accurate state tracking as well as automatically
+ # enabling 'systemctl reload' without needing to manually
+ # specify an ExecReload= line in the unit file.
+
+ notify_reloading()
+
+ # Do some reconfiguration work here.
+
+ print("Done reloading")
+ notify_ready()
+
+ # Do the real work here ...
+
+ print("Sleeping for five seconds")
+ time.sleep(5)
+
+ print("Terminating")
+ notify_stopping()
+
+if __name__ == "__main__":
+ sys.stdout.reconfigure(line_buffering=True)
+ print("Starting app")
+ main()
+ print("Stopped app")
diff --git a/doc-migration/source/code-examples/sh/check-os-release.sh b/doc-migration/source/code-examples/sh/check-os-release.sh
new file mode 100644
index 00000000000..12f7ee12cc5
--- /dev/null
+++ b/doc-migration/source/code-examples/sh/check-os-release.sh
@@ -0,0 +1,11 @@
+#!/bin/sh -eu
+# SPDX-License-Identifier: MIT-0
+
+test -e /etc/os-release && os_release='/etc/os-release' || os_release='/usr/lib/os-release'
+. "${os_release}"
+
+echo "Running on ${PRETTY_NAME:-Linux}"
+
+if [ "${ID:-linux}" = "debian" ] || [ "${ID_LIKE#*debian*}" != "${ID_LIKE}" ]; then
+ echo "Looks like Debian!"
+fi
diff --git a/doc-migration/source/conf.py b/doc-migration/source/conf.py
new file mode 100644
index 00000000000..36b1eaa0c1f
--- /dev/null
+++ b/doc-migration/source/conf.py
@@ -0,0 +1,212 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# Configuration file for the Sphinx documentation builder.
+#
+# For the full list of built-in configuration values, see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Project information -----------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
+
+import sys
+import os
+project = 'systemd'
+copyright = '2024, systemd'
+author = 'systemd'
+
+
+sys.path.append(os.path.abspath("./_ext"))
+
+
+# -- General configuration ---------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
+
+extensions = ['sphinxcontrib.globalsubs',
+ 'directive_roles', 'external_man_links', 'autogen_index']
+
+templates_path = ['_templates']
+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+
+
+# -- Options for HTML output -------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
+
+html_theme = 'furo'
+html_static_path = ['_static']
+html_title = ''
+html_css_files = [
+ 'css/custom.css',
+]
+html_theme_options = {
+ # TODO: update these `source`-options with the proper values
+ "source_repository": "https://github.com/neighbourhoodie/nh-systemd",
+ "source_branch": "man_pages_in_sphinx",
+ "source_directory": "doc-migration/source/",
+ "light_logo": "systemd-logo.svg",
+ "dark_logo": "systemd-logo.svg",
+ "light_css_variables": {
+ "color-brand-primary": "#35a764",
+ "color-brand-content": "#35a764",
+ },
+}
+
+
+man_pages = [
+ ('busctl', 'busctl', 'Introspect the bus', None, '1'),
+ ('journalctl', 'journalctl', 'Print log entries from the systemd journal', None, '1'),
+ ('os-release', 'os-release', 'Operating system identification', None, '5'),
+ ('systemd', 'systemd, init', 'systemd system and service manager', None, '1'),
+]
+
+global_substitutions = {f'v{n}': f'{n}' for n in range(183, 300)} | {
+ # Custom Entities
+ 'MOUNT_PATH': '{{MOUNT_PATH}}',
+ 'UMOUNT_PATH': '{{UMOUNT_PATH}}',
+ 'SYSTEM_GENERATOR_DIR': '{{SYSTEM_GENERATOR_DIR}}',
+ 'USER_GENERATOR_DIR': '{{USER_GENERATOR_DIR}}',
+ 'SYSTEM_ENV_GENERATOR_DIR': '{{SYSTEM_ENV_GENERATOR_DIR}}',
+ 'USER_ENV_GENERATOR_DIR': '{{USER_ENV_GENERATOR_DIR}}',
+ 'CERTIFICATE_ROOT': '{{CERTIFICATE_ROOT}}',
+ 'FALLBACK_HOSTNAME': '{{FALLBACK_HOSTNAME}}',
+ 'MEMORY_ACCOUNTING_DEFAULT': "{{ 'yes' if MEMORY_ACCOUNTING_DEFAULT else 'no' }}",
+ 'KILL_USER_PROCESSES': "{{ 'yes' if KILL_USER_PROCESSES else 'no' }}",
+ 'DEBUGTTY': '{{DEBUGTTY}}',
+ 'RC_LOCAL_PATH': '{{RC_LOCAL_PATH}}',
+ 'HIGH_RLIMIT_NOFILE': '{{HIGH_RLIMIT_NOFILE}}',
+ 'DEFAULT_DNSSEC_MODE': '{{DEFAULT_DNSSEC_MODE_STR}}',
+ 'DEFAULT_DNS_OVER_TLS_MODE': '{{DEFAULT_DNS_OVER_TLS_MODE_STR}}',
+ 'DEFAULT_TIMEOUT': '{{DEFAULT_TIMEOUT_SEC}} s',
+ 'DEFAULT_USER_TIMEOUT': '{{DEFAULT_USER_TIMEOUT_SEC}} s',
+ 'DEFAULT_KEYMAP': '{{SYSTEMD_DEFAULT_KEYMAP}}',
+ 'fedora_latest_version': '40',
+ 'fedora_cloud_release': '1.10',
+}
+
+# Existing lists of directive groups
+directives_data = [
+ {
+ "id": "unit-directives",
+ "title": "Unit directives",
+ "description": "Directives for configuring units, used in unit files."
+ },
+ {
+ "id": "kernel-commandline-options",
+ "title": "Options on the kernel command line",
+ "description": "Kernel boot options for configuring the behaviour of the systemd process."
+ },
+ {
+ "id": "smbios-type-11-options",
+ "title": "SMBIOS Type 11 Variables",
+ "description": "Data passed from VMM to system via SMBIOS Type 11."
+ },
+ {
+ "id": "environment-variables",
+ "title": "Environment variables",
+ "description": "Environment variables understood by the systemd manager and other programs and environment variable-compatible settings."
+ },
+ {
+ "id": "system-credentials",
+ "title": "System Credentials",
+ "description": "System credentials understood by the system and service manager and various other components:"
+ },
+ {
+ "id": "efi-variables",
+ "title": "EFI variables",
+ "description": "EFI variables understood by\n "
+ },
+ {
+ "id": "home-directives",
+ "title": "Home Area/User Account directives",
+ "description": "Directives for configuring home areas and user accounts via\n "
+ },
+ {
+ "id": "udev-directives",
+ "title": "UDEV directives",
+ "description": "Directives for configuring systemd units through the udev database."
+ },
+ {
+ "id": "network-directives",
+ "title": "Network directives",
+ "description": "Directives for configuring network links through the net-setup-link udev builtin and networks\n through systemd-networkd."
+ },
+ {
+ "id": "journal-directives",
+ "title": "Journal fields",
+ "description": "Fields in the journal events with a well known meaning."
+ },
+ {
+ "id": "pam-directives",
+ "title": "PAM configuration directives",
+ "description": "Directives for configuring PAM behaviour."
+ },
+ {
+ "id": "fstab-options",
+ "title": 'fstab-options',
+ "description": "Options which influence mounted filesystems and encrypted volumes."
+ },
+ {
+ "id": "nspawn-directives",
+ "title": 'nspawn-directives',
+ "description": "Directives for configuring systemd-nspawn containers."
+ },
+ {
+ "id": "config-directives",
+ "title": "Program configuration options",
+ "description": "Directives for configuring the behaviour of the systemd process and other tools through\n configuration files."
+ },
+ {
+ "id": "options",
+ "title": "Command line options",
+ "description": "Command-line options accepted by programs in the systemd suite."
+ },
+ {
+ "id": "constants",
+ "title": "Constants",
+ "description": "Various constants used and/or defined by systemd."
+ },
+ {
+ "id": "dns",
+ "title": "DNS resource record types",
+ "description": "No description available"
+ },
+ {
+ "id": "miscellaneous",
+ "title": "Miscellaneous options and directives",
+ "description": "Other configuration elements which don't fit in any of the above groups."
+ },
+ {
+ "id": "specifiers",
+ "title": "Specifiers",
+ "description": "Short strings which are substituted in configuration directives."
+ },
+ {
+ "id": "filenames",
+ "title": "Files and directories",
+ "description": "Paths and file names referred to in the documentation."
+ },
+ {
+ "id": "dbus-interface",
+ "title": "D-Bus interfaces",
+ "description": "Interfaces exposed over D-Bus."
+ },
+ {
+ "id": "dbus-method",
+ "title": "D-Bus methods",
+ "description": "Methods exposed in the D-Bus interface."
+ },
+ {
+ "id": "dbus-property",
+ "title": "D-Bus properties",
+ "description": "Properties exposed in the D-Bus interface."
+ },
+ {
+ "id": "dbus-signal",
+ "title": "D-Bus signals",
+ "description": "Signals emitted in the D-Bus interface."
+ }
+]
+
+role_types = [
+ 'constant',
+ 'var',
+ 'option'
+]
diff --git a/doc-migration/source/docs/busctl.rst b/doc-migration/source/docs/busctl.rst
new file mode 100644
index 00000000000..df67b5eca56
--- /dev/null
+++ b/doc-migration/source/docs/busctl.rst
@@ -0,0 +1,593 @@
+.. SPDX-License-Identifier: LGPL-2.1-or-later:
+
+:title: busctl
+
+:manvolnum: 1
+
+.. _busctl(1):
+
+=========
+busctl(1)
+=========
+
+.. only:: html
+
+ busctl — Introspect the bus
+ ###########################
+
+ Synopsis
+ ########
+
+``busctl`` [OPTIONS...] [COMMAND] [...]
+
+Description
+===========
+
+``busctl`` may be used to
+introspect and monitor the D-Bus bus.
+
+Commands
+========
+
+The following commands are understood:
+
+``list``
+--------
+
+Show all peers on the bus, by their service
+names. By default, shows both unique and well-known names, but
+this may be changed with the ``--unique`` and
+``--acquired`` switches. This is the default
+operation if no command is specified.
+
+.. only:: html
+
+ .. versionadded:: 209
+
+``status []``
+----------------------
+
+Show process information and credentials of a
+bus service (if one is specified by its unique or well-known
+name), a process (if one is specified by its numeric PID), or
+the owner of the bus (if no parameter is
+specified).
+
+.. only:: html
+
+ .. versionadded:: 209
+
+``monitor [...]``
+--------------------------
+
+Dump messages being exchanged. If
+ is specified, show messages
+to or from this peer, identified by its well-known or unique
+name. Otherwise, show all messages on the bus. Use
+:kbd:`Ctrl` + :kbd:`C`
+to terminate the dump.
+
+.. only:: html
+
+ .. versionadded:: 209
+
+``capture [...]``
+--------------------------
+
+Similar to ``monitor`` but
+writes the output in pcapng format (for details, see
+`PCAP Next Generation (pcapng) Capture File Format `_).
+Make sure to redirect standard output to a file or pipe. Tools like
+:die-net:`wireshark(1)`
+may be used to dissect and view the resulting
+files.
+
+.. only:: html
+
+ .. versionadded:: 218
+
+``tree [...]``
+-----------------------
+
+Shows an object tree of one or more
+services. If is specified,
+show object tree of the specified services only. Otherwise,
+show all object trees of all services on the bus that acquired
+at least one well-known name.
+
+.. only:: html
+
+ .. versionadded:: 218
+
+``introspect