mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2026-07-23 01:37:32 -04:00
The last maintainer's entry ("THE REST") is meant to be at the
end. Ensure that.
While here, use a case-insensitive sort to avoid placing "iSCSI"
near the end.
Signed-off-by: Mauro Carvalho Chehab <mchehab+huawei@kernel.org>
Signed-off-by: Jonathan Corbet <corbet@lwn.net>
Message-ID: <b4f45565eff4ba6f01e84a6877813038a23ba83b.1778952682.git.mchehab+huawei@kernel.org>
416 lines
13 KiB
Python
Executable File
416 lines
13 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# SPDX-License-Identifier: GPL-2.0
|
|
# -*- coding: utf-8; mode: python -*-
|
|
# pylint: disable=C0209, C0301, E0401, R0022, R0902, R0903, R0912, R0914
|
|
|
|
"""
|
|
Implementation of the ``maintainers-include`` reST-directive.
|
|
|
|
:copyright: Copyright (C) 2019 Kees Cook <keescook@chromium.org>
|
|
: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.
|
|
"""
|
|
|
|
import os.path
|
|
import re
|
|
|
|
from glob import glob
|
|
|
|
from docutils import statemachine
|
|
from docutils.parsers.rst.directives.misc import Include
|
|
|
|
#
|
|
# Base URL for intersphinx-like links to maintainer profiles
|
|
#
|
|
KERNELDOC_URL = "https://docs.kernel.org/"
|
|
|
|
__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 = `
|
|
<p>Filter:
|
|
<input type="search" id="filter-table" placeholder="search string"/>
|
|
subsystem or property (case-sensitive)
|
|
</p>
|
|
`;
|
|
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
|
|
return f"{exc.__class__.__name}: {exc}" # pylint: disable=W0212
|
|
|
|
class MaintainersParser:
|
|
"""Parse MAINTAINERS file(s) content"""
|
|
|
|
def __init__(self, base_dir, app_dir, path):
|
|
self.path = path
|
|
|
|
# Poor man's state machine.
|
|
self.descriptions = False
|
|
self.maintainers = False
|
|
self.subsystems = False
|
|
|
|
self.subsystem_name = None
|
|
|
|
self.base_dir = base_dir
|
|
self.app_dir = app_dir
|
|
|
|
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.header = ""
|
|
self.maint_entries = {}
|
|
self.fields = {}
|
|
|
|
prev = None
|
|
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.header += line
|
|
elif self.subsystems:
|
|
self.parse_subsystems(line)
|
|
else:
|
|
self.header += 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
|
|
|
|
# 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(os.path.join(self.base_dir, m.group(1)))
|
|
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} </{entry}>`'
|
|
|
|
return entries
|
|
|
|
def linkify(self, text):
|
|
"""Return a list of doc files converted to cross-references"""
|
|
|
|
entries = self.get_entries(text)
|
|
if not entries:
|
|
return text
|
|
|
|
return self.re_doc.sub(", ".join(entries.values()), text)
|
|
|
|
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.header += "\n" + line
|
|
return
|
|
|
|
# Look for and record field letter to field name mappings:
|
|
# R: Designated *reviewer*: FullName <address@domain>
|
|
m = re.match(r"\s+(\S):\s+(\S+)", line)
|
|
if m:
|
|
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)
|
|
|
|
|
|
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
|
|
|
|
if line[1] != ':':
|
|
self.subsystem_name = re.sub(r"\s+", " ", self.linkify(line))
|
|
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
|
|
#
|
|
if field == "P":
|
|
entries = self.get_entries(details)
|
|
if entries:
|
|
for e, link in entries.items():
|
|
if "html" not in link:
|
|
self.profile_toc.add(e)
|
|
|
|
self.profile_entries[self.subsystem_name] = link
|
|
|
|
details = ", ".join(entries.values())
|
|
else:
|
|
match = re.match(r"(https?://.*)", details)
|
|
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)
|
|
else:
|
|
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 ':doc:' not in details and "http" not in details:
|
|
details = '``%s``' % (details)
|
|
|
|
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)
|
|
|
|
self.field_prev = field
|
|
|
|
|
|
class MaintainersInclude(Include):
|
|
"""MaintainersInclude (``maintainers-include``) directive"""
|
|
|
|
required_arguments = 0
|
|
|
|
def emit(self):
|
|
"""Parse all the MAINTAINERS lines into ReST for human-readability"""
|
|
path = maint_parser.path
|
|
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
|
|
|
|
# Keep the last entry ("THE REST") in the end
|
|
entries = list(maint_parser.maint_entries.keys())
|
|
entries = sorted(entries[:-1], key=str.casefold) + [entries[-1]]
|
|
|
|
for name in entries:
|
|
fields = maint_parser.maint_entries[name]
|
|
output += f" * - {name}\n"
|
|
tag = "-"
|
|
for field, lines in fields.items():
|
|
field_name = maint_parser.fields.get(field, field)
|
|
|
|
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"))
|
|
|
|
self.state.document.settings.record_dependencies.add(path)
|
|
self.state_machine.insert_input(statemachine.string2lines(output), path)
|
|
|
|
def run(self):
|
|
"""Include the MAINTAINERS file as part of this reST file."""
|
|
if not self.state.document.settings.file_insertion_enabled:
|
|
raise self.warning('"%s" directive disabled.' % self.name)
|
|
|
|
try:
|
|
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"""
|
|
env = self.state.document.settings.env
|
|
docdir = os.path.dirname(os.path.join(env.srcdir, env.docname))
|
|
path = maint_parser.path
|
|
|
|
#
|
|
# Produce a list with all maintainer profiles, sorted by subsystem name
|
|
#
|
|
output = ""
|
|
for profile, entry in sorted(maint_parser.profile_entries.items()):
|
|
name = profile.title()
|
|
|
|
if entry.startswith("http"):
|
|
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"- {entry}\n"
|
|
|
|
#
|
|
# Create a hidden TOC table with all profiles. That allows adding
|
|
# profiles without needing to add them on any index.rst file.
|
|
#
|
|
output += "\n.. toctree::\n"
|
|
output += " :hidden:\n\n"
|
|
|
|
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"
|
|
|
|
# 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)
|
|
|
|
def run(self):
|
|
"""Include the MAINTAINERS file as part of this reST file."""
|
|
if not self.state.document.settings.file_insertion_enabled:
|
|
raise self.warning('"%s" directive disabled.' % self.name)
|
|
|
|
try:
|
|
self.emit()
|
|
except IOError as error:
|
|
raise self.severe('Problems with "%s" directive path:\n%s.' %
|
|
(self.name, ErrorString(error)))
|
|
|
|
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
|
|
|
|
app_dir = os.path.abspath(app.srcdir)
|
|
match = re.match(r"(.*/)Documentation", app_dir)
|
|
if not match:
|
|
raise ValueError('Documentation directory not found.')
|
|
|
|
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)
|
|
|
|
app.connect("html-page-context", add_filter_script)
|
|
|
|
return {
|
|
"version": __version__,
|
|
"parallel_read_safe": True,
|
|
"parallel_write_safe": True,
|
|
}
|