Linux kernel mirror (for testing)
git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel
os
linux
1#!/usr/bin/env python3
2# pylint: disable=R0902,R0911,R0912,R0914,R0915
3# Copyright(c) 2025: Mauro Carvalho Chehab <mchehab@kernel.org>.
4# SPDX-License-Identifier: GPL-2.0
5
6
7"""
8Library to parse the Linux Feature files and produce a ReST book.
9"""
10
11import os
12import re
13import sys
14
15from glob import iglob
16
17
18class ParseFeature:
19 """
20 Parses Documentation/features, allowing to generate ReST documentation
21 from it.
22 """
23
24 #: feature header string.
25 h_name = "Feature"
26
27 #: Kernel config header string.
28 h_kconfig = "Kconfig"
29
30 #: description header string.
31 h_description = "Description"
32
33 #: subsystem header string.
34 h_subsys = "Subsystem"
35
36 #: status header string.
37 h_status = "Status"
38
39 #: architecture header string.
40 h_arch = "Architecture"
41
42 #: Sort order for status. Others will be mapped at the end.
43 status_map = {
44 "ok": 0,
45 "TODO": 1,
46 "N/A": 2,
47 # The only missing status is "..", which was mapped as "---",
48 # as this is an special ReST cell value. Let it get the
49 # default order (99).
50 }
51
52 def __init__(self, prefix, debug=0, enable_fname=False):
53 """
54 Sets internal variables.
55 """
56
57 self.prefix = prefix
58 self.debug = debug
59 self.enable_fname = enable_fname
60
61 self.data = {}
62
63 # Initial maximum values use just the headers
64 self.max_size_name = len(self.h_name)
65 self.max_size_kconfig = len(self.h_kconfig)
66 self.max_size_description = len(self.h_description)
67 self.max_size_desc_word = 0
68 self.max_size_subsys = len(self.h_subsys)
69 self.max_size_status = len(self.h_status)
70 self.max_size_arch = len(self.h_arch)
71 self.max_size_arch_with_header = self.max_size_arch + self.max_size_arch
72 self.description_size = 1
73
74 self.msg = ""
75
76 def emit(self, msg="", end="\n"):
77 """Helper function to append a new message for feature output."""
78
79 self.msg += msg + end
80
81 def parse_error(self, fname, ln, msg, data=None):
82 """
83 Displays an error message, printing file name and line.
84 """
85
86 if ln:
87 fname += f"#{ln}"
88
89 print(f"Warning: file {fname}: {msg}", file=sys.stderr, end="")
90
91 if data:
92 data = data.rstrip()
93 print(f":\n\t{data}", file=sys.stderr)
94 else:
95 print("", file=sys.stderr)
96
97 def parse_feat_file(self, fname):
98 """Parses a single arch-support.txt feature file."""
99
100 if os.path.isdir(fname):
101 return
102
103 base = os.path.basename(fname)
104
105 if base != "arch-support.txt":
106 if self.debug:
107 print(f"ignoring {fname}", file=sys.stderr)
108 return
109
110 subsys = os.path.dirname(fname).split("/")[-2]
111 self.max_size_subsys = max(self.max_size_subsys, len(subsys))
112
113 feature_name = ""
114 kconfig = ""
115 description = ""
116 comments = ""
117 arch_table = {}
118
119 if self.debug > 1:
120 print(f"Opening {fname}", file=sys.stderr)
121
122 if self.enable_fname:
123 full_fname = os.path.abspath(fname)
124 self.emit(f".. FILE {full_fname}")
125
126 with open(fname, encoding="utf-8") as f:
127 for ln, line in enumerate(f, start=1):
128 line = line.strip()
129
130 match = re.match(r"^\#\s+Feature\s+name:\s*(.*\S)", line)
131 if match:
132 feature_name = match.group(1)
133
134 self.max_size_name = max(self.max_size_name,
135 len(feature_name))
136 continue
137
138 match = re.match(r"^\#\s+Kconfig:\s*(.*\S)", line)
139 if match:
140 kconfig = match.group(1)
141
142 self.max_size_kconfig = max(self.max_size_kconfig,
143 len(kconfig))
144 continue
145
146 match = re.match(r"^\#\s+description:\s*(.*\S)", line)
147 if match:
148 description = match.group(1)
149
150 self.max_size_description = max(self.max_size_description,
151 len(description))
152
153 words = re.split(r"\s+", line)[1:]
154 for word in words:
155 self.max_size_desc_word = max(self.max_size_desc_word,
156 len(word))
157
158 continue
159
160 if re.search(r"^\\s*$", line):
161 continue
162
163 if re.match(r"^\s*\-+\s*$", line):
164 continue
165
166 if re.search(r"^\s*\|\s*arch\s*\|\s*status\s*\|\s*$", line):
167 continue
168
169 match = re.match(r"^\#\s*(.*)$", line)
170 if match:
171 comments += match.group(1)
172 continue
173
174 match = re.match(r"^\s*\|\s*(\S+):\s*\|\s*(\S+)\s*\|\s*$", line)
175 if match:
176 arch = match.group(1)
177 status = match.group(2)
178
179 self.max_size_status = max(self.max_size_status,
180 len(status))
181 self.max_size_arch = max(self.max_size_arch, len(arch))
182
183 if status == "..":
184 status = "---"
185
186 arch_table[arch] = status
187
188 continue
189
190 self.parse_error(fname, ln, "Line is invalid", line)
191
192 if not feature_name:
193 self.parse_error(fname, 0, "Feature name not found")
194 return
195 if not subsys:
196 self.parse_error(fname, 0, "Subsystem not found")
197 return
198 if not kconfig:
199 self.parse_error(fname, 0, "Kconfig not found")
200 return
201 if not description:
202 self.parse_error(fname, 0, "Description not found")
203 return
204 if not arch_table:
205 self.parse_error(fname, 0, "Architecture table not found")
206 return
207
208 self.data[feature_name] = {
209 "where": fname,
210 "subsys": subsys,
211 "kconfig": kconfig,
212 "description": description,
213 "comments": comments,
214 "table": arch_table,
215 }
216
217 self.max_size_arch_with_header = self.max_size_arch + len(self.h_arch)
218
219 def parse(self):
220 """Parses all arch-support.txt feature files inside self.prefix."""
221
222 path = os.path.expanduser(self.prefix)
223
224 if self.debug > 2:
225 print(f"Running parser for {path}")
226
227 example_path = os.path.join(path, "arch-support.txt")
228
229 for fname in iglob(os.path.join(path, "**"), recursive=True):
230 if fname != example_path:
231 self.parse_feat_file(fname)
232
233 return self.data
234
235 def output_arch_table(self, arch, feat=None):
236 """
237 Output feature(s) for a given architecture.
238 """
239
240 title = f"Feature status on {arch} architecture"
241
242 self.emit("=" * len(title))
243 self.emit(title)
244 self.emit("=" * len(title))
245 self.emit()
246
247 self.emit("=" * self.max_size_subsys + " ", end="")
248 self.emit("=" * self.max_size_name + " ", end="")
249 self.emit("=" * self.max_size_kconfig + " ", end="")
250 self.emit("=" * self.max_size_status + " ", end="")
251 self.emit("=" * self.max_size_description)
252
253 self.emit(f"{self.h_subsys:<{self.max_size_subsys}} ", end="")
254 self.emit(f"{self.h_name:<{self.max_size_name}} ", end="")
255 self.emit(f"{self.h_kconfig:<{self.max_size_kconfig}} ", end="")
256 self.emit(f"{self.h_status:<{self.max_size_status}} ", end="")
257 self.emit(f"{self.h_description:<{self.max_size_description}}")
258
259 self.emit("=" * self.max_size_subsys + " ", end="")
260 self.emit("=" * self.max_size_name + " ", end="")
261 self.emit("=" * self.max_size_kconfig + " ", end="")
262 self.emit("=" * self.max_size_status + " ", end="")
263 self.emit("=" * self.max_size_description)
264
265 sorted_features = sorted(self.data.keys(),
266 key=lambda x: (self.data[x]["subsys"],
267 x.lower()))
268
269 for name in sorted_features:
270 if feat and name != feat:
271 continue
272
273 arch_table = self.data[name]["table"]
274
275 if not arch in arch_table:
276 continue
277
278 self.emit(f"{self.data[name]['subsys']:<{self.max_size_subsys}} ",
279 end="")
280 self.emit(f"{name:<{self.max_size_name}} ", end="")
281 self.emit(f"{self.data[name]['kconfig']:<{self.max_size_kconfig}} ",
282 end="")
283 self.emit(f"{arch_table[arch]:<{self.max_size_status}} ",
284 end="")
285 self.emit(f"{self.data[name]['description']}")
286
287 self.emit("=" * self.max_size_subsys + " ", end="")
288 self.emit("=" * self.max_size_name + " ", end="")
289 self.emit("=" * self.max_size_kconfig + " ", end="")
290 self.emit("=" * self.max_size_status + " ", end="")
291 self.emit("=" * self.max_size_description)
292
293 return self.msg
294
295 def output_feature(self, feat):
296 """
297 Output a feature on all architectures.
298 """
299
300 title = f"Feature {feat}"
301
302 self.emit("=" * len(title))
303 self.emit(title)
304 self.emit("=" * len(title))
305 self.emit()
306
307 if not feat in self.data:
308 return
309
310 if self.data[feat]["subsys"]:
311 self.emit(f":Subsystem: {self.data[feat]['subsys']}")
312 if self.data[feat]["kconfig"]:
313 self.emit(f":Kconfig: {self.data[feat]['kconfig']}")
314
315 desc = self.data[feat]["description"]
316 desc = desc[0].upper() + desc[1:]
317 desc = desc.rstrip(". \t")
318 self.emit(f"\n{desc}.\n")
319
320 com = self.data[feat]["comments"].strip()
321 if com:
322 self.emit("Comments")
323 self.emit("--------")
324 self.emit(f"\n{com}\n")
325
326 self.emit("=" * self.max_size_arch + " ", end="")
327 self.emit("=" * self.max_size_status)
328
329 self.emit(f"{self.h_arch:<{self.max_size_arch}} ", end="")
330 self.emit(f"{self.h_status:<{self.max_size_status}}")
331
332 self.emit("=" * self.max_size_arch + " ", end="")
333 self.emit("=" * self.max_size_status)
334
335 arch_table = self.data[feat]["table"]
336 for arch in sorted(arch_table.keys()):
337 self.emit(f"{arch:<{self.max_size_arch}} ", end="")
338 self.emit(f"{arch_table[arch]:<{self.max_size_status}}")
339
340 self.emit("=" * self.max_size_arch + " ", end="")
341 self.emit("=" * self.max_size_status)
342
343 return self.msg
344
345 def matrix_lines(self, desc_size, max_size_status, header):
346 """
347 Helper function to split element tables at the output matrix.
348 """
349
350 if header:
351 ln_marker = "="
352 else:
353 ln_marker = "-"
354
355 self.emit("+" + ln_marker * self.max_size_name + "+", end="")
356 self.emit(ln_marker * desc_size, end="")
357 self.emit("+" + ln_marker * max_size_status + "+")
358
359 def output_matrix(self):
360 """
361 Generates a set of tables, groped by subsystem, containing
362 what's the feature state on each architecture.
363 """
364
365 title = "Feature status on all architectures"
366
367 self.emit("=" * len(title))
368 self.emit(title)
369 self.emit("=" * len(title))
370 self.emit()
371
372 desc_title = f"{self.h_kconfig} / {self.h_description}"
373
374 desc_size = self.max_size_kconfig + 4
375 if not self.description_size:
376 desc_size = max(self.max_size_description, desc_size)
377 else:
378 desc_size = max(self.description_size, desc_size)
379
380 desc_size = max(self.max_size_desc_word, desc_size, len(desc_title))
381
382 notcompat = "Not compatible"
383 self.max_size_status = max(self.max_size_status, len(notcompat))
384
385 min_status_size = self.max_size_status + self.max_size_arch + 4
386 max_size_status = max(min_status_size, self.max_size_status)
387
388 h_status_per_arch = "Status per architecture"
389 max_size_status = max(max_size_status, len(h_status_per_arch))
390
391 cur_subsys = None
392 for name in sorted(self.data.keys(),
393 key=lambda x: (self.data[x]["subsys"], x.lower())):
394 if not cur_subsys or cur_subsys != self.data[name]["subsys"]:
395 if cur_subsys:
396 self.emit()
397
398 cur_subsys = self.data[name]["subsys"]
399
400 title = f"Subsystem: {cur_subsys}"
401 self.emit(title)
402 self.emit("=" * len(title))
403 self.emit()
404
405 self.matrix_lines(desc_size, max_size_status, 0)
406
407 self.emit(f"|{self.h_name:<{self.max_size_name}}", end="")
408 self.emit(f"|{desc_title:<{desc_size}}", end="")
409 self.emit(f"|{h_status_per_arch:<{max_size_status}}|")
410
411 self.matrix_lines(desc_size, max_size_status, 1)
412
413 lines = []
414 descs = []
415 cur_status = ""
416 line = ""
417
418 arch_table = sorted(self.data[name]["table"].items(),
419 key=lambda x: (self.status_map.get(x[1], 99),
420 x[0].lower()))
421
422 for arch, status in arch_table:
423 if status == "---":
424 status = notcompat
425
426 if status != cur_status:
427 if line != "":
428 lines.append(line)
429 line = ""
430 line = f"- **{status}**: {arch}"
431 elif len(line) + len(arch) + 2 < max_size_status:
432 line += f", {arch}"
433 else:
434 lines.append(line)
435 line = f" {arch}"
436 cur_status = status
437
438 if line != "":
439 lines.append(line)
440
441 description = self.data[name]["description"]
442 while len(description) > desc_size:
443 desc_line = description[:desc_size]
444
445 last_space = desc_line.rfind(" ")
446 if last_space != -1:
447 desc_line = desc_line[:last_space]
448 descs.append(desc_line)
449 description = description[last_space + 1:]
450 else:
451 desc_line = desc_line[:-1]
452 descs.append(desc_line + "\\")
453 description = description[len(desc_line):]
454
455 if description:
456 descs.append(description)
457
458 while len(lines) < 2 + len(descs):
459 lines.append("")
460
461 for ln, line in enumerate(lines):
462 col = ["", ""]
463
464 if not ln:
465 col[0] = name
466 col[1] = f"``{self.data[name]['kconfig']}``"
467 else:
468 if ln >= 2 and descs:
469 col[1] = descs.pop(0)
470
471 self.emit(f"|{col[0]:<{self.max_size_name}}", end="")
472 self.emit(f"|{col[1]:<{desc_size}}", end="")
473 self.emit(f"|{line:<{max_size_status}}|")
474
475 self.matrix_lines(desc_size, max_size_status, 0)
476
477 return self.msg
478
479 def list_arch_features(self, arch, feat):
480 """
481 Print a matrix of kernel feature support for the chosen architecture.
482 """
483 self.emit("#")
484 self.emit(f"# Kernel feature support matrix of the '{arch}' architecture:")
485 self.emit("#")
486
487 # Sort by subsystem, then by feature name (case‑insensitive)
488 for name in sorted(self.data.keys(),
489 key=lambda n: (self.data[n]["subsys"].lower(),
490 n.lower())):
491 if feat and name != feat:
492 continue
493
494 feature = self.data[name]
495 arch_table = feature["table"]
496 status = arch_table.get(arch, "")
497 status = " " * ((4 - len(status)) // 2) + status
498
499 self.emit(f"{feature['subsys']:>{self.max_size_subsys + 1}}/ ",
500 end="")
501 self.emit(f"{name:<{self.max_size_name}}: ", end="")
502 self.emit(f"{status:<5}| ", end="")
503 self.emit(f"{feature['kconfig']:>{self.max_size_kconfig}} ",
504 end="")
505 self.emit(f"# {feature['description']}")
506
507 return self.msg