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