gen-manual-lists.py 14 KB

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