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