1#!/usr/bin/env python 2# SPDX-License-Identifier: GPL-2.0 3# -*- coding: utf-8; mode: python -*- 4# pylint: disable=R0903, C0330, R0914, R0912, E0401 5 6""" 7 maintainers-include 8 ~~~~~~~~~~~~~~~~~~~ 9 10 Implementation of the ``maintainers-include`` reST-directive. 11 12 :copyright: Copyright (C) 2019 Kees Cook <keescook@chromium.org> 13 :license: GPL Version 2, June 1991 see linux/COPYING for details. 14 15 The ``maintainers-include`` reST-directive performs extensive parsing 16 specific to the Linux kernel's standard "MAINTAINERS" file, in an 17 effort to avoid needing to heavily mark up the original plain text. 18""" 19 20import sys 21import re 22import os.path 23 24from glob import glob 25 26from docutils import statemachine 27from docutils.parsers.rst import Directive 28from docutils.parsers.rst.directives.misc import Include 29 30# 31# Base URL for intersphinx-like links to maintainer profiles 32# 33KERNELDOC_URL = "https://docs.kernel.org/" 34 35def ErrorString(exc): # Shamelessly stolen from docutils 36 return f'{exc.__class__.__name}: {exc}' 37 38__version__ = '1.0' 39 40maint_parser = None 41 42class MaintainersParser: 43 """Parse MAINTAINERS file(s) content""" 44 45 def __init__(self, app_dir, path): 46 self.path = path 47 self.profile_toc = set() 48 self.profile_entries = {} 49 50 result = list() 51 result.append(".. _maintainers:") 52 result.append("") 53 54 # Poor man's state machine. 55 descriptions = False 56 maintainers = False 57 subsystems = False 58 59 # Field letter to field name mapping. 60 field_letter = None 61 fields = dict() 62 63 prev = None 64 field_prev = "" 65 field_content = "" 66 subsystem_name = None 67 68 base_dir, doc_dir, sphinx_dir = app_dir.partition("Documentation") 69 70 for line in open(path): 71 # Have we reached the end of the preformatted Descriptions text? 72 if descriptions and line.startswith('Maintainers'): 73 descriptions = False 74 # Ensure a blank line following the last "|"-prefixed line. 75 result.append("") 76 77 # Start subsystem processing? This is to skip processing the text 78 # between the Maintainers heading and the first subsystem name. 79 if maintainers and not subsystems: 80 if re.search('^[A-Z0-9]', line): 81 subsystems = True 82 83 # Drop needless input whitespace. 84 line = line.rstrip() 85 86 # 87 # Handle profile entries - either as files or as https refs 88 # 89 match = re.match(rf"P:\s*({doc_dir})(/\S+)\.rst", line) 90 if match: 91 name = "".join(match.groups()) 92 entry = os.path.relpath(base_dir + name, app_dir) 93 94 full_name = os.path.join(base_dir, name) 95 path = os.path.relpath(full_name, app_dir) 96 # 97 # When SPHINXDIRS is used, it will try to reference files 98 # outside srctree, causing warnings. To avoid that, point 99 # to the latest official documentation 100 # 101 if path.startswith("../"): 102 entry = KERNELDOC_URL + match.group(2) + ".html" 103 else: 104 entry = "/" + entry 105 106 if "*" in entry: 107 for e in glob(entry): 108 self.profile_toc.add(e) 109 self.profile_entries[subsystem_name] = e 110 else: 111 self.profile_toc.add(entry) 112 self.profile_entries[subsystem_name] = entry 113 else: 114 match = re.match(r"P:\s*(https?://.*)", line) 115 if match: 116 entry = match.group(1).strip() 117 self.profile_entries[subsystem_name] = entry 118 119 # Linkify all non-wildcard refs to ReST files in Documentation/. 120 pat = r'(Documentation/([^\s\?\*]*)\.rst)' 121 m = re.search(pat, line) 122 if m: 123 # maintainers.rst is in a subdirectory, so include "../". 124 line = re.sub(pat, ':doc:`%s <../%s>`' % (m.group(2), m.group(2)), line) 125 126 # Check state machine for output rendering behavior. 127 output = None 128 if descriptions: 129 # Escape the escapes in preformatted text. 130 output = "| %s" % (line.replace("\\", "\\\\")) 131 # Look for and record field letter to field name mappings: 132 # R: Designated *reviewer*: FullName <address@domain> 133 m = re.search(r"\s(\S):\s", line) 134 if m: 135 field_letter = m.group(1) 136 if field_letter and not field_letter in fields: 137 m = re.search(r"\*([^\*]+)\*", line) 138 if m: 139 fields[field_letter] = m.group(1) 140 elif subsystems: 141 # Skip empty lines: subsystem parser adds them as needed. 142 if len(line) == 0: 143 continue 144 # Subsystem fields are batched into "field_content" 145 if line[1] != ':': 146 # Render a subsystem entry as: 147 # SUBSYSTEM NAME 148 # ~~~~~~~~~~~~~~ 149 150 # Flush pending field content. 151 output = field_content + "\n\n" 152 field_content = "" 153 154 subsystem_name = line.title() 155 156 # Collapse whitespace in subsystem name. 157 heading = re.sub(r"\s+", " ", line) 158 output = output + "%s\n%s" % (heading, "~" * len(heading)) 159 field_prev = "" 160 else: 161 # Render a subsystem field as: 162 # :Field: entry 163 # entry... 164 field, details = line.split(':', 1) 165 details = details.strip() 166 167 # Mark paths (and regexes) as literal text for improved 168 # readability and to escape any escapes. 169 if field in ['F', 'N', 'X', 'K']: 170 # But only if not already marked :) 171 if not ':doc:' in details: 172 details = '``%s``' % (details) 173 174 # Comma separate email field continuations. 175 if field == field_prev and field_prev in ['M', 'R', 'L']: 176 field_content = field_content + "," 177 178 # Do not repeat field names, so that field entries 179 # will be collapsed together. 180 if field != field_prev: 181 output = field_content + "\n" 182 field_content = ":%s:" % (fields.get(field, field)) 183 field_content = field_content + "\n\t%s" % (details) 184 field_prev = field 185 else: 186 output = line 187 188 # Re-split on any added newlines in any above parsing. 189 if output != None: 190 for separated in output.split('\n'): 191 result.append(separated) 192 193 # Update the state machine when we find heading separators. 194 if line.startswith('----------'): 195 if prev.startswith('Descriptions'): 196 descriptions = True 197 if prev.startswith('Maintainers'): 198 maintainers = True 199 200 # Retain previous line for state machine transitions. 201 prev = line 202 203 # Flush pending field contents. 204 if field_content != "": 205 for separated in field_content.split('\n'): 206 result.append(separated) 207 208 self.output = "\n".join(result) 209 210 # Create a TOC class 211 212class MaintainersInclude(Include): 213 """MaintainersInclude (``maintainers-include``) directive""" 214 required_arguments = 0 215 216 def emit(self): 217 """Parse all the MAINTAINERS lines into ReST for human-readability""" 218 global maint_parser 219 220 path = maint_parser.path 221 output = maint_parser.output 222 223 # For debugging the pre-rendered results... 224 #print(output, file=open("/tmp/MAINTAINERS.rst", "w")) 225 226 self.state.document.settings.record_dependencies.add(path) 227 self.state_machine.insert_input(statemachine.string2lines(output), path) 228 229 def run(self): 230 """Include the MAINTAINERS file as part of this reST file.""" 231 if not self.state.document.settings.file_insertion_enabled: 232 raise self.warning('"%s" directive disabled.' % self.name) 233 234 try: 235 lines = self.emit() 236 except IOError as error: 237 raise self.severe('Problems with "%s" directive path:\n%s.' % 238 (self.name, ErrorString(error))) 239 240 return [] 241 242class MaintainersProfile(Include): 243 required_arguments = 0 244 245 def emit(self): 246 """Parse all the MAINTAINERS lines looking for profile entries""" 247 global maint_parser 248 249 path = maint_parser.path 250 251 # 252 # Produce a list with all maintainer profiles, sorted by subsystem name 253 # 254 output = "" 255 for profile, entry in sorted(maint_parser.profile_entries.items()): 256 if entry.startswith("http"): 257 output += f"- `{profile} <{entry}>`_\n" 258 else: 259 output += f"- :doc:`{profile} <{entry}>`\n" 260 261 # 262 # Create a hidden TOC table with all profiles. That allows adding 263 # profiles without needing to add them on any index.rst file. 264 # 265 output += "\n.. toctree::\n" 266 output += " :hidden:\n\n" 267 268 for fname in maint_parser.profile_toc: 269 output += f" {fname}\n" 270 271 output += "\n" 272 273 self.state.document.settings.record_dependencies.add(path) 274 self.state_machine.insert_input(statemachine.string2lines(output), path) 275 276 def run(self): 277 """Include the MAINTAINERS file as part of this reST file.""" 278 if not self.state.document.settings.file_insertion_enabled: 279 raise self.warning('"%s" directive disabled.' % self.name) 280 281 try: 282 lines = self.emit() 283 except IOError as error: 284 raise self.severe('Problems with "%s" directive path:\n%s.' % 285 (self.name, ErrorString(error))) 286 287 return [] 288 289def setup(app): 290 global maint_parser 291 292 # 293 # NOTE: we're using os.fspath() here because of a Sphinx warning: 294 # RemovedInSphinx90Warning: Sphinx 9 will drop support for representing paths as strings. Use "pathlib.Path" or "os.fspath" instead. 295 # 296 app_dir = os.fspath(app.srcdir) 297 srctree = os.path.abspath(os.environ["srctree"]) 298 path = os.path.join(srctree, "MAINTAINERS") 299 300 maint_parser = MaintainersParser(app_dir, path) 301 302 app.add_directive("maintainers-include", MaintainersInclude) 303 app.add_directive("maintainers-profile-toc", MaintainersProfile) 304 return dict( 305 version = __version__, 306 parallel_read_safe = True, 307 parallel_write_safe = True 308 ) 309