maintainers_include.py 7.4 KB

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