Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
torvalds
GitHub Repository: torvalds/linux
Path: blob/master/tools/lib/python/feat/parse_features.py
122941 views
1
#!/usr/bin/env python3
2
# pylint: disable=R0902,R0911,R0912,R0914,R0915
3
# Copyright(c) 2025: Mauro Carvalho Chehab <[email protected]>.
4
# SPDX-License-Identifier: GPL-2.0
5
6
7
"""
8
Library to parse the Linux Feature files and produce a ReST book.
9
"""
10
11
import os
12
import re
13
import sys
14
15
from glob import iglob
16
17
18
class 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
508
509