1#!/usr/bin/env python3
2
3#
4# Copyright (C) 2018 The Android Open Source Project
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10#      http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17#
18
19"""This module implements a Android.bp parser."""
20
21import collections
22import glob
23import itertools
24import os
25import re
26import sys
27
28
29#------------------------------------------------------------------------------
30# Python 2 compatibility
31#------------------------------------------------------------------------------
32
33if sys.version_info >= (3,):
34    py3_chr = chr  # pylint: disable=invalid-name
35else:
36    def py3_chr(codepoint):
37        """Convert an integer character codepoint into a utf-8 string."""
38        return unichr(codepoint).encode('utf-8')
39
40try:
41    from enum import Enum
42except ImportError:
43    class _Enum(object):  # pylint: disable=too-few-public-methods
44        """A name-value pair for each enumeration."""
45
46        __slot__ = ('name', 'value')
47
48
49        def __init__(self, name, value):
50            """Create a name-value pair."""
51            self.name = name
52            self.value = value
53
54
55        def __repr__(self):
56            """Return the name of the enumeration."""
57            return self.name
58
59
60    class _EnumMeta(type):  # pylint: disable=too-few-public-methods
61        """Metaclass for Enum base class."""
62
63        def __new__(mcs, name, bases, attrs):
64            """Collects enumerations from attributes of the derived classes."""
65            enums = []
66            new_attrs = {'_enums': enums}
67
68            for key, value in attrs.iteritems():
69                if key.startswith('_'):
70                    new_attrs[key] = value
71                else:
72                    item = _Enum(key, value)
73                    enums.append(item)
74                    new_attrs[key] = item
75
76            return type.__new__(mcs, name, bases, new_attrs)
77
78
79        def __iter__(cls):
80            """Iterate the list of enumerations."""
81            return iter(cls._enums)
82
83
84    class Enum(object):  # pylint: disable=too-few-public-methods
85        """Enum base class."""
86        __metaclass__ = _EnumMeta
87
88
89#------------------------------------------------------------------------------
90# Lexer
91#------------------------------------------------------------------------------
92
93class Token(Enum):  # pylint: disable=too-few-public-methods
94    """Token enumerations."""
95
96    EOF = 0
97
98    IDENT = 1
99    LPAREN = 2
100    RPAREN = 3
101    LBRACKET = 4
102    RBRACKET = 5
103    LBRACE = 6
104    RBRACE = 7
105    COLON = 8
106    ASSIGN = 9
107    ASSIGNPLUS = 10
108    PLUS = 11
109    COMMA = 12
110    STRING = 13
111    INTEGER = 14
112
113    COMMENT = 15
114    SPACE = 16
115
116
117class LexerError(ValueError):
118    """Lexer error exception class."""
119
120    def __init__(self, buf, pos, message):
121        """Create a lexer error exception object."""
122        super(LexerError, self).__init__(message)
123        self.message = message
124        self.line, self.column = Lexer.compute_line_column(buf, pos)
125
126
127    def __str__(self):
128        """Convert lexer error to string representation."""
129        return 'LexerError: {}:{}: {}'.format(
130            self.line, self.column, self.message)
131
132
133class Lexer(object):
134    """Lexer to tokenize the input string."""
135
136    def __init__(self, buf, offset=0, path=None):
137        """Tokenize the source code in buf starting from offset.
138
139        Args:
140            buf (string) The source code to be tokenized.
141            offset (int) The position to start.
142        """
143
144        self.buf = buf
145
146        self.start = None
147        self.end = offset
148        self.token = None
149        self.literal = None
150        self.path = path
151
152        self._next()
153
154
155    def consume(self, *tokens):
156        """Consume one or more token."""
157
158        for token in tokens:
159            if token == self.token:
160                self._next()
161            else:
162                raise LexerError(self.buf, self.start,
163                                 'unexpected token ' + self.token.name)
164
165
166    def _next(self):
167        """Read next non-comment non-space token."""
168
169        buf_len = len(self.buf)
170        while self.end < buf_len:
171            self.start = self.end
172            self.token, self.end, self.literal = self.lex(self.buf, self.start)
173            if self.token != Token.SPACE and self.token != Token.COMMENT:
174                return
175
176        self.start = self.end
177        self.token = Token.EOF
178        self.literal = None
179
180
181    @staticmethod
182    def compute_line_column(buf, pos):
183        """Compute the line number and the column number of a given position in
184        the buffer."""
185
186        prior = buf[0:pos]
187        newline_pos = prior.rfind('\n')
188        if newline_pos == -1:
189            return (1, pos + 1)
190        return (prior.count('\n') + 1, pos - newline_pos)
191
192
193    UNICODE_CHARS_PATTERN = re.compile('[^\\\\\\n"]+')
194
195
196    ESCAPE_CHAR_TABLE = {
197        'a': '\a', 'b': '\b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t',
198        'v': '\v', '\\': '\\', '\'': '\'', '\"': '\"',
199    }
200
201
202    OCT_TABLE = {str(i) for i in range(8)}
203
204
205    @staticmethod
206    def decode_oct(buf, offset, start, end):
207        """Read characters from buf[start:end] and interpret them as an octal
208        integer."""
209
210        if end > len(buf):
211            raise LexerError(buf, offset, 'bad octal escape sequence')
212        try:
213            codepoint = int(buf[start:end], 8)
214        except ValueError:
215            raise LexerError(buf, offset, 'bad octal escape sequence')
216        if codepoint > 0xff:
217            raise LexerError(buf, offset, 'bad octal escape sequence')
218        return codepoint
219
220
221    @staticmethod
222    def decode_hex(buf, offset, start, end):
223        """Read characters from buf[start:end] and interpret them as a
224        hexadecimal integer."""
225
226        if end > len(buf):
227            raise LexerError(buf, offset, 'bad hex escape sequence')
228        try:
229            return int(buf[start:end], 16)
230        except ValueError:
231            raise LexerError(buf, offset, 'bad hex escape sequence')
232
233
234    @classmethod
235    def lex_interpreted_string(cls, buf, offset):
236        """Tokenize a golang interpreted string.
237
238        Args:
239            buf (str)    The source code buffer.
240            offset (int) The position to find a golang interpreted string
241                         literal.
242
243        Returns:
244            A tuple with the end of matched buffer and the interpreted string
245            literal.
246        """
247
248        buf_len = len(buf)
249        pos = offset + 1
250        literal = ''
251        while pos < buf_len:
252            # Match unicode characters
253            match = cls.UNICODE_CHARS_PATTERN.match(buf, pos)
254            if match:
255                literal += match.group(0)
256                pos = match.end()
257            # Read the next character
258            try:
259                char = buf[pos]
260            except IndexError:
261                raise LexerError(buf, pos,
262                                 'unclosed interpreted string literal')
263            if char == '\\':
264                # Escape sequences
265                try:
266                    char = buf[pos + 1]
267                except IndexError:
268                    raise LexerError(buf, pos, 'bad escape sequence')
269                if char in cls.OCT_TABLE:
270                    literal += chr(cls.decode_oct(buf, pos, pos + 1, pos + 4))
271                    pos += 4
272                elif char == 'x':
273                    literal += chr(cls.decode_hex(buf, pos, pos + 2, pos + 4))
274                    pos += 4
275                elif char == 'u':
276                    literal += py3_chr(
277                        cls.decode_hex(buf, pos, pos + 2, pos + 6))
278                    pos += 6
279                elif char == 'U':
280                    literal += py3_chr(
281                        cls.decode_hex(buf, pos, pos + 2, pos + 10))
282                    pos += 10
283                else:
284                    try:
285                        literal += cls.ESCAPE_CHAR_TABLE[char]
286                        pos += 2
287                    except KeyError:
288                        raise LexerError(buf, pos, 'bad escape sequence')
289                continue
290            if char == '"':
291                # End of string literal
292                return (pos + 1, literal)
293            raise LexerError(buf, pos, 'unclosed interpreted string literal')
294
295
296    @classmethod
297    def lex_string(cls, buf, offset):
298        """Tokenize a golang string literal.
299
300        Args:
301            buf (str)    The source code buffer.
302            offset (int) The position to find a golang string literal.
303
304        Returns:
305            A tuple with the end of matched buffer and the interpreted string
306            literal.
307        """
308
309        char = buf[offset]
310        if char == '`':
311            try:
312                end = buf.index('`', offset + 1)
313                return (end + 1, buf[offset + 1 : end])
314            except ValueError:
315                raise LexerError(buf, len(buf), 'unclosed raw string literal')
316        if char == '"':
317            return cls.lex_interpreted_string(buf, offset)
318        raise LexerError(buf, offset, 'no string literal start character')
319
320
321    LEXER_PATTERNS = (
322        (Token.IDENT, '[A-Za-z_][0-9A-Za-z_]*'),
323        (Token.LPAREN, '\\('),
324        (Token.RPAREN, '\\)'),
325        (Token.LBRACKET, '\\['),
326        (Token.RBRACKET, '\\]'),
327        (Token.LBRACE, '\\{'),
328        (Token.RBRACE, '\\}'),
329        (Token.COLON, ':'),
330        (Token.ASSIGN, '='),
331        (Token.ASSIGNPLUS, '\\+='),
332        (Token.PLUS, '\\+'),
333        (Token.COMMA, ','),
334        (Token.STRING, '["`]'),
335        (Token.INTEGER, '-{0,1}[0-9]+'),
336
337        (Token.COMMENT,
338         '/(?:(?:/[^\\n]*)|(?:\\*(?:(?:[^*]*)|(?:\\*+[^/*]))*\\*+/))'),
339        (Token.SPACE, '\\s+'),
340    )
341
342
343    LEXER_MATCHER = re.compile('|'.join(
344        '(' + pattern + ')' for _, pattern in LEXER_PATTERNS))
345
346
347    @classmethod
348    def lex(cls, buf, offset):
349        """Tokenize a token from buf[offset].
350
351        Args:
352            buf (string) The source code buffer.
353            offset (int) The position to find and tokenize a token.
354
355        Return:
356            A tuple with three elements.  The first element is the token id.
357            The second element is the end of the token.  The third element is
358            the value for strings or identifiers.
359        """
360
361        match = cls.LEXER_MATCHER.match(buf, offset)
362        if not match:
363            raise LexerError(buf, offset, 'unknown token')
364        token = cls.LEXER_PATTERNS[match.lastindex - 1][0]
365
366        if token == Token.STRING:
367            end, literal = cls.lex_string(buf, offset)
368        else:
369            end = match.end()
370            if token in {Token.IDENT, Token.INTEGER}:
371                literal = buf[offset:end]
372            else:
373                literal = None
374
375        return (token, end, literal)
376
377
378#------------------------------------------------------------------------------
379# AST
380#------------------------------------------------------------------------------
381
382class Expr(object):  # pylint: disable=too-few-public-methods
383    """Base class for all expressions."""
384
385    def eval(self, env):
386        """Evaluate the expression under an environment."""
387        raise NotImplementedError()
388
389
390class String(Expr, str):
391    """String constant literal."""
392
393    def eval(self, env):
394        """Evaluate the string expression under an environment."""
395        return self
396
397
398class Bool(Expr):  # pylint: disable=too-few-public-methods
399    """Boolean constant literal."""
400
401    __slots__ = ('value',)
402
403
404    def __init__(self, value):
405        """Create a boolean constant literal."""
406        self.value = value
407
408
409    def __repr__(self):
410        """Convert a boolean constant literal to string representation."""
411        return repr(self.value)
412
413
414    def __bool__(self):
415        """Convert boolean constant literal to Python bool type."""
416        return self.value
417
418    __nonzero__ = __bool__
419
420
421    def __eq__(self, rhs):
422        """Compare whether two instances are equal."""
423        return self.value == rhs.value
424
425
426    def __hash__(self):
427        """Compute the hashed value."""
428        return hash(self.value)
429
430
431    def eval(self, env):
432        """Evaluate the boolean expression under an environment."""
433        return self
434
435
436class Integer(Expr):  # pylint: disable=too-few-public-methods
437    """Integer constant literal."""
438
439    __slots__ = ('value',)
440
441
442    def __init__(self, value):
443        """Create an integer constant literal."""
444        self.value = value
445
446
447    def __repr__(self):
448        """Convert an integer constant literal to string representation."""
449        return repr(self.value)
450
451
452    def __bool__(self):
453        """Convert an integer constant literal to Python bool type."""
454        return bool(self.value)
455
456    __nonzero__ = __bool__
457
458
459    def __int__(self):
460        """Convert an integer constant literal to Python int type."""
461        return self.value
462
463
464    def __eq__(self, rhs):
465        """Compare whether two instances are equal."""
466        return self.value == rhs.value
467
468
469    def __hash__(self):
470        """Compute the hashed value."""
471        return hash(self.value)
472
473
474    def eval(self, env):
475        """Evaluate the integer expression under an environment."""
476        return self
477
478
479class VarRef(Expr):  # pylint: disable=too-few-public-methods
480    """A reference to a variable."""
481
482    def __init__(self, name, value):
483        """Create a variable reference with a name and the value under static
484        scoping."""
485        self.name = name
486        self.value = value
487
488
489    def __repr__(self):
490        """Convert a variable reference to string representation."""
491        return self.name
492
493
494    def eval(self, env):
495        """Evaluate the identifier under an environment."""
496        if self.value is None:
497            return env[self.name].eval(env)
498        return self.value.eval(env)
499
500
501class List(Expr, list):
502    """List expression."""
503
504    def eval(self, env):
505        """Evaluate list elements under an environment."""
506        return List(item.eval(env) for item in self)
507
508
509class Dict(Expr, collections.OrderedDict):
510    """Dictionary expression."""
511
512    def __repr__(self):
513        attrs = ', '.join(key + ': ' + repr(value)
514                          for key, value in self.items())
515        return '{' + attrs + '}'
516
517
518    def eval(self, env):
519        """Evaluate dictionary values under an environment."""
520        return Dict((key, value.eval(env)) for key, value in self.items())
521
522
523class Concat(Expr):  # pylint: disable=too-few-public-methods
524    """List/string/integer plus operator."""
525
526    __slots__ = ('lhs', 'rhs')
527
528
529    def __init__(self, lhs, rhs):
530        """Create a list/string/integer plus expression."""
531        self.lhs = lhs
532        self.rhs = rhs
533
534
535    def __repr__(self):
536        return '(' + repr(self.lhs) + ' + ' + repr(self.rhs) + ')'
537
538
539    def eval(self, env):
540        """Evaluate list/string/integer plus operator under an environment."""
541        lhs = self.lhs.eval(env)
542        rhs = self.rhs.eval(env)
543        if isinstance(lhs, List) and isinstance(rhs, List):
544            return List(itertools.chain(lhs, rhs))
545        if isinstance(lhs, String) and isinstance(rhs, String):
546            return String(lhs + rhs)
547        if isinstance(lhs, Integer) and isinstance(rhs, Integer):
548            return Integer(int(lhs) + int(rhs))
549        raise TypeError('bad plus operands')
550
551
552#------------------------------------------------------------------------------
553# Parser
554#------------------------------------------------------------------------------
555
556class ParseError(ValueError):
557    """Parser error exception class."""
558
559    def __init__(self, lexer, message):
560        """Create a parser error exception object."""
561        super(ParseError, self).__init__(message)
562        self.message = message
563        self.line, self.column = \
564            Lexer.compute_line_column(lexer.buf, lexer.start)
565
566
567    def __str__(self):
568        """Convert parser error to string representation."""
569        return 'ParseError: {}:{}: {}'.format(
570            self.line, self.column, self.message)
571
572
573class Parser(object):
574    """Parser to parse Android.bp files."""
575
576    def __init__(self, lexer, inherited_env=None):
577        """Initialize the parser with the lexer."""
578        self.lexer = lexer
579
580        self.var_defs = []
581        self.vars = {} if inherited_env is None else dict(inherited_env)
582        self.modules = []
583
584
585    def parse(self):
586        """Parse AST from tokens."""
587        lexer = self.lexer
588        while lexer.token != Token.EOF:
589            if lexer.token == Token.IDENT:
590                ident = self.parse_ident_lvalue()
591                if lexer.token in {Token.ASSIGN, Token.ASSIGNPLUS}:
592                    self.parse_assign(ident, lexer.token)
593                elif lexer.token in {Token.LBRACE, Token.LPAREN}:
594                    self.parse_module_definition(ident)
595                else:
596                    raise ParseError(lexer,
597                                     'unexpected token ' + lexer.token.name)
598            else:
599                raise ParseError(lexer, 'unexpected token ' + lexer.token.name)
600        lexer.consume(Token.EOF)
601
602
603    def create_var_ref(self, name):
604        """Create a variable reference."""
605        return VarRef(name, self.vars.get(name))
606
607
608    def define_var(self, name, value):
609        """Define a variable."""
610        self.var_defs.append((name, value))
611        self.vars[name] = value
612
613
614    def parse_assign(self, ident, assign_token):
615        """Parse an assignment statement."""
616        lexer = self.lexer
617        lexer.consume(assign_token)
618        value = self.parse_expression()
619        if assign_token == Token.ASSIGNPLUS:
620            value = Concat(self.create_var_ref(ident), value)
621        self.define_var(ident, value)
622
623
624    def parse_module_definition(self, module_ident):
625        """Parse a module definition."""
626        properties = self.parse_dict()
627        properties['_path'] = String(self.lexer.path)
628        self.modules.append((module_ident, properties))
629
630
631    def parse_ident_lvalue(self):
632        """Parse an identifier as an l-value."""
633        ident = self.lexer.literal
634        self.lexer.consume(Token.IDENT)
635        return ident
636
637
638    def parse_ident_rvalue(self):
639        """Parse an identifier as a r-value.
640
641        Returns:
642            Returns VarRef if the literal is not 'true' nor 'false'.
643
644            Returns Bool(true/false) if the literal is either 'true' or 'false'.
645        """
646        lexer = self.lexer
647        if lexer.literal in {'true', 'false'}:
648            result = Bool(lexer.literal == 'true')
649        else:
650            result = self.create_var_ref(lexer.literal)
651        lexer.consume(Token.IDENT)
652        return result
653
654
655    def parse_string(self):
656        """Parse a string."""
657        lexer = self.lexer
658        string = String(lexer.literal)
659        lexer.consume(Token.STRING)
660        return string
661
662
663    def parse_integer(self):
664        """Parse an integer."""
665        lexer = self.lexer
666        integer = Integer(int(lexer.literal))
667        lexer.consume(Token.INTEGER)
668        return integer
669
670
671    def parse_operand(self):
672        """Parse an operand."""
673        lexer = self.lexer
674        token = lexer.token
675        if token == Token.STRING:
676            return self.parse_string()
677        if token == Token.IDENT:
678            return self.parse_ident_rvalue()
679        if token == Token.INTEGER:
680            return self.parse_integer()
681        if token == Token.LBRACKET:
682            return self.parse_list()
683        if token == Token.LBRACE:
684            return self.parse_dict()
685        if token == Token.LPAREN:
686            lexer.consume(Token.LPAREN)
687            operand = self.parse_expression()
688            lexer.consume(Token.RPAREN)
689            return operand
690        raise ParseError(lexer, 'unexpected token ' + token.name)
691
692
693    def parse_expression(self):
694        """Parse an expression."""
695        lexer = self.lexer
696        expr = self.parse_operand()
697        while lexer.token == Token.PLUS:
698            lexer.consume(Token.PLUS)
699            expr = Concat(expr, self.parse_operand())
700        return expr
701
702
703    def parse_list(self):
704        """Parse a list."""
705        result = List()
706        lexer = self.lexer
707        lexer.consume(Token.LBRACKET)
708        while lexer.token != Token.RBRACKET:
709            result.append(self.parse_expression())
710            if lexer.token == Token.COMMA:
711                lexer.consume(Token.COMMA)
712        lexer.consume(Token.RBRACKET)
713        return result
714
715
716    def parse_dict(self):
717        """Parse a dict."""
718        result = Dict()
719        lexer = self.lexer
720
721        is_func_syntax = lexer.token == Token.LPAREN
722        if is_func_syntax:
723            lexer.consume(Token.LPAREN)
724        else:
725            lexer.consume(Token.LBRACE)
726
727        while lexer.token != Token.RBRACE and lexer.token != Token.RPAREN:
728            if lexer.token != Token.IDENT:
729                raise ParseError(lexer, 'unexpected token ' + lexer.token.name)
730            key = self.parse_ident_lvalue()
731
732            if lexer.token == Token.ASSIGN:
733                lexer.consume(Token.ASSIGN)
734            else:
735                lexer.consume(Token.COLON)
736
737            value = self.parse_expression()
738            result[key] = value
739
740            if lexer.token == Token.COMMA:
741                lexer.consume(Token.COMMA)
742
743        if is_func_syntax:
744            lexer.consume(Token.RPAREN)
745        else:
746            lexer.consume(Token.RBRACE)
747
748        return result
749
750
751class RecursiveParser(object):
752    """This is a recursive parser which will parse blueprint files
753    recursively."""
754
755
756    # Default Blueprint file name
757    _DEFAULT_SUB_NAME = 'Android.bp'
758
759
760    def __init__(self):
761        """Initialize a recursive parser."""
762        self.visited = set()
763        self.modules = []
764
765
766    @staticmethod
767    def glob_sub_files(pattern, sub_file_name):
768        """List the sub file paths that match with the pattern with
769        wildcards."""
770
771        for path in glob.glob(pattern):
772            if os.path.isfile(path):
773                if os.path.basename(path) == sub_file_name:
774                    yield path
775            else:
776                sub_file_path = os.path.join(path, sub_file_name)
777                if os.path.isfile(sub_file_path):
778                    yield sub_file_path
779
780
781    @classmethod
782    def find_sub_files_from_env(cls, rootdir, env, use_subdirs,
783                                default_sub_name=_DEFAULT_SUB_NAME):
784        """Find the sub files from the names specified in build, subdirs, and
785        optional_subdirs."""
786
787        subs = []
788
789        if 'build' in env:
790            subs.extend(os.path.join(rootdir, filename)
791                        for filename in env['build'].eval(env))
792        if use_subdirs:
793            sub_name = env['subname'] if 'subname' in env else default_sub_name
794
795            if 'subdirs' in env:
796                for path in env['subdirs'].eval(env):
797                    subs.extend(cls.glob_sub_files(os.path.join(rootdir, path),
798                                                   sub_name))
799            if 'optional_subdirs' in env:
800                for path in env['optional_subdirs'].eval(env):
801                    subs.extend(cls.glob_sub_files(os.path.join(rootdir, path),
802                                                   sub_name))
803        return subs
804
805
806    @staticmethod
807    def _read_file(path, env):
808        """Read a blueprint file and return modules and the environment."""
809        with open(path, 'r') as bp_file:
810            content = bp_file.read()
811        parser = Parser(Lexer(content, path=path), env)
812        parser.parse()
813        return (parser.modules, parser.vars)
814
815
816    def _parse_file(self, path, env, evaluate):
817        """Parse a blueprint file and append to self.modules."""
818        modules, sub_env = self._read_file(path, env)
819        if evaluate:
820            modules = [(ident, attrs.eval(env)) for ident, attrs in modules]
821        self.modules += modules
822        return sub_env
823
824
825    def _parse_file_recursive(self, path, env, evaluate, use_subdirs):
826        """Parse a blueprint file and recursively."""
827
828        self.visited.add(path)
829
830        sub_env = self._parse_file(path, env, evaluate)
831
832        rootdir = os.path.dirname(path)
833
834        sub_file_paths = self.find_sub_files_from_env(rootdir, sub_env,
835                                                      use_subdirs)
836
837        sub_env.pop('build', None)
838        sub_env.pop('subdirs', None)
839        sub_env.pop('optional_subdirs', None)
840
841        for sub_file_path in sub_file_paths:
842            if sub_file_path not in self.visited:
843                self._parse_file_recursive(sub_file_path, sub_env, evaluate,
844                                           use_subdirs)
845        return sub_env
846
847
848    def _scan_and_parse_all_file_recursive(self, filename, path, env, evaluate):
849        """Scan all files with the specified name and parse them."""
850
851        rootdir = os.path.dirname(path)
852        assert rootdir, 'rootdir is empty but must be non-empty'
853
854        envs = [(rootdir, env)]
855        assert env is not None
856
857        # Scan directories for all blueprint files
858        for basedir, dirnames, filenames in os.walk(rootdir):
859            # Drop irrelevant environments
860            while not basedir.startswith(envs[-1][0]):
861                envs.pop()
862
863            # Filter sub directories
864            if '.out-dir' in filenames:
865                # Stop at OUT_DIR
866                dirnames[:] = []
867                continue
868            new_dirnames = []
869            for name in dirnames:
870                if name in {'.git', '.repo'}:
871                    continue
872                if basedir == rootdir and name == 'out':
873                    continue
874                new_dirnames.append(name)
875            dirnames[:] = new_dirnames
876
877            # Parse blueprint files
878            if filename in filenames:
879                try:
880                    path = os.path.join(basedir, filename)
881                    sys.stdout.flush()
882                    sub_env = self._parse_file_recursive(path, envs[-1][1],
883                                                         evaluate, False)
884                    assert sub_env is not None
885                    envs.append((basedir, sub_env))
886                except IOError:
887                    pass
888
889
890    def parse_file(self, path, env=None, evaluate=True,
891                   default_sub_name=_DEFAULT_SUB_NAME):
892        """Parse blueprint files recursively."""
893
894        if env is None:
895            env = {}
896
897        path = os.path.abspath(path)
898
899        sub_env = self._read_file(path, env)[1]
900
901        if 'subdirs' in sub_env or 'optional_subdirs' in sub_env:
902            self._parse_file_recursive(path, env, evaluate, True)
903        else:
904            self._scan_and_parse_all_file_recursive(
905                default_sub_name, path, env, evaluate)
906
907
908#------------------------------------------------------------------------------
909# Transformation
910#------------------------------------------------------------------------------
911
912def _build_named_modules_dict(modules):
913    """Build a name-to-module dict."""
914    named_modules = {}
915    for i, (ident, attrs) in enumerate(modules):
916        name = attrs.get('name')
917        if name is not None:
918            named_modules[name] = [ident, attrs, i]
919    return named_modules
920
921
922def _po_sorted_modules(modules, named_modules):
923    """Sort modules in post order."""
924    modules = [(ident, attrs, i) for i, (ident, attrs) in enumerate(modules)]
925
926    # Build module dependency graph.
927    edges = {}
928    for ident, attrs, module_id in modules:
929        defaults = attrs.get('defaults')
930        if defaults:
931            edges[module_id] = set(
932                named_modules[default][2] for default in defaults)
933
934    # Traverse module graph in post order.
935    post_order = []
936    visited = set()
937
938    def _traverse(module_id):
939        visited.add(module_id)
940        for next_module_id in edges.get(module_id, []):
941            if next_module_id not in visited:
942                _traverse(next_module_id)
943        post_order.append(modules[module_id])
944
945    for module_id in range(len(modules)):
946        if module_id not in visited:
947            _traverse(module_id)
948
949    return post_order
950
951
952def evaluate_default(attrs, default_attrs):
953    """Add default attributes if the keys do not exist."""
954    for key, value in default_attrs.items():
955        if key not in attrs:
956            attrs[key] = value
957        else:
958            attrs_value = attrs[key]
959            if isinstance(value, Dict) and isinstance(attrs_value, Dict):
960                attrs[key] = evaluate_default(attrs_value, value)
961    return attrs
962
963
964def evaluate_defaults(modules):
965    """Add default attributes to all modules if the keys do not exist."""
966    named_modules = _build_named_modules_dict(modules)
967    for ident, attrs, i in _po_sorted_modules(modules, named_modules):
968        for default in attrs.get('defaults', []):
969            attrs = evaluate_default(attrs, named_modules[default][1])
970        modules[i] = (ident, attrs)
971    return modules
972
973
974def fill_module_namespaces(root_bp, modules):
975    """Collect soong_namespace definition and set a `_namespace` property to
976    each module definitions."""
977
978    # Collect all namespaces
979    rootdir = os.path.dirname(os.path.abspath(root_bp))
980    namespaces = {rootdir}
981    for ident, attrs in modules:
982        if ident == 'soong_namespace':
983            namespaces.add(os.path.dirname(attrs['_path']))
984
985    # Build a path matcher for module namespaces
986    namespaces = sorted(namespaces, reverse=True)
987    path_matcher = re.compile(
988        '|'.join('(' + re.escape(x) + '/.*)' for x in namespaces))
989
990    # Trim the root directory prefix
991    rootdir_prefix_len = len(rootdir) + 1
992    namespaces = [path[rootdir_prefix_len:] for path in namespaces]
993
994    # Fill in module namespaces
995    for ident, attrs in modules:
996        match = path_matcher.match(attrs['_path'])
997        attrs['_namespace'] = namespaces[match.lastindex - 1]
998
999    return modules
1000