Path: blob/master/tools/lib/python/feat/parse_features.py
38186 views
#!/usr/bin/env python31# pylint: disable=R0902,R0911,R0912,R0914,R09152# Copyright(c) 2025: Mauro Carvalho Chehab <[email protected]>.3# SPDX-License-Identifier: GPL-2.0456"""7Library to parse the Linux Feature files and produce a ReST book.8"""910import os11import re12import sys1314from glob import iglob151617class ParseFeature:18"""19Parses Documentation/features, allowing to generate ReST documentation20from it.21"""2223h_name = "Feature"24h_kconfig = "Kconfig"25h_description = "Description"26h_subsys = "Subsystem"27h_status = "Status"28h_arch = "Architecture"2930# Sort order for status. Others will be mapped at the end.31status_map = {32"ok": 0,33"TODO": 1,34"N/A": 2,35# The only missing status is "..", which was mapped as "---",36# as this is an special ReST cell value. Let it get the37# default order (99).38}3940def __init__(self, prefix, debug=0, enable_fname=False):41"""42Sets internal variables43"""4445self.prefix = prefix46self.debug = debug47self.enable_fname = enable_fname4849self.data = {}5051# Initial maximum values use just the headers52self.max_size_name = len(self.h_name)53self.max_size_kconfig = len(self.h_kconfig)54self.max_size_description = len(self.h_description)55self.max_size_desc_word = 056self.max_size_subsys = len(self.h_subsys)57self.max_size_status = len(self.h_status)58self.max_size_arch = len(self.h_arch)59self.max_size_arch_with_header = self.max_size_arch + self.max_size_arch60self.description_size = 16162self.msg = ""6364def emit(self, msg="", end="\n"):65self.msg += msg + end6667def parse_error(self, fname, ln, msg, data=None):68"""69Displays an error message, printing file name and line70"""7172if ln:73fname += f"#{ln}"7475print(f"Warning: file {fname}: {msg}", file=sys.stderr, end="")7677if data:78data = data.rstrip()79print(f":\n\t{data}", file=sys.stderr)80else:81print("", file=sys.stderr)8283def parse_feat_file(self, fname):84"""Parses a single arch-support.txt feature file"""8586if os.path.isdir(fname):87return8889base = os.path.basename(fname)9091if base != "arch-support.txt":92if self.debug:93print(f"ignoring {fname}", file=sys.stderr)94return9596subsys = os.path.dirname(fname).split("/")[-2]97self.max_size_subsys = max(self.max_size_subsys, len(subsys))9899feature_name = ""100kconfig = ""101description = ""102comments = ""103arch_table = {}104105if self.debug > 1:106print(f"Opening {fname}", file=sys.stderr)107108if self.enable_fname:109full_fname = os.path.abspath(fname)110self.emit(f".. FILE {full_fname}")111112with open(fname, encoding="utf-8") as f:113for ln, line in enumerate(f, start=1):114line = line.strip()115116match = re.match(r"^\#\s+Feature\s+name:\s*(.*\S)", line)117if match:118feature_name = match.group(1)119120self.max_size_name = max(self.max_size_name,121len(feature_name))122continue123124match = re.match(r"^\#\s+Kconfig:\s*(.*\S)", line)125if match:126kconfig = match.group(1)127128self.max_size_kconfig = max(self.max_size_kconfig,129len(kconfig))130continue131132match = re.match(r"^\#\s+description:\s*(.*\S)", line)133if match:134description = match.group(1)135136self.max_size_description = max(self.max_size_description,137len(description))138139words = re.split(r"\s+", line)[1:]140for word in words:141self.max_size_desc_word = max(self.max_size_desc_word,142len(word))143144continue145146if re.search(r"^\\s*$", line):147continue148149if re.match(r"^\s*\-+\s*$", line):150continue151152if re.search(r"^\s*\|\s*arch\s*\|\s*status\s*\|\s*$", line):153continue154155match = re.match(r"^\#\s*(.*)$", line)156if match:157comments += match.group(1)158continue159160match = re.match(r"^\s*\|\s*(\S+):\s*\|\s*(\S+)\s*\|\s*$", line)161if match:162arch = match.group(1)163status = match.group(2)164165self.max_size_status = max(self.max_size_status,166len(status))167self.max_size_arch = max(self.max_size_arch, len(arch))168169if status == "..":170status = "---"171172arch_table[arch] = status173174continue175176self.parse_error(fname, ln, "Line is invalid", line)177178if not feature_name:179self.parse_error(fname, 0, "Feature name not found")180return181if not subsys:182self.parse_error(fname, 0, "Subsystem not found")183return184if not kconfig:185self.parse_error(fname, 0, "Kconfig not found")186return187if not description:188self.parse_error(fname, 0, "Description not found")189return190if not arch_table:191self.parse_error(fname, 0, "Architecture table not found")192return193194self.data[feature_name] = {195"where": fname,196"subsys": subsys,197"kconfig": kconfig,198"description": description,199"comments": comments,200"table": arch_table,201}202203self.max_size_arch_with_header = self.max_size_arch + len(self.h_arch)204205def parse(self):206"""Parses all arch-support.txt feature files inside self.prefix"""207208path = os.path.expanduser(self.prefix)209210if self.debug > 2:211print(f"Running parser for {path}")212213example_path = os.path.join(path, "arch-support.txt")214215for fname in iglob(os.path.join(path, "**"), recursive=True):216if fname != example_path:217self.parse_feat_file(fname)218219return self.data220221def output_arch_table(self, arch, feat=None):222"""223Output feature(s) for a given architecture.224"""225226title = f"Feature status on {arch} architecture"227228self.emit("=" * len(title))229self.emit(title)230self.emit("=" * len(title))231self.emit()232233self.emit("=" * self.max_size_subsys + " ", end="")234self.emit("=" * self.max_size_name + " ", end="")235self.emit("=" * self.max_size_kconfig + " ", end="")236self.emit("=" * self.max_size_status + " ", end="")237self.emit("=" * self.max_size_description)238239self.emit(f"{self.h_subsys:<{self.max_size_subsys}} ", end="")240self.emit(f"{self.h_name:<{self.max_size_name}} ", end="")241self.emit(f"{self.h_kconfig:<{self.max_size_kconfig}} ", end="")242self.emit(f"{self.h_status:<{self.max_size_status}} ", end="")243self.emit(f"{self.h_description:<{self.max_size_description}}")244245self.emit("=" * self.max_size_subsys + " ", end="")246self.emit("=" * self.max_size_name + " ", end="")247self.emit("=" * self.max_size_kconfig + " ", end="")248self.emit("=" * self.max_size_status + " ", end="")249self.emit("=" * self.max_size_description)250251sorted_features = sorted(self.data.keys(),252key=lambda x: (self.data[x]["subsys"],253x.lower()))254255for name in sorted_features:256if feat and name != feat:257continue258259arch_table = self.data[name]["table"]260261if not arch in arch_table:262continue263264self.emit(f"{self.data[name]['subsys']:<{self.max_size_subsys}} ",265end="")266self.emit(f"{name:<{self.max_size_name}} ", end="")267self.emit(f"{self.data[name]['kconfig']:<{self.max_size_kconfig}} ",268end="")269self.emit(f"{arch_table[arch]:<{self.max_size_status}} ",270end="")271self.emit(f"{self.data[name]['description']}")272273self.emit("=" * self.max_size_subsys + " ", end="")274self.emit("=" * self.max_size_name + " ", end="")275self.emit("=" * self.max_size_kconfig + " ", end="")276self.emit("=" * self.max_size_status + " ", end="")277self.emit("=" * self.max_size_description)278279return self.msg280281def output_feature(self, feat):282"""283Output a feature on all architectures284"""285286title = f"Feature {feat}"287288self.emit("=" * len(title))289self.emit(title)290self.emit("=" * len(title))291self.emit()292293if not feat in self.data:294return295296if self.data[feat]["subsys"]:297self.emit(f":Subsystem: {self.data[feat]['subsys']}")298if self.data[feat]["kconfig"]:299self.emit(f":Kconfig: {self.data[feat]['kconfig']}")300301desc = self.data[feat]["description"]302desc = desc[0].upper() + desc[1:]303desc = desc.rstrip(". \t")304self.emit(f"\n{desc}.\n")305306com = self.data[feat]["comments"].strip()307if com:308self.emit("Comments")309self.emit("--------")310self.emit(f"\n{com}\n")311312self.emit("=" * self.max_size_arch + " ", end="")313self.emit("=" * self.max_size_status)314315self.emit(f"{self.h_arch:<{self.max_size_arch}} ", end="")316self.emit(f"{self.h_status:<{self.max_size_status}}")317318self.emit("=" * self.max_size_arch + " ", end="")319self.emit("=" * self.max_size_status)320321arch_table = self.data[feat]["table"]322for arch in sorted(arch_table.keys()):323self.emit(f"{arch:<{self.max_size_arch}} ", end="")324self.emit(f"{arch_table[arch]:<{self.max_size_status}}")325326self.emit("=" * self.max_size_arch + " ", end="")327self.emit("=" * self.max_size_status)328329return self.msg330331def matrix_lines(self, desc_size, max_size_status, header):332"""333Helper function to split element tables at the output matrix334"""335336if header:337ln_marker = "="338else:339ln_marker = "-"340341self.emit("+" + ln_marker * self.max_size_name + "+", end="")342self.emit(ln_marker * desc_size, end="")343self.emit("+" + ln_marker * max_size_status + "+")344345def output_matrix(self):346"""347Generates a set of tables, groped by subsystem, containing348what's the feature state on each architecture.349"""350351title = "Feature status on all architectures"352353self.emit("=" * len(title))354self.emit(title)355self.emit("=" * len(title))356self.emit()357358desc_title = f"{self.h_kconfig} / {self.h_description}"359360desc_size = self.max_size_kconfig + 4361if not self.description_size:362desc_size = max(self.max_size_description, desc_size)363else:364desc_size = max(self.description_size, desc_size)365366desc_size = max(self.max_size_desc_word, desc_size, len(desc_title))367368notcompat = "Not compatible"369self.max_size_status = max(self.max_size_status, len(notcompat))370371min_status_size = self.max_size_status + self.max_size_arch + 4372max_size_status = max(min_status_size, self.max_size_status)373374h_status_per_arch = "Status per architecture"375max_size_status = max(max_size_status, len(h_status_per_arch))376377cur_subsys = None378for name in sorted(self.data.keys(),379key=lambda x: (self.data[x]["subsys"], x.lower())):380if not cur_subsys or cur_subsys != self.data[name]["subsys"]:381if cur_subsys:382self.emit()383384cur_subsys = self.data[name]["subsys"]385386title = f"Subsystem: {cur_subsys}"387self.emit(title)388self.emit("=" * len(title))389self.emit()390391self.matrix_lines(desc_size, max_size_status, 0)392393self.emit(f"|{self.h_name:<{self.max_size_name}}", end="")394self.emit(f"|{desc_title:<{desc_size}}", end="")395self.emit(f"|{h_status_per_arch:<{max_size_status}}|")396397self.matrix_lines(desc_size, max_size_status, 1)398399lines = []400descs = []401cur_status = ""402line = ""403404arch_table = sorted(self.data[name]["table"].items(),405key=lambda x: (self.status_map.get(x[1], 99),406x[0].lower()))407408for arch, status in arch_table:409if status == "---":410status = notcompat411412if status != cur_status:413if line != "":414lines.append(line)415line = ""416line = f"- **{status}**: {arch}"417elif len(line) + len(arch) + 2 < max_size_status:418line += f", {arch}"419else:420lines.append(line)421line = f" {arch}"422cur_status = status423424if line != "":425lines.append(line)426427description = self.data[name]["description"]428while len(description) > desc_size:429desc_line = description[:desc_size]430431last_space = desc_line.rfind(" ")432if last_space != -1:433desc_line = desc_line[:last_space]434descs.append(desc_line)435description = description[last_space + 1:]436else:437desc_line = desc_line[:-1]438descs.append(desc_line + "\\")439description = description[len(desc_line):]440441if description:442descs.append(description)443444while len(lines) < 2 + len(descs):445lines.append("")446447for ln, line in enumerate(lines):448col = ["", ""]449450if not ln:451col[0] = name452col[1] = f"``{self.data[name]['kconfig']}``"453else:454if ln >= 2 and descs:455col[1] = descs.pop(0)456457self.emit(f"|{col[0]:<{self.max_size_name}}", end="")458self.emit(f"|{col[1]:<{desc_size}}", end="")459self.emit(f"|{line:<{max_size_status}}|")460461self.matrix_lines(desc_size, max_size_status, 0)462463return self.msg464465def list_arch_features(self, arch, feat):466"""467Print a matrix of kernel feature support for the chosen architecture.468"""469self.emit("#")470self.emit(f"# Kernel feature support matrix of the '{arch}' architecture:")471self.emit("#")472473# Sort by subsystem, then by feature name (case‑insensitive)474for name in sorted(self.data.keys(),475key=lambda n: (self.data[n]["subsys"].lower(),476n.lower())):477if feat and name != feat:478continue479480feature = self.data[name]481arch_table = feature["table"]482status = arch_table.get(arch, "")483status = " " * ((4 - len(status)) // 2) + status484485self.emit(f"{feature['subsys']:>{self.max_size_subsys + 1}}/ ",486end="")487self.emit(f"{name:<{self.max_size_name}}: ", end="")488self.emit(f"{status:<5}| ", end="")489self.emit(f"{feature['kconfig']:>{self.max_size_kconfig}} ",490end="")491self.emit(f"# {feature['description']}")492493return self.msg494495496