From 3859c1a7967be31b63b9c0f51ad1d23f54f0b280 Mon Sep 17 00:00:00 2001 From: Mauro Carvalho Chehab Date: Sat, 9 May 2026 08:56:34 +0200 Subject: [PATCH 01/13] docs: maintainers_include: keep hidden TOC sorted There's no practical difference on keeping it sorted, but it helps a lot when checking for differences after patches to the tool. Signed-off-by: Mauro Carvalho Chehab Message-ID: Signed-off-by: Jonathan Corbet Message-ID: <2742993764f2193101536a1cea40e034d75e0539.1778309595.git.mchehab+huawei@kernel.org> --- Documentation/sphinx/maintainers_include.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/sphinx/maintainers_include.py b/Documentation/sphinx/maintainers_include.py index 436e7ac42ffc..694cdbdc4caf 100755 --- a/Documentation/sphinx/maintainers_include.py +++ b/Documentation/sphinx/maintainers_include.py @@ -265,7 +265,7 @@ class MaintainersProfile(Include): output += "\n.. toctree::\n" output += " :hidden:\n\n" - for fname in maint_parser.profile_toc: + for fname in sorted(maint_parser.profile_toc): output += f" {fname}\n" output += "\n" From 4cd160e4eaa894d65a04716ced87b0dccdfe290d Mon Sep 17 00:00:00 2001 From: Mauro Carvalho Chehab Date: Sat, 9 May 2026 08:56:35 +0200 Subject: [PATCH 02/13] docs: maintainers_include: split state machine on multiple funcs Instead of one big __init__ code, split the MaintainersParser code in a way that the state machine remains on __init__, but the actual parser for descriptions and subsystems are moved to separate functions. To make parser easier, instead storing parsed results on a list, place them directly on a string. That granted 15% of performance increase(*) with Python 3.14 and made the logic simpler. (*) measured by creating a new directory under Documentation/, and placing justmaintainers.rst and an index file there, building it via sphinx-build-wrapper. Signed-off-by: Mauro Carvalho Chehab Message-ID: <0b72530cf496ce5e2987ca784058a50f4dc814d2.1777987027.git.mchehab+huawei@kernel.org> Signed-off-by: Jonathan Corbet Message-ID: <26376e92a5e31e5d285b8676ed443a247b42e4c5.1778309595.git.mchehab+huawei@kernel.org> --- Documentation/sphinx/maintainers_include.py | 297 +++++++++++--------- 1 file changed, 158 insertions(+), 139 deletions(-) diff --git a/Documentation/sphinx/maintainers_include.py b/Documentation/sphinx/maintainers_include.py index 694cdbdc4caf..6d47d55f5b73 100755 --- a/Documentation/sphinx/maintainers_include.py +++ b/Documentation/sphinx/maintainers_include.py @@ -47,167 +47,186 @@ class MaintainersParser: self.profile_toc = set() self.profile_entries = {} - result = list() - result.append(".. _maintainers:") - result.append("") + self.output = ".. _maintainers:\n\n" # Poor man's state machine. - descriptions = False - maintainers = False - subsystems = False + self.descriptions = False + self.maintainers = False + self.subsystems = False # Field letter to field name mapping. - field_letter = None - fields = dict() + self.field_letter = None + self.fields = dict() + + self.field_prev = "" + self.field_content = "" + self.subsystem_name = None + + self.app_dir = app_dir + self.base_dir, self.doc_dir, self.sphinx_dir = app_dir.partition("Documentation") + + self.re_doc = re.compile(r'(Documentation/([^\s\?\*]*)\.rst)') prev = None - field_prev = "" - field_content = "" - subsystem_name = None - - base_dir, doc_dir, sphinx_dir = app_dir.partition("Documentation") - for line in open(path): - # Have we reached the end of the preformatted Descriptions text? - if descriptions and line.startswith('Maintainers'): - descriptions = False - # Ensure a blank line following the last "|"-prefixed line. - result.append("") - - # Start subsystem processing? This is to skip processing the text - # between the Maintainers heading and the first subsystem name. - if maintainers and not subsystems: + if self.descriptions: + self.parse_descriptions(line) + elif self.maintainers and not self.subsystems: if re.search('^[A-Z0-9]', line): - subsystems = True - - # Drop needless input whitespace. - line = line.rstrip() - - # - # Handle profile entries - either as files or as https refs - # - match = re.match(rf"P:\s*({doc_dir})(/\S+)\.rst", line) - if match: - name = "".join(match.groups()) - entry = os.path.relpath(base_dir + name, app_dir) - - full_name = os.path.join(base_dir, name) - path = os.path.relpath(full_name, app_dir) - # - # When SPHINXDIRS is used, it will try to reference files - # outside srctree, causing warnings. To avoid that, point - # to the latest official documentation - # - if path.startswith("../"): - entry = KERNELDOC_URL + match.group(2) + ".html" + self.subsystems = True + self.parse_subsystems(line) else: - entry = "/" + entry - - if "*" in entry: - for e in glob(entry): - self.profile_toc.add(e) - self.profile_entries[subsystem_name] = e - else: - self.profile_toc.add(entry) - self.profile_entries[subsystem_name] = entry + self.output += line + elif self.subsystems: + self.parse_subsystems(line) else: - match = re.match(r"P:\s*(https?://.*)", line) - if match: - entry = match.group(1).strip() - self.profile_entries[subsystem_name] = entry - - # Linkify all non-wildcard refs to ReST files in Documentation/. - pat = r'(Documentation/([^\s\?\*]*)\.rst)' - m = re.search(pat, line) - if m: - # maintainers.rst is in a subdirectory, so include "../". - line = re.sub(pat, ':doc:`%s <../%s>`' % (m.group(2), m.group(2)), line) - - # Check state machine for output rendering behavior. - output = None - if descriptions: - # Escape the escapes in preformatted text. - output = "| %s" % (line.replace("\\", "\\\\")) - # Look for and record field letter to field name mappings: - # R: Designated *reviewer*: FullName - m = re.search(r"\s(\S):\s", line) - if m: - field_letter = m.group(1) - if field_letter and not field_letter in fields: - m = re.search(r"\*([^\*]+)\*", line) - if m: - fields[field_letter] = m.group(1) - elif subsystems: - # Skip empty lines: subsystem parser adds them as needed. - if len(line) == 0: - continue - # Subsystem fields are batched into "field_content" - if line[1] != ':': - # Render a subsystem entry as: - # SUBSYSTEM NAME - # ~~~~~~~~~~~~~~ - - # Flush pending field content. - output = field_content + "\n\n" - field_content = "" - - subsystem_name = line.title() - - # Collapse whitespace in subsystem name. - heading = re.sub(r"\s+", " ", line) - output = output + "%s\n%s" % (heading, "~" * len(heading)) - field_prev = "" - else: - # Render a subsystem field as: - # :Field: entry - # entry... - field, details = line.split(':', 1) - details = details.strip() - - # Mark paths (and regexes) as literal text for improved - # readability and to escape any escapes. - if field in ['F', 'N', 'X', 'K']: - # But only if not already marked :) - if not ':doc:' in details: - details = '``%s``' % (details) - - # Comma separate email field continuations. - if field == field_prev and field_prev in ['M', 'R', 'L']: - field_content = field_content + "," - - # Do not repeat field names, so that field entries - # will be collapsed together. - if field != field_prev: - output = field_content + "\n" - field_content = ":%s:" % (fields.get(field, field)) - field_content = field_content + "\n\t%s" % (details) - field_prev = field - else: - output = line - - # Re-split on any added newlines in any above parsing. - if output != None: - for separated in output.split('\n'): - result.append(separated) + self.output += line # Update the state machine when we find heading separators. if line.startswith('----------'): if prev.startswith('Descriptions'): - descriptions = True + self.descriptions = True if prev.startswith('Maintainers'): - maintainers = True + self.maintainers = True # Retain previous line for state machine transitions. prev = line # Flush pending field contents. - if field_content != "": - for separated in field_content.split('\n'): - result.append(separated) + if self.field_content: + self.output += self.field_content + "\n\n" - self.output = "\n".join(result) + self.output = self.output.rstrip() + + def parse_descriptions(self, line): + """Handle contents of the descriptions section.""" + + # Have we reached the end of the preformatted Descriptions text? + if line.startswith('Maintainers'): + self.descriptions = False + self.output += "\n" + line + return + + # Linkify all non-wildcard refs to ReST files in Documentation/. + m = self.re_doc.search(line) + if m: + # maintainers.rst is in a subdirectory, so include "../". + line = self.re_doc.sub(':doc:`%s <../%s>`' % (m.group(2), m.group(2)), line) + + # Escape the escapes in preformatted text. + output = "| %s" % (line.replace("\\", "\\\\")) + + # Look for and record field letter to field name mappings: + # R: Designated *reviewer*: FullName + m = re.search(r"\s(\S):\s", line) + if m: + self.field_letter = m.group(1) + + if self.field_letter and self.field_letter not in self.fields: + m = re.search(r"\*([^\*]+)\*", line) + if m: + self.fields[self.field_letter] = m.group(1) + + # Append parsed content to self.output + self.output += output + + def parse_subsystems(self, line): + """Handle contents of the per-subsystem sections.""" + + # Drop needless input whitespace. + line = line.rstrip() + + # + # Handle profile entries - either as files or as https refs + # + match = re.match(rf"P:\s*({self.doc_dir})(/\S+)\.rst", line) + if match: + name = "".join(match.groups()) + entry = os.path.relpath(self.base_dir + name, self.app_dir) + + full_name = os.path.join(self.base_dir, name) + path = os.path.relpath(full_name, self.app_dir) + # + # When SPHINXDIRS is used, it will try to reference files + # outside srctree, causing warnings. To avoid that, point + # to the latest official documentation + # + if path.startswith("../"): + entry = KERNELDOC_URL + match.group(2) + ".html" + else: + entry = "/" + entry + + if "*" in entry: + for e in glob(entry): + self.profile_toc.add(e) + self.profile_entries[self.subsystem_name] = e + else: + self.profile_toc.add(entry) + self.profile_entries[self.subsystem_name] = entry + else: + match = re.match(r"P:\s*(https?://.*)", line) + if match: + entry = match.group(1).strip() + self.profile_entries[self.subsystem_name] = entry + + # Linkify all non-wildcard refs to ReST files in Documentation/. + m = self.re_doc.search(line) + if m: + # maintainers.rst is in a subdirectory, so include "../". + line = self.re_doc.sub(':doc:`%s <../%s>`' % (m.group(2), m.group(2)), line) + + # Check state machine for output rendering behavior. + output = None + if self.subsystems: + # Skip empty lines: subsystem parser adds them as needed. + if len(line) == 0: + return + # Subsystem fields are batched into "field_content" + if line[1] != ':': + # Render a subsystem entry as: + # SUBSYSTEM NAME + # ~~~~~~~~~~~~~~ + # Flush pending field content. + output = self.field_content + "\n\n" + self.field_content = "" + + self.subsystem_name = line.title() + + # Collapse whitespace in subsystem name. + heading = re.sub(r"\s+", " ", line) + output = output + "%s\n%s" % (heading, "~" * len(heading)) + self.field_prev = "" + else: + # Render a subsystem field as: + # :Field: entry + # entry... + field, details = line.split(':', 1) + details = details.strip() + + # Mark paths (and regexes) as literal text for improved + # readability and to escape any escapes. + if field in ['F', 'N', 'X', 'K']: + # But only if not already marked :) + if not ':doc:' in details: + details = '``%s``' % (details) + + # Comma separate email field continuations. + if field == self.field_prev and self.field_prev in ['M', 'R', 'L']: + self.field_content = self.field_content + "," + + # Do not repeat field names, so that field entries + # will be collapsed together. + if field != self.field_prev: + output = self.field_content + "\n" + self.field_content = ":%s:" % (self.fields.get(field, field)) + self.field_content = self.field_content + "\n\t%s" % (details) + self.field_prev = field + elif not self.descriptions: + output = line + + if output is not None: + self.output += output + "\n" - # Create a TOC class class MaintainersInclude(Include): """MaintainersInclude (``maintainers-include``) directive""" From ec027efe85308b6b8e87637f2675c16b86a3ac99 Mon Sep 17 00:00:00 2001 From: Mauro Carvalho Chehab Date: Sat, 9 May 2026 08:56:36 +0200 Subject: [PATCH 03/13] docs: maintainers_include: cleanup the code Simplify the logic without affecting the output result. Signed-off-by: Mauro Carvalho Chehab Message-ID: <24a83995778de8710cac40a3089c2f2fe5c38dbd.1777987027.git.mchehab+huawei@kernel.org> Signed-off-by: Jonathan Corbet Message-ID: <5f48bf411925e923d206239a4de0a3e34592de88.1778309595.git.mchehab+huawei@kernel.org> --- Documentation/sphinx/maintainers_include.py | 184 ++++++++++---------- 1 file changed, 91 insertions(+), 93 deletions(-) diff --git a/Documentation/sphinx/maintainers_include.py b/Documentation/sphinx/maintainers_include.py index 6d47d55f5b73..615af227a8f8 100755 --- a/Documentation/sphinx/maintainers_include.py +++ b/Documentation/sphinx/maintainers_include.py @@ -44,10 +44,6 @@ class MaintainersParser: def __init__(self, app_dir, path): self.path = path - self.profile_toc = set() - self.profile_entries = {} - - self.output = ".. _maintainers:\n\n" # Poor man's state machine. self.descriptions = False @@ -67,6 +63,13 @@ class MaintainersParser: self.re_doc = re.compile(r'(Documentation/([^\s\?\*]*)\.rst)') + # + # Output variables with maintainers content to be stored + # + self.profile_toc = set() + self.profile_entries = {} + self.output = ".. _maintainers:\n\n" + prev = None for line in open(path): if self.descriptions: @@ -98,6 +101,16 @@ class MaintainersParser: self.output = self.output.rstrip() + + def linkify(self, text): + """Linkify all non-wildcard refs to ReST files in Documentation/""" + m = self.re_doc.search(text) + if m: + # maintainers.rst is in a subdirectory, so include "../". + text = self.re_doc.sub(':doc:`%s <../%s>`' % (m.group(2), m.group(2)), text) + + return text + def parse_descriptions(self, line): """Handle contents of the descriptions section.""" @@ -107,14 +120,8 @@ class MaintainersParser: self.output += "\n" + line return - # Linkify all non-wildcard refs to ReST files in Documentation/. - m = self.re_doc.search(line) - if m: - # maintainers.rst is in a subdirectory, so include "../". - line = self.re_doc.sub(':doc:`%s <../%s>`' % (m.group(2), m.group(2)), line) - # Escape the escapes in preformatted text. - output = "| %s" % (line.replace("\\", "\\\\")) + self.output += "| " + self.linkify(line).replace("\\", "\\\\") # Look for and record field letter to field name mappings: # R: Designated *reviewer*: FullName @@ -127,105 +134,96 @@ class MaintainersParser: if m: self.fields[self.field_letter] = m.group(1) - # Append parsed content to self.output - self.output += output - def parse_subsystems(self, line): """Handle contents of the per-subsystem sections.""" # Drop needless input whitespace. line = line.rstrip() + # Skip empty lines: subsystem parser adds them as needed. + if not line: + return + + # Subsystem fields are batched into "field_content" + if line[1] != ':': + line = self.linkify(line) + + # Render a subsystem entry as: + # SUBSYSTEM NAME + # ~~~~~~~~~~~~~~ + # Flush pending field content. + self.output += self.field_content + "\n\n" + self.field_content = "" + + self.subsystem_name = line.title() + + # Collapse whitespace in subsystem name. + heading = re.sub(r"\s+", " ", line) + self.output += "%s\n%s" % (heading, "~" * len(heading)) + "\n" + self.field_prev = "" + + return + + # Render a subsystem field as: + # :Field: entry + # entry... + field, details = line.split(':', 1) + details = details.strip() + # # Handle profile entries - either as files or as https refs # - match = re.match(rf"P:\s*({self.doc_dir})(/\S+)\.rst", line) - if match: - name = "".join(match.groups()) - entry = os.path.relpath(self.base_dir + name, self.app_dir) - - full_name = os.path.join(self.base_dir, name) - path = os.path.relpath(full_name, self.app_dir) - # - # When SPHINXDIRS is used, it will try to reference files - # outside srctree, causing warnings. To avoid that, point - # to the latest official documentation - # - if path.startswith("../"): - entry = KERNELDOC_URL + match.group(2) + ".html" - else: - entry = "/" + entry - - if "*" in entry: - for e in glob(entry): - self.profile_toc.add(e) - self.profile_entries[self.subsystem_name] = e - else: - self.profile_toc.add(entry) - self.profile_entries[self.subsystem_name] = entry - else: - match = re.match(r"P:\s*(https?://.*)", line) + if field == "P": + match = self.re_doc.match(details) if match: - entry = match.group(1).strip() - self.profile_entries[self.subsystem_name] = entry + name = "".join(match.groups()) + entry = os.path.relpath(self.base_dir + name, self.app_dir) - # Linkify all non-wildcard refs to ReST files in Documentation/. - m = self.re_doc.search(line) - if m: - # maintainers.rst is in a subdirectory, so include "../". - line = self.re_doc.sub(':doc:`%s <../%s>`' % (m.group(2), m.group(2)), line) + full_name = os.path.join(self.base_dir, name) + path = os.path.relpath(full_name, self.app_dir) + # + # When SPHINXDIRS is used, it will try to reference files + # outside srctree, causing warnings. To avoid that, point + # to the latest official documentation + # + if path.startswith("../"): + entry = KERNELDOC_URL + "/" + match.group(2) + ".html" + else: + entry = "/" + entry - # Check state machine for output rendering behavior. - output = None - if self.subsystems: - # Skip empty lines: subsystem parser adds them as needed. - if len(line) == 0: - return - # Subsystem fields are batched into "field_content" - if line[1] != ':': - # Render a subsystem entry as: - # SUBSYSTEM NAME - # ~~~~~~~~~~~~~~ - # Flush pending field content. - output = self.field_content + "\n\n" - self.field_content = "" - - self.subsystem_name = line.title() - - # Collapse whitespace in subsystem name. - heading = re.sub(r"\s+", " ", line) - output = output + "%s\n%s" % (heading, "~" * len(heading)) - self.field_prev = "" + if "*" in entry: + for e in glob(entry): + self.profile_toc.add(e) + self.profile_entries[self.subsystem_name] = e + else: + self.profile_toc.add(entry) + self.profile_entries[self.subsystem_name] = entry else: - # Render a subsystem field as: - # :Field: entry - # entry... - field, details = line.split(':', 1) - details = details.strip() + match = re.match(r"(https?://.*)", details) + if match: + entry = match.group(1).strip() + self.profile_entries[self.subsystem_name] = entry - # Mark paths (and regexes) as literal text for improved - # readability and to escape any escapes. - if field in ['F', 'N', 'X', 'K']: - # But only if not already marked :) - if not ':doc:' in details: - details = '``%s``' % (details) + details = self.linkify(details) - # Comma separate email field continuations. - if field == self.field_prev and self.field_prev in ['M', 'R', 'L']: - self.field_content = self.field_content + "," + # Mark paths (and regexes) as literal text for improved + # readability and to escape any escapes. + if field in ['F', 'N', 'X', 'K']: + # But only if not already marked :) + if not ':doc:' in details: + details = '``%s``' % (details) - # Do not repeat field names, so that field entries - # will be collapsed together. - if field != self.field_prev: - output = self.field_content + "\n" - self.field_content = ":%s:" % (self.fields.get(field, field)) - self.field_content = self.field_content + "\n\t%s" % (details) - self.field_prev = field - elif not self.descriptions: - output = line + # Comma separate email field continuations. + if field == self.field_prev and self.field_prev in ['M', 'R', 'L']: + self.field_content = self.field_content + "," - if output is not None: - self.output += output + "\n" + # Do not repeat field names, so that field entries + # will be collapsed together. + if field != self.field_prev: + self.output += self.field_content + "\n\n" + self.field_content = ":%s:" % (self.fields.get(field, field)) + self.field_content = self.field_content + "\n\t%s" % (details) + self.field_prev = field class MaintainersInclude(Include): From 8cf515e2597f90b0a460777a56d6dd3e8495f533 Mon Sep 17 00:00:00 2001 From: Mauro Carvalho Chehab Date: Sat, 9 May 2026 08:56:37 +0200 Subject: [PATCH 04/13] docs: maintainers_include: clean most SPHINXDIRS=process warnings building docs with SPHINXDIRS=process is too noisy, as it generates lots of undefined refs. Fixing it is easy: just let linkify generate html URLs for the broken links when SPHINXDIRS is used. Signed-off-by: Mauro Carvalho Chehab Message-ID: Signed-off-by: Jonathan Corbet Message-ID: <567200712771590d08e4da096b4def92bf729ffe.1778309595.git.mchehab+huawei@kernel.org> --- Documentation/sphinx/maintainers_include.py | 44 +++++++++++++++------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/Documentation/sphinx/maintainers_include.py b/Documentation/sphinx/maintainers_include.py index 615af227a8f8..d3ad01e5309e 100755 --- a/Documentation/sphinx/maintainers_include.py +++ b/Documentation/sphinx/maintainers_include.py @@ -58,8 +58,8 @@ class MaintainersParser: self.field_content = "" self.subsystem_name = None - self.app_dir = app_dir - self.base_dir, self.doc_dir, self.sphinx_dir = app_dir.partition("Documentation") + self.app_dir = os.path.abspath(app_dir) + self.base_dir, _, self.sphinx_dir = self.app_dir.partition("Documentation") self.re_doc = re.compile(r'(Documentation/([^\s\?\*]*)\.rst)') @@ -104,10 +104,25 @@ class MaintainersParser: def linkify(self, text): """Linkify all non-wildcard refs to ReST files in Documentation/""" + m = self.re_doc.search(text) if m: - # maintainers.rst is in a subdirectory, so include "../". - text = self.re_doc.sub(':doc:`%s <../%s>`' % (m.group(2), m.group(2)), text) + fname = m.group(1) + ename = m.group(2) + + entry = os.path.relpath(self.base_dir + fname, self.app_dir) + entry = entry.removesuffix(".rst") + + # + # When SPHINXDIRS is used, it will try to reference files + # outside srctree, causing warnings. To avoid that, point + # to the latest official documentation + # + if entry.startswith("../"): + html = KERNELDOC_URL + ename + ".html" + text = self.re_doc.sub(f'`{ename} <{html}>`_', text) + else: + text = self.re_doc.sub(f':doc:`{ename} `', text) return text @@ -176,27 +191,32 @@ class MaintainersParser: if field == "P": match = self.re_doc.match(details) if match: - name = "".join(match.groups()) - entry = os.path.relpath(self.base_dir + name, self.app_dir) + fname = match.group(1) + ename = match.group(2) - full_name = os.path.join(self.base_dir, name) - path = os.path.relpath(full_name, self.app_dir) + entry = os.path.relpath(self.base_dir + fname, self.app_dir) + entry = entry.removesuffix(".rst") # # When SPHINXDIRS is used, it will try to reference files # outside srctree, causing warnings. To avoid that, point # to the latest official documentation # - if path.startswith("../"): - entry = KERNELDOC_URL + "/" + match.group(2) + ".html" + + if entry.startswith("../"): + entry = KERNELDOC_URL + ename + ".html" else: entry = "/" + entry if "*" in entry: for e in glob(entry): - self.profile_toc.add(e) + if "html" not in e: + self.profile_toc.add(e) + self.profile_entries[self.subsystem_name] = e else: - self.profile_toc.add(entry) + if "html" not in entry: + self.profile_toc.add(entry) + self.profile_entries[self.subsystem_name] = entry else: match = re.match(r"(https?://.*)", details) From b15fd8c043f79d57c6bf56edb4cf036644842647 Mon Sep 17 00:00:00 2001 From: Mauro Carvalho Chehab Date: Sat, 9 May 2026 08:56:38 +0200 Subject: [PATCH 05/13] docs: maintainers_include: do some coding style cleanups Minor coding style adjustments to use the style most python doc scripts are following. No functional changes. Assisted-by: pylint, black Signed-off-by: Mauro Carvalho Chehab Message-ID: <460aabd0518f080b34e12fdc0beb7ec7685d5866.1777987027.git.mchehab+huawei@kernel.org> Signed-off-by: Jonathan Corbet Message-ID: --- Documentation/sphinx/maintainers_include.py | 101 ++++++++++---------- 1 file changed, 51 insertions(+), 50 deletions(-) diff --git a/Documentation/sphinx/maintainers_include.py b/Documentation/sphinx/maintainers_include.py index d3ad01e5309e..345eb28804ff 100755 --- a/Documentation/sphinx/maintainers_include.py +++ b/Documentation/sphinx/maintainers_include.py @@ -1,30 +1,25 @@ #!/usr/bin/env python # SPDX-License-Identifier: GPL-2.0 # -*- coding: utf-8; mode: python -*- -# pylint: disable=R0903, C0330, R0914, R0912, E0401 +# pylint: disable=C0209, C0301, E0401, R0022, R0902, R0903, R0912, R0914 """ - maintainers-include - ~~~~~~~~~~~~~~~~~~~ +Implementation of the ``maintainers-include`` reST-directive. - Implementation of the ``maintainers-include`` reST-directive. +:copyright: Copyright (C) 2019 Kees Cook +:license: GPL Version 2, June 1991 see linux/COPYING for details. - :copyright: Copyright (C) 2019 Kees Cook - :license: GPL Version 2, June 1991 see linux/COPYING for details. - - The ``maintainers-include`` reST-directive performs extensive parsing - specific to the Linux kernel's standard "MAINTAINERS" file, in an - effort to avoid needing to heavily mark up the original plain text. +The ``maintainers-include`` reST-directive performs extensive parsing +specific to the Linux kernel's standard "MAINTAINERS" file, in an +effort to avoid needing to heavily mark up the original plain text. """ -import sys -import re import os.path +import re from glob import glob from docutils import statemachine -from docutils.parsers.rst import Directive from docutils.parsers.rst.directives.misc import Include # @@ -32,12 +27,14 @@ from docutils.parsers.rst.directives.misc import Include # KERNELDOC_URL = "https://docs.kernel.org/" -def ErrorString(exc): # Shamelessly stolen from docutils - return f'{exc.__class__.__name}: {exc}' +__version__ = "1.0" -__version__ = '1.0' +maint_parser = None # pylint: disable=C0103 -maint_parser = None + +# Shamelessly stolen from docutils +def ErrorString(exc): # pylint: disable=C0103, C0116 + return f"{exc.__class__.__name}: {exc}" # pylint: disable=W0212 class MaintainersParser: """Parse MAINTAINERS file(s) content""" @@ -52,7 +49,7 @@ class MaintainersParser: # Field letter to field name mapping. self.field_letter = None - self.fields = dict() + self.fields = {} self.field_prev = "" self.field_content = "" @@ -71,29 +68,30 @@ class MaintainersParser: self.output = ".. _maintainers:\n\n" prev = None - for line in open(path): - if self.descriptions: - self.parse_descriptions(line) - elif self.maintainers and not self.subsystems: - if re.search('^[A-Z0-9]', line): - self.subsystems = True + with open(path, "r", encoding="utf-8") as fp: + for line in fp: + if self.descriptions: + self.parse_descriptions(line) + elif self.maintainers and not self.subsystems: + if re.search('^[A-Z0-9]', line): + self.subsystems = True + self.parse_subsystems(line) + else: + self.output += line + elif self.subsystems: self.parse_subsystems(line) else: self.output += line - elif self.subsystems: - self.parse_subsystems(line) - else: - self.output += line - # Update the state machine when we find heading separators. - if line.startswith('----------'): - if prev.startswith('Descriptions'): - self.descriptions = True - if prev.startswith('Maintainers'): - self.maintainers = True + # Update the state machine when we find heading separators. + if line.startswith("----------"): + if prev.startswith("Descriptions"): + self.descriptions = True + if prev.startswith("Maintainers"): + self.maintainers = True - # Retain previous line for state machine transitions. - prev = line + # Retain previous line for state machine transitions. + prev = line # Flush pending field contents. if self.field_content: @@ -130,7 +128,7 @@ class MaintainersParser: """Handle contents of the descriptions section.""" # Have we reached the end of the preformatted Descriptions text? - if line.startswith('Maintainers'): + if line.startswith("Maintainers"): self.descriptions = False self.output += "\n" + line return @@ -182,7 +180,7 @@ class MaintainersParser: # Render a subsystem field as: # :Field: entry # entry... - field, details = line.split(':', 1) + field, details = line.split(":", 1) details = details.strip() # @@ -248,12 +246,11 @@ class MaintainersParser: class MaintainersInclude(Include): """MaintainersInclude (``maintainers-include``) directive""" + required_arguments = 0 def emit(self): """Parse all the MAINTAINERS lines into ReST for human-readability""" - global maint_parser - path = maint_parser.path output = maint_parser.output @@ -269,20 +266,21 @@ class MaintainersInclude(Include): raise self.warning('"%s" directive disabled.' % self.name) try: - lines = self.emit() + self.emit() except IOError as error: raise self.severe('Problems with "%s" directive path:\n%s.' % (self.name, ErrorString(error))) return [] + class MaintainersProfile(Include): + """Generate a list with all maintainer's profiles""" + required_arguments = 0 def emit(self): """Parse all the MAINTAINERS lines looking for profile entries""" - global maint_parser - path = maint_parser.path # @@ -316,15 +314,17 @@ class MaintainersProfile(Include): raise self.warning('"%s" directive disabled.' % self.name) try: - lines = self.emit() + self.emit() except IOError as error: raise self.severe('Problems with "%s" directive path:\n%s.' % (self.name, ErrorString(error))) return [] + def setup(app): - global maint_parser + """Setup Sphinx extension""" + global maint_parser # pylint: disable=W0603 # # NOTE: we're using os.fspath() here because of a Sphinx warning: @@ -338,8 +338,9 @@ def setup(app): app.add_directive("maintainers-include", MaintainersInclude) app.add_directive("maintainers-profile-toc", MaintainersProfile) - return dict( - version = __version__, - parallel_read_safe = True, - parallel_write_safe = True - ) + + return { + "version": __version__, + "parallel_read_safe": True, + "parallel_write_safe": True, + } From 1aac8eff9a9e6d6e23909bd4aaa583a98a8f850a Mon Sep 17 00:00:00 2001 From: Mauro Carvalho Chehab Date: Sat, 9 May 2026 08:56:39 +0200 Subject: [PATCH 06/13] docs: maintainers_include: store maintainers entries on a dict Instead of creating just a big output data, store entries inside a dictionary. Doing that simplifies the parser a little bit and make the code clearer. Signed-off-by: Mauro Carvalho Chehab Message-ID: <4ad88179e03436984f29780ae380d50591f86c67.1777987027.git.mchehab+huawei@kernel.org> Signed-off-by: Jonathan Corbet Message-ID: --- Documentation/sphinx/maintainers_include.py | 70 +++++++++------------ 1 file changed, 28 insertions(+), 42 deletions(-) diff --git a/Documentation/sphinx/maintainers_include.py b/Documentation/sphinx/maintainers_include.py index 345eb28804ff..ed9cd7b3dc66 100755 --- a/Documentation/sphinx/maintainers_include.py +++ b/Documentation/sphinx/maintainers_include.py @@ -49,10 +49,7 @@ class MaintainersParser: # Field letter to field name mapping. self.field_letter = None - self.fields = {} - self.field_prev = "" - self.field_content = "" self.subsystem_name = None self.app_dir = os.path.abspath(app_dir) @@ -65,7 +62,9 @@ class MaintainersParser: # self.profile_toc = set() self.profile_entries = {} - self.output = ".. _maintainers:\n\n" + self.header = ".. _maintainers:\n\n" + self.maint_entries = {} + self.fields = {} prev = None with open(path, "r", encoding="utf-8") as fp: @@ -77,11 +76,11 @@ class MaintainersParser: self.subsystems = True self.parse_subsystems(line) else: - self.output += line + self.header += line elif self.subsystems: self.parse_subsystems(line) else: - self.output += line + self.header += line # Update the state machine when we find heading separators. if line.startswith("----------"): @@ -93,13 +92,6 @@ class MaintainersParser: # Retain previous line for state machine transitions. prev = line - # Flush pending field contents. - if self.field_content: - self.output += self.field_content + "\n\n" - - self.output = self.output.rstrip() - - def linkify(self, text): """Linkify all non-wildcard refs to ReST files in Documentation/""" @@ -130,11 +122,11 @@ class MaintainersParser: # Have we reached the end of the preformatted Descriptions text? if line.startswith("Maintainers"): self.descriptions = False - self.output += "\n" + line + self.header += "\n" + line return # Escape the escapes in preformatted text. - self.output += "| " + self.linkify(line).replace("\\", "\\\\") + self.header += "| " + self.linkify(line).replace("\\", "\\\\") # Look for and record field letter to field name mappings: # R: Designated *reviewer*: FullName @@ -157,24 +149,8 @@ class MaintainersParser: if not line: return - # Subsystem fields are batched into "field_content" if line[1] != ':': - line = self.linkify(line) - - # Render a subsystem entry as: - # SUBSYSTEM NAME - # ~~~~~~~~~~~~~~ - # Flush pending field content. - self.output += self.field_content + "\n\n" - self.field_content = "" - - self.subsystem_name = line.title() - - # Collapse whitespace in subsystem name. - heading = re.sub(r"\s+", " ", line) - self.output += "%s\n%s" % (heading, "~" * len(heading)) + "\n" - self.field_prev = "" - + self.subsystem_name = re.sub(r"\s+", " ", self.linkify(line)) return # Render a subsystem field as: @@ -224,23 +200,23 @@ class MaintainersParser: details = self.linkify(details) + # # Mark paths (and regexes) as literal text for improved # readability and to escape any escapes. + # if field in ['F', 'N', 'X', 'K']: # But only if not already marked :) if not ':doc:' in details: details = '``%s``' % (details) - # Comma separate email field continuations. - if field == self.field_prev and self.field_prev in ['M', 'R', 'L']: - self.field_content = self.field_content + "," + if self.subsystem_name not in self.maint_entries: + self.maint_entries[self.subsystem_name] = {} + + if field not in self.maint_entries[self.subsystem_name]: + self.maint_entries[self.subsystem_name][field] = [] + + self.maint_entries[self.subsystem_name][field].append(details) - # Do not repeat field names, so that field entries - # will be collapsed together. - if field != self.field_prev: - self.output += self.field_content + "\n\n" - self.field_content = ":%s:" % (self.fields.get(field, field)) - self.field_content = self.field_content + "\n\t%s" % (details) self.field_prev = field @@ -252,7 +228,15 @@ class MaintainersInclude(Include): def emit(self): """Parse all the MAINTAINERS lines into ReST for human-readability""" path = maint_parser.path - output = maint_parser.output + output = maint_parser.header + + for name, fields in sorted(maint_parser.maint_entries.items()): + output += "\n" + name + "\n" + output += "~" * len(name) + "\n" + + for field, lines in fields.items(): + field_name = maint_parser.fields.get(field, field) + output += f":{field_name}:\n\t" + ",\n\t".join(lines) + "\n\n" # For debugging the pre-rendered results... #print(output, file=open("/tmp/MAINTAINERS.rst", "w")) @@ -288,6 +272,8 @@ class MaintainersProfile(Include): # output = "" for profile, entry in sorted(maint_parser.profile_entries.items()): + profile = profile.title() + if entry.startswith("http"): output += f"- `{profile} <{entry}>`_\n" else: From 083f0b10f540ae16009a26cd441d7dc6f94e1c13 Mon Sep 17 00:00:00 2001 From: Mauro Carvalho Chehab Date: Sat, 9 May 2026 08:56:40 +0200 Subject: [PATCH 07/13] docs: maintainers_include: properly handle file patterns handling asterisks inside file patterns atdescription part is problematic, as ReST has special meaning for them. Due to that, convert such patterns to literal strings. Reported-by: Matteo Croce Fixes: 420849332f9f ("get_maintainer: add ** glob pattern support") Signed-off-by: Mauro Carvalho Chehab Message-ID: <89127706fb3493d00ecb21e528c8a27081e5ed40.1777987027.git.mchehab+huawei@kernel.org> Signed-off-by: Jonathan Corbet Message-ID: --- Documentation/sphinx/maintainers_include.py | 26 ++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Documentation/sphinx/maintainers_include.py b/Documentation/sphinx/maintainers_include.py index ed9cd7b3dc66..5361774b61bc 100755 --- a/Documentation/sphinx/maintainers_include.py +++ b/Documentation/sphinx/maintainers_include.py @@ -47,9 +47,6 @@ class MaintainersParser: self.maintainers = False self.subsystems = False - # Field letter to field name mapping. - self.field_letter = None - self.subsystem_name = None self.app_dir = os.path.abspath(app_dir) @@ -125,19 +122,22 @@ class MaintainersParser: self.header += "\n" + line return - # Escape the escapes in preformatted text. - self.header += "| " + self.linkify(line).replace("\\", "\\\\") - # Look for and record field letter to field name mappings: # R: Designated *reviewer*: FullName - m = re.search(r"\s(\S):\s", line) + m = re.match(r"\s+(\S):\s+(\S+)", line) if m: - self.field_letter = m.group(1) + field = m.group(1) + details = m.group(2) + + if field not in self.fields: + m = re.search(r"\*([^\*]+)\*", line) + if m: + self.fields[field] = m.group(1) + elif field in ['F', 'N', 'X', 'K']: + line = line.replace(details, f'``{details}``') + + self.header += "| " + self.linkify(line) - if self.field_letter and self.field_letter not in self.fields: - m = re.search(r"\*([^\*]+)\*", line) - if m: - self.fields[self.field_letter] = m.group(1) def parse_subsystems(self, line): """Handle contents of the per-subsystem sections.""" @@ -206,7 +206,7 @@ class MaintainersParser: # if field in ['F', 'N', 'X', 'K']: # But only if not already marked :) - if not ':doc:' in details: + if ':doc:' not in details and "http" not in details: details = '``%s``' % (details) if self.subsystem_name not in self.maint_entries: From 11bb115dd5b04d2188894da096555466537480b3 Mon Sep 17 00:00:00 2001 From: Mauro Carvalho Chehab Date: Sat, 9 May 2026 08:56:41 +0200 Subject: [PATCH 08/13] docs: maintainers_include: add a filtering javascript The maintainers table is big. Add a javascript to allow filtering it. Such script is only added at the page which contains the maintainers-include tag. I opted to keep the search case-sensitive, as, this way, upper case searches at subsystem. Signed-off-by: Mauro Carvalho Chehab Message-ID: Signed-off-by: Jonathan Corbet Message-ID: <6f87c233351595358fddba11d42171be464f0a65.1778309595.git.mchehab+huawei@kernel.org> --- Documentation/sphinx/maintainers_include.py | 78 +++++++++++++++++++-- 1 file changed, 72 insertions(+), 6 deletions(-) diff --git a/Documentation/sphinx/maintainers_include.py b/Documentation/sphinx/maintainers_include.py index 5361774b61bc..7035754a1c66 100755 --- a/Documentation/sphinx/maintainers_include.py +++ b/Documentation/sphinx/maintainers_include.py @@ -31,6 +31,49 @@ __version__ = "1.0" maint_parser = None # pylint: disable=C0103 +JS_FILTER = """ +(function() { + function filterTable(table) { + const filter = document.getElementById("filter-table").value.trim(); + const rows = table.querySelectorAll("tbody tr"); + for (let i = 0; i < rows.length; i++) { + const tds = rows[i].getElementsByTagName("td"); + let match = false; + for (let j = 0; j < tds.length; j++) { + const cellText = (tds[j].textContent || tds[j].innerText); + if (cellText.includes(filter)) { + match = true; + break; + } + } + rows[i].style.display = match ? "table-row" : "none"; + } + } + function addInput() { + const table = document.getElementById("maintainers-table"); + if (!table) return; + let input = document.getElementById("filter-table"); + if (!input) { + const filt_div = document.createElement('div'); + filt_div.innerHTML = ` +

Filter: + + subsystem or property (case-sensitive) +

+ `; + table.parentNode.insertBefore(filt_div, table); + const input = document.getElementById("filter-table") + input.addEventListener('input', () => filterTable(table)); + } + } + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', addInput); + } else { + addInput(); + } +})(); +""" + # Shamelessly stolen from docutils def ErrorString(exc): # pylint: disable=C0103, C0116 @@ -59,7 +102,7 @@ class MaintainersParser: # self.profile_toc = set() self.profile_entries = {} - self.header = ".. _maintainers:\n\n" + self.header = "" self.maint_entries = {} self.fields = {} @@ -228,15 +271,28 @@ class MaintainersInclude(Include): def emit(self): """Parse all the MAINTAINERS lines into ReST for human-readability""" path = maint_parser.path - output = maint_parser.header + output = ".. _maintainers:\n\n" + output += maint_parser.header + + output += ".. _maintainers_table:\n\n" + output += ".. flat-table::\n" + output += " :header-rows: 1\n\n" + output += " * - Subsystem\n" + output += " - Properties\n\n" + + self.state.document['maintainers_included'] = True for name, fields in sorted(maint_parser.maint_entries.items()): - output += "\n" + name + "\n" - output += "~" * len(name) + "\n" - + output += f" * - {name}\n" + tag = "-" for field, lines in fields.items(): field_name = maint_parser.fields.get(field, field) - output += f":{field_name}:\n\t" + ",\n\t".join(lines) + "\n\n" + + output += f" {tag} :{field_name}:\n " + output += ",\n ".join(lines) + "\n" + tag = " " + + output += "\n" # For debugging the pre-rendered results... #print(output, file=open("/tmp/MAINTAINERS.rst", "w")) @@ -308,6 +364,14 @@ class MaintainersProfile(Include): return [] +# pylint: disable=W0613 +def add_filter_script(app, pagename, templatename, context, doctree): + """Add Filter javascript only to maintainers page""" + + if doctree and doctree.get('maintainers_included'): + app.add_js_file(None, body=JS_FILTER) + + def setup(app): """Setup Sphinx extension""" global maint_parser # pylint: disable=W0603 @@ -325,6 +389,8 @@ def setup(app): app.add_directive("maintainers-include", MaintainersInclude) app.add_directive("maintainers-profile-toc", MaintainersProfile) + app.connect("html-page-context", add_filter_script) + return { "version": __version__, "parallel_read_safe": True, From 247cb381a5a474aee3865980404e33403cb6d10a Mon Sep 17 00:00:00 2001 From: Mauro Carvalho Chehab Date: Sat, 9 May 2026 08:56:42 +0200 Subject: [PATCH 09/13] docs: maintainers_include: don't ignore invalid profile entries Currently, there is a "P" entry for Rust pin-init that is neither a valid ReST file inside Documentation nor an URL. A proper fix is to either convert/move the file or point to a URL. Yet, the parser should be able to pick what's there and show on its output. Add a logic to produce a warning when this happens. Message-ID: <63228e005fcf3dc4583cee06905341e8bce84181.1777987027.git.mchehab+huawei@kernel.org> Signed-off-by: Mauro Carvalho Chehab Signed-off-by: Jonathan Corbet Message-ID: <98915f35442fa12c6d59611083aea3adbf8c6aff.1778309595.git.mchehab+huawei@kernel.org> --- Documentation/sphinx/maintainers_include.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Documentation/sphinx/maintainers_include.py b/Documentation/sphinx/maintainers_include.py index 7035754a1c66..073a10575872 100755 --- a/Documentation/sphinx/maintainers_include.py +++ b/Documentation/sphinx/maintainers_include.py @@ -240,6 +240,8 @@ class MaintainersParser: if match: entry = match.group(1).strip() self.profile_entries[self.subsystem_name] = entry + else: + self.profile_entries[self.subsystem_name] = f"``{details}``" details = self.linkify(details) @@ -328,12 +330,15 @@ class MaintainersProfile(Include): # output = "" for profile, entry in sorted(maint_parser.profile_entries.items()): - profile = profile.title() + name = profile.title() if entry.startswith("http"): - output += f"- `{profile} <{entry}>`_\n" + output += f"- `{name} <{entry}>`_\n" + elif entry.startswith("`"): + output += f"- {name}: {entry}\n" + self.warning(f"{profile}: Invalid 'P' tag: {entry}\n") else: - output += f"- :doc:`{profile} <{entry}>`\n" + output += f"- :doc:`{name} <{entry}>`\n" # # Create a hidden TOC table with all profiles. That allows adding From 84a9658e842b86b949f39e42f26f5400ba972ab0 Mon Sep 17 00:00:00 2001 From: Mauro Carvalho Chehab Date: Sat, 9 May 2026 08:56:43 +0200 Subject: [PATCH 10/13] docs: maintainers_include: better handle directories The TOC tree needs to use paths relative to the document containing the maintainers-profile-toc directive. Fix it. While here, address a warning from sashiko-bot, which points that using partition can be problematic if the root Linux path ends being something like: foo/Documentation/linux/ causing the documentation dir to be at: foo/Documentation/linux/Documentation Very unlikely, but fixing it is trivial: just use regex to pick the last one. Notice that I dropped the comment about using os.fspath() as the logic already uses os.path.abspath() which should work equally well. Signed-off-by: Mauro Carvalho Chehab Signed-off-by: Jonathan Corbet Message-ID: <2b07e12eaa07bf81824ad427335783b170e01dba.1778309595.git.mchehab+huawei@kernel.org> --- Documentation/sphinx/maintainers_include.py | 28 ++++++++++++--------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/Documentation/sphinx/maintainers_include.py b/Documentation/sphinx/maintainers_include.py index 073a10575872..8c7b79721edd 100755 --- a/Documentation/sphinx/maintainers_include.py +++ b/Documentation/sphinx/maintainers_include.py @@ -82,7 +82,7 @@ def ErrorString(exc): # pylint: disable=C0103, C0116 class MaintainersParser: """Parse MAINTAINERS file(s) content""" - def __init__(self, app_dir, path): + def __init__(self, base_dir, app_dir, path): self.path = path # Poor man's state machine. @@ -92,8 +92,8 @@ class MaintainersParser: self.subsystem_name = None - self.app_dir = os.path.abspath(app_dir) - self.base_dir, _, self.sphinx_dir = self.app_dir.partition("Documentation") + self.base_dir = base_dir + self.app_dir = app_dir self.re_doc = re.compile(r'(Documentation/([^\s\?\*]*)\.rst)') @@ -323,6 +323,8 @@ class MaintainersProfile(Include): def emit(self): """Parse all the MAINTAINERS lines looking for profile entries""" + env = self.state.document.settings.env + docdir = os.path.dirname(os.path.join(env.srcdir, env.docname)) path = maint_parser.path # @@ -347,7 +349,9 @@ class MaintainersProfile(Include): output += "\n.. toctree::\n" output += " :hidden:\n\n" - for fname in sorted(maint_parser.profile_toc): + for f in sorted(maint_parser.profile_toc): + fname = os.path.join(maint_parser.base_dir, "Documentation", f) + fname = os.path.relpath(fname, docdir) output += f" {fname}\n" output += "\n" @@ -381,15 +385,15 @@ def setup(app): """Setup Sphinx extension""" global maint_parser # pylint: disable=W0603 - # - # NOTE: we're using os.fspath() here because of a Sphinx warning: - # RemovedInSphinx90Warning: Sphinx 9 will drop support for representing paths as strings. Use "pathlib.Path" or "os.fspath" instead. - # - app_dir = os.fspath(app.srcdir) - srctree = os.path.abspath(os.environ["srctree"]) - path = os.path.join(srctree, "MAINTAINERS") + app_dir = os.path.abspath(app.srcdir) + match = re.match(r"(.*/)Documentation", app_dir) + if not match: + raise ValueError('Documentation directory not found.') - maint_parser = MaintainersParser(app_dir, path) + base_dir = match.group(1) + path = os.path.join(base_dir, "MAINTAINERS") + + maint_parser = MaintainersParser(base_dir, app_dir, path) app.add_directive("maintainers-include", MaintainersInclude) app.add_directive("maintainers-profile-toc", MaintainersProfile) From d5ab486874696074c99a0912ab09ae9e3ddc0f27 Mon Sep 17 00:00:00 2001 From: Mauro Carvalho Chehab Date: Sat, 9 May 2026 08:56:44 +0200 Subject: [PATCH 11/13] docs: maintainers_include: better handle doc wildcards As warned by sashiko-bot, the new logic doesn't handle wildcards on Documentation/. Change the logic to properly handle it, cleaning up the code to remove some code duplication. Signed-off-by: Mauro Carvalho Chehab Signed-off-by: Jonathan Corbet Message-ID: --- Documentation/sphinx/maintainers_include.py | 103 ++++++++++---------- 1 file changed, 53 insertions(+), 50 deletions(-) diff --git a/Documentation/sphinx/maintainers_include.py b/Documentation/sphinx/maintainers_include.py index 8c7b79721edd..be8e566e0363 100755 --- a/Documentation/sphinx/maintainers_include.py +++ b/Documentation/sphinx/maintainers_include.py @@ -95,7 +95,7 @@ class MaintainersParser: self.base_dir = base_dir self.app_dir = app_dir - self.re_doc = re.compile(r'(Documentation/([^\s\?\*]*)\.rst)') + self.re_doc = re.compile(r'(Documentation/(\S*)\.rst)') # # Output variables with maintainers content to be stored @@ -132,29 +132,47 @@ class MaintainersParser: # Retain previous line for state machine transitions. prev = line + def get_entries(self, text): + """Generate refs to ReST files in Documentation/""" + + if "Documentation/" not in text: + return None + + if "*" in text or "?" in text: + m = self.re_doc.search(text) + if not m: + return None + + doc_list = glob(m.group(1), root_dir=self.base_dir) + else: + doc_list = [text] + + entries = {} + for doc in doc_list: + m = self.re_doc.search(doc) + if m: + fname = m.group(1) + ename = m.group(2) + + entry = os.path.relpath(self.base_dir + fname, self.app_dir) + entry = entry.removesuffix(".rst") + + if entry.startswith("../"): + html = KERNELDOC_URL + ename + ".html" + entries[entry] = f'`{ename} <{html}>`_' + else: + entries[entry] = f':doc:`{ename} `' + + return entries + def linkify(self, text): - """Linkify all non-wildcard refs to ReST files in Documentation/""" + """Return a list of doc files converted to cross-references""" - m = self.re_doc.search(text) - if m: - fname = m.group(1) - ename = m.group(2) + entries = self.get_entries(text) + if not entries: + return text - entry = os.path.relpath(self.base_dir + fname, self.app_dir) - entry = entry.removesuffix(".rst") - - # - # When SPHINXDIRS is used, it will try to reference files - # outside srctree, causing warnings. To avoid that, point - # to the latest official documentation - # - if entry.startswith("../"): - html = KERNELDOC_URL + ename + ".html" - text = self.re_doc.sub(f'`{ename} <{html}>`_', text) - else: - text = self.re_doc.sub(f':doc:`{ename} `', text) - - return text + return self.re_doc.sub(", ".join(entries.values()), text) def parse_descriptions(self, line): """Handle contents of the descriptions section.""" @@ -206,35 +224,15 @@ class MaintainersParser: # Handle profile entries - either as files or as https refs # if field == "P": - match = self.re_doc.match(details) - if match: - fname = match.group(1) - ename = match.group(2) + entries = self.get_entries(details) + if entries: + for e, link in entries.items(): + if "html" not in link: + self.profile_toc.add(e) - entry = os.path.relpath(self.base_dir + fname, self.app_dir) - entry = entry.removesuffix(".rst") - # - # When SPHINXDIRS is used, it will try to reference files - # outside srctree, causing warnings. To avoid that, point - # to the latest official documentation - # + self.profile_entries[self.subsystem_name] = link - if entry.startswith("../"): - entry = KERNELDOC_URL + ename + ".html" - else: - entry = "/" + entry - - if "*" in entry: - for e in glob(entry): - if "html" not in e: - self.profile_toc.add(e) - - self.profile_entries[self.subsystem_name] = e - else: - if "html" not in entry: - self.profile_toc.add(entry) - - self.profile_entries[self.subsystem_name] = entry + details = ", ".join(entries.values()) else: match = re.match(r"(https?://.*)", details) if match: @@ -243,7 +241,9 @@ class MaintainersParser: else: self.profile_entries[self.subsystem_name] = f"``{details}``" - details = self.linkify(details) + details = self.linkify(details) + else: + details = self.linkify(details) # # Mark paths (and regexes) as literal text for improved @@ -340,7 +340,7 @@ class MaintainersProfile(Include): output += f"- {name}: {entry}\n" self.warning(f"{profile}: Invalid 'P' tag: {entry}\n") else: - output += f"- :doc:`{name} <{entry}>`\n" + output += f"- {entry}\n" # # Create a hidden TOC table with all profiles. That allows adding @@ -356,6 +356,9 @@ class MaintainersProfile(Include): output += "\n" + # For debugging the pre-rendered results... + #print(output, file=open("/tmp/profiles.rst", "w")) + self.state.document.settings.record_dependencies.add(path) self.state_machine.insert_input(statemachine.string2lines(output), path) From 446c78d6ab2dc8442c9e23d2042c396f1bc6cae2 Mon Sep 17 00:00:00 2001 From: Mauro Carvalho Chehab Date: Sat, 9 May 2026 08:56:45 +0200 Subject: [PATCH 12/13] MAINTAINERS: make clearer about what's expected for "P" field The "P" field is meant to point to a subsystem maintainer's profile, stored either at the Kernel documentation or on an external site. Make it clearer. Signed-off-by: Mauro Carvalho Chehab Message-ID: <921e5e6a074f9d8cf77483d73e6801f49254bbb8.1777987027.git.mchehab+huawei@kernel.org> Signed-off-by: Jonathan Corbet Message-ID: <21e6def8b69bac36795dcd4047d663bb31407dcb.1778309595.git.mchehab+huawei@kernel.org> --- MAINTAINERS | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MAINTAINERS b/MAINTAINERS index 2fb1c75afd16..77244b7f9545 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -25,9 +25,9 @@ Descriptions of section entries and preferred order C: URI for *chat* protocol, server and channel where developers usually hang out, for example irc://server/channel. P: *Subsystem Profile* document for more details submitting - patches to the given subsystem. This is either an in-tree file, - or a URI. See Documentation/maintainer/maintainer-entry-profile.rst - for details. + patches to the given subsystem. This is either an in-tree .rst file + inside Documentation/, or a URI. + See Documentation/maintainer/maintainer-entry-profile.rst for details. T: *SCM* tree type and location. Type is one of: git, hg, quilt, stgit, topgit F: *Files* and directories wildcard patterns. From 09958f69fa8f4e5d9ccb2e25f1b4700906f61632 Mon Sep 17 00:00:00 2001 From: Mauro Carvalho Chehab Date: Sat, 9 May 2026 08:56:46 +0200 Subject: [PATCH 13/13] MAINTAINERS: use a URL for pin-init maintainer's profile entry This maintainer's entry is not inside documentation nor is ReST, preventing Sphinx to create a hyperlink to it. Change it to point to the already-formatted URL. Signed-off-by: Mauro Carvalho Chehab Message-ID: <1bceee886b9027d66bbb48d9d6c8d1250ce8dbcb.1777987028.git.mchehab+huawei@kernel.org> Signed-off-by: Mauro Carvalho Chehab Acked-by: Gary Guo Signed-off-by: Jonathan Corbet Message-ID: <59144e7323b95166e61a7c7f84096a0b9bb5d26e.1778309595.git.mchehab+huawei@kernel.org> --- MAINTAINERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MAINTAINERS b/MAINTAINERS index 77244b7f9545..dd424a4f9f3b 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -23402,7 +23402,7 @@ S: Maintained W: https://rust-for-linux.com/pin-init B: https://github.com/Rust-for-Linux/pin-init/issues C: zulip://rust-for-linux.zulipchat.com -P: rust/pin-init/CONTRIBUTING.md +P: https://github.com/Rust-for-Linux/pin-init/blob/main/CONTRIBUTING.md T: git https://github.com/Rust-for-Linux/linux.git pin-init-next F: rust/kernel/init.rs F: rust/pin-init/