codeparser.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. import ast
  2. import sys
  3. import codegen
  4. import logging
  5. import pickle
  6. import bb.pysh as pysh
  7. import os.path
  8. import bb.utils, bb.data
  9. from itertools import chain
  10. from bb.pysh import pyshyacc, pyshlex, sherrors
  11. from bb.cache import MultiProcessCache
  12. logger = logging.getLogger('BitBake.CodeParser')
  13. def check_indent(codestr):
  14. """If the code is indented, add a top level piece of code to 'remove' the indentation"""
  15. i = 0
  16. while codestr[i] in ["\n", "\t", " "]:
  17. i = i + 1
  18. if i == 0:
  19. return codestr
  20. if codestr[i-1] == "\t" or codestr[i-1] == " ":
  21. if codestr[0] == "\n":
  22. # Since we're adding a line, we need to remove one line of any empty padding
  23. # to ensure line numbers are correct
  24. codestr = codestr[1:]
  25. return "if 1:\n" + codestr
  26. return codestr
  27. # Basically pickle, in python 2.7.3 at least, does badly with data duplication
  28. # upon pickling and unpickling. Combine this with duplicate objects and things
  29. # are a mess.
  30. #
  31. # When the sets are originally created, python calls intern() on the set keys
  32. # which significantly improves memory usage. Sadly the pickle/unpickle process
  33. # doesn't call intern() on the keys and results in the same strings being duplicated
  34. # in memory. This also means pickle will save the same string multiple times in
  35. # the cache file.
  36. #
  37. # By having shell and python cacheline objects with setstate/getstate, we force
  38. # the object creation through our own routine where we can call intern (via internSet).
  39. #
  40. # We also use hashable frozensets and ensure we use references to these so that
  41. # duplicates can be removed, both in memory and in the resulting pickled data.
  42. #
  43. # By playing these games, the size of the cache file shrinks dramatically
  44. # meaning faster load times and the reloaded cache files also consume much less
  45. # memory. Smaller cache files, faster load times and lower memory usage is good.
  46. #
  47. # A custom getstate/setstate using tuples is actually worth 15% cachesize by
  48. # avoiding duplication of the attribute names!
  49. class SetCache(object):
  50. def __init__(self):
  51. self.setcache = {}
  52. def internSet(self, items):
  53. new = []
  54. for i in items:
  55. new.append(sys.intern(i))
  56. s = frozenset(new)
  57. if hash(s) in self.setcache:
  58. return self.setcache[hash(s)]
  59. self.setcache[hash(s)] = s
  60. return s
  61. codecache = SetCache()
  62. class pythonCacheLine(object):
  63. def __init__(self, refs, execs, contains):
  64. self.refs = codecache.internSet(refs)
  65. self.execs = codecache.internSet(execs)
  66. self.contains = {}
  67. for c in contains:
  68. self.contains[c] = codecache.internSet(contains[c])
  69. def __getstate__(self):
  70. return (self.refs, self.execs, self.contains)
  71. def __setstate__(self, state):
  72. (refs, execs, contains) = state
  73. self.__init__(refs, execs, contains)
  74. def __hash__(self):
  75. l = (hash(self.refs), hash(self.execs))
  76. for c in sorted(self.contains.keys()):
  77. l = l + (c, hash(self.contains[c]))
  78. return hash(l)
  79. def __repr__(self):
  80. return " ".join([str(self.refs), str(self.execs), str(self.contains)])
  81. class shellCacheLine(object):
  82. def __init__(self, execs):
  83. self.execs = codecache.internSet(execs)
  84. def __getstate__(self):
  85. return (self.execs)
  86. def __setstate__(self, state):
  87. (execs) = state
  88. self.__init__(execs)
  89. def __hash__(self):
  90. return hash(self.execs)
  91. def __repr__(self):
  92. return str(self.execs)
  93. class CodeParserCache(MultiProcessCache):
  94. cache_file_name = "bb_codeparser.dat"
  95. CACHE_VERSION = 8
  96. def __init__(self):
  97. MultiProcessCache.__init__(self)
  98. self.pythoncache = self.cachedata[0]
  99. self.shellcache = self.cachedata[1]
  100. self.pythoncacheextras = self.cachedata_extras[0]
  101. self.shellcacheextras = self.cachedata_extras[1]
  102. # To avoid duplication in the codeparser cache, keep
  103. # a lookup of hashes of objects we already have
  104. self.pythoncachelines = {}
  105. self.shellcachelines = {}
  106. def newPythonCacheLine(self, refs, execs, contains):
  107. cacheline = pythonCacheLine(refs, execs, contains)
  108. h = hash(cacheline)
  109. if h in self.pythoncachelines:
  110. return self.pythoncachelines[h]
  111. self.pythoncachelines[h] = cacheline
  112. return cacheline
  113. def newShellCacheLine(self, execs):
  114. cacheline = shellCacheLine(execs)
  115. h = hash(cacheline)
  116. if h in self.shellcachelines:
  117. return self.shellcachelines[h]
  118. self.shellcachelines[h] = cacheline
  119. return cacheline
  120. def init_cache(self, d):
  121. # Check if we already have the caches
  122. if self.pythoncache:
  123. return
  124. MultiProcessCache.init_cache(self, d)
  125. # cachedata gets re-assigned in the parent
  126. self.pythoncache = self.cachedata[0]
  127. self.shellcache = self.cachedata[1]
  128. def create_cachedata(self):
  129. data = [{}, {}]
  130. return data
  131. codeparsercache = CodeParserCache()
  132. def parser_cache_init(d):
  133. codeparsercache.init_cache(d)
  134. def parser_cache_save():
  135. codeparsercache.save_extras()
  136. def parser_cache_savemerge():
  137. codeparsercache.save_merge()
  138. Logger = logging.getLoggerClass()
  139. class BufferedLogger(Logger):
  140. def __init__(self, name, level=0, target=None):
  141. Logger.__init__(self, name)
  142. self.setLevel(level)
  143. self.buffer = []
  144. self.target = target
  145. def handle(self, record):
  146. self.buffer.append(record)
  147. def flush(self):
  148. for record in self.buffer:
  149. self.target.handle(record)
  150. self.buffer = []
  151. class PythonParser():
  152. getvars = (".getVar", ".appendVar", ".prependVar")
  153. getvarflags = (".getVarFlag", ".appendVarFlag", ".prependVarFlag")
  154. containsfuncs = ("bb.utils.contains", "base_contains", "bb.utils.contains_any")
  155. execfuncs = ("bb.build.exec_func", "bb.build.exec_task")
  156. def warn(self, func, arg):
  157. """Warn about calls of bitbake APIs which pass a non-literal
  158. argument for the variable name, as we're not able to track such
  159. a reference.
  160. """
  161. try:
  162. funcstr = codegen.to_source(func)
  163. argstr = codegen.to_source(arg)
  164. except TypeError:
  165. self.log.debug(2, 'Failed to convert function and argument to source form')
  166. else:
  167. self.log.debug(1, self.unhandled_message % (funcstr, argstr))
  168. def visit_Call(self, node):
  169. name = self.called_node_name(node.func)
  170. if name and (name.endswith(self.getvars) or name.endswith(self.getvarflags) or name in self.containsfuncs):
  171. if isinstance(node.args[0], ast.Str):
  172. varname = node.args[0].s
  173. if name in self.containsfuncs and isinstance(node.args[1], ast.Str):
  174. if varname not in self.contains:
  175. self.contains[varname] = set()
  176. self.contains[varname].add(node.args[1].s)
  177. elif name.endswith(self.getvarflags):
  178. if isinstance(node.args[1], ast.Str):
  179. self.references.add('%s[%s]' % (varname, node.args[1].s))
  180. else:
  181. self.warn(node.func, node.args[1])
  182. else:
  183. self.references.add(varname)
  184. else:
  185. self.warn(node.func, node.args[0])
  186. elif name and name.endswith(".expand"):
  187. if isinstance(node.args[0], ast.Str):
  188. value = node.args[0].s
  189. d = bb.data.init()
  190. parser = d.expandWithRefs(value, self.name)
  191. self.references |= parser.references
  192. self.execs |= parser.execs
  193. for varname in parser.contains:
  194. if varname not in self.contains:
  195. self.contains[varname] = set()
  196. self.contains[varname] |= parser.contains[varname]
  197. elif name in self.execfuncs:
  198. if isinstance(node.args[0], ast.Str):
  199. self.var_execs.add(node.args[0].s)
  200. else:
  201. self.warn(node.func, node.args[0])
  202. elif name and isinstance(node.func, (ast.Name, ast.Attribute)):
  203. self.execs.add(name)
  204. def called_node_name(self, node):
  205. """Given a called node, return its original string form"""
  206. components = []
  207. while node:
  208. if isinstance(node, ast.Attribute):
  209. components.append(node.attr)
  210. node = node.value
  211. elif isinstance(node, ast.Name):
  212. components.append(node.id)
  213. return '.'.join(reversed(components))
  214. else:
  215. break
  216. def __init__(self, name, log):
  217. self.name = name
  218. self.var_execs = set()
  219. self.contains = {}
  220. self.execs = set()
  221. self.references = set()
  222. self.log = BufferedLogger('BitBake.Data.PythonParser', logging.DEBUG, log)
  223. self.unhandled_message = "in call of %s, argument '%s' is not a string literal"
  224. self.unhandled_message = "while parsing %s, %s" % (name, self.unhandled_message)
  225. def parse_python(self, node, lineno=0, filename="<string>"):
  226. if not node or not node.strip():
  227. return
  228. h = hash(str(node))
  229. if h in codeparsercache.pythoncache:
  230. self.references = set(codeparsercache.pythoncache[h].refs)
  231. self.execs = set(codeparsercache.pythoncache[h].execs)
  232. self.contains = {}
  233. for i in codeparsercache.pythoncache[h].contains:
  234. self.contains[i] = set(codeparsercache.pythoncache[h].contains[i])
  235. return
  236. if h in codeparsercache.pythoncacheextras:
  237. self.references = set(codeparsercache.pythoncacheextras[h].refs)
  238. self.execs = set(codeparsercache.pythoncacheextras[h].execs)
  239. self.contains = {}
  240. for i in codeparsercache.pythoncacheextras[h].contains:
  241. self.contains[i] = set(codeparsercache.pythoncacheextras[h].contains[i])
  242. return
  243. # We can't add to the linenumbers for compile, we can pad to the correct number of blank lines though
  244. node = "\n" * int(lineno) + node
  245. code = compile(check_indent(str(node)), filename, "exec",
  246. ast.PyCF_ONLY_AST)
  247. for n in ast.walk(code):
  248. if n.__class__.__name__ == "Call":
  249. self.visit_Call(n)
  250. self.execs.update(self.var_execs)
  251. codeparsercache.pythoncacheextras[h] = codeparsercache.newPythonCacheLine(self.references, self.execs, self.contains)
  252. class ShellParser():
  253. def __init__(self, name, log):
  254. self.funcdefs = set()
  255. self.allexecs = set()
  256. self.execs = set()
  257. self.log = BufferedLogger('BitBake.Data.%s' % name, logging.DEBUG, log)
  258. self.unhandled_template = "unable to handle non-literal command '%s'"
  259. self.unhandled_template = "while parsing %s, %s" % (name, self.unhandled_template)
  260. def parse_shell(self, value):
  261. """Parse the supplied shell code in a string, returning the external
  262. commands it executes.
  263. """
  264. h = hash(str(value))
  265. if h in codeparsercache.shellcache:
  266. self.execs = set(codeparsercache.shellcache[h].execs)
  267. return self.execs
  268. if h in codeparsercache.shellcacheextras:
  269. self.execs = set(codeparsercache.shellcacheextras[h].execs)
  270. return self.execs
  271. self._parse_shell(value)
  272. self.execs = set(cmd for cmd in self.allexecs if cmd not in self.funcdefs)
  273. codeparsercache.shellcacheextras[h] = codeparsercache.newShellCacheLine(self.execs)
  274. return self.execs
  275. def _parse_shell(self, value):
  276. try:
  277. tokens, _ = pyshyacc.parse(value, eof=True, debug=False)
  278. except pyshlex.NeedMore:
  279. raise sherrors.ShellSyntaxError("Unexpected EOF")
  280. for token in tokens:
  281. self.process_tokens(token)
  282. def process_tokens(self, tokens):
  283. """Process a supplied portion of the syntax tree as returned by
  284. pyshyacc.parse.
  285. """
  286. def function_definition(value):
  287. self.funcdefs.add(value.name)
  288. return [value.body], None
  289. def case_clause(value):
  290. # Element 0 of each item in the case is the list of patterns, and
  291. # Element 1 of each item in the case is the list of commands to be
  292. # executed when that pattern matches.
  293. words = chain(*[item[0] for item in value.items])
  294. cmds = chain(*[item[1] for item in value.items])
  295. return cmds, words
  296. def if_clause(value):
  297. main = chain(value.cond, value.if_cmds)
  298. rest = value.else_cmds
  299. if isinstance(rest, tuple) and rest[0] == "elif":
  300. return chain(main, if_clause(rest[1]))
  301. else:
  302. return chain(main, rest)
  303. def simple_command(value):
  304. return None, chain(value.words, (assign[1] for assign in value.assigns))
  305. token_handlers = {
  306. "and_or": lambda x: ((x.left, x.right), None),
  307. "async": lambda x: ([x], None),
  308. "brace_group": lambda x: (x.cmds, None),
  309. "for_clause": lambda x: (x.cmds, x.items),
  310. "function_definition": function_definition,
  311. "if_clause": lambda x: (if_clause(x), None),
  312. "pipeline": lambda x: (x.commands, None),
  313. "redirect_list": lambda x: ([x.cmd], None),
  314. "subshell": lambda x: (x.cmds, None),
  315. "while_clause": lambda x: (chain(x.condition, x.cmds), None),
  316. "until_clause": lambda x: (chain(x.condition, x.cmds), None),
  317. "simple_command": simple_command,
  318. "case_clause": case_clause,
  319. }
  320. for token in tokens:
  321. name, value = token
  322. try:
  323. more_tokens, words = token_handlers[name](value)
  324. except KeyError:
  325. raise NotImplementedError("Unsupported token type " + name)
  326. if more_tokens:
  327. self.process_tokens(more_tokens)
  328. if words:
  329. self.process_words(words)
  330. def process_words(self, words):
  331. """Process a set of 'words' in pyshyacc parlance, which includes
  332. extraction of executed commands from $() blocks, as well as grabbing
  333. the command name argument.
  334. """
  335. words = list(words)
  336. for word in list(words):
  337. wtree = pyshlex.make_wordtree(word[1])
  338. for part in wtree:
  339. if not isinstance(part, list):
  340. continue
  341. if part[0] in ('`', '$('):
  342. command = pyshlex.wordtree_as_string(part[1:-1])
  343. self._parse_shell(command)
  344. if word[0] in ("cmd_name", "cmd_word"):
  345. if word in words:
  346. words.remove(word)
  347. usetoken = False
  348. for word in words:
  349. if word[0] in ("cmd_name", "cmd_word") or \
  350. (usetoken and word[0] == "TOKEN"):
  351. if "=" in word[1]:
  352. usetoken = True
  353. continue
  354. cmd = word[1]
  355. if cmd.startswith("$"):
  356. self.log.debug(1, self.unhandled_template % cmd)
  357. elif cmd == "eval":
  358. command = " ".join(word for _, word in words[1:])
  359. self._parse_shell(command)
  360. else:
  361. self.allexecs.add(cmd)
  362. break