| #! /usr/bin/env python3 |
| |
| 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() |
| if suffix: |
| parser.optionxform = lambda option: option.replace("_" + suffix, "") |
| with open(filename) as lines: |
| lines = chain(("[fake]",), 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): |
| 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() |