15f9996aaSopenharmony_ci#!/usr/bin/env python
25f9996aaSopenharmony_ci# -*- coding: utf-8 -*-
35f9996aaSopenharmony_ci# Copyright 2014 The Chromium Authors. All rights reserved.
45f9996aaSopenharmony_ci# Use of this source code is governed by a BSD-style license that can be
55f9996aaSopenharmony_ci# found in the LICENSE file.
65f9996aaSopenharmony_ci
75f9996aaSopenharmony_ci"""Helper functions useful when writing scripts that integrate with GN.
85f9996aaSopenharmony_ci
95f9996aaSopenharmony_ciThe main functions are ToGNString and from_gn_string which convert between
105f9996aaSopenharmony_ciserialized GN variables and Python variables.
115f9996aaSopenharmony_ci
125f9996aaSopenharmony_ciTo use in a random python file in the build:
135f9996aaSopenharmony_ci
145f9996aaSopenharmony_ci  import os
155f9996aaSopenharmony_ci  import sys
165f9996aaSopenharmony_ci
175f9996aaSopenharmony_ci  sys.path.append(os.path.join(os.path.dirname(__file__),
185f9996aaSopenharmony_ci                               os.pardir, os.pardir, "build"))
195f9996aaSopenharmony_ci  import gn_helpers
205f9996aaSopenharmony_ci
215f9996aaSopenharmony_ciWhere the sequence of parameters to join is the relative path from your source
225f9996aaSopenharmony_cifile to the build directory.
235f9996aaSopenharmony_ci"""
245f9996aaSopenharmony_ci
255f9996aaSopenharmony_ci
265f9996aaSopenharmony_ciclass GNException(Exception):
275f9996aaSopenharmony_ci    pass
285f9996aaSopenharmony_ci
295f9996aaSopenharmony_ci
305f9996aaSopenharmony_cidef to_gn_string(value: str, allow_dicts: bool = True) -> str:
315f9996aaSopenharmony_ci    """Returns a stringified GN equivalent of the Python value.
325f9996aaSopenharmony_ci
335f9996aaSopenharmony_ci    allow_dicts indicates if this function will allow converting dictionaries
345f9996aaSopenharmony_ci    to GN scopes. This is only possible at the top level, you can't nest a
355f9996aaSopenharmony_ci    GN scope in a list, so this should be set to False for recursive calls.
365f9996aaSopenharmony_ci    """
375f9996aaSopenharmony_ci    if isinstance(value, str):
385f9996aaSopenharmony_ci        if value.find('\n') >= 0:
395f9996aaSopenharmony_ci            raise GNException("Trying to print a string with a newline in it.")
405f9996aaSopenharmony_ci        return '"' + \
415f9996aaSopenharmony_ci               value.replace('\\', '\\\\').replace('"', '\\"').replace('$', '\\$') + \
425f9996aaSopenharmony_ci               '"'
435f9996aaSopenharmony_ci
445f9996aaSopenharmony_ci    if isinstance(value, str):
455f9996aaSopenharmony_ci        return to_gn_string(value.encode('utf-8'))
465f9996aaSopenharmony_ci
475f9996aaSopenharmony_ci    if isinstance(value, bool):
485f9996aaSopenharmony_ci        if value:
495f9996aaSopenharmony_ci            return "true"
505f9996aaSopenharmony_ci        return "false"
515f9996aaSopenharmony_ci
525f9996aaSopenharmony_ci    if isinstance(value, list):
535f9996aaSopenharmony_ci        return '[ %s ]' % ', '.join(to_gn_string(v) for v in value)
545f9996aaSopenharmony_ci
555f9996aaSopenharmony_ci    if isinstance(value, dict):
565f9996aaSopenharmony_ci        if not allow_dicts:
575f9996aaSopenharmony_ci            raise GNException("Attempting to recursively print a dictionary.")
585f9996aaSopenharmony_ci        result = ""
595f9996aaSopenharmony_ci        for key in sorted(value):
605f9996aaSopenharmony_ci            if not isinstance(key, str):
615f9996aaSopenharmony_ci                raise GNException("Dictionary key is not a string.")
625f9996aaSopenharmony_ci            result += "%s = %s\n" % (key, to_gn_string(value[key], False))
635f9996aaSopenharmony_ci        return result
645f9996aaSopenharmony_ci
655f9996aaSopenharmony_ci    if isinstance(value, int):
665f9996aaSopenharmony_ci        return str(value)
675f9996aaSopenharmony_ci
685f9996aaSopenharmony_ci    raise GNException("Unsupported type when printing to GN.")
695f9996aaSopenharmony_ci
705f9996aaSopenharmony_ci
715f9996aaSopenharmony_cidef from_gn_string(input_string: str) -> dict:
725f9996aaSopenharmony_ci    """Converts the input string from a GN serialized value to Python values.
735f9996aaSopenharmony_ci
745f9996aaSopenharmony_ci    For details on supported types see GNValueParser.parse() below.
755f9996aaSopenharmony_ci
765f9996aaSopenharmony_ci    If your GN script did:
775f9996aaSopenharmony_ci      something = [ "file1", "file2" ]
785f9996aaSopenharmony_ci      args = [ "--values=$something" ]
795f9996aaSopenharmony_ci    The command line would look something like:
805f9996aaSopenharmony_ci      --values="[ \"file1\", \"file2\" ]"
815f9996aaSopenharmony_ci    Which when interpreted as a command line gives the value:
825f9996aaSopenharmony_ci      [ "file1", "file2" ]
835f9996aaSopenharmony_ci
845f9996aaSopenharmony_ci    You can parse this into a Python list using GN rules with:
855f9996aaSopenharmony_ci      input_values = FromGNValues(options.values)
865f9996aaSopenharmony_ci    Although the Python 'ast' module will parse many forms of such input, it
875f9996aaSopenharmony_ci    will not handle GN escaping properly, nor GN booleans. You should use this
885f9996aaSopenharmony_ci    function instead.
895f9996aaSopenharmony_ci
905f9996aaSopenharmony_ci
915f9996aaSopenharmony_ci    A NOTE ON STRING HANDLING:
925f9996aaSopenharmony_ci
935f9996aaSopenharmony_ci    If you just pass a string on the command line to your Python script, or use
945f9996aaSopenharmony_ci    string interpolation on a string variable, the strings will not be quoted:
955f9996aaSopenharmony_ci      str = "asdf"
965f9996aaSopenharmony_ci      args = [ str, "--value=$str" ]
975f9996aaSopenharmony_ci    Will yield the command line:
985f9996aaSopenharmony_ci      asdf --value=asdf
995f9996aaSopenharmony_ci    The unquoted asdf string will not be valid input to this function, which
1005f9996aaSopenharmony_ci    accepts only quoted strings like GN scripts. In such cases, you can just
1015f9996aaSopenharmony_ci    use the Python string literal directly.
1025f9996aaSopenharmony_ci
1035f9996aaSopenharmony_ci    The main use cases for this is for other types, in particular lists. When
1045f9996aaSopenharmony_ci    using string interpolation on a list (as in the top example) the embedded
1055f9996aaSopenharmony_ci    strings will be quoted and escaped according to GN rules so the list can be
1065f9996aaSopenharmony_ci    re-parsed to get the same result.
1075f9996aaSopenharmony_ci    """
1085f9996aaSopenharmony_ci    parser = GNValueParser(input_string)
1095f9996aaSopenharmony_ci    return parser.parse()
1105f9996aaSopenharmony_ci
1115f9996aaSopenharmony_ci
1125f9996aaSopenharmony_cidef from_gn_args(input_string: str) -> dict:
1135f9996aaSopenharmony_ci    """Converts a string with a bunch of gn arg assignments into a Python dict.
1145f9996aaSopenharmony_ci
1155f9996aaSopenharmony_ci    Given a whitespace-separated list of
1165f9996aaSopenharmony_ci
1175f9996aaSopenharmony_ci      <ident> = (integer | string | boolean | <list of the former>)
1185f9996aaSopenharmony_ci
1195f9996aaSopenharmony_ci    gn assignments, this returns a Python dict, i.e.:
1205f9996aaSopenharmony_ci
1215f9996aaSopenharmony_ci      from_gn_args("foo=true\nbar=1\n") -> { 'foo': True, 'bar': 1 }.
1225f9996aaSopenharmony_ci
1235f9996aaSopenharmony_ci    Only simple types and lists supported; variables, structs, calls
1245f9996aaSopenharmony_ci    and other, more complicated things are not.
1255f9996aaSopenharmony_ci
1265f9996aaSopenharmony_ci    This routine is meant to handle only the simple sorts of values that
1275f9996aaSopenharmony_ci    arise in parsing --args.
1285f9996aaSopenharmony_ci    """
1295f9996aaSopenharmony_ci    parser = GNValueParser(input_string)
1305f9996aaSopenharmony_ci    return parser.parse_args()
1315f9996aaSopenharmony_ci
1325f9996aaSopenharmony_ci
1335f9996aaSopenharmony_cidef unescape_gn_special_char(char_after_backslash: str) -> str:
1345f9996aaSopenharmony_ci    # Process the GN escape character and return it if it is a valid escape character; Otherwise, return a back slash
1355f9996aaSopenharmony_ci    if char_after_backslash in ('$', '"', '\\'):
1365f9996aaSopenharmony_ci        return char_after_backslash
1375f9996aaSopenharmony_ci    else:
1385f9996aaSopenharmony_ci        return '\\'
1395f9996aaSopenharmony_ci
1405f9996aaSopenharmony_ci
1415f9996aaSopenharmony_cidef unescape_gn_string(value: list) -> str:
1425f9996aaSopenharmony_ci    """Given a string with GN escaping, returns the unescaped string.
1435f9996aaSopenharmony_ci
1445f9996aaSopenharmony_ci    Be careful not to feed with input from a Python parsing function like
1455f9996aaSopenharmony_ci    'ast' because it will do Python unescaping, which will be incorrect when
1465f9996aaSopenharmony_ci    fed into the GN unescaper.
1475f9996aaSopenharmony_ci    """
1485f9996aaSopenharmony_ci    result = []
1495f9996aaSopenharmony_ci    i = 0
1505f9996aaSopenharmony_ci    skip_char = False
1515f9996aaSopenharmony_ci    while i < len(value):
1525f9996aaSopenharmony_ci        if value[i] == '\\':
1535f9996aaSopenharmony_ci            if i < len(value) - 1:
1545f9996aaSopenharmony_ci            # If it is not the last element of the list and the current character is a back slash
1555f9996aaSopenharmony_ci                next_char = value[i + 1]
1565f9996aaSopenharmony_ci                result.append(unescape_gn_special_char(next_char))
1575f9996aaSopenharmony_ci                skip_char = next_char in ('$', '"', '\\')
1585f9996aaSopenharmony_ci        else:
1595f9996aaSopenharmony_ci            result.append(value[i])
1605f9996aaSopenharmony_ci        i += 2 if skip_char else 1
1615f9996aaSopenharmony_ci        skip_char = False
1625f9996aaSopenharmony_ci    return ''.join(result)
1635f9996aaSopenharmony_ci
1645f9996aaSopenharmony_ci
1655f9996aaSopenharmony_cidef _is_digit_or_minus(char: str):
1665f9996aaSopenharmony_ci    return char in "-0123456789"
1675f9996aaSopenharmony_ci
1685f9996aaSopenharmony_ci
1695f9996aaSopenharmony_ciclass GNValueParser(object):
1705f9996aaSopenharmony_ci    """Duplicates GN parsing of values and converts to Python types.
1715f9996aaSopenharmony_ci
1725f9996aaSopenharmony_ci    Normally you would use the wrapper function FromGNValue() below.
1735f9996aaSopenharmony_ci
1745f9996aaSopenharmony_ci    If you expect input as a specific type, you can also call one of the Parse*
1755f9996aaSopenharmony_ci    functions directly. All functions throw GNException on invalid input.
1765f9996aaSopenharmony_ci    """
1775f9996aaSopenharmony_ci
1785f9996aaSopenharmony_ci    def __init__(self, string: str):
1795f9996aaSopenharmony_ci        self.input = string
1805f9996aaSopenharmony_ci        self.cur = 0
1815f9996aaSopenharmony_ci
1825f9996aaSopenharmony_ci    def is_done(self) -> bool:
1835f9996aaSopenharmony_ci        return self.cur == len(self.input)
1845f9996aaSopenharmony_ci
1855f9996aaSopenharmony_ci    def consume_whitespace(self):
1865f9996aaSopenharmony_ci        while not self.is_done() and self.input[self.cur] in ' \t\n':
1875f9996aaSopenharmony_ci            self.cur += 1
1885f9996aaSopenharmony_ci
1895f9996aaSopenharmony_ci    def parse(self):
1905f9996aaSopenharmony_ci        """Converts a string representing a printed GN value to the Python type.
1915f9996aaSopenharmony_ci
1925f9996aaSopenharmony_ci        See additional usage notes on from_gn_string above.
1935f9996aaSopenharmony_ci
1945f9996aaSopenharmony_ci        - GN booleans ('true', 'false') will be converted to Python booleans.
1955f9996aaSopenharmony_ci
1965f9996aaSopenharmony_ci        - GN numbers ('123') will be converted to Python numbers.
1975f9996aaSopenharmony_ci
1985f9996aaSopenharmony_ci        - GN strings (double-quoted as in '"asdf"') will be converted to Python
1995f9996aaSopenharmony_ci          strings with GN escaping rules. GN string interpolation (embedded
2005f9996aaSopenharmony_ci          variables preceded by $) are not supported and will be returned as
2015f9996aaSopenharmony_ci          literals.
2025f9996aaSopenharmony_ci
2035f9996aaSopenharmony_ci        - GN lists ('[1, "asdf", 3]') will be converted to Python lists.
2045f9996aaSopenharmony_ci
2055f9996aaSopenharmony_ci        - GN scopes ('{ ... }') are not supported.
2065f9996aaSopenharmony_ci        """
2075f9996aaSopenharmony_ci        result = self._parse_allow_trailing()
2085f9996aaSopenharmony_ci        self.consume_whitespace()
2095f9996aaSopenharmony_ci        if not self.is_done():
2105f9996aaSopenharmony_ci            raise GNException("Trailing input after parsing:\n  " +
2115f9996aaSopenharmony_ci                              self.input[self.cur:])
2125f9996aaSopenharmony_ci        return result
2135f9996aaSopenharmony_ci
2145f9996aaSopenharmony_ci    def parse_args(self) -> dict:
2155f9996aaSopenharmony_ci        """Converts a whitespace-separated list of ident=literals to a dict.
2165f9996aaSopenharmony_ci
2175f9996aaSopenharmony_ci        See additional usage notes on from_gn_args, above.
2185f9996aaSopenharmony_ci        """
2195f9996aaSopenharmony_ci        d = {}
2205f9996aaSopenharmony_ci
2215f9996aaSopenharmony_ci        self.consume_whitespace()
2225f9996aaSopenharmony_ci        while not self.is_done():
2235f9996aaSopenharmony_ci            ident = self._parse_ident()
2245f9996aaSopenharmony_ci            self.consume_whitespace()
2255f9996aaSopenharmony_ci            if self.input[self.cur] != '=':
2265f9996aaSopenharmony_ci                raise GNException("Unexpected token: " + self.input[self.cur:])
2275f9996aaSopenharmony_ci            self.cur += 1
2285f9996aaSopenharmony_ci            self.consume_whitespace()
2295f9996aaSopenharmony_ci            val = self._parse_allow_trailing()
2305f9996aaSopenharmony_ci            self.consume_whitespace()
2315f9996aaSopenharmony_ci            d[ident] = val
2325f9996aaSopenharmony_ci
2335f9996aaSopenharmony_ci        return d
2345f9996aaSopenharmony_ci
2355f9996aaSopenharmony_ci    def parse_number(self) -> int:
2365f9996aaSopenharmony_ci        self.consume_whitespace()
2375f9996aaSopenharmony_ci        if self.is_done():
2385f9996aaSopenharmony_ci            raise GNException('Expected number but got nothing.')
2395f9996aaSopenharmony_ci
2405f9996aaSopenharmony_ci        begin = self.cur
2415f9996aaSopenharmony_ci
2425f9996aaSopenharmony_ci        # The first character can include a negative sign.
2435f9996aaSopenharmony_ci        if not self.is_done() and _is_digit_or_minus(self.input[self.cur]):
2445f9996aaSopenharmony_ci            self.cur += 1
2455f9996aaSopenharmony_ci        while not self.is_done() and self.input[self.cur].isdigit():
2465f9996aaSopenharmony_ci            self.cur += 1
2475f9996aaSopenharmony_ci
2485f9996aaSopenharmony_ci        number_string = self.input[begin:self.cur]
2495f9996aaSopenharmony_ci        if not len(number_string) or number_string == '-':
2505f9996aaSopenharmony_ci            raise GNException("Not a valid number.")
2515f9996aaSopenharmony_ci        return int(number_string)
2525f9996aaSopenharmony_ci
2535f9996aaSopenharmony_ci    def parse_string(self) -> str:
2545f9996aaSopenharmony_ci        self.consume_whitespace()
2555f9996aaSopenharmony_ci        if self.is_done():
2565f9996aaSopenharmony_ci            raise GNException('Expected string but got nothing.')
2575f9996aaSopenharmony_ci
2585f9996aaSopenharmony_ci        if self.input[self.cur] != '"':
2595f9996aaSopenharmony_ci            raise GNException('Expected string beginning in a " but got:\n  ' +
2605f9996aaSopenharmony_ci                              self.input[self.cur:])
2615f9996aaSopenharmony_ci        self.cur += 1  # Skip over quote.
2625f9996aaSopenharmony_ci
2635f9996aaSopenharmony_ci        begin = self.cur
2645f9996aaSopenharmony_ci        while not self.is_done() and self.input[self.cur] != '"':
2655f9996aaSopenharmony_ci            if self.input[self.cur] == '\\':
2665f9996aaSopenharmony_ci                self.cur += 1  # Skip over the backslash.
2675f9996aaSopenharmony_ci                if self.is_done():
2685f9996aaSopenharmony_ci                    raise GNException("String ends in a backslash in:\n  " +
2695f9996aaSopenharmony_ci                                      self.input)
2705f9996aaSopenharmony_ci            self.cur += 1
2715f9996aaSopenharmony_ci
2725f9996aaSopenharmony_ci        if self.is_done():
2735f9996aaSopenharmony_ci            raise GNException('Unterminated string:\n  ' + self.input[begin:])
2745f9996aaSopenharmony_ci
2755f9996aaSopenharmony_ci        end = self.cur
2765f9996aaSopenharmony_ci        self.cur += 1  # Consume trailing ".
2775f9996aaSopenharmony_ci
2785f9996aaSopenharmony_ci        return unescape_gn_string(self.input[begin:end])
2795f9996aaSopenharmony_ci
2805f9996aaSopenharmony_ci    def parse_list(self):
2815f9996aaSopenharmony_ci        self.consume_whitespace()
2825f9996aaSopenharmony_ci        if self.is_done():
2835f9996aaSopenharmony_ci            raise GNException('Expected list but got nothing.')
2845f9996aaSopenharmony_ci
2855f9996aaSopenharmony_ci        # Skip over opening '['.
2865f9996aaSopenharmony_ci        if self.input[self.cur] != '[':
2875f9996aaSopenharmony_ci            raise GNException("Expected [ for list but got:\n  " +
2885f9996aaSopenharmony_ci                              self.input[self.cur:])
2895f9996aaSopenharmony_ci        self.cur += 1
2905f9996aaSopenharmony_ci        self.consume_whitespace()
2915f9996aaSopenharmony_ci        if self.is_done():
2925f9996aaSopenharmony_ci            raise GNException("Unterminated list:\n  " + self.input)
2935f9996aaSopenharmony_ci
2945f9996aaSopenharmony_ci        list_result = []
2955f9996aaSopenharmony_ci        previous_had_trailing_comma = True
2965f9996aaSopenharmony_ci        while not self.is_done():
2975f9996aaSopenharmony_ci            if self.input[self.cur] == ']':
2985f9996aaSopenharmony_ci                self.cur += 1  # Skip over ']'.
2995f9996aaSopenharmony_ci                return list_result
3005f9996aaSopenharmony_ci
3015f9996aaSopenharmony_ci            if not previous_had_trailing_comma:
3025f9996aaSopenharmony_ci                raise GNException("List items not separated by comma.")
3035f9996aaSopenharmony_ci
3045f9996aaSopenharmony_ci            list_result += [self._parse_allow_trailing()]
3055f9996aaSopenharmony_ci            self.consume_whitespace()
3065f9996aaSopenharmony_ci            if self.is_done():
3075f9996aaSopenharmony_ci                break
3085f9996aaSopenharmony_ci
3095f9996aaSopenharmony_ci            # Consume comma if there is one.
3105f9996aaSopenharmony_ci            previous_had_trailing_comma = self.input[self.cur] == ','
3115f9996aaSopenharmony_ci            if previous_had_trailing_comma:
3125f9996aaSopenharmony_ci                # Consume comma.
3135f9996aaSopenharmony_ci                self.cur += 1
3145f9996aaSopenharmony_ci                self.consume_whitespace()
3155f9996aaSopenharmony_ci
3165f9996aaSopenharmony_ci        raise GNException("Unterminated list:\n  " + self.input)
3175f9996aaSopenharmony_ci
3185f9996aaSopenharmony_ci    def _constant_follows(self, constant) -> bool:
3195f9996aaSopenharmony_ci        """Returns true if the given constant follows immediately at the
3205f9996aaSopenharmony_ci        current location in the input. If it does, the text is consumed and
3215f9996aaSopenharmony_ci        the function returns true. Otherwise, returns false and the current
3225f9996aaSopenharmony_ci        position is unchanged."""
3235f9996aaSopenharmony_ci        end = self.cur + len(constant)
3245f9996aaSopenharmony_ci        if end > len(self.input):
3255f9996aaSopenharmony_ci            return False  # Not enough room.
3265f9996aaSopenharmony_ci        if self.input[self.cur:end] == constant:
3275f9996aaSopenharmony_ci            self.cur = end
3285f9996aaSopenharmony_ci            return True
3295f9996aaSopenharmony_ci        return False
3305f9996aaSopenharmony_ci
3315f9996aaSopenharmony_ci    def _parse_allow_trailing(self):
3325f9996aaSopenharmony_ci        """Internal version of Parse that doesn't check for trailing stuff."""
3335f9996aaSopenharmony_ci        self.consume_whitespace()
3345f9996aaSopenharmony_ci        if self.is_done():
3355f9996aaSopenharmony_ci            raise GNException("Expected input to parse.")
3365f9996aaSopenharmony_ci
3375f9996aaSopenharmony_ci        next_char = self.input[self.cur]
3385f9996aaSopenharmony_ci        if next_char == '[':
3395f9996aaSopenharmony_ci            return self.parse_list()
3405f9996aaSopenharmony_ci        elif _is_digit_or_minus(next_char):
3415f9996aaSopenharmony_ci            return self.parse_number()
3425f9996aaSopenharmony_ci        elif next_char == '"':
3435f9996aaSopenharmony_ci            return self.parse_string()
3445f9996aaSopenharmony_ci        elif self._constant_follows('true'):
3455f9996aaSopenharmony_ci            return True
3465f9996aaSopenharmony_ci        elif self._constant_follows('false'):
3475f9996aaSopenharmony_ci            return False
3485f9996aaSopenharmony_ci        else:
3495f9996aaSopenharmony_ci            raise GNException("Unexpected token: " + self.input[self.cur:])
3505f9996aaSopenharmony_ci
3515f9996aaSopenharmony_ci    def _parse_ident(self) -> str:
3525f9996aaSopenharmony_ci        ident = ''
3535f9996aaSopenharmony_ci
3545f9996aaSopenharmony_ci        next_char = self.input[self.cur]
3555f9996aaSopenharmony_ci        if not next_char.isalpha() and not next_char == '_':
3565f9996aaSopenharmony_ci            raise GNException("Expected an identifier: " + self.input[self.cur:])
3575f9996aaSopenharmony_ci
3585f9996aaSopenharmony_ci        ident += next_char
3595f9996aaSopenharmony_ci        self.cur += 1
3605f9996aaSopenharmony_ci
3615f9996aaSopenharmony_ci        next_char = self.input[self.cur]
3625f9996aaSopenharmony_ci        while next_char.isalpha() or next_char.isdigit() or next_char == '_':
3635f9996aaSopenharmony_ci            ident += next_char
3645f9996aaSopenharmony_ci            self.cur += 1
3655f9996aaSopenharmony_ci            next_char = self.input[self.cur]
3665f9996aaSopenharmony_ci
3675f9996aaSopenharmony_ci        return ident
368