2 # SPDX-License-Identifier: GPL-2.0
3 # -*- coding: utf-8; mode: python -*-
6 Script to auto generate the documentation for Netlink specifications.
8 :copyright: Copyright (C) 2023 Breno Leitao <leitao@debian.org>
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 if key
in ['request', 'reply']:
160 lines
.append(parse_do_attributes(do_dict
[key
], level
+ 1) + "\n")
162 lines
.append(headroom(level
+ 2) + do_dict
[key
] + "\n")
164 return "\n".join(lines
)
167 def parse_do_attributes(attrs
: Dict
[str, Any
], level
: int = 0) -> str:
168 """Parse 'attributes' section"""
169 if "attributes" not in attrs
:
171 lines
= [rst_fields("attributes", rst_list_inline(attrs
["attributes"]), level
+ 1)]
173 return "\n".join(lines
)
176 def parse_operations(operations
: List
[Dict
[str, Any
]], namespace
: str) -> str:
177 """Parse operations block"""
178 preprocessed
= ["name", "doc", "title", "do", "dump", "flags"]
179 linkable
= ["fixed-header", "attribute-set"]
182 for operation
in operations
:
183 lines
.append(rst_section(namespace
, 'operation', operation
["name"]))
184 lines
.append(rst_paragraph(operation
["doc"]) + "\n")
186 for key
in operation
.keys():
187 if key
in preprocessed
:
188 # Skip the special fields
190 value
= operation
[key
]
192 value
= rst_ref(namespace
, key
, value
)
193 lines
.append(rst_fields(key
, value
, 0))
194 if 'flags' in operation
:
195 lines
.append(rst_fields('flags', rst_list_inline(operation
['flags'])))
197 if "do" in operation
:
198 lines
.append(rst_paragraph(":do:", 0))
199 lines
.append(parse_do(operation
["do"], 0))
200 if "dump" in operation
:
201 lines
.append(rst_paragraph(":dump:", 0))
202 lines
.append(parse_do(operation
["dump"], 0))
204 # New line after fields
207 return "\n".join(lines
)
210 def parse_entries(entries
: List
[Dict
[str, Any
]], level
: int) -> str:
211 """Parse a list of entries"""
214 for entry
in entries
:
215 if isinstance(entry
, dict):
216 # entries could be a list or a dictionary
217 field_name
= entry
.get("name", "")
218 if field_name
in ignored
:
220 type_
= entry
.get("type")
222 field_name
+= f
" ({inline(type_)})"
224 rst_fields(field_name
, sanitize(entry
.get("doc", "")), level
)
226 elif isinstance(entry
, list):
227 lines
.append(rst_list_inline(entry
, level
))
229 lines
.append(rst_bullet(inline(sanitize(entry
)), level
))
232 return "\n".join(lines
)
235 def parse_definitions(defs
: Dict
[str, Any
], namespace
: str) -> str:
236 """Parse definitions section"""
237 preprocessed
= ["name", "entries", "members"]
238 ignored
= ["render-max"] # This is not printed
241 for definition
in defs
:
242 lines
.append(rst_section(namespace
, 'definition', definition
["name"]))
243 for k
in definition
.keys():
244 if k
in preprocessed
+ ignored
:
246 lines
.append(rst_fields(k
, sanitize(definition
[k
]), 0))
248 # Field list needs to finish with a new line
250 if "entries" in definition
:
251 lines
.append(rst_paragraph(":entries:", 0))
252 lines
.append(parse_entries(definition
["entries"], 1))
253 if "members" in definition
:
254 lines
.append(rst_paragraph(":members:", 0))
255 lines
.append(parse_entries(definition
["members"], 1))
257 return "\n".join(lines
)
260 def parse_attr_sets(entries
: List
[Dict
[str, Any
]], namespace
: str) -> str:
261 """Parse attribute from attribute-set"""
262 preprocessed
= ["name", "type"]
263 linkable
= ["enum", "nested-attributes", "struct", "sub-message"]
267 for entry
in entries
:
268 lines
.append(rst_section(namespace
, 'attribute-set', entry
["name"]))
269 for attr
in entry
["attributes"]:
270 type_
= attr
.get("type")
271 attr_line
= attr
["name"]
273 # Add the attribute type in the same line
274 attr_line
+= f
" ({inline(type_)})"
276 lines
.append(rst_subsubsection(attr_line
))
278 for k
in attr
.keys():
279 if k
in preprocessed
+ ignored
:
282 value
= rst_ref(namespace
, k
, attr
[k
])
284 value
= sanitize(attr
[k
])
285 lines
.append(rst_fields(k
, value
, 0))
288 return "\n".join(lines
)
291 def parse_sub_messages(entries
: List
[Dict
[str, Any
]], namespace
: str) -> str:
292 """Parse sub-message definitions"""
295 for entry
in entries
:
296 lines
.append(rst_section(namespace
, 'sub-message', entry
["name"]))
297 for fmt
in entry
["formats"]:
300 lines
.append(rst_bullet(bold(value
)))
301 for attr
in ['fixed-header', 'attribute-set']:
303 lines
.append(rst_fields(attr
,
304 rst_ref(namespace
, attr
, fmt
[attr
]),
308 return "\n".join(lines
)
311 def parse_yaml(obj
: Dict
[str, Any
]) -> str:
312 """Format the whole YAML into a RST string"""
317 lines
.append(rst_header())
321 title
= f
"Family ``{family}`` netlink specification"
322 lines
.append(rst_title(title
))
323 lines
.append(rst_paragraph(".. contents:: :depth: 3\n"))
326 lines
.append(rst_subtitle("Summary"))
327 lines
.append(rst_paragraph(obj
["doc"], 0))
330 if "operations" in obj
:
331 lines
.append(rst_subtitle("Operations"))
332 lines
.append(parse_operations(obj
["operations"]["list"], family
))
335 if "mcast-groups" in obj
:
336 lines
.append(rst_subtitle("Multicast groups"))
337 lines
.append(parse_mcast_group(obj
["mcast-groups"]["list"]))
340 if "definitions" in obj
:
341 lines
.append(rst_subtitle("Definitions"))
342 lines
.append(parse_definitions(obj
["definitions"], family
))
345 if "attribute-sets" in obj
:
346 lines
.append(rst_subtitle("Attribute sets"))
347 lines
.append(parse_attr_sets(obj
["attribute-sets"], family
))
350 if "sub-messages" in obj
:
351 lines
.append(rst_subtitle("Sub-messages"))
352 lines
.append(parse_sub_messages(obj
["sub-messages"], family
))
354 return "\n".join(lines
)
361 def parse_arguments() -> argparse
.Namespace
:
362 """Parse arguments from user"""
363 parser
= argparse
.ArgumentParser(description
="Netlink RST generator")
365 parser
.add_argument("-v", "--verbose", action
="store_true")
366 parser
.add_argument("-o", "--output", help="Output file name")
368 # Index and input are mutually exclusive
369 group
= parser
.add_mutually_exclusive_group()
371 "-x", "--index", action
="store_true", help="Generate the index page"
373 group
.add_argument("-i", "--input", help="YAML file name")
375 args
= parser
.parse_args()
378 logging
.basicConfig(level
=logging
.DEBUG
)
380 if args
.input and not os
.path
.isfile(args
.input):
381 logging
.warning("%s is not a valid file.", args
.input)
385 logging
.error("No output file specified.")
388 if os
.path
.isfile(args
.output
):
389 logging
.debug("%s already exists. Overwriting it.", args
.output
)
394 def parse_yaml_file(filename
: str) -> str:
395 """Transform the YAML specified by filename into a rst-formmated string"""
396 with
open(filename
, "r", encoding
="utf-8") as spec_file
:
397 yaml_data
= yaml
.safe_load(spec_file
)
398 content
= parse_yaml(yaml_data
)
403 def write_to_rstfile(content
: str, filename
: str) -> None:
404 """Write the generated content into an RST file"""
405 logging
.debug("Saving RST file to %s", filename
)
407 with
open(filename
, "w", encoding
="utf-8") as rst_file
:
408 rst_file
.write(content
)
411 def generate_main_index_rst(output
: str) -> None:
412 """Generate the `networking_spec/index` content and write to the file"""
415 lines
.append(rst_header())
416 lines
.append(rst_label("specs"))
417 lines
.append(rst_title("Netlink Family Specifications"))
418 lines
.append(rst_toctree(1))
420 index_dir
= os
.path
.dirname(output
)
421 logging
.debug("Looking for .rst files in %s", index_dir
)
422 for filename
in sorted(os
.listdir(index_dir
)):
423 if not filename
.endswith(".rst") or filename
== "index.rst":
425 lines
.append(f
" {filename.replace('.rst', '')}\n")
427 logging
.debug("Writing an index file at %s", output
)
428 write_to_rstfile("".join(lines
), output
)
432 """Main function that reads the YAML files and generates the RST files"""
434 args
= parse_arguments()
437 logging
.debug("Parsing %s", args
.input)
439 content
= parse_yaml_file(os
.path
.join(args
.input))
440 except Exception as exception
:
441 logging
.warning("Failed to parse %s.", args
.input)
442 logging
.warning(exception
)
445 write_to_rstfile(content
, args
.output
)
448 # Generate the index RST file
449 generate_main_index_rst(args
.output
)
452 if __name__
== "__main__":