#!/usr/bin/env python3 # Script to extract information from image manifests # # Copyright (C) 2018 Intel Corporation # Copyright (C) 2021 Wind River Systems, Inc. # # SPDX-License-Identifier: GPL-2.0-only # import sys import os import argparse import logging import json import shutil import tempfile import tarfile from collections import OrderedDict scripts_path = os.path.dirname(__file__) lib_path = scripts_path + '/../lib' sys.path = sys.path + [lib_path] import scriptutils logger = scriptutils.logger_create(os.path.basename(__file__)) import argparse_oe import scriptpath bitbakepath = scriptpath.add_bitbake_lib_path() if not bitbakepath: logger.error("Unable to find bitbake by searching parent directory of this script or PATH") sys.exit(1) logger.debug('Using standard bitbake path %s' % bitbakepath) scriptpath.add_oe_lib_path() import bb.tinfoil import bb.utils import oe.utils import oe.recipeutils def get_pkg_list(manifest): pkglist = [] with open(manifest, 'r') as f: for line in f: linesplit = line.split() if len(linesplit) == 3: # manifest file pkglist.append(linesplit[0]) elif len(linesplit) == 1: # build dependency file pkglist.append(linesplit[0]) return sorted(pkglist) def list_packages(args): pkglist = get_pkg_list(args.manifest) for pkg in pkglist: print('%s' % pkg) def pkg2recipe(tinfoil, pkg): if "-native" in pkg: logger.info('skipping %s' % pkg) return None pkgdata_dir = tinfoil.config_data.getVar('PKGDATA_DIR') pkgdatafile = os.path.join(pkgdata_dir, 'runtime-reverse', pkg) logger.debug('pkgdatafile %s' % pkgdatafile) try: f = open(pkgdatafile, 'r') for line in f: if line.startswith('PN:'): recipe = line.split(':', 1)[1].strip() return recipe except Exception: logger.warning('%s is missing' % pkgdatafile) return None def get_recipe_list(manifest, tinfoil): pkglist = get_pkg_list(manifest) recipelist = [] for pkg in pkglist: recipe = pkg2recipe(tinfoil,pkg) if recipe: if not recipe in recipelist: recipelist.append(recipe) return sorted(recipelist) def list_recipes(args): import bb.tinfoil with bb.tinfoil.Tinfoil() as tinfoil: tinfoil.logger.setLevel(logger.getEffectiveLevel()) tinfoil.prepare(config_only=True) recipelist = get_recipe_list(args.manifest, tinfoil) for recipe in sorted(recipelist): print('%s' % recipe) def list_layers(args): def find_git_repo(pth): checkpth = pth while checkpth != os.sep: if os.path.exists(os.path.join(checkpth, '.git')): return checkpth checkpth = os.path.dirname(checkpth) return None def get_git_remote_branch(repodir): try: stdout, _ = bb.process.run(['git', 'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], cwd=repodir) except bb.process.ExecutionError as e: stdout = None if stdout: return stdout.strip() else: return None def get_git_head_commit(repodir): try: stdout, _ = bb.process.run(['git', 'rev-parse', 'HEAD'], cwd=repodir) except bb.process.ExecutionError as e: stdout = None if stdout: return stdout.strip() else: return None def get_git_repo_url(repodir, remote='origin'): import bb.process # Try to get upstream repo location from origin remote try: stdout, _ = bb.process.run(['git', 'remote', '-v'], cwd=repodir) except bb.process.ExecutionError as e: stdout = None if stdout: for line in stdout.splitlines(): splitline = line.split() if len(splitline) > 1: if splitline[0] == remote and scriptutils.is_src_url(splitline[1]): return splitline[1] return None with bb.tinfoil.Tinfoil() as tinfoil: tinfoil.logger.setLevel(logger.getEffectiveLevel()) tinfoil.prepare(config_only=False) layers = OrderedDict() for layerdir in tinfoil.config_data.getVar('BBLAYERS').split(): layerdata = OrderedDict() layername = os.path.basename(layerdir) logger.debug('layername %s, layerdir %s' % (layername, layerdir)) if layername in layers: logger.warning('layername %s is not unique in configuration' % layername) layername = os.path.basename(os.path.dirname(layerdir)) + '_' + os.path.basename(layerdir) logger.debug('trying layername %s' % layername) if layername in layers: logger.error('Layer name %s is not unique in configuration' % layername) sys.exit(2) repodir = find_git_repo(layerdir) if repodir: remotebranch = get_git_remote_branch(repodir) remote = 'origin' if remotebranch and '/' in remotebranch: rbsplit = remotebranch.split('/', 1) layerdata['actual_branch'] = rbsplit[1] remote = rbsplit[0] layerdata['vcs_url'] = get_git_repo_url(repodir, remote) if os.path.abspath(repodir) != os.path.abspath(layerdir): layerdata['vcs_subdir'] = os.path.relpath(layerdir, repodir) commit = get_git_head_commit(repodir) if commit: layerdata['vcs_commit'] = commit layers[layername] = layerdata json.dump(layers, args.output, indent=2) def get_recipe(args): with bb.tinfoil.Tinfoil() as tinfoil: tinfoil.logger.setLevel(logger.getEffectiveLevel()) tinfoil.prepare(config_only=True) recipe = pkg2recipe(tinfoil, args.package) print(' %s package provided by %s' % (args.package, recipe)) def pkg_dependencies(args): def get_recipe_info(tinfoil, recipe): try: info = tinfoil.get_recipe_info(recipe) except Exception: logger.error('Failed to get recipe info for: %s' % recipe) sys.exit(1) if not info: logger.warning('No recipe info found for: %s' % recipe) sys.exit(1) append_files = tinfoil.get_file_appends(info.fn) appends = True data = tinfoil.parse_recipe_file(info.fn, appends, append_files) data.pn = info.pn data.pv = info.pv return data def find_dependencies(tinfoil, assume_provided, recipe_info, packages, rn, order): spaces = ' ' * order data = recipe_info[rn] if args.native: logger.debug('%s- %s' % (spaces, data.pn)) elif "-native" not in data.pn: if "cross" not in data.pn: logger.debug('%s- %s' % (spaces, data.pn)) depends = [] for dep in data.depends: if dep not in assume_provided: depends.append(dep) # First find all dependencies not in package list. for dep in depends: if dep not in packages: packages.append(dep) dep_data = get_recipe_info(tinfoil, dep) # Do this once now to reduce the number of bitbake calls. dep_data.depends = dep_data.getVar('DEPENDS').split() recipe_info[dep] = dep_data # Then recursively analyze all of the dependencies for the current recipe. for dep in depends: find_dependencies(tinfoil, assume_provided, recipe_info, packages, dep, order + 1) with bb.tinfoil.Tinfoil() as tinfoil: tinfoil.logger.setLevel(logger.getEffectiveLevel()) tinfoil.prepare() assume_provided = tinfoil.config_data.getVar('ASSUME_PROVIDED').split() logger.debug('assumed provided:') for ap in sorted(assume_provided): logger.debug(' - %s' % ap) recipe = pkg2recipe(tinfoil, args.package) data = get_recipe_info(tinfoil, recipe) data.depends = [] depends = data.getVar('DEPENDS').split() for dep in depends: if dep not in assume_provided: data.depends.append(dep) recipe_info = dict([(recipe, data)]) packages = [] find_dependencies(tinfoil, assume_provided, recipe_info, packages, recipe, order=1) print('\nThe following packages are required to build %s' % recipe) for p in sorted(packages): data = recipe_info[p] if "-native" not in data.pn: if "cross" not in data.pn: print(" %s (%s)" % (data.pn,p)) if args.native: print('\nThe following native packages are required to build %s' % recipe) for p in sorted(packages): data = recipe_info[p] if "-native" in data.pn: print(" %s(%s)" % (data.pn,p)) if "cross" in data.pn: print(" %s(%s)" % (data.pn,p)) def default_config(): vlist = OrderedDict() vlist['PV'] = 'yes' vlist['SUMMARY'] = 'no' vlist['DESCRIPTION'] = 'no' vlist['SECTION'] = 'no' vlist['LICENSE'] = 'yes' vlist['HOMEPAGE'] = 'no' vlist['BUGTRACKER'] = 'no' vlist['PROVIDES'] = 'no' vlist['BBCLASSEXTEND'] = 'no' vlist['DEPENDS'] = 'no' vlist['PACKAGECONFIG'] = 'no' vlist['SRC_URI'] = 'yes' vlist['SRCREV'] = 'yes' vlist['EXTRA_OECONF'] = 'no' vlist['EXTRA_OESCONS'] = 'no' vlist['EXTRA_OECMAKE'] = 'no' vlist['EXTRA_OEMESON'] = 'no' clist = OrderedDict() clist['variables'] = vlist clist['filepath'] = 'no' clist['sha256sum'] = 'no' clist['layerdir'] = 'no' clist['layer'] = 'no' clist['inherits'] = 'no' clist['source_urls'] = 'no' clist['packageconfig_opts'] = 'no' clist['patches'] = 'no' clist['packagedir'] = 'no' return clist def dump_config(args): config = default_config() f = open('default_config.json', 'w') json.dump(config, f, indent=2) logger.info('Default config list dumped to default_config.json') def export_manifest_info(args): def handle_value(value): if value: return oe.utils.squashspaces(value) else: return value if args.config: logger.debug('config: %s' % args.config) f = open(args.config, 'r') config = json.load(f, object_pairs_hook=OrderedDict) else: config = default_config() if logger.isEnabledFor(logging.DEBUG): print('Configuration:') json.dump(config, sys.stdout, indent=2) print('') tmpoutdir = tempfile.mkdtemp(prefix=os.path.basename(__file__)+'-') logger.debug('tmp dir: %s' % tmpoutdir) # export manifest shutil.copy2(args.manifest,os.path.join(tmpoutdir, "manifest")) with bb.tinfoil.Tinfoil(tracking=True) as tinfoil: tinfoil.logger.setLevel(logger.getEffectiveLevel()) tinfoil.prepare(config_only=False) pkglist = get_pkg_list(args.manifest) # export pkg list f = open(os.path.join(tmpoutdir, "pkgs"), 'w') for pkg in pkglist: f.write('%s\n' % pkg) f.close() recipelist = [] for pkg in pkglist: recipe = pkg2recipe(tinfoil,pkg) if recipe: if not recipe in recipelist: recipelist.append(recipe) recipelist.sort() # export recipe list f = open(os.path.join(tmpoutdir, "recipes"), 'w') for recipe in recipelist: f.write('%s\n' % recipe) f.close() try: rvalues = OrderedDict() for pn in sorted(recipelist): logger.debug('Package: %s' % pn) rd = tinfoil.parse_recipe(pn) rvalues[pn] = OrderedDict() for varname in config['variables']: if config['variables'][varname] == 'yes': rvalues[pn][varname] = handle_value(rd.getVar(varname)) fpth = rd.getVar('FILE') layerdir = oe.recipeutils.find_layerdir(fpth) if config['filepath'] == 'yes': rvalues[pn]['filepath'] = os.path.relpath(fpth, layerdir) if config['sha256sum'] == 'yes': rvalues[pn]['sha256sum'] = bb.utils.sha256_file(fpth) if config['layerdir'] == 'yes': rvalues[pn]['layerdir'] = layerdir if config['layer'] == 'yes': rvalues[pn]['layer'] = os.path.basename(layerdir) if config['inherits'] == 'yes': gr = set(tinfoil.config_data.getVar("__inherit_cache") or []) lr = set(rd.getVar("__inherit_cache") or []) rvalues[pn]['inherits'] = sorted({os.path.splitext(os.path.basename(r))[0] for r in lr if r not in gr}) if config['source_urls'] == 'yes': rvalues[pn]['source_urls'] = [] for url in (rd.getVar('SRC_URI') or '').split(): if not url.startswith('file://'): url = url.split(';')[0] rvalues[pn]['source_urls'].append(url) if config['packageconfig_opts'] == 'yes': rvalues[pn]['packageconfig_opts'] = OrderedDict() for key in rd.getVarFlags('PACKAGECONFIG').keys(): if key == 'doc': continue rvalues[pn]['packageconfig_opts'][key] = rd.getVarFlag('PACKAGECONFIG', key) if config['patches'] == 'yes': patches = oe.recipeutils.get_recipe_patches(rd) rvalues[pn]['patches'] = [] if patches: recipeoutdir = os.path.join(tmpoutdir, pn, 'patches') bb.utils.mkdirhier(recipeoutdir) for patch in patches: # Patches may be in other layers too patchlayerdir = oe.recipeutils.find_layerdir(patch) # patchlayerdir will be None for remote patches, which we ignore # (since currently they are considered as part of sources) if patchlayerdir: rvalues[pn]['patches'].append((os.path.basename(patchlayerdir), os.path.relpath(patch, patchlayerdir))) shutil.copy(patch, recipeoutdir) if config['packagedir'] == 'yes': pn_dir = os.path.join(tmpoutdir, pn) bb.utils.mkdirhier(pn_dir) f = open(os.path.join(pn_dir, 'recipe.json'), 'w') json.dump(rvalues[pn], f, indent=2) f.close() with open(os.path.join(tmpoutdir, 'recipes.json'), 'w') as f: json.dump(rvalues, f, indent=2) if args.output: outname = os.path.basename(args.output) else: outname = os.path.splitext(os.path.basename(args.manifest))[0] if outname.endswith('.tar.gz'): outname = outname[:-7] elif outname.endswith('.tgz'): outname = outname[:-4] tarfn = outname if tarfn.endswith(os.sep): tarfn = tarfn[:-1] if not tarfn.endswith(('.tar.gz', '.tgz')): tarfn += '.tar.gz' with open(tarfn, 'wb') as f: with tarfile.open(None, "w:gz", f) as tar: tar.add(tmpoutdir, outname) finally: shutil.rmtree(tmpoutdir) def main(): parser = argparse_oe.ArgumentParser(description="Image manifest utility", epilog="Use %(prog)s --help to get help on a specific command") parser.add_argument('-d', '--debug', help='Enable debug output', action='store_true') parser.add_argument('-q', '--quiet', help='Print only errors', action='store_true') subparsers = parser.add_subparsers(dest="subparser_name", title='subcommands', metavar='') subparsers.required = True # get recipe info parser_get_recipes = subparsers.add_parser('recipe-info', help='Get recipe info', description='Get recipe information for a package') parser_get_recipes.add_argument('package', help='Package name') parser_get_recipes.set_defaults(func=get_recipe) # list runtime dependencies parser_pkg_dep = subparsers.add_parser('list-depends', help='List dependencies', description='List dependencies required to build the package') parser_pkg_dep.add_argument('--native', help='also print native and cross packages', action='store_true') parser_pkg_dep.add_argument('package', help='Package name') parser_pkg_dep.set_defaults(func=pkg_dependencies) # list recipes parser_recipes = subparsers.add_parser('list-recipes', help='List recipes producing packages within an image', description='Lists recipes producing the packages that went into an image, using the manifest and pkgdata') parser_recipes.add_argument('manifest', help='Manifest file') parser_recipes.set_defaults(func=list_recipes) # list packages parser_packages = subparsers.add_parser('list-packages', help='List packages within an image', description='Lists packages that went into an image, using the manifest') parser_packages.add_argument('manifest', help='Manifest file') parser_packages.set_defaults(func=list_packages) # list layers parser_layers = subparsers.add_parser('list-layers', help='List included layers', description='Lists included layers') parser_layers.add_argument('-o', '--output', help='Output file - defaults to stdout if not specified', default=sys.stdout, type=argparse.FileType('w')) parser_layers.set_defaults(func=list_layers) # dump default configuration file parser_dconfig = subparsers.add_parser('dump-config', help='Dump default config', description='Dump default config to default_config.json') parser_dconfig.set_defaults(func=dump_config) # export recipe info for packages in manifest parser_export = subparsers.add_parser('manifest-info', help='Export recipe info for a manifest', description='Export recipe information using the manifest') parser_export.add_argument('-c', '--config', help='load config from json file') parser_export.add_argument('-o', '--output', help='Output file (tarball) - defaults to manifest name if not specified') parser_export.add_argument('manifest', help='Manifest file') parser_export.set_defaults(func=export_manifest_info) args = parser.parse_args() if args.debug: logger.setLevel(logging.DEBUG) logger.debug("Debug Enabled") elif args.quiet: logger.setLevel(logging.ERROR) ret = args.func(args) return ret if __name__ == "__main__": try: ret = main() except Exception: ret = 1 import traceback traceback.print_exc() sys.exit(ret)