1"""
2   class for handling .bb files
3
4   Reads a .bb file and obtains its metadata
5
6"""
7
8
9#  Copyright (C) 2003, 2004  Chris Larson
10#  Copyright (C) 2003, 2004  Phil Blundell
11#
12# SPDX-License-Identifier: GPL-2.0-only
13#
14
15import re, bb, os
16import bb.build, bb.utils, bb.data_smart
17
18from . import ConfHandler
19from .. import resolve_file, ast, logger, ParseError
20from .ConfHandler import include, init
21
22__func_start_regexp__    = re.compile(r"(((?P<py>python(?=(\s|\()))|(?P<fr>fakeroot(?=\s)))\s*)*(?P<func>[\w\.\-\+\{\}\$:]+)?\s*\(\s*\)\s*{$" )
23__inherit_regexp__       = re.compile(r"inherit\s+(.+)" )
24__export_func_regexp__   = re.compile(r"EXPORT_FUNCTIONS\s+(.+)" )
25__addtask_regexp__       = re.compile(r"addtask\s+(?P<func>\w+)\s*((before\s*(?P<before>((.*(?=after))|(.*))))|(after\s*(?P<after>((.*(?=before))|(.*)))))*")
26__deltask_regexp__       = re.compile(r"deltask\s+(.+)")
27__addhandler_regexp__    = re.compile(r"addhandler\s+(.+)" )
28__def_regexp__           = re.compile(r"def\s+(\w+).*:" )
29__python_func_regexp__   = re.compile(r"(\s+.*)|(^$)|(^#)" )
30__python_tab_regexp__    = re.compile(r" *\t")
31
32__infunc__ = []
33__inpython__ = False
34__body__   = []
35__classname__ = ""
36
37cached_statements = {}
38
39def supports(fn, d):
40    """Return True if fn has a supported extension"""
41    return os.path.splitext(fn)[-1] in [".bb", ".bbclass", ".inc"]
42
43def inherit(files, fn, lineno, d):
44    __inherit_cache = d.getVar('__inherit_cache', False) or []
45    files = d.expand(files).split()
46    for file in files:
47        if not os.path.isabs(file) and not file.endswith(".bbclass"):
48            file = os.path.join('classes', '%s.bbclass' % file)
49
50        if not os.path.isabs(file):
51            bbpath = d.getVar("BBPATH")
52            abs_fn, attempts = bb.utils.which(bbpath, file, history=True)
53            for af in attempts:
54                if af != abs_fn:
55                    bb.parse.mark_dependency(d, af)
56            if abs_fn:
57                file = abs_fn
58
59        if not file in __inherit_cache:
60            logger.debug("Inheriting %s (from %s:%d)" % (file, fn, lineno))
61            __inherit_cache.append( file )
62            d.setVar('__inherit_cache', __inherit_cache)
63            include(fn, file, lineno, d, "inherit")
64            __inherit_cache = d.getVar('__inherit_cache', False) or []
65
66def get_statements(filename, absolute_filename, base_name):
67    global cached_statements
68
69    try:
70        return cached_statements[absolute_filename]
71    except KeyError:
72        with open(absolute_filename, 'r') as f:
73            statements = ast.StatementGroup()
74
75            lineno = 0
76            while True:
77                lineno = lineno + 1
78                s = f.readline()
79                if not s: break
80                s = s.rstrip()
81                feeder(lineno, s, filename, base_name, statements)
82
83        if __inpython__:
84            # add a blank line to close out any python definition
85            feeder(lineno, "", filename, base_name, statements, eof=True)
86
87        if filename.endswith(".bbclass") or filename.endswith(".inc"):
88            cached_statements[absolute_filename] = statements
89        return statements
90
91def handle(fn, d, include):
92    global __func_start_regexp__, __inherit_regexp__, __export_func_regexp__, __addtask_regexp__, __addhandler_regexp__, __infunc__, __body__, __residue__, __classname__
93    __body__ = []
94    __infunc__ = []
95    __classname__ = ""
96    __residue__ = []
97
98    base_name = os.path.basename(fn)
99    (root, ext) = os.path.splitext(base_name)
100    init(d)
101
102    if ext == ".bbclass":
103        __classname__ = root
104        __inherit_cache = d.getVar('__inherit_cache', False) or []
105        if not fn in __inherit_cache:
106            __inherit_cache.append(fn)
107            d.setVar('__inherit_cache', __inherit_cache)
108
109    if include != 0:
110        oldfile = d.getVar('FILE', False)
111    else:
112        oldfile = None
113
114    abs_fn = resolve_file(fn, d)
115
116    # actual loading
117    statements = get_statements(fn, abs_fn, base_name)
118
119    # DONE WITH PARSING... time to evaluate
120    if ext != ".bbclass" and abs_fn != oldfile:
121        d.setVar('FILE', abs_fn)
122
123    try:
124        statements.eval(d)
125    except bb.parse.SkipRecipe:
126        d.setVar("__SKIPPED", True)
127        if include == 0:
128            return { "" : d }
129
130    if __infunc__:
131        raise ParseError("Shell function %s is never closed" % __infunc__[0], __infunc__[1], __infunc__[2])
132    if __residue__:
133        raise ParseError("Leftover unparsed (incomplete?) data %s from %s" % __residue__, fn)
134
135    if ext != ".bbclass" and include == 0:
136        return ast.multi_finalize(fn, d)
137
138    if ext != ".bbclass" and oldfile and abs_fn != oldfile:
139        d.setVar("FILE", oldfile)
140
141    return d
142
143def feeder(lineno, s, fn, root, statements, eof=False):
144    global __func_start_regexp__, __inherit_regexp__, __export_func_regexp__, __addtask_regexp__, __addhandler_regexp__, __def_regexp__, __python_func_regexp__, __inpython__, __infunc__, __body__, bb, __residue__, __classname__
145
146    # Check tabs in python functions:
147    # - def py_funcname(): covered by __inpython__
148    # - python(): covered by '__anonymous' == __infunc__[0]
149    # - python funcname(): covered by __infunc__[3]
150    if __inpython__ or (__infunc__ and ('__anonymous' == __infunc__[0] or __infunc__[3])):
151        tab = __python_tab_regexp__.match(s)
152        if tab:
153            bb.warn('python should use 4 spaces indentation, but found tabs in %s, line %s' % (root, lineno))
154
155    if __infunc__:
156        if s == '}':
157            __body__.append('')
158            ast.handleMethod(statements, fn, lineno, __infunc__[0], __body__, __infunc__[3], __infunc__[4])
159            __infunc__ = []
160            __body__ = []
161        else:
162            __body__.append(s)
163        return
164
165    if __inpython__:
166        m = __python_func_regexp__.match(s)
167        if m and not eof:
168            __body__.append(s)
169            return
170        else:
171            ast.handlePythonMethod(statements, fn, lineno, __inpython__,
172                                   root, __body__)
173            __body__ = []
174            __inpython__ = False
175
176            if eof:
177                return
178
179    if s and s[0] == '#':
180        if len(__residue__) != 0 and __residue__[0][0] != "#":
181            bb.fatal("There is a comment on line %s of file %s (%s) which is in the middle of a multiline expression.\nBitbake used to ignore these but no longer does so, please fix your metadata as errors are likely as a result of this change." % (lineno, fn, s))
182
183    if len(__residue__) != 0 and __residue__[0][0] == "#" and (not s or s[0] != "#"):
184        bb.fatal("There is a confusing multiline, partially commented expression on line %s of file %s (%s).\nPlease clarify whether this is all a comment or should be parsed." % (lineno, fn, s))
185
186    if s and s[-1] == '\\':
187        __residue__.append(s[:-1])
188        return
189
190    s = "".join(__residue__) + s
191    __residue__ = []
192
193    # Skip empty lines
194    if s == '':
195        return
196
197    # Skip comments
198    if s[0] == '#':
199        return
200
201    m = __func_start_regexp__.match(s)
202    if m:
203        __infunc__ = [m.group("func") or "__anonymous", fn, lineno, m.group("py") is not None, m.group("fr") is not None]
204        return
205
206    m = __def_regexp__.match(s)
207    if m:
208        __body__.append(s)
209        __inpython__ = m.group(1)
210
211        return
212
213    m = __export_func_regexp__.match(s)
214    if m:
215        ast.handleExportFuncs(statements, fn, lineno, m, __classname__)
216        return
217
218    m = __addtask_regexp__.match(s)
219    if m:
220        if len(m.group().split()) == 2:
221            # Check and warn for "addtask task1 task2"
222            m2 = re.match(r"addtask\s+(?P<func>\w+)(?P<ignores>.*)", s)
223            if m2 and m2.group('ignores'):
224                logger.warning('addtask ignored: "%s"' % m2.group('ignores'))
225
226        # Check and warn for "addtask task1 before task2 before task3", the
227        # similar to "after"
228        taskexpression = s.split()
229        for word in ('before', 'after'):
230            if taskexpression.count(word) > 1:
231                logger.warning("addtask contained multiple '%s' keywords, only one is supported" % word)
232
233        # Check and warn for having task with exprssion as part of task name
234        for te in taskexpression:
235            if any( ( "%s_" % keyword ) in te for keyword in bb.data_smart.__setvar_keyword__ ):
236                raise ParseError("Task name '%s' contains a keyword which is not recommended/supported.\nPlease rename the task not to include the keyword.\n%s" % (te, ("\n".join(map(str, bb.data_smart.__setvar_keyword__)))), fn)
237        ast.handleAddTask(statements, fn, lineno, m)
238        return
239
240    m = __deltask_regexp__.match(s)
241    if m:
242        ast.handleDelTask(statements, fn, lineno, m)
243        return
244
245    m = __addhandler_regexp__.match(s)
246    if m:
247        ast.handleBBHandlers(statements, fn, lineno, m)
248        return
249
250    m = __inherit_regexp__.match(s)
251    if m:
252        ast.handleInherit(statements, fn, lineno, m)
253        return
254
255    return ConfHandler.feeder(lineno, s, fn, statements)
256
257# Add us to the handlers list
258from .. import handlers
259handlers.append({'supports': supports, 'handle': handle, 'init': init})
260del handlers
261