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 6u""" 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 docutils import statemachine 25from docutils.utils.error_reporting import ErrorString 26from docutils.parsers.rst import Directive 27from docutils.parsers.rst.directives.misc import Include 28 29__version__ = '1.0' 30 31def setup(app): 32 app.add_directive("maintainers-include", MaintainersInclude) 33 return dict( 34 version = __version__, 35 parallel_read_safe = True, 36 parallel_write_safe = True 37 ) 38 39class MaintainersInclude(Include): 40 u"""MaintainersInclude (``maintainers-include``) directive""" 41 required_arguments = 0 42 43 def parse_maintainers(self, path): 44 """Parse all the MAINTAINERS lines into ReST for human-readability""" 45 46 result = list() 47 result.append(".. _maintainers:") 48 result.append("") 49 50 # Poor man's state machine. 51 descriptions = False 52 maintainers = False 53 subsystems = False 54 55 # Field letter to field name mapping. 56 field_letter = None 57 fields = dict() 58 59 prev = None 60 field_prev = "" 61 field_content = "" 62 63 for line in open(path): 64 if sys.version_info.major == 2: 65 line = unicode(line, 'utf-8') 66 # Have we reached the end of the preformatted Descriptions text? 67 if descriptions and line.startswith('Maintainers'): 68 descriptions = False 69 # Ensure a blank line following the last "|"-prefixed line. 70 result.append("") 71 72 # Start subsystem processing? This is to skip processing the text 73 # between the Maintainers heading and the first subsystem name. 74 if maintainers and not subsystems: 75 if re.search('^[A-Z0-9]', line): 76 subsystems = True 77 78 # Drop needless input whitespace. 79 line = line.rstrip() 80 81 # Linkify all non-wildcard refs to ReST files in Documentation/. 82 pat = '(Documentation/([^\s\?\*]*)\.rst)' 83 m = re.search(pat, line) 84 if m: 85 # maintainers.rst is in a subdirectory, so include "../". 86 line = re.sub(pat, ':doc:`%s <../%s>`' % (m.group(2), m.group(2)), line) 87 88 # Check state machine for output rendering behavior. 89 output = None 90 if descriptions: 91 # Escape the escapes in preformatted text. 92 output = "| %s" % (line.replace("\\", "\\\\")) 93 # Look for and record field letter to field name mappings: 94 # R: Designated *reviewer*: FullName <address@domain> 95 m = re.search("\s(\S):\s", line) 96 if m: 97 field_letter = m.group(1) 98 if field_letter and not field_letter in fields: 99 m = re.search("\*([^\*]+)\*", line) 100 if m: 101 fields[field_letter] = m.group(1) 102 elif subsystems: 103 # Skip empty lines: subsystem parser adds them as needed. 104 if len(line) == 0: 105 continue 106 # Subsystem fields are batched into "field_content" 107 if line[1] != ':': 108 # Render a subsystem entry as: 109 # SUBSYSTEM NAME 110 # ~~~~~~~~~~~~~~ 111 112 # Flush pending field content. 113 output = field_content + "\n\n" 114 field_content = "" 115 116 # Collapse whitespace in subsystem name. 117 heading = re.sub("\s+", " ", line) 118 output = output + "%s\n%s" % (heading, "~" * len(heading)) 119 field_prev = "" 120 else: 121 # Render a subsystem field as: 122 # :Field: entry 123 # entry... 124 field, details = line.split(':', 1) 125 details = details.strip() 126 127 # Mark paths (and regexes) as literal text for improved 128 # readability and to escape any escapes. 129 if field in ['F', 'N', 'X', 'K']: 130 # But only if not already marked :) 131 if not ':doc:' in details: 132 details = '``%s``' % (details) 133 134 # Comma separate email field continuations. 135 if field == field_prev and field_prev in ['M', 'R', 'L']: 136 field_content = field_content + "," 137 138 # Do not repeat field names, so that field entries 139 # will be collapsed together. 140 if field != field_prev: 141 output = field_content + "\n" 142 field_content = ":%s:" % (fields.get(field, field)) 143 field_content = field_content + "\n\t%s" % (details) 144 field_prev = field 145 else: 146 output = line 147 148 # Re-split on any added newlines in any above parsing. 149 if output != None: 150 for separated in output.split('\n'): 151 result.append(separated) 152 153 # Update the state machine when we find heading separators. 154 if line.startswith('----------'): 155 if prev.startswith('Descriptions'): 156 descriptions = True 157 if prev.startswith('Maintainers'): 158 maintainers = True 159 160 # Retain previous line for state machine transitions. 161 prev = line 162 163 # Flush pending field contents. 164 if field_content != "": 165 for separated in field_content.split('\n'): 166 result.append(separated) 167 168 output = "\n".join(result) 169 # For debugging the pre-rendered results... 170 #print(output, file=open("/tmp/MAINTAINERS.rst", "w")) 171 172 self.state_machine.insert_input( 173 statemachine.string2lines(output), path) 174 175 def run(self): 176 """Include the MAINTAINERS file as part of this reST file.""" 177 if not self.state.document.settings.file_insertion_enabled: 178 raise self.warning('"%s" directive disabled.' % self.name) 179 180 # Walk up source path directories to find Documentation/../ 181 path = self.state_machine.document.attributes['source'] 182 path = os.path.realpath(path) 183 tail = path 184 while tail != "Documentation" and tail != "": 185 (path, tail) = os.path.split(path) 186 187 # Append "MAINTAINERS" 188 path = os.path.join(path, "MAINTAINERS") 189 190 try: 191 self.state.document.settings.record_dependencies.add(path) 192 lines = self.parse_maintainers(path) 193 except IOError as error: 194 raise self.severe('Problems with "%s" directive path:\n%s.' % 195 (self.name, ErrorString(error))) 196 197 return [] 198