bitbake: ast/data/codeparser: Add dependencies from python module functions

Moving code into python modules is a very effective way to reduce parsing
time and overhead in recipes. The downside has always been that any
dependency information on which variables those functions access is lost
and the hashes can therefore become less reliable.

This patch adds parsing of the imported module functions and that dependency
information is them injected back into the hash dependency information.

Intermodule function references are resolved to the full function
call names in our module namespace to ensure interfunction dependencies
are correctly handled too.

(Bitbake rev: 605c478ce14cdc3c02d6ef6d57146a76d436a83c)

(Bitbake rev: 91441e157e495b02db44e19e836afad366ee8924)

Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
This commit is contained in:
Richard Purdie
2022-11-27 21:16:16 +00:00
parent a225aa3ec4
commit f3bcd3c9a9
4 changed files with 68 additions and 16 deletions

View File

@@ -27,6 +27,7 @@ import ast
import sys import sys
import codegen import codegen
import logging import logging
import inspect
import bb.pysh as pysh import bb.pysh as pysh
import bb.utils, bb.data import bb.utils, bb.data
import hashlib import hashlib
@@ -58,10 +59,33 @@ def check_indent(codestr):
return codestr return codestr
modulecode_deps = {}
def add_module_functions(fn, functions, namespace):
fstat = os.stat(fn)
fixedhash = fn + ":" + str(fstat.st_size) + ":" + str(fstat.st_mtime)
for f in functions:
name = "%s.%s" % (namespace, f)
parser = PythonParser(name, logger)
try:
parser.parse_python(None, filename=fn, lineno=1, fixedhash=fixedhash+f)
#bb.warn("Cached %s" % f)
except KeyError:
lines, lineno = inspect.getsourcelines(functions[f])
src = "".join(lines)
parser.parse_python(src, filename=fn, lineno=lineno, fixedhash=fixedhash+f)
#bb.warn("Not cached %s" % f)
execs = parser.execs.copy()
# Expand internal module exec references
for e in parser.execs:
if e in functions:
execs.remove(e)
execs.add(namespace + "." + e)
modulecode_deps[name] = [parser.references.copy(), execs, parser.var_execs.copy(), parser.contains.copy()]
#bb.warn("%s: %s\nRefs:%s Execs: %s %s %s" % (name, src, parser.references, parser.execs, parser.var_execs, parser.contains))
# A custom getstate/setstate using tuples is actually worth 15% cachesize by # A custom getstate/setstate using tuples is actually worth 15% cachesize by
# avoiding duplication of the attribute names! # avoiding duplication of the attribute names!
class SetCache(object): class SetCache(object):
def __init__(self): def __init__(self):
self.setcache = {} self.setcache = {}
@@ -289,11 +313,17 @@ class PythonParser():
self.unhandled_message = "in call of %s, argument '%s' is not a string literal" self.unhandled_message = "in call of %s, argument '%s' is not a string literal"
self.unhandled_message = "while parsing %s, %s" % (name, self.unhandled_message) self.unhandled_message = "while parsing %s, %s" % (name, self.unhandled_message)
def parse_python(self, node, lineno=0, filename="<string>"): # For the python module code it is expensive to have the function text so it is
if not node or not node.strip(): # uses a different fixedhash to cache against. We can take the hit on obtaining the
# text if it isn't in the cache.
def parse_python(self, node, lineno=0, filename="<string>", fixedhash=None):
if not fixedhash and (not node or not node.strip()):
return return
h = bbhash(str(node)) if fixedhash:
h = fixedhash
else:
h = bbhash(str(node))
if h in codeparsercache.pythoncache: if h in codeparsercache.pythoncache:
self.references = set(codeparsercache.pythoncache[h].refs) self.references = set(codeparsercache.pythoncache[h].refs)
@@ -311,6 +341,9 @@ class PythonParser():
self.contains[i] = set(codeparsercache.pythoncacheextras[h].contains[i]) self.contains[i] = set(codeparsercache.pythoncacheextras[h].contains[i])
return return
if fixedhash and not node:
raise KeyError
# Need to parse so take the hit on the real log buffer # Need to parse so take the hit on the real log buffer
self.log = BufferedLogger('BitBake.Data.PythonParser', logging.DEBUG, self._log) self.log = BufferedLogger('BitBake.Data.PythonParser', logging.DEBUG, self._log)

View File

@@ -261,7 +261,7 @@ def emit_func_python(func, o=sys.__stdout__, d = init()):
newdeps |= set((d.getVarFlag(dep, "vardeps") or "").split()) newdeps |= set((d.getVarFlag(dep, "vardeps") or "").split())
newdeps -= seen newdeps -= seen
def build_dependencies(key, keys, shelldeps, varflagsexcl, ignored_vars, d): def build_dependencies(key, keys, mod_funcs, shelldeps, varflagsexcl, ignored_vars, d):
def handle_contains(value, contains, exclusions, d): def handle_contains(value, contains, exclusions, d):
newvalue = [] newvalue = []
if value: if value:
@@ -289,6 +289,12 @@ def build_dependencies(key, keys, shelldeps, varflagsexcl, ignored_vars, d):
deps = set() deps = set()
try: try:
if key in mod_funcs:
exclusions = set()
moddep = bb.codeparser.modulecode_deps[key]
value = handle_contains("", moddep[3], exclusions, d)
return frozenset((moddep[0] | keys & moddep[1]) - ignored_vars), value
if key[-1] == ']': if key[-1] == ']':
vf = key[:-1].split('[') vf = key[:-1].split('[')
if vf[1] == "vardepvalueexclude": if vf[1] == "vardepvalueexclude":
@@ -367,7 +373,8 @@ def build_dependencies(key, keys, shelldeps, varflagsexcl, ignored_vars, d):
def generate_dependencies(d, ignored_vars): def generate_dependencies(d, ignored_vars):
keys = set(key for key in d if not key.startswith("__")) mod_funcs = set(bb.codeparser.modulecode_deps.keys())
keys = set(key for key in d if not key.startswith("__")) | mod_funcs
shelldeps = set(key for key in d.getVar("__exportlist", False) if d.getVarFlag(key, "export", False) and not d.getVarFlag(key, "unexport", False)) shelldeps = set(key for key in d.getVar("__exportlist", False) if d.getVarFlag(key, "export", False) and not d.getVarFlag(key, "unexport", False))
varflagsexcl = d.getVar('BB_SIGNATURE_EXCLUDE_FLAGS') varflagsexcl = d.getVar('BB_SIGNATURE_EXCLUDE_FLAGS')
@@ -376,7 +383,7 @@ def generate_dependencies(d, ignored_vars):
tasklist = d.getVar('__BBTASKS', False) or [] tasklist = d.getVar('__BBTASKS', False) or []
for task in tasklist: for task in tasklist:
deps[task], values[task] = build_dependencies(task, keys, shelldeps, varflagsexcl, ignored_vars, d) deps[task], values[task] = build_dependencies(task, keys, mod_funcs, shelldeps, varflagsexcl, ignored_vars, d)
newdeps = deps[task] newdeps = deps[task]
seen = set() seen = set()
while newdeps: while newdeps:
@@ -385,7 +392,7 @@ def generate_dependencies(d, ignored_vars):
newdeps = set() newdeps = set()
for dep in nextdeps: for dep in nextdeps:
if dep not in deps: if dep not in deps:
deps[dep], values[dep] = build_dependencies(dep, keys, shelldeps, varflagsexcl, ignored_vars, d) deps[dep], values[dep] = build_dependencies(dep, keys, mod_funcs, shelldeps, varflagsexcl, ignored_vars, d)
newdeps |= deps[dep] newdeps |= deps[dep]
newdeps -= seen newdeps -= seen
#print "For %s: %s" % (task, str(deps[task])) #print "For %s: %s" % (task, str(deps[task]))

View File

@@ -290,6 +290,18 @@ class PyLibNode(AstNode):
toimport = getattr(bb.utils._context[self.namespace], "BBIMPORTS", []) toimport = getattr(bb.utils._context[self.namespace], "BBIMPORTS", [])
for i in toimport: for i in toimport:
bb.utils._context[self.namespace] = __import__(self.namespace + "." + i) bb.utils._context[self.namespace] = __import__(self.namespace + "." + i)
mod = getattr(bb.utils._context[self.namespace], i)
fn = getattr(mod, "__file__")
funcs = {}
for f in dir(mod):
if f.startswith("_"):
continue
fcall = getattr(mod, f)
if not callable(fcall):
continue
funcs[f] = fcall
bb.codeparser.add_module_functions(fn, funcs, "%s.%s" % (self.namespace, i))
except AttributeError as e: except AttributeError as e:
bb.error("Error importing OE modules: %s" % str(e)) bb.error("Error importing OE modules: %s" % str(e))

View File

@@ -318,7 +318,7 @@ d.getVar(a(), False)
"filename": "example.bb", "filename": "example.bb",
}) })
deps, values = bb.data.build_dependencies("FOO", set(self.d.keys()), set(), set(), set(), self.d) deps, values = bb.data.build_dependencies("FOO", set(self.d.keys()), set(), set(), set(), set(), self.d)
self.assertEqual(deps, set(["somevar", "bar", "something", "inexpand", "test", "test2", "a"])) self.assertEqual(deps, set(["somevar", "bar", "something", "inexpand", "test", "test2", "a"]))
@@ -365,7 +365,7 @@ esac
self.d.setVarFlags("FOO", {"func": True}) self.d.setVarFlags("FOO", {"func": True})
self.setEmptyVars(execs) self.setEmptyVars(execs)
deps, values = bb.data.build_dependencies("FOO", set(self.d.keys()), set(), set(), set(), self.d) deps, values = bb.data.build_dependencies("FOO", set(self.d.keys()), set(), set(), set(), set(), self.d)
self.assertEqual(deps, set(["somevar", "inverted"] + execs)) self.assertEqual(deps, set(["somevar", "inverted"] + execs))
@@ -375,7 +375,7 @@ esac
self.d.setVar("FOO", "foo=oe_libinstall; eval $foo") self.d.setVar("FOO", "foo=oe_libinstall; eval $foo")
self.d.setVarFlag("FOO", "vardeps", "oe_libinstall") self.d.setVarFlag("FOO", "vardeps", "oe_libinstall")
deps, values = bb.data.build_dependencies("FOO", set(self.d.keys()), set(), set(), set(), self.d) deps, values = bb.data.build_dependencies("FOO", set(self.d.keys()), set(), set(), set(), set(), self.d)
self.assertEqual(deps, set(["oe_libinstall"])) self.assertEqual(deps, set(["oe_libinstall"]))
@@ -384,7 +384,7 @@ esac
self.d.setVar("FOO", "foo=oe_libinstall; eval $foo") self.d.setVar("FOO", "foo=oe_libinstall; eval $foo")
self.d.setVarFlag("FOO", "vardeps", "${@'oe_libinstall'}") self.d.setVarFlag("FOO", "vardeps", "${@'oe_libinstall'}")
deps, values = bb.data.build_dependencies("FOO", set(self.d.keys()), set(), set(), set(), self.d) deps, values = bb.data.build_dependencies("FOO", set(self.d.keys()), set(), set(), set(), set(), self.d)
self.assertEqual(deps, set(["oe_libinstall"])) self.assertEqual(deps, set(["oe_libinstall"]))
@@ -399,7 +399,7 @@ esac
# Check dependencies # Check dependencies
self.d.setVar('ANOTHERVAR', expr) self.d.setVar('ANOTHERVAR', expr)
self.d.setVar('TESTVAR', 'anothervalue testval testval2') self.d.setVar('TESTVAR', 'anothervalue testval testval2')
deps, values = bb.data.build_dependencies("ANOTHERVAR", set(self.d.keys()), set(), set(), set(), self.d) deps, values = bb.data.build_dependencies("ANOTHERVAR", set(self.d.keys()), set(), set(), set(), set(), self.d)
self.assertEqual(sorted(values.splitlines()), self.assertEqual(sorted(values.splitlines()),
sorted([expr, sorted([expr,
'TESTVAR{anothervalue} = Set', 'TESTVAR{anothervalue} = Set',
@@ -418,14 +418,14 @@ esac
self.d.setVar('ANOTHERVAR', varval) self.d.setVar('ANOTHERVAR', varval)
self.d.setVar('TESTVAR', 'anothervalue testval testval2') self.d.setVar('TESTVAR', 'anothervalue testval testval2')
self.d.setVar('TESTVAR2', 'testval3') self.d.setVar('TESTVAR2', 'testval3')
deps, values = bb.data.build_dependencies("ANOTHERVAR", set(self.d.keys()), set(), set(), set(["TESTVAR"]), self.d) deps, values = bb.data.build_dependencies("ANOTHERVAR", set(self.d.keys()), set(), set(), set(), set(["TESTVAR"]), self.d)
self.assertEqual(sorted(values.splitlines()), sorted([varval])) self.assertEqual(sorted(values.splitlines()), sorted([varval]))
self.assertEqual(deps, set(["TESTVAR2"])) self.assertEqual(deps, set(["TESTVAR2"]))
self.assertEqual(self.d.getVar('ANOTHERVAR').split(), ['testval3', 'anothervalue']) self.assertEqual(self.d.getVar('ANOTHERVAR').split(), ['testval3', 'anothervalue'])
# Check the vardepsexclude flag is handled by contains functionality # Check the vardepsexclude flag is handled by contains functionality
self.d.setVarFlag('ANOTHERVAR', 'vardepsexclude', 'TESTVAR') self.d.setVarFlag('ANOTHERVAR', 'vardepsexclude', 'TESTVAR')
deps, values = bb.data.build_dependencies("ANOTHERVAR", set(self.d.keys()), set(), set(), set(), self.d) deps, values = bb.data.build_dependencies("ANOTHERVAR", set(self.d.keys()), set(), set(), set(), set(), self.d)
self.assertEqual(sorted(values.splitlines()), sorted([varval])) self.assertEqual(sorted(values.splitlines()), sorted([varval]))
self.assertEqual(deps, set(["TESTVAR2"])) self.assertEqual(deps, set(["TESTVAR2"]))
self.assertEqual(self.d.getVar('ANOTHERVAR').split(), ['testval3', 'anothervalue']) self.assertEqual(self.d.getVar('ANOTHERVAR').split(), ['testval3', 'anothervalue'])