2
1

gen-manual-lists.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. ## gen-manual-lists.py
  2. ##
  3. ## This script generates the following Buildroot manual appendices:
  4. ## - the package tables (one for the target, the other for host tools);
  5. ## - the deprecated items.
  6. ##
  7. ## Author(s):
  8. ## - Samuel Martin <s.martin49@gmail.com>
  9. ##
  10. ## Copyright (C) 2013 Samuel Martin
  11. ##
  12. ## This program is free software; you can redistribute it and/or modify
  13. ## it under the terms of the GNU General Public License as published by
  14. ## the Free Software Foundation; either version 2 of the License, or
  15. ## (at your option) any later version.
  16. ##
  17. ## This program is distributed in the hope that it will be useful,
  18. ## but WITHOUT ANY WARRANTY; without even the implied warranty of
  19. ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  20. ## GNU General Public License for more details.
  21. ##
  22. ## You should have received a copy of the GNU General Public License
  23. ## along with this program; if not, write to the Free Software
  24. ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  25. ##
  26. ## Note about python2.
  27. ##
  28. ## This script can currently only be run using python2 interpreter due to
  29. ## its kconfiglib dependency (which is not yet python3 friendly).
  30. from __future__ import print_function
  31. from __future__ import unicode_literals
  32. import os
  33. import re
  34. import sys
  35. import datetime
  36. from argparse import ArgumentParser
  37. try:
  38. import kconfiglib
  39. except ImportError:
  40. message = """
  41. Could not find the module 'kconfiglib' in the PYTHONPATH:
  42. """
  43. message += "\n".join([" {0}".format(path) for path in sys.path])
  44. message += """
  45. Make sure the Kconfiglib directory is in the PYTHONPATH, then relaunch the
  46. script.
  47. You can get kconfiglib from:
  48. https://github.com/ulfalizer/Kconfiglib
  49. """
  50. sys.stderr.write(message)
  51. raise
  52. def get_symbol_subset(root, filter_func):
  53. """ Return a generator of kconfig items.
  54. :param root_item: Root item of the generated subset of items
  55. :param filter_func: Filter function
  56. """
  57. if hasattr(root, "get_items"):
  58. get_items = root.get_items
  59. elif hasattr(root, "get_top_level_items"):
  60. get_items = root.get_top_level_items
  61. else:
  62. message = "The symbol does not contain any subset of symbols"
  63. raise Exception(message)
  64. for item in get_items():
  65. if item.is_symbol():
  66. if not filter_func(item):
  67. continue
  68. yield item
  69. elif item.is_menu() or item.is_choice():
  70. for i in get_symbol_subset(item, filter_func):
  71. yield i
  72. def get_symbol_parents(item, root=None, enable_choice=False):
  73. """ Return the list of the item's parents. The last item of the list is
  74. the closest parent, the first the furthest.
  75. :param item: Item from which the the parent list is generated
  76. :param root: Root item stopping the search (not included in the
  77. parent list)
  78. :param enable_choice: Flag enabling choices to appear in the parent list
  79. """
  80. parent = item.get_parent()
  81. parents = []
  82. while parent and parent != root:
  83. if parent.is_menu():
  84. parents.append(parent.get_title())
  85. elif enable_choice and parent.is_choice():
  86. parents.append(parent.prompts[0][0])
  87. parent = parent.get_parent()
  88. if isinstance(root, kconfiglib.Menu) or \
  89. (enable_choice and isinstance(root, kconfiglib.Choice)):
  90. parents.append("") # Dummy empty parent to get a leading arrow ->
  91. parents.reverse()
  92. return parents
  93. def format_asciidoc_table(root, get_label_func, filter_func=lambda x: True,
  94. format_func=lambda x: x,
  95. enable_choice=False, sorted=True,
  96. item_label=None):
  97. """ Return the asciidoc formatted table of the items and their location.
  98. :param root: Root item of the item subset
  99. :param get_label_func: Item's label getter function
  100. :param filter_func: Filter function to apply on the item subset
  101. :param format_func: Function to format a symbol and the table header
  102. :param enable_choice: Enable choices to appear as part of the item's
  103. location
  104. :param sorted: Flag to alphabetically sort the table
  105. """
  106. lines = []
  107. for item in get_symbol_subset(root, filter_func):
  108. lines.append(format_func(what="symbol", symbol=item, root=root,
  109. get_label_func=get_label_func,
  110. enable_choice=enable_choice))
  111. if sorted:
  112. lines.sort(key=lambda x: x.lower())
  113. table = ":halign: center\n\n"
  114. width, columns = format_func(what="layout")
  115. table = "[width=\"{0}\",cols=\"{1}\",options=\"header\"]\n".format(width, columns)
  116. table += "|===================================================\n"
  117. table += format_func(what="header", header=item_label, root=root)
  118. table += "\n" + "".join(lines) + "\n"
  119. table += "|===================================================\n"
  120. return table
  121. class Buildroot:
  122. """ Buildroot configuration object.
  123. """
  124. root_config = "Config.in"
  125. package_dirname = "package"
  126. package_prefixes = ["BR2_PACKAGE_", "BR2_PACKAGE_HOST_"]
  127. re_pkg_prefix = re.compile(r"^(" + "|".join(package_prefixes) + ").*")
  128. deprecated_symbol = "BR2_DEPRECATED"
  129. list_in = """\
  130. //
  131. // Automatically generated list for Buildroot manual.
  132. //
  133. {table}
  134. """
  135. list_info = {
  136. 'target-packages': {
  137. 'filename': "package-list",
  138. 'root_menu': "Target packages",
  139. 'filter': "_is_real_package",
  140. 'format': "_format_symbol_prompt_location",
  141. 'sorted': True,
  142. },
  143. 'host-packages': {
  144. 'filename': "host-package-list",
  145. 'root_menu': "Host utilities",
  146. 'filter': "_is_real_package",
  147. 'format': "_format_symbol_prompt",
  148. 'sorted': True,
  149. },
  150. 'virtual-packages': {
  151. 'filename': "virtual-package-list",
  152. 'root_menu': "Target packages",
  153. 'filter': "_is_virtual_package",
  154. 'format': "_format_symbol_virtual",
  155. 'sorted': True,
  156. },
  157. 'deprecated': {
  158. 'filename': "deprecated-list",
  159. 'root_menu': None,
  160. 'filter': "_is_deprecated",
  161. 'format': "_format_symbol_prompt_location",
  162. 'sorted': False,
  163. },
  164. }
  165. def __init__(self):
  166. self.base_dir = os.environ.get("TOPDIR")
  167. self.output_dir = os.environ.get("O")
  168. self.package_dir = os.path.join(self.base_dir, self.package_dirname)
  169. # The kconfiglib requires an environment variable named "srctree" to
  170. # load the configuration, so set it.
  171. os.environ.update({'srctree': self.base_dir})
  172. self.config = kconfiglib.Config(os.path.join(self.base_dir,
  173. self.root_config))
  174. self._deprecated = self.config.get_symbol(self.deprecated_symbol)
  175. self.gen_date = datetime.datetime.utcnow()
  176. self.br_version_full = os.environ.get("BR2_VERSION_FULL")
  177. if self.br_version_full and self.br_version_full.endswith("-git"):
  178. self.br_version_full = self.br_version_full[:-4]
  179. if not self.br_version_full:
  180. self.br_version_full = "undefined"
  181. def _get_package_symbols(self, package_name):
  182. """ Return a tuple containing the target and host package symbol.
  183. """
  184. symbols = re.sub("[-+.]", "_", package_name)
  185. symbols = symbols.upper()
  186. symbols = tuple([prefix + symbols for prefix in self.package_prefixes])
  187. return symbols
  188. def _is_deprecated(self, symbol):
  189. """ Return True if the symbol is marked as deprecated, otherwise False.
  190. """
  191. # This also catches BR2_DEPRECATED_SINCE_xxxx_xx
  192. return bool([ symbol for x in symbol.get_referenced_symbols()
  193. if x.get_name().startswith(self._deprecated.get_name()) ])
  194. def _is_package(self, symbol, type='real'):
  195. """ Return True if the symbol is a package or a host package, otherwise
  196. False.
  197. :param symbol: The symbol to check
  198. :param type: Limit to 'real' or 'virtual' types of packages,
  199. with 'real' being the default.
  200. Note: only 'real' is (implictly) handled for now
  201. """
  202. if not symbol.is_symbol():
  203. return False
  204. if type == 'real' and not symbol.prompts:
  205. return False
  206. if type == 'virtual' and symbol.prompts:
  207. return False
  208. if not self.re_pkg_prefix.match(symbol.get_name()):
  209. return False
  210. pkg_name = self._get_pkg_name(symbol)
  211. pattern = "^(HOST_)?" + pkg_name + "$"
  212. pattern = re.sub("_", ".", pattern)
  213. pattern = re.compile(pattern, re.IGNORECASE)
  214. # Here, we cannot just check for the location of the Config.in because
  215. # of the "virtual" package.
  216. #
  217. # So, to check that a symbol is a package (not a package option or
  218. # anything else), we check for the existence of the package *.mk file.
  219. #
  220. # By the way, to actually check for a package, we should grep all *.mk
  221. # files for the following regex:
  222. # "\$\(eval \$\((host-)?(generic|autotools|cmake)-package\)\)"
  223. #
  224. # Implementation details:
  225. #
  226. # * The package list is generated from the *.mk file existence, the
  227. # first time this function is called. Despite the memory consumption,
  228. # this list is stored because the execution time of this script is
  229. # noticeably shorter than rescanning the package sub-tree for each
  230. # symbol.
  231. if not hasattr(self, "_package_list"):
  232. pkg_list = []
  233. for _, _, files in os.walk(self.package_dir):
  234. for file_ in (f for f in files if f.endswith(".mk")):
  235. pkg_list.append(re.sub(r"(.*?)\.mk", r"\1", file_))
  236. setattr(self, "_package_list", pkg_list)
  237. for pkg in getattr(self, "_package_list"):
  238. if type == 'real':
  239. if pattern.match(pkg) and not self._exists_virt_symbol(pkg):
  240. return True
  241. if type == 'virtual':
  242. if pattern.match('has_' + pkg):
  243. return True
  244. return False
  245. def _is_real_package(self, symbol):
  246. return self._is_package(symbol, 'real')
  247. def _is_virtual_package(self, symbol):
  248. return self._is_package(symbol, 'virtual')
  249. def _exists_virt_symbol(self, pkg_name):
  250. """ Return True if a symbol exists that defines the package as
  251. a virtual package, False otherwise
  252. :param pkg_name: The name of the package, for which to check if
  253. a symbol exists defining it as a virtual package
  254. """
  255. virt_pattern = "BR2_PACKAGE_HAS_" + pkg_name + "$"
  256. virt_pattern = re.sub("_", ".", virt_pattern)
  257. virt_pattern = re.compile(virt_pattern, re.IGNORECASE)
  258. for sym in self.config:
  259. if virt_pattern.match(sym.get_name()):
  260. return True
  261. return False
  262. def _get_pkg_name(self, symbol):
  263. """ Return the package name of the specified symbol.
  264. :param symbol: The symbol to get the package name of
  265. """
  266. return re.sub("BR2_PACKAGE_(HOST_)?(.*)", r"\2", symbol.get_name())
  267. def _get_symbol_label(self, symbol, mark_deprecated=True):
  268. """ Return the label (a.k.a. prompt text) of the symbol.
  269. :param symbol: The symbol
  270. :param mark_deprecated: Append a 'deprecated' to the label
  271. """
  272. label = symbol.prompts[0][0]
  273. if self._is_deprecated(symbol) and mark_deprecated:
  274. label += " *(deprecated)*"
  275. return label
  276. def _format_symbol_prompt(self, what=None, symbol=None, root=None,
  277. enable_choice=False, header=None,
  278. get_label_func=lambda x: x):
  279. if what == "layout":
  280. return ( "30%", "^1" )
  281. if what == "header":
  282. return "| {0:<40}\n".format(header)
  283. if what == "symbol":
  284. return "| {0:<40}\n".format(get_label_func(symbol))
  285. message = "Invalid argument 'what': '%s'\n" % str(what)
  286. message += "Allowed values are: 'layout', 'header' and 'symbol'"
  287. raise Exception(message)
  288. def _format_symbol_prompt_location(self, what=None, symbol=None, root=None,
  289. enable_choice=False, header=None,
  290. get_label_func=lambda x: x):
  291. if what == "layout":
  292. return ( "100%", "^1,4" )
  293. if what == "header":
  294. if hasattr(root, "get_title"):
  295. loc_label = get_symbol_parents(root, None, enable_choice=enable_choice)
  296. loc_label += [root.get_title(), "..."]
  297. else:
  298. loc_label = ["Location"]
  299. return "| {0:<40} <| {1}\n".format(header, " -> ".join(loc_label))
  300. if what == "symbol":
  301. parents = get_symbol_parents(symbol, root, enable_choice)
  302. return "| {0:<40} <| {1}\n".format(get_label_func(symbol),
  303. " -> ".join(parents))
  304. message = "Invalid argument 'what': '%s'\n" % str(what)
  305. message += "Allowed values are: 'layout', 'header' and 'symbol'"
  306. raise Exception(message)
  307. def _format_symbol_virtual(self, what=None, symbol=None, root=None,
  308. enable_choice=False, header=None,
  309. get_label_func=lambda x: "?"):
  310. def _symbol_is_legacy(symbol):
  311. selects = [ s.get_name() for s in symbol.get_selected_symbols() ]
  312. return ("BR2_LEGACY" in selects)
  313. def _get_parent_package(sym):
  314. if self._is_real_package(sym):
  315. return None
  316. # Trim the symbol name from its last component (separated with
  317. # underscores), until we either find a symbol which is a real
  318. # package, or until we have no component (i.e. just 'BR2')
  319. name = sym.get_name()
  320. while name != "BR2":
  321. name = name.rsplit("_", 1)[0]
  322. s = self.config.get_symbol(name)
  323. if s is None:
  324. continue
  325. if self._is_real_package(s):
  326. return s
  327. return None
  328. def _get_providers(symbol):
  329. providers = list()
  330. for sym in self.config:
  331. if not sym.is_symbol():
  332. continue
  333. selects = sym.get_selected_symbols()
  334. if not selects:
  335. continue
  336. for s in selects:
  337. if s == symbol:
  338. if _symbol_is_legacy(sym):
  339. continue
  340. if sym.prompts:
  341. l = self._get_symbol_label(sym,False)
  342. parent_pkg = _get_parent_package(sym)
  343. if parent_pkg is not None:
  344. l = self._get_symbol_label(parent_pkg, False) \
  345. + " (w/ " + l + ")"
  346. providers.append(l)
  347. else:
  348. providers.extend(_get_providers(sym))
  349. return providers
  350. if what == "layout":
  351. return ( "100%", "^1,4,4" )
  352. if what == "header":
  353. return "| {0:<20} <| {1:<32} <| Providers\n".format("Virtual packages", "Symbols")
  354. if what == "symbol":
  355. pkg = re.sub(r"^BR2_PACKAGE_HAS_(.+)$", r"\1", symbol.get_name())
  356. providers = _get_providers(symbol)
  357. return "| {0:<20} <| {1:<32} <| {2}\n".format(pkg.lower(),
  358. '+' + symbol.get_name() + '+',
  359. ", ".join(providers))
  360. message = "Invalid argument 'what': '%s'\n" % str(what)
  361. message += "Allowed values are: 'layout', 'header' and 'symbol'"
  362. raise Exception(message)
  363. def print_list(self, list_type, enable_choice=True, enable_deprecated=True,
  364. dry_run=False, output=None):
  365. """ Print the requested list. If not dry run, then the list is
  366. automatically written in its own file.
  367. :param list_type: The list type to be generated
  368. :param enable_choice: Flag enabling choices to appear in the list
  369. :param enable_deprecated: Flag enabling deprecated items to appear in
  370. the package lists
  371. :param dry_run: Dry run (print the list in stdout instead of
  372. writing the list file
  373. """
  374. def _get_menu(title):
  375. """ Return the first symbol menu matching the given title.
  376. """
  377. menus = self.config.get_menus()
  378. menu = [m for m in menus if m.get_title().lower() == title.lower()]
  379. if not menu:
  380. message = "No such menu: '{0}'".format(title)
  381. raise Exception(message)
  382. return menu[0]
  383. list_config = self.list_info[list_type]
  384. root_title = list_config.get('root_menu')
  385. if root_title:
  386. root_item = _get_menu(root_title)
  387. else:
  388. root_item = self.config
  389. filter_ = getattr(self, list_config.get('filter'))
  390. filter_func = lambda x: filter_(x)
  391. format_func = getattr(self, list_config.get('format'))
  392. if not enable_deprecated and list_type != "deprecated":
  393. filter_func = lambda x: filter_(x) and not self._is_deprecated(x)
  394. mark_depr = list_type != "deprecated"
  395. get_label = lambda x: self._get_symbol_label(x, mark_depr)
  396. item_label = "Features" if list_type == "deprecated" else "Packages"
  397. table = format_asciidoc_table(root_item, get_label,
  398. filter_func=filter_func,
  399. format_func=format_func,
  400. enable_choice=enable_choice,
  401. sorted=list_config.get('sorted'),
  402. item_label=item_label)
  403. content = self.list_in.format(table=table)
  404. if dry_run:
  405. print(content)
  406. return
  407. if not output:
  408. output_dir = self.output_dir
  409. if not output_dir:
  410. print("Warning: Undefined output directory.")
  411. print("\tUse source directory as output location.")
  412. output_dir = self.base_dir
  413. output = os.path.join(output_dir,
  414. list_config.get('filename') + ".txt")
  415. if not os.path.exists(os.path.dirname(output)):
  416. os.makedirs(os.path.dirname(output))
  417. print("Writing the {0} list in:\n\t{1}".format(list_type, output))
  418. with open(output, 'w') as fout:
  419. fout.write(content)
  420. if __name__ == '__main__':
  421. list_types = ['target-packages', 'host-packages', 'virtual-packages', 'deprecated']
  422. parser = ArgumentParser()
  423. parser.add_argument("list_type", nargs="?", choices=list_types,
  424. help="""\
  425. Generate the given list (generate all lists if unspecified)""")
  426. parser.add_argument("-n", "--dry-run", dest="dry_run", action='store_true',
  427. help="Output the generated list to stdout")
  428. parser.add_argument("--output-target", dest="output_target",
  429. help="Output target package file")
  430. parser.add_argument("--output-host", dest="output_host",
  431. help="Output host package file")
  432. parser.add_argument("--output-virtual", dest="output_virtual",
  433. help="Output virtual package file")
  434. parser.add_argument("--output-deprecated", dest="output_deprecated",
  435. help="Output deprecated file")
  436. args = parser.parse_args()
  437. lists = [args.list_type] if args.list_type else list_types
  438. buildroot = Buildroot()
  439. for list_name in lists:
  440. output = getattr(args, "output_" + list_name.split("-", 1)[0])
  441. buildroot.print_list(list_name, dry_run=args.dry_run, output=output)