1*4882a593Smuzhiyun#! /usr/bin/env python3 2*4882a593Smuzhiyun 3*4882a593Smuzhiyunimport os, sys, enum, ast 4*4882a593Smuzhiyun 5*4882a593Smuzhiyunscripts_path = os.path.dirname(os.path.realpath(__file__)) 6*4882a593Smuzhiyunlib_path = scripts_path + '/lib' 7*4882a593Smuzhiyunsys.path = sys.path + [lib_path] 8*4882a593Smuzhiyun 9*4882a593Smuzhiyunimport scriptpath 10*4882a593Smuzhiyunbitbakepath = scriptpath.add_bitbake_lib_path() 11*4882a593Smuzhiyunif not bitbakepath: 12*4882a593Smuzhiyun print("Unable to find bitbake by searching parent directory of this script or PATH") 13*4882a593Smuzhiyun sys.exit(1) 14*4882a593Smuzhiyunimport bb 15*4882a593Smuzhiyun 16*4882a593Smuzhiyunimport gi 17*4882a593Smuzhiyungi.require_version('Gtk', '3.0') 18*4882a593Smuzhiyunfrom gi.repository import Gtk, Gdk, GObject 19*4882a593Smuzhiyun 20*4882a593SmuzhiyunRecipeColumns = enum.IntEnum("RecipeColumns", {"Recipe": 0}) 21*4882a593SmuzhiyunPackageColumns = enum.IntEnum("PackageColumns", {"Package": 0, "Size": 1}) 22*4882a593SmuzhiyunFileColumns = enum.IntEnum("FileColumns", {"Filename": 0, "Size": 1}) 23*4882a593Smuzhiyun 24*4882a593Smuzhiyunimport time 25*4882a593Smuzhiyundef timeit(f): 26*4882a593Smuzhiyun def timed(*args, **kw): 27*4882a593Smuzhiyun ts = time.time() 28*4882a593Smuzhiyun print ("func:%r calling" % f.__name__) 29*4882a593Smuzhiyun result = f(*args, **kw) 30*4882a593Smuzhiyun te = time.time() 31*4882a593Smuzhiyun print ('func:%r args:[%r, %r] took: %2.4f sec' % \ 32*4882a593Smuzhiyun (f.__name__, args, kw, te-ts)) 33*4882a593Smuzhiyun return result 34*4882a593Smuzhiyun return timed 35*4882a593Smuzhiyun 36*4882a593Smuzhiyundef human_size(nbytes): 37*4882a593Smuzhiyun import math 38*4882a593Smuzhiyun suffixes = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'] 39*4882a593Smuzhiyun human = nbytes 40*4882a593Smuzhiyun rank = 0 41*4882a593Smuzhiyun if nbytes != 0: 42*4882a593Smuzhiyun rank = int((math.log10(nbytes)) / 3) 43*4882a593Smuzhiyun rank = min(rank, len(suffixes) - 1) 44*4882a593Smuzhiyun human = nbytes / (1000.0 ** rank) 45*4882a593Smuzhiyun f = ('%.2f' % human).rstrip('0').rstrip('.') 46*4882a593Smuzhiyun return '%s %s' % (f, suffixes[rank]) 47*4882a593Smuzhiyun 48*4882a593Smuzhiyundef load(filename, suffix=None): 49*4882a593Smuzhiyun from configparser import ConfigParser 50*4882a593Smuzhiyun from itertools import chain 51*4882a593Smuzhiyun 52*4882a593Smuzhiyun parser = ConfigParser(delimiters=('=')) 53*4882a593Smuzhiyun if suffix: 54*4882a593Smuzhiyun parser.optionxform = lambda option: option.replace(":" + suffix, "") 55*4882a593Smuzhiyun with open(filename) as lines: 56*4882a593Smuzhiyun lines = chain(("[fake]",), (line.replace(": ", " = ", 1) for line in lines)) 57*4882a593Smuzhiyun parser.read_file(lines) 58*4882a593Smuzhiyun 59*4882a593Smuzhiyun # TODO extract the data and put it into a real dict so we can transform some 60*4882a593Smuzhiyun # values to ints? 61*4882a593Smuzhiyun return parser["fake"] 62*4882a593Smuzhiyun 63*4882a593Smuzhiyundef find_pkgdata(): 64*4882a593Smuzhiyun import subprocess 65*4882a593Smuzhiyun output = subprocess.check_output(("bitbake", "-e"), universal_newlines=True) 66*4882a593Smuzhiyun for line in output.splitlines(): 67*4882a593Smuzhiyun if line.startswith("PKGDATA_DIR="): 68*4882a593Smuzhiyun return line.split("=", 1)[1].strip("\'\"") 69*4882a593Smuzhiyun # TODO exception or something 70*4882a593Smuzhiyun return None 71*4882a593Smuzhiyun 72*4882a593Smuzhiyundef packages_in_recipe(pkgdata, recipe): 73*4882a593Smuzhiyun """ 74*4882a593Smuzhiyun Load the recipe pkgdata to determine the list of runtime packages. 75*4882a593Smuzhiyun """ 76*4882a593Smuzhiyun data = load(os.path.join(pkgdata, recipe)) 77*4882a593Smuzhiyun packages = data["PACKAGES"].split() 78*4882a593Smuzhiyun return packages 79*4882a593Smuzhiyun 80*4882a593Smuzhiyundef load_runtime_package(pkgdata, package): 81*4882a593Smuzhiyun return load(os.path.join(pkgdata, "runtime", package), suffix=package) 82*4882a593Smuzhiyun 83*4882a593Smuzhiyundef recipe_from_package(pkgdata, package): 84*4882a593Smuzhiyun data = load(os.path.join(pkgdata, "runtime", package), suffix=package) 85*4882a593Smuzhiyun return data["PN"] 86*4882a593Smuzhiyun 87*4882a593Smuzhiyundef summary(data): 88*4882a593Smuzhiyun s = "" 89*4882a593Smuzhiyun s += "{0[PKG]} {0[PKGV]}-{0[PKGR]}\n{0[LICENSE]}\n{0[SUMMARY]}\n".format(data) 90*4882a593Smuzhiyun 91*4882a593Smuzhiyun return s 92*4882a593Smuzhiyun 93*4882a593Smuzhiyun 94*4882a593Smuzhiyunclass PkgUi(): 95*4882a593Smuzhiyun def __init__(self, pkgdata): 96*4882a593Smuzhiyun self.pkgdata = pkgdata 97*4882a593Smuzhiyun self.current_recipe = None 98*4882a593Smuzhiyun self.recipe_iters = {} 99*4882a593Smuzhiyun self.package_iters = {} 100*4882a593Smuzhiyun 101*4882a593Smuzhiyun builder = Gtk.Builder() 102*4882a593Smuzhiyun builder.add_from_file(os.path.join(os.path.dirname(__file__), "oe-pkgdata-browser.glade")) 103*4882a593Smuzhiyun 104*4882a593Smuzhiyun self.window = builder.get_object("window") 105*4882a593Smuzhiyun self.window.connect("delete-event", Gtk.main_quit) 106*4882a593Smuzhiyun 107*4882a593Smuzhiyun self.recipe_store = builder.get_object("recipe_store") 108*4882a593Smuzhiyun self.recipe_view = builder.get_object("recipe_view") 109*4882a593Smuzhiyun self.package_store = builder.get_object("package_store") 110*4882a593Smuzhiyun self.package_view = builder.get_object("package_view") 111*4882a593Smuzhiyun 112*4882a593Smuzhiyun # Somehow resizable does not get set via builder xml 113*4882a593Smuzhiyun package_name_column = builder.get_object("package_name_column") 114*4882a593Smuzhiyun package_name_column.set_resizable(True) 115*4882a593Smuzhiyun file_name_column = builder.get_object("file_name_column") 116*4882a593Smuzhiyun file_name_column.set_resizable(True) 117*4882a593Smuzhiyun 118*4882a593Smuzhiyun self.recipe_view.get_selection().connect("changed", self.on_recipe_changed) 119*4882a593Smuzhiyun self.package_view.get_selection().connect("changed", self.on_package_changed) 120*4882a593Smuzhiyun 121*4882a593Smuzhiyun self.package_store.set_sort_column_id(PackageColumns.Package, Gtk.SortType.ASCENDING) 122*4882a593Smuzhiyun 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]))) 123*4882a593Smuzhiyun 124*4882a593Smuzhiyun self.label = builder.get_object("label1") 125*4882a593Smuzhiyun self.depends_label = builder.get_object("depends_label") 126*4882a593Smuzhiyun self.recommends_label = builder.get_object("recommends_label") 127*4882a593Smuzhiyun self.suggests_label = builder.get_object("suggests_label") 128*4882a593Smuzhiyun self.provides_label = builder.get_object("provides_label") 129*4882a593Smuzhiyun 130*4882a593Smuzhiyun self.depends_label.connect("activate-link", self.on_link_activate) 131*4882a593Smuzhiyun self.recommends_label.connect("activate-link", self.on_link_activate) 132*4882a593Smuzhiyun self.suggests_label.connect("activate-link", self.on_link_activate) 133*4882a593Smuzhiyun 134*4882a593Smuzhiyun self.file_store = builder.get_object("file_store") 135*4882a593Smuzhiyun self.file_store.set_sort_column_id(FileColumns.Filename, Gtk.SortType.ASCENDING) 136*4882a593Smuzhiyun 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]))) 137*4882a593Smuzhiyun 138*4882a593Smuzhiyun self.files_view = builder.get_object("files_scrollview") 139*4882a593Smuzhiyun self.files_label = builder.get_object("files_label") 140*4882a593Smuzhiyun 141*4882a593Smuzhiyun self.load_recipes() 142*4882a593Smuzhiyun 143*4882a593Smuzhiyun self.recipe_view.set_cursor(Gtk.TreePath.new_first()) 144*4882a593Smuzhiyun 145*4882a593Smuzhiyun self.window.show() 146*4882a593Smuzhiyun 147*4882a593Smuzhiyun def on_link_activate(self, label, url_string): 148*4882a593Smuzhiyun from urllib.parse import urlparse 149*4882a593Smuzhiyun url = urlparse(url_string) 150*4882a593Smuzhiyun if url.scheme == "package": 151*4882a593Smuzhiyun package = url.path 152*4882a593Smuzhiyun recipe = recipe_from_package(self.pkgdata, package) 153*4882a593Smuzhiyun 154*4882a593Smuzhiyun it = self.recipe_iters[recipe] 155*4882a593Smuzhiyun path = self.recipe_store.get_path(it) 156*4882a593Smuzhiyun self.recipe_view.set_cursor(path) 157*4882a593Smuzhiyun self.recipe_view.scroll_to_cell(path) 158*4882a593Smuzhiyun 159*4882a593Smuzhiyun self.on_recipe_changed(self.recipe_view.get_selection()) 160*4882a593Smuzhiyun 161*4882a593Smuzhiyun it = self.package_iters[package] 162*4882a593Smuzhiyun path = self.package_store.get_path(it) 163*4882a593Smuzhiyun self.package_view.set_cursor(path) 164*4882a593Smuzhiyun self.package_view.scroll_to_cell(path) 165*4882a593Smuzhiyun 166*4882a593Smuzhiyun return True 167*4882a593Smuzhiyun else: 168*4882a593Smuzhiyun return False 169*4882a593Smuzhiyun 170*4882a593Smuzhiyun def on_recipe_changed(self, selection): 171*4882a593Smuzhiyun self.package_store.clear() 172*4882a593Smuzhiyun self.package_iters = {} 173*4882a593Smuzhiyun 174*4882a593Smuzhiyun (model, it) = selection.get_selected() 175*4882a593Smuzhiyun if not it: 176*4882a593Smuzhiyun return 177*4882a593Smuzhiyun 178*4882a593Smuzhiyun recipe = model[it][RecipeColumns.Recipe] 179*4882a593Smuzhiyun packages = packages_in_recipe(self.pkgdata, recipe) 180*4882a593Smuzhiyun for package in packages: 181*4882a593Smuzhiyun # TODO also show PKG after debian-renaming? 182*4882a593Smuzhiyun data = load_runtime_package(self.pkgdata, package) 183*4882a593Smuzhiyun # TODO stash data to avoid reading in on_package_changed 184*4882a593Smuzhiyun self.package_iters[package] = self.package_store.append([package, int(data["PKGSIZE"])]) 185*4882a593Smuzhiyun 186*4882a593Smuzhiyun package = recipe if recipe in packages else sorted(packages)[0] 187*4882a593Smuzhiyun path = self.package_store.get_path(self.package_iters[package]) 188*4882a593Smuzhiyun self.package_view.set_cursor(path) 189*4882a593Smuzhiyun self.package_view.scroll_to_cell(path) 190*4882a593Smuzhiyun 191*4882a593Smuzhiyun def on_package_changed(self, selection): 192*4882a593Smuzhiyun self.label.set_text("") 193*4882a593Smuzhiyun self.file_store.clear() 194*4882a593Smuzhiyun self.depends_label.hide() 195*4882a593Smuzhiyun self.recommends_label.hide() 196*4882a593Smuzhiyun self.suggests_label.hide() 197*4882a593Smuzhiyun self.provides_label.hide() 198*4882a593Smuzhiyun self.files_view.hide() 199*4882a593Smuzhiyun self.files_label.hide() 200*4882a593Smuzhiyun 201*4882a593Smuzhiyun (model, it) = selection.get_selected() 202*4882a593Smuzhiyun if it is None: 203*4882a593Smuzhiyun return 204*4882a593Smuzhiyun 205*4882a593Smuzhiyun package = model[it][PackageColumns.Package] 206*4882a593Smuzhiyun data = load_runtime_package(self.pkgdata, package) 207*4882a593Smuzhiyun 208*4882a593Smuzhiyun self.label.set_text(summary(data)) 209*4882a593Smuzhiyun 210*4882a593Smuzhiyun files = ast.literal_eval(data["FILES_INFO"]) 211*4882a593Smuzhiyun if files: 212*4882a593Smuzhiyun self.files_label.set_text("{0} files take {1}.".format(len(files), human_size(int(data["PKGSIZE"])))) 213*4882a593Smuzhiyun self.files_view.show() 214*4882a593Smuzhiyun for filename, size in files.items(): 215*4882a593Smuzhiyun self.file_store.append([filename, size]) 216*4882a593Smuzhiyun else: 217*4882a593Smuzhiyun self.files_view.hide() 218*4882a593Smuzhiyun self.files_label.set_text("This package has no files.") 219*4882a593Smuzhiyun self.files_label.show() 220*4882a593Smuzhiyun 221*4882a593Smuzhiyun def update_deps(field, prefix, label, clickable=True): 222*4882a593Smuzhiyun if field in data: 223*4882a593Smuzhiyun l = [] 224*4882a593Smuzhiyun for name, version in bb.utils.explode_dep_versions2(data[field]).items(): 225*4882a593Smuzhiyun if clickable: 226*4882a593Smuzhiyun l.append("<a href='package:{0}'>{0}</a> {1}".format(name, " ".join(version)).strip()) 227*4882a593Smuzhiyun else: 228*4882a593Smuzhiyun l.append("{0} {1}".format(name, " ".join(version)).strip()) 229*4882a593Smuzhiyun label.set_markup(prefix + ", ".join(l)) 230*4882a593Smuzhiyun label.show() 231*4882a593Smuzhiyun else: 232*4882a593Smuzhiyun label.hide() 233*4882a593Smuzhiyun update_deps("RDEPENDS", "Depends: ", self.depends_label) 234*4882a593Smuzhiyun update_deps("RRECOMMENDS", "Recommends: ", self.recommends_label) 235*4882a593Smuzhiyun update_deps("RSUGGESTS", "Suggests: ", self.suggests_label) 236*4882a593Smuzhiyun update_deps("RPROVIDES", "Provides: ", self.provides_label, clickable=False) 237*4882a593Smuzhiyun 238*4882a593Smuzhiyun def load_recipes(self): 239*4882a593Smuzhiyun if not os.path.exists(pkgdata): 240*4882a593Smuzhiyun sys.exit("Error: Please ensure %s exists by generating packages before using this tool." % pkgdata) 241*4882a593Smuzhiyun for recipe in sorted(os.listdir(pkgdata)): 242*4882a593Smuzhiyun if os.path.isfile(os.path.join(pkgdata, recipe)): 243*4882a593Smuzhiyun self.recipe_iters[recipe] = self.recipe_store.append([recipe]) 244*4882a593Smuzhiyun 245*4882a593Smuzhiyunif __name__ == "__main__": 246*4882a593Smuzhiyun import argparse 247*4882a593Smuzhiyun 248*4882a593Smuzhiyun parser = argparse.ArgumentParser(description='pkgdata browser') 249*4882a593Smuzhiyun parser.add_argument('-p', '--pkgdata', help="Optional location of pkgdata") 250*4882a593Smuzhiyun 251*4882a593Smuzhiyun args = parser.parse_args() 252*4882a593Smuzhiyun pkgdata = args.pkgdata if args.pkgdata else find_pkgdata() 253*4882a593Smuzhiyun # TODO assert pkgdata is a directory 254*4882a593Smuzhiyun window = PkgUi(pkgdata) 255*4882a593Smuzhiyun Gtk.main() 256