| #!/usr/bin/env python |
| ## |
| ## gen-manual-lists.py |
| ## |
| ## This script generates the following Buildroot manual appendices: |
| ## - the package tables (one for the target, the other for host tools); |
| ## - the deprecated items. |
| ## |
| ## Author(s): |
| ## - Samuel Martin <s.martin49@gmail.com> |
| ## |
| ## Copyright (C) 2013 Samuel Martin |
| ## |
| ## This program is free software; you can redistribute it and/or modify |
| ## it under the terms of the GNU General Public License as published by |
| ## the Free Software Foundation; either version 2 of the License, or |
| ## (at your option) any later version. |
| ## |
| ## This program is distributed in the hope that it will be useful, |
| ## but WITHOUT ANY WARRANTY; without even the implied warranty of |
| ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| ## GNU General Public License for more details. |
| ## |
| ## You should have received a copy of the GNU General Public License |
| ## along with this program; if not, write to the Free Software |
| ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| ## |
| |
| ## Note about python2. |
| ## |
| ## This script can currently only be run using python2 interpreter due to |
| ## its kconfiglib dependency (which is not yet python3 friendly). |
| |
| from __future__ import print_function |
| from __future__ import unicode_literals |
| |
| import os |
| import re |
| import sys |
| import datetime |
| from argparse import ArgumentParser |
| |
| try: |
| import kconfiglib |
| except ImportError: |
| message = """ |
| Could not find the module 'kconfiglib' in the PYTHONPATH: |
| """ |
| message += "\n".join([" {0}".format(path) for path in sys.path]) |
| message += """ |
| |
| Make sure the Kconfiglib directory is in the PYTHONPATH, then relaunch the |
| script. |
| |
| You can get kconfiglib from: |
| https://github.com/ulfalizer/Kconfiglib |
| |
| |
| """ |
| sys.stderr.write(message) |
| raise |
| |
| |
| def get_symbol_subset(root, filter_func): |
| """ Return a generator of kconfig items. |
| |
| :param root_item: Root item of the generated subset of items |
| :param filter_func: Filter function |
| |
| """ |
| if hasattr(root, "get_items"): |
| get_items = root.get_items |
| elif hasattr(root, "get_top_level_items"): |
| get_items = root.get_top_level_items |
| else: |
| message = "The symbol does not contain any subset of symbols" |
| raise Exception(message) |
| for item in get_items(): |
| if item.is_symbol(): |
| if not item.prompts: |
| continue |
| if not filter_func(item): |
| continue |
| yield item |
| elif item.is_menu() or item.is_choice(): |
| for i in get_symbol_subset(item, filter_func): |
| yield i |
| |
| |
| def get_symbol_parents(item, root=None, enable_choice=False): |
| """ Return the list of the item's parents. The lasst item of the list is |
| the closest parent, the first the furthest. |
| |
| :param item: Item from which the the parent list is generated |
| :param root: Root item stopping the search (not included in the |
| parent list) |
| :param enable_choice: Flag enabling choices to appear in the parent list |
| |
| """ |
| parent = item.get_parent() |
| parents = [] |
| while parent and parent != root: |
| if parent.is_menu(): |
| parents.append(parent.get_title()) |
| elif enable_choice and parent.is_choice(): |
| parents.append(parent.prompts[0][0]) |
| parent = parent.get_parent() |
| if isinstance(root, kconfiglib.Menu) or \ |
| (enable_choice and isinstance(root, kconfiglib.Choice)): |
| parents.append("") # Dummy empty parrent to get a leading arrow -> |
| parents.reverse() |
| return parents |
| |
| |
| def format_asciidoc_table(root, get_label_func, filter_func=lambda x: True, |
| enable_choice=False, sorted=True, sub_menu=True, |
| item_label=None): |
| """ Return the asciidoc formatted table of the items and their location. |
| |
| :param root: Root item of the item subset |
| :param get_label_func: Item's label getter function |
| :param filter_func: Filter function to apply on the item subset |
| :param enable_choice: Enable choices to appear as part of the item's |
| location |
| :param sorted: Flag to alphabetically sort the table |
| :param sub_menu: Output the column with the sub-menu path |
| |
| """ |
| def _format_entry(label, parents, sub_menu): |
| """ Format an asciidoc table entry. |
| |
| """ |
| if sub_menu: |
| return "| {0:<40} <| {1}\n".format(label, " -> ".join(parents)) |
| else: |
| return "| {0:<40}\n".format(label) |
| |
| lines = [] |
| for item in get_symbol_subset(root, filter_func): |
| if not item.is_symbol() or not item.prompts: |
| continue |
| loc = get_symbol_parents(item, root, enable_choice=enable_choice) |
| lines.append(_format_entry(get_label_func(item), loc, sub_menu)) |
| if sorted: |
| lines.sort(key=lambda x: x.lower()) |
| if hasattr(root, "get_title"): |
| loc_label = get_symbol_parents(root, None, enable_choice=enable_choice) |
| loc_label += [root.get_title(), "..."] |
| else: |
| loc_label = ["Location"] |
| if not item_label: |
| item_label = "Items" |
| table = ":halign: center\n\n" |
| if sub_menu: |
| width = "100%" |
| columns = "^1,4" |
| else: |
| width = "30%" |
| columns = "^1" |
| table = "[width=\"{0}\",cols=\"{1}\",options=\"header\"]\n".format(width, columns) |
| table += "|===================================================\n" |
| table += _format_entry(item_label, loc_label, sub_menu) |
| table += "\n" + "".join(lines) + "\n" |
| table += "|===================================================\n" |
| return table |
| |
| |
| class Buildroot: |
| """ Buildroot configuration object. |
| |
| """ |
| root_config = "Config.in" |
| package_dirname = "package" |
| package_prefixes = ["BR2_PACKAGE_", "BR2_PACKAGE_HOST_"] |
| re_pkg_prefix = re.compile(r"^(" + "|".join(package_prefixes) + ").*") |
| deprecated_symbol = "BR2_DEPRECATED" |
| list_in = """\ |
| // |
| // Automatically generated list for Buildroot manual. |
| // |
| |
| {table} |
| """ |
| |
| list_info = { |
| 'target-packages': { |
| 'filename': "package-list", |
| 'root_menu': "Package Selection for the target", |
| 'filter': "_is_package", |
| 'sorted': True, |
| 'sub_menu': True, |
| }, |
| 'host-packages': { |
| 'filename': "host-package-list", |
| 'root_menu': "Host utilities", |
| 'filter': "_is_package", |
| 'sorted': True, |
| 'sub_menu': False, |
| }, |
| 'deprecated': { |
| 'filename': "deprecated-list", |
| 'root_menu': None, |
| 'filter': "_is_deprecated", |
| 'sorted': False, |
| 'sub_menu': True, |
| }, |
| } |
| |
| def __init__(self): |
| self.base_dir = os.environ.get("TOPDIR") |
| self.output_dir = os.environ.get("O") |
| self.package_dir = os.path.join(self.base_dir, self.package_dirname) |
| # The kconfiglib requires an environment variable named "srctree" to |
| # load the configuration, so set it. |
| os.environ.update({'srctree': self.base_dir}) |
| self.config = kconfiglib.Config(os.path.join(self.base_dir, |
| self.root_config)) |
| self._deprecated = self.config.get_symbol(self.deprecated_symbol) |
| |
| self.gen_date = datetime.datetime.utcnow() |
| self.br_version_full = os.environ.get("BR2_VERSION_FULL") |
| if self.br_version_full and self.br_version_full.endswith("-git"): |
| self.br_version_full = self.br_version_full[:-4] |
| if not self.br_version_full: |
| self.br_version_full = "undefined" |
| |
| def _get_package_symbols(self, package_name): |
| """ Return a tuple containing the target and host package symbol. |
| |
| """ |
| symbols = re.sub("[-+.]", "_", package_name) |
| symbols = symbols.upper() |
| symbols = tuple([prefix + symbols for prefix in self.package_prefixes]) |
| return symbols |
| |
| def _is_deprecated(self, symbol): |
| """ Return True if the symbol is marked as deprecated, otherwise False. |
| |
| """ |
| return self._deprecated in symbol.get_referenced_symbols() |
| |
| def _is_package(self, symbol): |
| """ Return True if the symbol is a package or a host package, otherwise |
| False. |
| |
| """ |
| if not self.re_pkg_prefix.match(symbol.get_name()): |
| return False |
| pkg_name = re.sub("BR2_PACKAGE_(HOST_)?(.*)", r"\2", symbol.get_name()) |
| |
| pattern = "^(HOST_)?" + pkg_name + "$" |
| pattern = re.sub("_", ".", pattern) |
| pattern = re.compile(pattern, re.IGNORECASE) |
| # Here, we cannot just check for the location of the Config.in because |
| # of the "virtual" package. |
| # |
| # So, to check that a symbol is a package (not a package option or |
| # anything else), we check for the existence of the package *.mk file. |
| # |
| # By the way, to actually check for a package, we should grep all *.mk |
| # files for the following regex: |
| # "\$\(eval \$\((host-)?(generic|autotools|cmake)-package\)\)" |
| # |
| # Implementation details: |
| # |
| # * The package list is generated from the *.mk file existence, the |
| # first time this function is called. Despite the memory consumtion, |
| # this list is stored because the execution time of this script is |
| # noticebly shorter than re-scannig the package sub-tree for each |
| # symbol. |
| if not hasattr(self, "_package_list"): |
| pkg_list = [] |
| for _, _, files in os.walk(self.package_dir): |
| for file_ in (f for f in files if f.endswith(".mk")): |
| pkg_list.append(re.sub(r"(.*?)\.mk", r"\1", file_)) |
| setattr(self, "_package_list", pkg_list) |
| for pkg in getattr(self, "_package_list"): |
| if pattern.match(pkg): |
| return True |
| return False |
| |
| def _get_symbol_label(self, symbol, mark_deprecated=True): |
| """ Return the label (a.k.a. prompt text) of the symbol. |
| |
| :param symbol: The symbol |
| :param mark_deprecated: Append a 'deprecated' to the label |
| |
| """ |
| label = symbol.prompts[0][0] |
| if self._is_deprecated(symbol) and mark_deprecated: |
| label += " *(deprecated)*" |
| return label |
| |
| def print_list(self, list_type, enable_choice=True, enable_deprecated=True, |
| dry_run=False, output=None): |
| """ Print the requested list. If not dry run, then the list is |
| automatically written in its own file. |
| |
| :param list_type: The list type to be generated |
| :param enable_choice: Flag enabling choices to appear in the list |
| :param enable_deprecated: Flag enabling deprecated items to appear in |
| the package lists |
| :param dry_run: Dry run (print the list in stdout instead of |
| writing the list file |
| |
| """ |
| def _get_menu(title): |
| """ Return the first symbol menu matching the given title. |
| |
| """ |
| menus = self.config.get_menus() |
| menu = [m for m in menus if m.get_title().lower() == title.lower()] |
| if not menu: |
| message = "No such menu: '{0}'".format(title) |
| raise Exception(message) |
| return menu[0] |
| |
| list_config = self.list_info[list_type] |
| root_title = list_config.get('root_menu') |
| if root_title: |
| root_item = _get_menu(root_title) |
| else: |
| root_item = self.config |
| filter_ = getattr(self, list_config.get('filter')) |
| filter_func = lambda x: filter_(x) |
| if not enable_deprecated and list_type != "deprecated": |
| filter_func = lambda x: filter_(x) and not self._is_deprecated(x) |
| mark_depr = list_type != "deprecated" |
| get_label = lambda x: self._get_symbol_label(x, mark_depr) |
| item_label = "Features" if list_type == "deprecated" else "Packages" |
| |
| table = format_asciidoc_table(root_item, get_label, |
| filter_func=filter_func, |
| enable_choice=enable_choice, |
| sorted=list_config.get('sorted'), |
| sub_menu=list_config.get('sub_menu'), |
| item_label=item_label) |
| |
| content = self.list_in.format(table=table) |
| |
| if dry_run: |
| print(content) |
| return |
| |
| if not output: |
| output_dir = self.output_dir |
| if not output_dir: |
| print("Warning: Undefined output directory.") |
| print("\tUse source directory as output location.") |
| output_dir = self.base_dir |
| output = os.path.join(output_dir, |
| list_config.get('filename') + ".txt") |
| if not os.path.exists(os.path.dirname(output)): |
| os.makedirs(os.path.dirname(output)) |
| print("Writing the {0} list in:\n\t{1}".format(list_type, output)) |
| with open(output, 'w') as fout: |
| fout.write(content) |
| |
| |
| if __name__ == '__main__': |
| list_types = ['target-packages', 'host-packages', 'deprecated'] |
| parser = ArgumentParser() |
| parser.add_argument("list_type", nargs="?", choices=list_types, |
| help="""\ |
| Generate the given list (generate all lists if unspecified)""") |
| parser.add_argument("-n", "--dry-run", dest="dry_run", action='store_true', |
| help="Output the generated list to stdout") |
| parser.add_argument("--output-target", dest="output_target", |
| help="Output target package file") |
| parser.add_argument("--output-host", dest="output_host", |
| help="Output host package file") |
| parser.add_argument("--output-deprecated", dest="output_deprecated", |
| help="Output deprecated file") |
| args = parser.parse_args() |
| lists = [args.list_type] if args.list_type else list_types |
| buildroot = Buildroot() |
| for list_name in lists: |
| output = getattr(args, "output_" + list_name.split("-", 1)[0]) |
| buildroot.print_list(list_name, dry_run=args.dry_run, output=output) |