diff options
Diffstat (limited to 'scripts/oe-pkgdata-browser')
| -rwxr-xr-x | scripts/oe-pkgdata-browser | 260 |
1 files changed, 0 insertions, 260 deletions
diff --git a/scripts/oe-pkgdata-browser b/scripts/oe-pkgdata-browser deleted file mode 100755 index c152c82b25..0000000000 --- a/scripts/oe-pkgdata-browser +++ /dev/null | |||
| @@ -1,260 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # Copyright OpenEmbedded Contributors | ||
| 4 | # | ||
| 5 | # SPDX-License-Identifier: MIT | ||
| 6 | # | ||
| 7 | |||
| 8 | import os, sys, enum, ast | ||
| 9 | |||
| 10 | scripts_path = os.path.dirname(os.path.realpath(__file__)) | ||
| 11 | lib_path = scripts_path + '/lib' | ||
| 12 | sys.path = sys.path + [lib_path] | ||
| 13 | |||
| 14 | import scriptpath | ||
| 15 | bitbakepath = scriptpath.add_bitbake_lib_path() | ||
| 16 | if not bitbakepath: | ||
| 17 | print("Unable to find bitbake by searching parent directory of this script or PATH") | ||
| 18 | sys.exit(1) | ||
| 19 | import bb | ||
| 20 | |||
| 21 | import gi | ||
| 22 | gi.require_version('Gtk', '3.0') | ||
| 23 | from gi.repository import Gtk, Gdk, GObject | ||
| 24 | |||
| 25 | RecipeColumns = enum.IntEnum("RecipeColumns", {"Recipe": 0}) | ||
| 26 | PackageColumns = enum.IntEnum("PackageColumns", {"Package": 0, "Size": 1}) | ||
| 27 | FileColumns = enum.IntEnum("FileColumns", {"Filename": 0, "Size": 1}) | ||
| 28 | |||
| 29 | import time | ||
| 30 | def 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 | |||
| 41 | def 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 | |||
| 53 | def 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 | |||
| 68 | def 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 | |||
| 77 | def 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 | |||
| 85 | def load_runtime_package(pkgdata, package): | ||
| 86 | return load(os.path.join(pkgdata, "runtime", package), suffix=package) | ||
| 87 | |||
| 88 | def recipe_from_package(pkgdata, package): | ||
| 89 | data = load(os.path.join(pkgdata, "runtime", package), suffix=package) | ||
| 90 | return data["PN"] | ||
| 91 | |||
| 92 | def 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 | |||
| 99 | class 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 | |||
| 250 | if __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() | ||
