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