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