#! /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(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() 
 |