bitbake/codeparser: Correctly handle a missing/empty cache file
[bitbake.git] / lib / bb / codeparser.py
1 import ast
2 import codegen
3 import logging
4 import os.path
5 import bb.utils, bb.data
6 from itertools import chain
7 from pysh import pyshyacc, pyshlex, sherrors
8
9
10 logger = logging.getLogger('BitBake.CodeParser')
11 PARSERCACHE_VERSION = 2
12
13 try:
14     import cPickle as pickle
15 except ImportError:
16     import pickle
17     logger.info('Importing cPickle failed.  Falling back to a very slow implementation.')
18
19
20 def check_indent(codestr):
21     """If the code is indented, add a top level piece of code to 'remove' the indentation"""
22
23     i = 0
24     while codestr[i] in ["\n", "\t", " "]:
25         i = i + 1
26
27     if i == 0:
28         return codestr
29
30     if codestr[i-1] == "\t" or codestr[i-1] == " ":
31         return "if 1:\n" + codestr
32
33     return codestr
34
35 pythonparsecache = {}
36 shellparsecache = {}
37
38 def parser_cachefile(d):
39     cachedir = (bb.data.getVar("PERSISTENT_DIR", d, True) or
40                 bb.data.getVar("CACHE", d, True))
41     if cachedir in [None, '']:
42         return None
43     bb.utils.mkdirhier(cachedir)
44     cachefile = os.path.join(cachedir, "bb_codeparser.dat")
45     logger.debug(1, "Using cache in '%s' for codeparser cache", cachefile)
46     return cachefile
47
48 def parser_cache_init(d):
49     global pythonparsecache
50     global shellparsecache
51
52     cachefile = parser_cachefile(d)
53     if not cachefile:
54         return
55
56     try:
57         p = pickle.Unpickler(file(cachefile, "rb"))
58         data, version = p.load()
59     except:
60         return
61
62     if version != PARSERCACHE_VERSION:
63         return
64
65     pythonparsecache = data[0]
66     shellparsecache = data[1]
67
68 def parser_cache_save(d):
69     cachefile = parser_cachefile(d)
70     if not cachefile:
71         return
72
73     lf = bb.utils.lockfile(cachefile + ".lock")
74
75     try:
76         p = pickle.Unpickler(file(cachefile, "rb"))
77         data, version = p.load()
78     except IOError:
79         data, version = None, None
80
81     if version == PARSERCACHE_VERSION:
82         for h in data[0]:
83             if h not in pythonparsecache:
84                 pythonparsecache[h] = data[0][h]
85         for h in data[1]:
86             if h not in pythonparsecache:
87                 shellparsecache[h] = data[1][h]
88
89     p = pickle.Pickler(file(cachefile, "wb"), -1)
90     p.dump([[pythonparsecache, shellparsecache], PARSERCACHE_VERSION])
91     bb.utils.unlockfile(lf)
92
93 class PythonParser():
94     class ValueVisitor():
95         """Visitor to traverse a python abstract syntax tree and obtain
96         the variables referenced via bitbake metadata APIs, and the external
97         functions called.
98         """
99
100         getvars = ("d.getVar", "bb.data.getVar", "data.getVar")
101         expands = ("d.expand", "bb.data.expand", "data.expand")
102         execs = ("bb.build.exec_func", "bb.build.exec_task")
103
104         @classmethod
105         def _compare_name(cls, strparts, node):
106             """Given a sequence of strings representing a python name,
107             where the last component is the actual Name and the prior
108             elements are Attribute nodes, determine if the supplied node
109             matches.
110             """
111
112             if not strparts:
113                 return True
114
115             current, rest = strparts[0], strparts[1:]
116             if isinstance(node, ast.Attribute):
117                 if current == node.attr:
118                     return cls._compare_name(rest, node.value)
119             elif isinstance(node, ast.Name):
120                 if current == node.id:
121                     return True
122             return False
123
124         @classmethod
125         def compare_name(cls, value, node):
126             """Convenience function for the _compare_node method, which
127             can accept a string (which is split by '.' for you), or an
128             iterable of strings, in which case it checks to see if any of
129             them match, similar to isinstance.
130             """
131
132             if isinstance(value, basestring):
133                 return cls._compare_name(tuple(reversed(value.split("."))),
134                                          node)
135             else:
136                 return any(cls.compare_name(item, node) for item in value)
137
138         def __init__(self, value):
139             self.var_references = set()
140             self.var_execs = set()
141             self.direct_func_calls = set()
142             self.var_expands = set()
143             self.value = value
144
145         @classmethod
146         def warn(cls, func, arg):
147             """Warn about calls of bitbake APIs which pass a non-literal
148             argument for the variable name, as we're not able to track such
149             a reference.
150             """
151
152             try:
153                 funcstr = codegen.to_source(func)
154                 argstr = codegen.to_source(arg)
155             except TypeError:
156                 logger.debug(2, 'Failed to convert function and argument to source form')
157             else:
158                 logger.debug(1, "Warning: in call to '%s', argument '%s' is "
159                                 "not a literal", funcstr, argstr)
160
161         def visit_Call(self, node):
162             if self.compare_name(self.getvars, node.func):
163                 if isinstance(node.args[0], ast.Str):
164                     self.var_references.add(node.args[0].s)
165                 else:
166                     self.warn(node.func, node.args[0])
167             elif self.compare_name(self.expands, node.func):
168                 if isinstance(node.args[0], ast.Str):
169                     self.warn(node.func, node.args[0])
170                     self.var_expands.update(node.args[0].s)
171                 elif isinstance(node.args[0], ast.Call) and \
172                      self.compare_name(self.getvars, node.args[0].func):
173                     pass
174                 else:
175                     self.warn(node.func, node.args[0])
176             elif self.compare_name(self.execs, node.func):
177                 if isinstance(node.args[0], ast.Str):
178                     self.var_execs.add(node.args[0].s)
179                 else:
180                     self.warn(node.func, node.args[0])
181             elif isinstance(node.func, ast.Name):
182                 self.direct_func_calls.add(node.func.id)
183             elif isinstance(node.func, ast.Attribute):
184                 # We must have a qualified name.  Therefore we need
185                 # to walk the chain of 'Attribute' nodes to determine
186                 # the qualification.
187                 attr_node = node.func.value
188                 identifier = node.func.attr
189                 while isinstance(attr_node, ast.Attribute):
190                     identifier = attr_node.attr + "." + identifier
191                     attr_node = attr_node.value
192                 if isinstance(attr_node, ast.Name):
193                     identifier = attr_node.id + "." + identifier
194                 self.direct_func_calls.add(identifier)
195
196     def __init__(self):
197         #self.funcdefs = set()
198         self.execs = set()
199         #self.external_cmds = set()
200         self.references = set()
201
202     def parse_python(self, node):
203
204         h = hash(str(node))
205
206         if h in pythonparsecache:
207             self.references = pythonparsecache[h]["refs"]
208             self.execs = pythonparsecache[h]["execs"]
209             return
210
211         code = compile(check_indent(str(node)), "<string>", "exec",
212                        ast.PyCF_ONLY_AST)
213
214         visitor = self.ValueVisitor(code)
215         for n in ast.walk(code):
216             if n.__class__.__name__ == "Call":
217                 visitor.visit_Call(n)
218
219         self.references.update(visitor.var_references)
220         self.references.update(visitor.var_execs)
221         self.execs = visitor.direct_func_calls
222
223         pythonparsecache[h] = {}
224         pythonparsecache[h]["refs"] = self.references
225         pythonparsecache[h]["execs"] = self.execs
226
227 class ShellParser():
228     def __init__(self):
229         self.funcdefs = set()
230         self.allexecs = set()
231         self.execs = set()
232
233     def parse_shell(self, value):
234         """Parse the supplied shell code in a string, returning the external
235         commands it executes.
236         """
237
238         h = hash(str(value))
239
240         if h in shellparsecache:
241             self.execs = shellparsecache[h]["execs"]
242             return self.execs
243
244         try:
245             tokens, _ = pyshyacc.parse(value, eof=True, debug=False)
246         except pyshlex.NeedMore:
247             raise sherrors.ShellSyntaxError("Unexpected EOF")
248
249         for token in tokens:
250             self.process_tokens(token)
251         self.execs = set(cmd for cmd in self.allexecs if cmd not in self.funcdefs)
252
253         shellparsecache[h] = {}
254         shellparsecache[h]["execs"] = self.execs
255
256         return self.execs
257
258     def process_tokens(self, tokens):
259         """Process a supplied portion of the syntax tree as returned by
260         pyshyacc.parse.
261         """
262
263         def function_definition(value):
264             self.funcdefs.add(value.name)
265             return [value.body], None
266
267         def case_clause(value):
268             # Element 0 of each item in the case is the list of patterns, and
269             # Element 1 of each item in the case is the list of commands to be
270             # executed when that pattern matches.
271             words = chain(*[item[0] for item in value.items])
272             cmds  = chain(*[item[1] for item in value.items])
273             return cmds, words
274
275         def if_clause(value):
276             main = chain(value.cond, value.if_cmds)
277             rest = value.else_cmds
278             if isinstance(rest, tuple) and rest[0] == "elif":
279                 return chain(main, if_clause(rest[1]))
280             else:
281                 return chain(main, rest)
282
283         def simple_command(value):
284             return None, chain(value.words, (assign[1] for assign in value.assigns))
285
286         token_handlers = {
287             "and_or": lambda x: ((x.left, x.right), None),
288             "async": lambda x: ([x], None),
289             "brace_group": lambda x: (x.cmds, None),
290             "for_clause": lambda x: (x.cmds, x.items),
291             "function_definition": function_definition,
292             "if_clause": lambda x: (if_clause(x), None),
293             "pipeline": lambda x: (x.commands, None),
294             "redirect_list": lambda x: ([x.cmd], None),
295             "subshell": lambda x: (x.cmds, None),
296             "while_clause": lambda x: (chain(x.condition, x.cmds), None),
297             "until_clause": lambda x: (chain(x.condition, x.cmds), None),
298             "simple_command": simple_command,
299             "case_clause": case_clause,
300         }
301
302         for token in tokens:
303             name, value = token
304             try:
305                 more_tokens, words = token_handlers[name](value)
306             except KeyError:
307                 raise NotImplementedError("Unsupported token type " + name)
308
309             if more_tokens:
310                 self.process_tokens(more_tokens)
311
312             if words:
313                 self.process_words(words)
314
315     def process_words(self, words):
316         """Process a set of 'words' in pyshyacc parlance, which includes
317         extraction of executed commands from $() blocks, as well as grabbing
318         the command name argument.
319         """
320
321         words = list(words)
322         for word in list(words):
323             wtree = pyshlex.make_wordtree(word[1])
324             for part in wtree:
325                 if not isinstance(part, list):
326                     continue
327
328                 if part[0] in ('`', '$('):
329                     command = pyshlex.wordtree_as_string(part[1:-1])
330                     self.parse_shell(command)
331
332                     if word[0] in ("cmd_name", "cmd_word"):
333                         if word in words:
334                             words.remove(word)
335
336         usetoken = False
337         for word in words:
338             if word[0] in ("cmd_name", "cmd_word") or \
339                (usetoken and word[0] == "TOKEN"):
340                 if "=" in word[1]:
341                     usetoken = True
342                     continue
343
344                 cmd = word[1]
345                 if cmd.startswith("$"):
346                     logger.debug(1, "Warning: execution of non-literal "
347                                     "command '%s'", cmd)
348                 elif cmd == "eval":
349                     command = " ".join(word for _, word in words[1:])
350                     self.parse_shell(command)
351                 else:
352                     self.allexecs.add(cmd)
353                 break