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