594 lines
24 KiB
Python
Executable File
594 lines
24 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
# This script has subcommands which operate against your bitbake layers, either
|
|
# displaying useful information, or acting against them.
|
|
# See the help output for details on available commands.
|
|
|
|
# Copyright (C) 2011 Mentor Graphics Corporation
|
|
# Copyright (C) 2012 Intel Corporation
|
|
|
|
import cmd
|
|
import logging
|
|
import os
|
|
import sys
|
|
import fnmatch
|
|
from collections import defaultdict
|
|
|
|
bindir = os.path.dirname(__file__)
|
|
topdir = os.path.dirname(bindir)
|
|
sys.path[0:0] = [os.path.join(topdir, 'lib')]
|
|
|
|
import bb.cache
|
|
import bb.cooker
|
|
import bb.providers
|
|
import bb.utils
|
|
from bb.cooker import state
|
|
import bb.fetch2
|
|
|
|
|
|
logger = logging.getLogger('BitBake')
|
|
|
|
|
|
def main(args):
|
|
# Set up logging
|
|
console = logging.StreamHandler(sys.stdout)
|
|
format = bb.msg.BBLogFormatter("%(levelname)s: %(message)s")
|
|
bb.msg.addDefaultlogFilter(console)
|
|
console.setFormatter(format)
|
|
logger.addHandler(console)
|
|
|
|
initialenv = os.environ.copy()
|
|
bb.utils.clean_environment()
|
|
|
|
cmds = Commands(initialenv)
|
|
if args:
|
|
# Allow user to specify e.g. show-layers instead of show_layers
|
|
args = [args[0].replace('-', '_')] + args[1:]
|
|
cmds.onecmd(' '.join(args))
|
|
else:
|
|
cmds.do_help('')
|
|
return cmds.returncode
|
|
|
|
|
|
class Commands(cmd.Cmd):
|
|
def __init__(self, initialenv):
|
|
cmd.Cmd.__init__(self)
|
|
self.returncode = 0
|
|
self.config = Config(parse_only=True)
|
|
self.cooker = bb.cooker.BBCooker(self.config,
|
|
self.register_idle_function,
|
|
initialenv)
|
|
self.config_data = self.cooker.configuration.data
|
|
bb.providers.logger.setLevel(logging.ERROR)
|
|
self.cooker_data = None
|
|
self.bblayers = (self.config_data.getVar('BBLAYERS', True) or "").split()
|
|
|
|
def register_idle_function(self, function, data):
|
|
pass
|
|
|
|
def prepare_cooker(self):
|
|
sys.stderr.write("Parsing recipes..")
|
|
logger.setLevel(logging.WARNING)
|
|
|
|
try:
|
|
while self.cooker.state in (state.initial, state.parsing):
|
|
self.cooker.updateCache()
|
|
except KeyboardInterrupt:
|
|
self.cooker.shutdown()
|
|
self.cooker.updateCache()
|
|
sys.exit(2)
|
|
|
|
logger.setLevel(logging.INFO)
|
|
sys.stderr.write("done.\n")
|
|
|
|
self.cooker_data = self.cooker.status
|
|
self.cooker_data.appends = self.cooker.appendlist
|
|
|
|
def check_prepare_cooker(self):
|
|
if not self.cooker_data:
|
|
self.prepare_cooker()
|
|
|
|
def default(self, line):
|
|
"""Handle unrecognised commands"""
|
|
sys.stderr.write("Unrecognised command or option\n")
|
|
self.do_help('')
|
|
|
|
def do_help(self, topic):
|
|
"""display general help or help on a specified command"""
|
|
if topic:
|
|
sys.stdout.write('%s: ' % topic)
|
|
cmd.Cmd.do_help(self, topic.replace('-', '_'))
|
|
else:
|
|
sys.stdout.write("usage: bitbake-layers <command> [arguments]\n\n")
|
|
sys.stdout.write("Available commands:\n")
|
|
procnames = self.get_names()
|
|
for procname in procnames:
|
|
if procname[:3] == 'do_':
|
|
sys.stdout.write(" %s\n" % procname[3:].replace('_', '-'))
|
|
doc = getattr(self, procname).__doc__
|
|
if doc:
|
|
sys.stdout.write(" %s\n" % doc.splitlines()[0])
|
|
|
|
def do_show_layers(self, args):
|
|
"""show current configured layers"""
|
|
self.check_prepare_cooker()
|
|
logger.plain('')
|
|
logger.plain("%s %s %s" % ("layer".ljust(20), "path".ljust(40), "priority"))
|
|
logger.plain('=' * 74)
|
|
for layerdir in self.bblayers:
|
|
layername = self.get_layer_name(layerdir)
|
|
layerpri = 0
|
|
for layer, _, regex, pri in self.cooker.status.bbfile_config_priorities:
|
|
if regex.match(os.path.join(layerdir, 'test')):
|
|
layerpri = pri
|
|
break
|
|
|
|
logger.plain("%s %s %d" % (layername.ljust(20), layerdir.ljust(40), layerpri))
|
|
|
|
|
|
def version_str(self, pe, pv, pr = None):
|
|
verstr = "%s" % pv
|
|
if pr:
|
|
verstr = "%s-%s" % (verstr, pr)
|
|
if pe:
|
|
verstr = "%s:%s" % (pe, verstr)
|
|
return verstr
|
|
|
|
|
|
def do_show_overlayed(self, args):
|
|
"""list overlayed recipes (where the same recipe exists in another layer that has a higher layer priority)
|
|
|
|
usage: show-overlayed [-f] [-s]
|
|
|
|
Lists the names of overlayed recipes and the available versions in each
|
|
layer, with the preferred version first. Note that skipped recipes that
|
|
are overlayed will also be listed, with a " (skipped)" suffix.
|
|
|
|
Options:
|
|
-f instead of the default formatting, list filenames of higher priority
|
|
recipes with the ones they overlay indented underneath
|
|
-s only list overlayed recipes where the version is the same
|
|
"""
|
|
self.check_prepare_cooker()
|
|
|
|
show_filenames = False
|
|
show_same_ver_only = False
|
|
for arg in args.split():
|
|
if arg == '-f':
|
|
show_filenames = True
|
|
elif arg == '-s':
|
|
show_same_ver_only = True
|
|
else:
|
|
sys.stderr.write("show-overlayed: invalid option %s\n" % arg)
|
|
self.do_help('')
|
|
return
|
|
|
|
items_listed = self.list_recipes('Overlayed recipes', None, True, show_same_ver_only, show_filenames, True)
|
|
|
|
# Check for overlayed .bbclass files
|
|
classes = defaultdict(list)
|
|
for layerdir in self.bblayers:
|
|
classdir = os.path.join(layerdir, 'classes')
|
|
if os.path.exists(classdir):
|
|
for classfile in os.listdir(classdir):
|
|
if os.path.splitext(classfile)[1] == '.bbclass':
|
|
classes[classfile].append(classdir)
|
|
|
|
# Locating classes and other files is a bit more complicated than recipes -
|
|
# layer priority is not a factor; instead BitBake uses the first matching
|
|
# file in BBPATH, which is manipulated directly by each layer's
|
|
# conf/layer.conf in turn, thus the order of layers in bblayers.conf is a
|
|
# factor - however, each layer.conf is free to either prepend or append to
|
|
# BBPATH (or indeed do crazy stuff with it). Thus the order in BBPATH might
|
|
# not be exactly the order present in bblayers.conf either.
|
|
bbpath = str(self.config_data.getVar('BBPATH', True))
|
|
overlayed_class_found = False
|
|
for (classfile, classdirs) in classes.items():
|
|
if len(classdirs) > 1:
|
|
if not overlayed_class_found:
|
|
logger.plain('=== Overlayed classes ===')
|
|
overlayed_class_found = True
|
|
|
|
mainfile = bb.utils.which(bbpath, os.path.join('classes', classfile))
|
|
if show_filenames:
|
|
logger.plain('%s' % mainfile)
|
|
else:
|
|
# We effectively have to guess the layer here
|
|
logger.plain('%s:' % classfile)
|
|
mainlayername = '?'
|
|
for layerdir in self.bblayers:
|
|
classdir = os.path.join(layerdir, 'classes')
|
|
if mainfile.startswith(classdir):
|
|
mainlayername = self.get_layer_name(layerdir)
|
|
logger.plain(' %s' % mainlayername)
|
|
for classdir in classdirs:
|
|
fullpath = os.path.join(classdir, classfile)
|
|
if fullpath != mainfile:
|
|
if show_filenames:
|
|
print(' %s' % fullpath)
|
|
else:
|
|
print(' %s' % self.get_layer_name(os.path.dirname(classdir)))
|
|
|
|
if overlayed_class_found:
|
|
items_listed = True;
|
|
|
|
if not items_listed:
|
|
logger.note('No overlayed files found')
|
|
|
|
|
|
def do_show_recipes(self, args):
|
|
"""list available recipes, showing the layer they are provided by
|
|
|
|
usage: show-recipes [-f] [-m] [pnspec]
|
|
|
|
Lists the names of overlayed recipes and the available versions in each
|
|
layer, with the preferred version first. Optionally you may specify
|
|
pnspec to match a specified recipe name (supports wildcards). Note that
|
|
skipped recipes will also be listed, with a " (skipped)" suffix.
|
|
|
|
Options:
|
|
-f instead of the default formatting, list filenames of higher priority
|
|
recipes with other available recipes indented underneath
|
|
-m only list where multiple recipes (in the same layer or different
|
|
layers) exist for the same recipe name
|
|
"""
|
|
self.check_prepare_cooker()
|
|
|
|
show_filenames = False
|
|
show_multi_provider_only = False
|
|
pnspec = None
|
|
title = 'Available recipes:'
|
|
for arg in args.split():
|
|
if arg == '-f':
|
|
show_filenames = True
|
|
elif arg == '-m':
|
|
show_multi_provider_only = True
|
|
elif not arg.startswith('-'):
|
|
pnspec = arg
|
|
title = 'Available recipes matching %s:' % pnspec
|
|
else:
|
|
sys.stderr.write("show-recipes: invalid option %s\n" % arg)
|
|
self.do_help('')
|
|
return
|
|
self.list_recipes(title, pnspec, False, False, show_filenames, show_multi_provider_only)
|
|
|
|
|
|
def list_recipes(self, title, pnspec, show_overlayed_only, show_same_ver_only, show_filenames, show_multi_provider_only):
|
|
pkg_pn = self.cooker.status.pkg_pn
|
|
(latest_versions, preferred_versions) = bb.providers.findProviders(self.cooker.configuration.data, self.cooker.status, pkg_pn)
|
|
allproviders = bb.providers.allProviders(self.cooker.status)
|
|
|
|
# Ensure we list skipped recipes
|
|
# We are largely guessing about PN, PV and the preferred version here,
|
|
# but we have no choice since skipped recipes are not fully parsed
|
|
skiplist = self.cooker.skiplist.keys()
|
|
skiplist.sort( key=lambda fileitem: self.cooker.calc_bbfile_priority(fileitem) )
|
|
skiplist.reverse()
|
|
for fn in skiplist:
|
|
recipe_parts = os.path.splitext(os.path.basename(fn))[0].split('_')
|
|
p = recipe_parts[0]
|
|
if len(recipe_parts) > 1:
|
|
ver = (None, recipe_parts[1], None)
|
|
else:
|
|
ver = (None, 'unknown', None)
|
|
allproviders[p].append((ver, fn))
|
|
if not p in pkg_pn:
|
|
pkg_pn[p] = 'dummy'
|
|
preferred_versions[p] = (ver, fn)
|
|
|
|
def print_item(f, pn, ver, layer, ispref):
|
|
if f in skiplist:
|
|
skipped = ' (skipped)'
|
|
else:
|
|
skipped = ''
|
|
if show_filenames:
|
|
if ispref:
|
|
logger.plain("%s%s", f, skipped)
|
|
else:
|
|
logger.plain(" %s%s", f, skipped)
|
|
else:
|
|
if ispref:
|
|
logger.plain("%s:", pn)
|
|
logger.plain(" %s %s%s", layer.ljust(20), ver, skipped)
|
|
|
|
preffiles = []
|
|
items_listed = False
|
|
for p in sorted(pkg_pn):
|
|
if pnspec:
|
|
if not fnmatch.fnmatch(p, pnspec):
|
|
continue
|
|
|
|
if len(allproviders[p]) > 1 or not show_multi_provider_only:
|
|
pref = preferred_versions[p]
|
|
preffile = bb.cache.Cache.virtualfn2realfn(pref[1])[0]
|
|
if preffile not in preffiles:
|
|
preflayer = self.get_file_layer(preffile)
|
|
multilayer = False
|
|
same_ver = True
|
|
provs = []
|
|
for prov in allproviders[p]:
|
|
provfile = bb.cache.Cache.virtualfn2realfn(prov[1])[0]
|
|
provlayer = self.get_file_layer(provfile)
|
|
provs.append((provfile, provlayer, prov[0]))
|
|
if provlayer != preflayer:
|
|
multilayer = True
|
|
if prov[0] != pref[0]:
|
|
same_ver = False
|
|
|
|
if (multilayer or not show_overlayed_only) and (same_ver or not show_same_ver_only):
|
|
if not items_listed:
|
|
logger.plain('=== %s ===' % title)
|
|
items_listed = True
|
|
print_item(preffile, p, self.version_str(pref[0][0], pref[0][1]), preflayer, True)
|
|
for (provfile, provlayer, provver) in provs:
|
|
if provfile != preffile:
|
|
print_item(provfile, p, self.version_str(provver[0], provver[1]), provlayer, False)
|
|
# Ensure we don't show two entries for BBCLASSEXTENDed recipes
|
|
preffiles.append(preffile)
|
|
|
|
return items_listed
|
|
|
|
|
|
def do_flatten(self, args):
|
|
"""flattens layer configuration into a separate output directory.
|
|
|
|
usage: flatten [layer1 layer2 [layer3]...] <outputdir>
|
|
|
|
Takes the specified layers (or all layers in the current layer
|
|
configuration if none are specified) and builds a "flattened" directory
|
|
containing the contents of all layers, with any overlayed recipes removed
|
|
and bbappends appended to the corresponding recipes. Note that some manual
|
|
cleanup may still be necessary afterwards, in particular:
|
|
|
|
* where non-recipe files (such as patches) are overwritten (the flatten
|
|
command will show a warning for these)
|
|
* where anything beyond the normal layer setup has been added to
|
|
layer.conf (only the lowest priority number layer's layer.conf is used)
|
|
* overridden/appended items from bbappends will need to be tidied up
|
|
* when the flattened layers do not have the same directory structure (the
|
|
flatten command should show a warning when this will cause a problem)
|
|
|
|
Warning: if you flatten several layers where another layer is intended to
|
|
be used "inbetween" them (in layer priority order) such that recipes /
|
|
bbappends in the layers interact, and then attempt to use the new output
|
|
layer together with that other layer, you may no longer get the same
|
|
build results (as the layer priority order has effectively changed).
|
|
"""
|
|
arglist = args.split()
|
|
if len(arglist) < 1:
|
|
logger.error('Please specify an output directory')
|
|
self.do_help('flatten')
|
|
return
|
|
|
|
if len(arglist) == 2:
|
|
logger.error('If you specify layers to flatten you must specify at least two')
|
|
self.do_help('flatten')
|
|
return
|
|
|
|
outputdir = arglist[-1]
|
|
if os.path.exists(outputdir) and os.listdir(outputdir):
|
|
logger.error('Directory %s exists and is non-empty, please clear it out first' % outputdir)
|
|
return
|
|
|
|
self.check_prepare_cooker()
|
|
layers = self.bblayers
|
|
if len(arglist) > 2:
|
|
layernames = arglist[:-1]
|
|
found_layernames = []
|
|
found_layerdirs = []
|
|
for layerdir in layers:
|
|
layername = self.get_layer_name(layerdir)
|
|
if layername in layernames:
|
|
found_layerdirs.append(layerdir)
|
|
found_layernames.append(layername)
|
|
|
|
for layername in layernames:
|
|
if not layername in found_layernames:
|
|
logger.error('Unable to find layer %s in current configuration, please run "%s show-layers" to list configured layers' % (layername, os.path.basename(sys.argv[0])))
|
|
return
|
|
layers = found_layerdirs
|
|
else:
|
|
layernames = []
|
|
|
|
# Ensure a specified path matches our list of layers
|
|
def layer_path_match(path):
|
|
for layerdir in layers:
|
|
if path.startswith(os.path.join(layerdir, '')):
|
|
return layerdir
|
|
return None
|
|
|
|
appended_recipes = []
|
|
for layer in layers:
|
|
overlayed = []
|
|
for f in self.cooker.overlayed.iterkeys():
|
|
for of in self.cooker.overlayed[f]:
|
|
if of.startswith(layer):
|
|
overlayed.append(of)
|
|
|
|
logger.plain('Copying files from %s...' % layer )
|
|
for root, dirs, files in os.walk(layer):
|
|
for f1 in files:
|
|
f1full = os.sep.join([root, f1])
|
|
if f1full in overlayed:
|
|
logger.plain(' Skipping overlayed file %s' % f1full )
|
|
else:
|
|
ext = os.path.splitext(f1)[1]
|
|
if ext != '.bbappend':
|
|
fdest = f1full[len(layer):]
|
|
fdest = os.path.normpath(os.sep.join([outputdir,fdest]))
|
|
bb.utils.mkdirhier(os.path.dirname(fdest))
|
|
if os.path.exists(fdest):
|
|
if f1 == 'layer.conf' and root.endswith('/conf'):
|
|
logger.plain(' Skipping layer config file %s' % f1full )
|
|
continue
|
|
else:
|
|
logger.warn('Overwriting file %s', fdest)
|
|
bb.utils.copyfile(f1full, fdest)
|
|
if ext == '.bb':
|
|
if f1 in self.cooker_data.appends:
|
|
appends = self.cooker_data.appends[f1]
|
|
if appends:
|
|
logger.plain(' Applying appends to %s' % fdest )
|
|
for appendname in appends:
|
|
if layer_path_match(appendname):
|
|
self.apply_append(appendname, fdest)
|
|
appended_recipes.append(f1)
|
|
|
|
# Take care of when some layers are excluded and yet we have included bbappends for those recipes
|
|
for recipename in self.cooker_data.appends.iterkeys():
|
|
if recipename not in appended_recipes:
|
|
appends = self.cooker_data.appends[recipename]
|
|
first_append = None
|
|
for appendname in appends:
|
|
layer = layer_path_match(appendname)
|
|
if layer:
|
|
if first_append:
|
|
self.apply_append(appendname, first_append)
|
|
else:
|
|
fdest = appendname[len(layer):]
|
|
fdest = os.path.normpath(os.sep.join([outputdir,fdest]))
|
|
bb.utils.mkdirhier(os.path.dirname(fdest))
|
|
bb.utils.copyfile(appendname, fdest)
|
|
first_append = fdest
|
|
|
|
# Get the regex for the first layer in our list (which is where the conf/layer.conf file will
|
|
# have come from)
|
|
first_regex = None
|
|
layerdir = layers[0]
|
|
for layername, pattern, regex, _ in self.cooker.status.bbfile_config_priorities:
|
|
if regex.match(os.path.join(layerdir, 'test')):
|
|
first_regex = regex
|
|
break
|
|
|
|
if first_regex:
|
|
# Find the BBFILES entries that match (which will have come from this conf/layer.conf file)
|
|
bbfiles = str(self.config_data.getVar('BBFILES', True)).split()
|
|
bbfiles_layer = []
|
|
for item in bbfiles:
|
|
if first_regex.match(item):
|
|
newpath = os.path.join(outputdir, item[len(layerdir)+1:])
|
|
bbfiles_layer.append(newpath)
|
|
|
|
if bbfiles_layer:
|
|
# Check that all important layer files match BBFILES
|
|
for root, dirs, files in os.walk(outputdir):
|
|
for f1 in files:
|
|
ext = os.path.splitext(f1)[1]
|
|
if ext in ['.bb', '.bbappend']:
|
|
f1full = os.sep.join([root, f1])
|
|
entry_found = False
|
|
for item in bbfiles_layer:
|
|
if fnmatch.fnmatch(f1full, item):
|
|
entry_found = True
|
|
break
|
|
if not entry_found:
|
|
logger.warning("File %s does not match the flattened layer's BBFILES setting, you may need to edit conf/layer.conf or move the file elsewhere" % f1full)
|
|
|
|
def get_file_layer(self, filename):
|
|
for layer, _, regex, _ in self.cooker.status.bbfile_config_priorities:
|
|
if regex.match(filename):
|
|
for layerdir in self.bblayers:
|
|
if regex.match(os.path.join(layerdir, 'test')):
|
|
return self.get_layer_name(layerdir)
|
|
return "?"
|
|
|
|
def get_layer_name(self, layerdir):
|
|
return os.path.basename(layerdir.rstrip(os.sep))
|
|
|
|
def apply_append(self, appendname, recipename):
|
|
appendfile = open(appendname, 'r')
|
|
recipefile = open(recipename, 'a')
|
|
recipefile.write('\n')
|
|
recipefile.write('##### bbappended from %s #####\n' % self.get_file_layer(appendname))
|
|
recipefile.writelines(appendfile.readlines())
|
|
recipefile.close()
|
|
appendfile.close()
|
|
|
|
def do_show_appends(self, args):
|
|
"""list bbappend files and recipe files they apply to
|
|
|
|
usage: show-appends
|
|
|
|
Recipes are listed with the bbappends that apply to them as subitems.
|
|
"""
|
|
self.check_prepare_cooker()
|
|
if not self.cooker_data.appends:
|
|
logger.plain('No append files found')
|
|
return
|
|
|
|
logger.plain('State of append files:')
|
|
|
|
pnlist = list(self.cooker_data.pkg_pn.keys())
|
|
pnlist.sort()
|
|
for pn in pnlist:
|
|
self.show_appends_for_pn(pn)
|
|
|
|
self.show_appends_for_skipped()
|
|
|
|
def show_appends_for_pn(self, pn):
|
|
filenames = self.cooker_data.pkg_pn[pn]
|
|
|
|
best = bb.providers.findBestProvider(pn,
|
|
self.cooker.configuration.data,
|
|
self.cooker_data,
|
|
self.cooker_data.pkg_pn)
|
|
best_filename = os.path.basename(best[3])
|
|
|
|
self.show_appends_output(filenames, best_filename)
|
|
|
|
def show_appends_for_skipped(self):
|
|
filenames = [os.path.basename(f)
|
|
for f in self.cooker.skiplist.iterkeys()]
|
|
self.show_appends_output(filenames, None, " (skipped)")
|
|
|
|
def show_appends_output(self, filenames, best_filename, name_suffix = ''):
|
|
appended, missing = self.get_appends_for_files(filenames)
|
|
if appended:
|
|
for basename, appends in appended:
|
|
logger.plain('%s%s:', basename, name_suffix)
|
|
for append in appends:
|
|
logger.plain(' %s', append)
|
|
|
|
if best_filename:
|
|
if best_filename in missing:
|
|
logger.warn('%s: missing append for preferred version',
|
|
best_filename)
|
|
self.returncode |= 1
|
|
|
|
|
|
def get_appends_for_files(self, filenames):
|
|
appended, notappended = [], []
|
|
for filename in filenames:
|
|
_, cls = bb.cache.Cache.virtualfn2realfn(filename)
|
|
if cls:
|
|
continue
|
|
|
|
basename = os.path.basename(filename)
|
|
appends = self.cooker_data.appends.get(basename)
|
|
if appends:
|
|
appended.append((basename, list(appends)))
|
|
else:
|
|
notappended.append(basename)
|
|
return appended, notappended
|
|
|
|
|
|
class Config(object):
|
|
def __init__(self, **options):
|
|
self.pkgs_to_build = []
|
|
self.debug_domains = []
|
|
self.extra_assume_provided = []
|
|
self.prefile = []
|
|
self.postfile = []
|
|
self.debug = 0
|
|
self.__dict__.update(options)
|
|
|
|
def __getattr__(self, attribute):
|
|
try:
|
|
return super(Config, self).__getattribute__(attribute)
|
|
except AttributeError:
|
|
return None
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main(sys.argv[1:]) or 0)
|