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