2 # SPDX-License-Identifier: GPL-2.0
3 # -*- coding: utf-8; mode: python -*-
6 Script to auto generate the documentation for Netlink specifications.
9 :license: GPL Version 2, June 1991 see linux/COPYING for details.
11 This script performs extensive parsing to the Linux kernel's netlink YAML
12 spec files, in an effort to avoid needing to heavily mark up the original
15 This code is split in three big parts:
16 1) RST formatters: Use to convert a string to a RST output
17 2) Parser helpers: Functions to parse the YAML data structure
18 3) Main function and small helpers
21 from typing import Any, Dict, List
34 def headroom(level: int) -> str:
35 """Return space to format"""
36 return " " * (level * SPACE_PER_LEVEL)
39 def bold(text: str) -> str:
40 """Format bold text"""
44 def inline(text: str) -> str:
45 """Format inline text"""
49 def sanitize(text: str) -> str:
50 """Remove newlines and multiple spaces"""
51 # This is useful for some fields that are spread across multiple lines
52 return str(text).replace("\n", "").strip()
55 def rst_fields(key: str, value: str, level: int = 0) -> str:
56 """Return a RST formatted field"""
57 return headroom(level) + f":{key}: {value}"
60 def rst_definition(key: str, value: Any, level: int = 0) -> str:
61 """Format a single rst definition"""
62 return headroom(level) + key + "\n" + headroom(level + 1) + str(value)
65 def rst_paragraph(paragraph: str, level: int = 0) -> str:
66 """Return a formatted paragraph"""
67 return headroom(level) + paragraph
70 def rst_bullet(item: str, level: int = 0) -> str:
71 """Return a formatted a bullet"""
72 return headroom(level) + f"- {item}"
75 def rst_subsection(title: str) -> str:
76 """Add a sub-section to the document"""
77 return f"{title}\n" + "-" * len(title)
80 def rst_subsubsection(title: str) -> str:
81 """Add a sub-sub-section to the document"""
82 return f"{title}\n" + "~" * len(title)
85 def rst_section(namespace: str, prefix: str, title: str) -> str:
86 """Add a section to the document"""
87 return f".. _{namespace}-{prefix}-{title}:\n\n{title}\n" + "=" * len(title)
90 def rst_subtitle(title: str) -> str:
91 """Add a subtitle to the document"""
92 return "\n" + "-" * len(title) + f"\n{title}\n" + "-" * len(title) + "\n\n"
95 def rst_title(title: str) -> str:
96 """Add a title to the document"""
97 return "=" * len(title) + f"\n{title}\n" + "=" * len(title) + "\n\n"
100 def rst_list_inline(list_: List[str], level: int = 0) -> str:
101 """Format a list using inlines"""
102 return headroom(level) + "[" + ", ".join(inline(i) for i in list_) + "]"
105 def rst_ref(namespace: str, prefix: str, name: str) -> str:
106 """Add a hyperlink to the document"""
107 mappings = {'enum': 'definition',
108 'fixed-header': 'definition',
109 'nested-attributes': 'attribute-set',
110 'struct': 'definition'}
111 if prefix in mappings:
112 prefix = mappings[prefix]
113 return f":ref:`{namespace}-{prefix}-{name}`"
116 def rst_header() -> str:
117 """The headers for all the auto generated RST files"""
120 lines.append(rst_paragraph(".. SPDX-License-Identifier: GPL-2.0"))
121 lines.append(rst_paragraph(".. NOTE: This document was auto-generated.\n\n"))
123 return "\n".join(lines)
126 def rst_toctree(maxdepth: int = 2) -> str:
127 """Generate a toctree RST primitive"""
130 lines.append(".. toctree::")
131 lines.append(f" :maxdepth: {maxdepth}\n\n")
133 return "\n".join(lines)
136 def rst_label(title: str) -> str:
137 """Return a formatted label"""
138 return f".. _{title}:\n\n"
145 def parse_mcast_group(mcast_group: List[Dict[str, Any]]) -> str:
146 """Parse 'multicast' group list and return a formatted string"""
148 for group in mcast_group:
149 lines.append(rst_bullet(group["name"]))
151 return "\n".join(lines)
154 def parse_do(do_dict: Dict[str, Any], level: int = 0) -> str:
155 """Parse 'do' section and return a formatted string"""
157 for key in do_dict.keys():
158 lines.append(rst_paragraph(bold(key), level + 1))
159 lines.append(parse_do_attributes(do_dict[key], level + 1) + "\n")
161 return "\n".join(lines)
164 def parse_do_attributes(attrs: Dict[str, Any], level: int = 0) -> str:
165 """Parse 'attributes' section"""
166 if "attributes" not in attrs:
168 lines = [rst_fields("attributes", rst_list_inline(attrs["attributes"]), level + 1)]
170 return "\n".join(lines)
173 def parse_operations(operations: List[Dict[str, Any]], namespace: str) -> str:
174 """Parse operations block"""
175 preprocessed = ["name", "doc", "title", "do", "dump"]
176 linkable = ["fixed-header", "attribute-set"]
179 for operation in operations:
180 lines.append(rst_section(namespace, 'operation', operation["name"]))
181 lines.append(rst_paragraph(sanitize(operation["doc"])) + "\n")
183 for key in operation.keys():
184 if key in preprocessed:
185 # Skip the special fields
187 value = operation[key]
189 value = rst_ref(namespace, key, value)
190 lines.append(rst_fields(key, value, 0))
192 if "do" in operation:
193 lines.append(rst_paragraph(":do:", 0))
194 lines.append(parse_do(operation["do"], 0))
195 if "dump" in operation:
196 lines.append(rst_paragraph(":dump:", 0))
197 lines.append(parse_do(operation["dump"], 0))
199 # New line after fields
202 return "\n".join(lines)
205 def parse_entries(entries: List[Dict[str, Any]], level: int) -> str:
206 """Parse a list of entries"""
209 for entry in entries:
210 if isinstance(entry, dict):
211 # entries could be a list or a dictionary
212 field_name = entry.get("name", "")
213 if field_name in ignored:
215 type_ = entry.get("type")
217 field_name += f" ({inline(type_)})"
219 rst_fields(field_name, sanitize(entry.get("doc", "")), level)
221 elif isinstance(entry, list):
222 lines.append(rst_list_inline(entry, level))
224 lines.append(rst_bullet(inline(sanitize(entry)), level))
227 return "\n".join(lines)
230 def parse_definitions(defs: Dict[str, Any], namespace: str) -> str:
231 """Parse definitions section"""
232 preprocessed = ["name", "entries", "members"]
233 ignored = ["render-max"] # This is not printed
236 for definition in defs:
237 lines.append(rst_section(namespace, 'definition', definition["name"]))
238 for k in definition.keys():
239 if k in preprocessed + ignored:
241 lines.append(rst_fields(k, sanitize(definition[k]), 0))
243 # Field list needs to finish with a new line
245 if "entries" in definition:
246 lines.append(rst_paragraph(":entries:", 0))
247 lines.append(parse_entries(definition["entries"], 1))
248 if "members" in definition:
249 lines.append(rst_paragraph(":members:", 0))
250 lines.append(parse_entries(definition["members"], 1))
252 return "\n".join(lines)
255 def parse_attr_sets(entries: List[Dict[str, Any]], namespace: str) -> str:
256 """Parse attribute from attribute-set"""
257 preprocessed = ["name", "type"]
258 linkable = ["enum", "nested-attributes", "struct", "sub-message"]
262 for entry in entries:
263 lines.append(rst_section(namespace, 'attribute-set', entry["name"]))
264 for attr in entry["attributes"]:
265 type_ = attr.get("type")
266 attr_line = attr["name"]
268 # Add the attribute type in the same line
269 attr_line += f" ({inline(type_)})"
271 lines.append(rst_subsubsection(attr_line))
273 for k in attr.keys():
274 if k in preprocessed + ignored:
277 value = rst_ref(namespace, k, attr[k])
279 value = sanitize(attr[k])
280 lines.append(rst_fields(k, value, 0))
283 return "\n".join(lines)
286 def parse_sub_messages(entries: List[Dict[str, Any]], namespace: str) -> str:
287 """Parse sub-message definitions"""
290 for entry in entries:
291 lines.append(rst_section(namespace, 'sub-message', entry["name"]))
292 for fmt in entry["formats"]:
295 lines.append(rst_bullet(bold(value)))
296 for attr in ['fixed-header', 'attribute-set']:
298 lines.append(rst_fields(attr,
299 rst_ref(namespace, attr, fmt[attr]),
303 return "\n".join(lines)
306 def parse_yaml(obj: Dict[str, Any]) -> str:
307 """Format the whole YAML into a RST string"""
312 lines.append(rst_header())
316 title = f"Family ``{family}`` netlink specification"
317 lines.append(rst_title(title))
318 lines.append(rst_paragraph(".. contents:: :depth: 3\n"))
321 lines.append(rst_subtitle("Summary"))
322 lines.append(rst_paragraph(obj["doc"], 0))
325 if "operations" in obj:
326 lines.append(rst_subtitle("Operations"))
327 lines.append(parse_operations(obj["operations"]["list"], family))
330 if "mcast-groups" in obj:
331 lines.append(rst_subtitle("Multicast groups"))
332 lines.append(parse_mcast_group(obj["mcast-groups"]["list"]))
335 if "definitions" in obj:
336 lines.append(rst_subtitle("Definitions"))
337 lines.append(parse_definitions(obj["definitions"], family))
340 if "attribute-sets" in obj:
341 lines.append(rst_subtitle("Attribute sets"))
342 lines.append(parse_attr_sets(obj["attribute-sets"], family))
345 if "sub-messages" in obj:
346 lines.append(rst_subtitle("Sub-messages"))
347 lines.append(parse_sub_messages(obj["sub-messages"], family))
349 return "\n".join(lines)
356 def parse_arguments() -> argparse.Namespace:
357 """Parse arguments from user"""
358 parser = argparse.ArgumentParser(description="Netlink RST generator")
360 parser.add_argument("-v", "--verbose", action="store_true")
361 parser.add_argument("-o", "--output", help="Output file name")
363 # Index and input are mutually exclusive
364 group = parser.add_mutually_exclusive_group()
366 "-x", "--index", action="store_true", help="Generate the index page"
368 group.add_argument("-i", "--input", help="YAML file name")
370 args = parser.parse_args()
373 logging.basicConfig(level=logging.DEBUG)
375 if args.input and not os.path.isfile(args.input):
376 logging.warning("%s is not a valid file.", args.input)
380 logging.error("No output file specified.")
383 if os.path.isfile(args.output):
384 logging.debug("%s already exists. Overwriting it.", args.output)
389 def parse_yaml_file(filename: str) -> str:
390 """Transform the YAML specified by filename into a rst-formmated string"""
391 with open(filename, "r", encoding="utf-8") as spec_file:
392 yaml_data = yaml.safe_load(spec_file)
393 content = parse_yaml(yaml_data)
398 def write_to_rstfile(content: str, filename: str) -> None:
399 """Write the generated content into an RST file"""
400 logging.debug("Saving RST file to %s", filename)
402 with open(filename, "w", encoding="utf-8") as rst_file:
403 rst_file.write(content)
406 def generate_main_index_rst(output: str) -> None:
407 """Generate the `networking_spec/index` content and write to the file"""
410 lines.append(rst_header())
411 lines.append(rst_label("specs"))
412 lines.append(rst_title("Netlink Family Specifications"))
413 lines.append(rst_toctree(1))
415 index_dir = os.path.dirname(output)
416 logging.debug("Looking for .rst files in %s", index_dir)
417 for filename in sorted(os.listdir(index_dir)):
418 if not filename.endswith(".rst") or filename == "index.rst":
420 lines.append(f" {filename.replace('.rst', '')}\n")
422 logging.debug("Writing an index file at %s", output)
423 write_to_rstfile("".join(lines), output)
427 """Main function that reads the YAML files and generates the RST files"""
429 args = parse_arguments()
432 logging.debug("Parsing %s", args.input)
434 content = parse_yaml_file(os.path.join(args.input))
435 except Exception as exception:
436 logging.warning("Failed to parse %s.", args.input)
437 logging.warning(exception)
440 write_to_rstfile(content, args.output)
443 # Generate the index RST file
444 generate_main_index_rst(args.output)
447 if __name__ == "__main__":