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 508