| #! /usr/bin/env python3 | |
| # | |
| # Copyright OpenEmbedded Contributors | |
| # | |
| # SPDX-License-Identifier: MIT | |
| # | |
| import os, sys, enum, ast | |
| scripts_path = os.path.dirname(os.path.realpath(__file__)) | |
| lib_path = scripts_path + '/lib' | |
| sys.path = sys.path + [lib_path] | |
| import scriptpath | |
| bitbakepath = scriptpath.add_bitbake_lib_path() | |
| if not bitbakepath: | |
| print("Unable to find bitbake by searching parent directory of this script or PATH") | |
| sys.exit(1) | |
| import bb | |
| import gi | |
| gi.require_version('Gtk', '3.0') | |
| from gi.repository import Gtk, Gdk, GObject | |
| RecipeColumns = enum.IntEnum("RecipeColumns", {"Recipe": 0}) | |
| PackageColumns = enum.IntEnum("PackageColumns", {"Package": 0, "Size": 1}) | |
| FileColumns = enum.IntEnum("FileColumns", {"Filename": 0, "Size": 1}) | |
| import time | |
| def timeit(f): | |
| def timed(*args, **kw): | |
| ts = time.time() | |
| print ("func:%r calling" % f.__name__) | |
| result = f(*args, **kw) | |
| te = time.time() | |
| print ('func:%r args:[%r, %r] took: %2.4f sec' % \ | |
| (f.__name__, args, kw, te-ts)) | |
| return result | |
| return timed | |
| def human_size(nbytes): | |
| import math | |
| suffixes = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'] | |
| human = nbytes | |
| rank = 0 | |
| if nbytes != 0: | |
| rank = int((math.log10(nbytes)) / 3) | |
| rank = min(rank, len(suffixes) - 1) | |
| human = nbytes / (1000.0 ** rank) | |
| f = ('%.2f' % human).rstrip('0').rstrip('.') | |
| return '%s %s' % (f, suffixes[rank]) | |
| def load(filename, suffix=None): | |
| from configparser import ConfigParser | |
| from itertools import chain | |
| parser = ConfigParser(delimiters=('=')) | |
| if suffix: | |
| parser.optionxform = lambda option: option.replace(":" + suffix, "") | |
| with open(filename) as lines: | |
| lines = chain(("[fake]",), (line.replace(": ", " = ", 1) for line in lines)) | |
| parser.read_file(lines) | |
| # TODO extract the data and put it into a real dict so we can transform some | |
| # values to ints? | |
| return parser["fake"] | |
| def find_pkgdata(): | |
| import subprocess | |
| output = subprocess.check_output(("bitbake", "-e"), universal_newlines=True) | |
| for line in output.splitlines(): | |
| if line.startswith("PKGDATA_DIR="): | |
| return line.split("=", 1)[1].strip("\'\"") | |
| # TODO exception or something | |
| return None | |
| def packages_in_recipe(pkgdata, recipe): | |
| """ | |
| Load the recipe pkgdata to determine the list of runtime packages. | |
| """ | |
| data = load(os.path.join(pkgdata, recipe)) | |
| packages = data["PACKAGES"].split() | |
| return packages | |
| def load_runtime_package(pkgdata, package): | |
| return load(os.path.join(pkgdata, "runtime", package), suffix=package) | |
| def recipe_from_package(pkgdata, package): | |
| data = load(os.path.join(pkgdata, "runtime", package), suffix=package) | |
| return data["PN"] | |
| def summary(data): | |
| s = "" | |
| s += "{0[PKG]} {0[PKGV]}-{0[PKGR]}\n{0[LICENSE]}\n{0[SUMMARY]}\n".format(data) | |
| return s | |
| class PkgUi(): | |
| def __init__(self, pkgdata): | |
| self.pkgdata = pkgdata | |
| self.current_recipe = None | |
| self.recipe_iters = {} | |
| self.package_iters = {} | |
| builder = Gtk.Builder() | |
| builder.add_from_file(os.path.join(os.path.dirname(__file__), "oe-pkgdata-browser.glade")) | |
| self.window = builder.get_object("window") | |
| self.window.connect("delete-event", Gtk.main_quit) | |
| self.recipe_store = builder.get_object("recipe_store") | |
| self.recipe_view = builder.get_object("recipe_view") | |
| self.package_store = builder.get_object("package_store") | |
| self.package_view = builder.get_object("package_view") | |
| # Somehow resizable does not get set via builder xml | |
| package_name_column = builder.get_object("package_name_column") | |
| package_name_column.set_resizable(True) | |
| file_name_column = builder.get_object("file_name_column") | |
| file_name_column.set_resizable(True) | |
| self.recipe_view.get_selection().connect("changed", self.on_recipe_changed) | |
| self.package_view.get_selection().connect("changed", self.on_package_changed) | |
| self.package_store.set_sort_column_id(PackageColumns.Package, Gtk.SortType.ASCENDING) | |
| builder.get_object("package_size_column").set_cell_data_func(builder.get_object("package_size_cell"), lambda column, cell, model, iter, data: cell.set_property("text", human_size(model[iter][PackageColumns.Size]))) | |
| self.label = builder.get_object("label1") | |
| self.depends_label = builder.get_object("depends_label") | |
| self.recommends_label = builder.get_object("recommends_label") | |
| self.suggests_label = builder.get_object("suggests_label") | |
| self.provides_label = builder.get_object("provides_label") | |
| self.depends_label.connect("activate-link", self.on_link_activate) | |
| self.recommends_label.connect("activate-link", self.on_link_activate) | |
| self.suggests_label.connect("activate-link", self.on_link_activate) | |
| self.file_store = builder.get_object("file_store") | |
| self.file_store.set_sort_column_id(FileColumns.Filename, Gtk.SortType.ASCENDING) | |
| builder.get_object("file_size_column").set_cell_data_func(builder.get_object("file_size_cell"), lambda column, cell, model, iter, data: cell.set_property("text", human_size(model[iter][FileColumns.Size]))) | |
| self.files_view = builder.get_object("files_scrollview") | |
| self.files_label = builder.get_object("files_label") | |
| self.load_recipes() | |
| self.recipe_view.set_cursor(Gtk.TreePath.new_first()) | |
| self.window.show() | |
| def on_link_activate(self, label, url_string): | |
| from urllib.parse import urlparse | |
| url = urlparse(url_string) | |
| if url.scheme == "package": | |
| package = url.path | |
| recipe = recipe_from_package(self.pkgdata, package) | |
| it = self.recipe_iters[recipe] | |
| path = self.recipe_store.get_path(it) | |
| self.recipe_view.set_cursor(path) | |
| self.recipe_view.scroll_to_cell(path) | |
| self.on_recipe_changed(self.recipe_view.get_selection()) | |
| it = self.package_iters[package] | |
| path = self.package_store.get_path(it) | |
| self.package_view.set_cursor(path) | |
| self.package_view.scroll_to_cell(path) | |
| return True | |
| else: | |
| return False | |
| def on_recipe_changed(self, selection): | |
| self.package_store.clear() | |
| self.package_iters = {} | |
| (model, it) = selection.get_selected() | |
| if not it: | |
| return | |
| recipe = model[it][RecipeColumns.Recipe] | |
| packages = packages_in_recipe(self.pkgdata, recipe) | |
| for package in packages: | |
| # TODO also show PKG after debian-renaming? | |
| data = load_runtime_package(self.pkgdata, package) | |
| # TODO stash data to avoid reading in on_package_changed | |
| self.package_iters[package] = self.package_store.append([package, int(data["PKGSIZE"])]) | |
| package = recipe if recipe in packages else sorted(packages)[0] | |
| path = self.package_store.get_path(self.package_iters[package]) | |
| self.package_view.set_cursor(path) | |
| self.package_view.scroll_to_cell(path) | |
| def on_package_changed(self, selection): | |
| self.label.set_text("") | |
| self.file_store.clear() | |
| self.depends_label.hide() | |
| self.recommends_label.hide() | |
| self.suggests_label.hide() | |
| self.provides_label.hide() | |
| self.files_view.hide() | |
| self.files_label.hide() | |
| (model, it) = selection.get_selected() | |
| if it is None: | |
| return | |
| package = model[it][PackageColumns.Package] | |
| data = load_runtime_package(self.pkgdata, package) | |
| self.label.set_text(summary(data)) | |
| files = ast.literal_eval(data["FILES_INFO"]) | |
| if files: | |
| self.files_label.set_text("{0} files take {1}.".format(len(files), human_size(int(data["PKGSIZE"])))) | |
| self.files_view.show() | |
| for filename, size in files.items(): | |
| self.file_store.append([filename, size]) | |
| else: | |
| self.files_view.hide() | |
| self.files_label.set_text("This package has no files.") | |
| self.files_label.show() | |
| def update_deps(field, prefix, label, clickable=True): | |
| if field in data: | |
| l = [] | |
| for name, version in bb.utils.explode_dep_versions2(data[field]).items(): | |
| if clickable: | |
| l.append("<a href='package:{0}'>{0}</a> {1}".format(name, " ".join(version)).strip()) | |
| else: | |
| l.append("{0} {1}".format(name, " ".join(version)).strip()) | |
| label.set_markup(prefix + ", ".join(l)) | |
| label.show() | |
| else: | |
| label.hide() | |
| update_deps("RDEPENDS", "Depends: ", self.depends_label) | |
| update_deps("RRECOMMENDS", "Recommends: ", self.recommends_label) | |
| update_deps("RSUGGESTS", "Suggests: ", self.suggests_label) | |
| update_deps("RPROVIDES", "Provides: ", self.provides_label, clickable=False) | |
| def load_recipes(self): | |
| if not os.path.exists(pkgdata): | |
| sys.exit("Error: Please ensure %s exists by generating packages before using this tool." % pkgdata) | |
| for recipe in sorted(os.listdir(pkgdata)): | |
| if os.path.isfile(os.path.join(pkgdata, recipe)): | |
| self.recipe_iters[recipe] = self.recipe_store.append([recipe]) | |
| if __name__ == "__main__": | |
| import argparse | |
| parser = argparse.ArgumentParser(description='pkgdata browser') | |
| parser.add_argument('-p', '--pkgdata', help="Optional location of pkgdata") | |
| args = parser.parse_args() | |
| pkgdata = args.pkgdata if args.pkgdata else find_pkgdata() | |
| # TODO assert pkgdata is a directory | |
| window = PkgUi(pkgdata) | |
| Gtk.main() |