parse_features.py 16 KB

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