diff options
Diffstat (limited to 'chromium/build/gn_helpers.py')
-rw-r--r-- | chromium/build/gn_helpers.py | 262 |
1 files changed, 180 insertions, 82 deletions
diff --git a/chromium/build/gn_helpers.py b/chromium/build/gn_helpers.py index b90c2fbbd71..a240d807a0d 100644 --- a/chromium/build/gn_helpers.py +++ b/chromium/build/gn_helpers.py @@ -4,71 +4,151 @@ """Helper functions useful when writing scripts that integrate with GN. -The main functions are ToGNString and FromGNString which convert between +The main functions are ToGNString() and FromGNString(), to convert between serialized GN veriables and Python variables. -To use in a random python file in the build: +To use in an arbitrary Python file in the build: import os import sys sys.path.append(os.path.join(os.path.dirname(__file__), - os.pardir, os.pardir, "build")) + os.pardir, os.pardir, 'build')) import gn_helpers Where the sequence of parameters to join is the relative path from your source -file to the build directory.""" +file to the build directory. +""" import os import re import sys +_CHROMIUM_ROOT = os.path.join(os.path.dirname(__file__), os.pardir) + IMPORT_RE = re.compile(r'^import\("//(\S+)"\)') -class GNException(Exception): +class GNError(Exception): pass -def ToGNString(value, allow_dicts = True): - """Returns a stringified GN equivalent of the Python value. - - allow_dicts indicates if this function will allow converting dictionaries - to GN scopes. This is only possible at the top level, you can't nest a - GN scope in a list, so this should be set to False for recursive calls.""" - if isinstance(value, str): - if value.find('\n') >= 0: - raise GNException("Trying to print a string with a newline in it.") - return '"' + \ - value.replace('\\', '\\\\').replace('"', '\\"').replace('$', '\\$') + \ - '"' - - if sys.version_info.major < 3 and isinstance(value, unicode): - return ToGNString(value.encode('utf-8')) - - if isinstance(value, bool): - if value: - return "true" - return "false" - - if isinstance(value, list): - return '[ %s ]' % ', '.join(ToGNString(v) for v in value) - - if isinstance(value, dict): - if not allow_dicts: - raise GNException("Attempting to recursively print a dictionary.") - result = "" - for key in sorted(value): - if not isinstance(key, str) and not isinstance(key, unicode): - raise GNException("Dictionary key is not a string.") - result += "%s = %s\n" % (key, ToGNString(value[key], False)) - return result +# Computes ASCII code of an element of encoded Python 2 str / Python 3 bytes. +_Ord = ord if sys.version_info.major < 3 else lambda c: c + + +def _TranslateToGnChars(s): + for decoded_ch in s.encode('utf-8'): # str in Python 2, bytes in Python 3. + code = _Ord(decoded_ch) # int + if code in (34, 36, 92): # For '"', '$', or '\\'. + yield '\\' + chr(code) + elif 32 <= code < 127: + yield chr(code) + else: + yield '$0x%02X' % code - if isinstance(value, int): - return str(value) - raise GNException("Unsupported type when printing to GN.") +def ToGNString(value, pretty=False): + """Returns a stringified GN equivalent of a Python value. + + Args: + value: The Python value to convert. + pretty: Whether to pretty print. If true, then non-empty lists are rendered + recursively with one item per line, with indents. Otherwise lists are + rendered without new line. + Returns: + The stringified GN equivalent to |value|. + + Raises: + GNError: |value| cannot be printed to GN. + """ + + if sys.version_info.major < 3: + basestring_compat = basestring + else: + basestring_compat = str + + # Emits all output tokens without intervening whitespaces. + def GenerateTokens(v, level): + if isinstance(v, basestring_compat): + yield '"' + ''.join(_TranslateToGnChars(v)) + '"' + + elif isinstance(v, bool): + yield 'true' if v else 'false' + + elif isinstance(v, int): + yield str(v) + + elif isinstance(v, list): + yield '[' + for i, item in enumerate(v): + if i > 0: + yield ',' + for tok in GenerateTokens(item, level + 1): + yield tok + yield ']' + + elif isinstance(v, dict): + if level > 0: + raise GNError('Attempting to recursively print a dictionary.') + for key in sorted(v): + if not isinstance(key, basestring_compat): + raise GNError('Dictionary key is not a string.') + if not key or key[0].isdigit() or not key.replace('_', '').isalnum(): + raise GNError('Dictionary key is not a valid GN identifier.') + yield key # No quotations. + yield '=' + for tok in GenerateTokens(value[key], level + 1): + yield tok + + else: # Not supporting float: Add only when needed. + raise GNError('Unsupported type when printing to GN.') + + can_start = lambda tok: tok and tok not in ',]=' + can_end = lambda tok: tok and tok not in ',[=' + + # Adds whitespaces, trying to keep everything (except dicts) in 1 line. + def PlainGlue(gen): + prev_tok = None + for i, tok in enumerate(gen): + if i > 0: + if can_end(prev_tok) and can_start(tok): + yield '\n' # New dict item. + elif prev_tok == '[' and tok == ']': + yield ' ' # Special case for []. + elif tok != ',': + yield ' ' + yield tok + prev_tok = tok + + # Adds whitespaces so non-empty lists can span multiple lines, with indent. + def PrettyGlue(gen): + prev_tok = None + level = 0 + for i, tok in enumerate(gen): + if i > 0: + if can_end(prev_tok) and can_start(tok): + yield '\n' + ' ' * level # New dict item. + elif tok == '=' or prev_tok in '=': + yield ' ' # Separator before and after '=', on same line. + if tok == ']': + level -= 1 + if int(prev_tok == '[') + int(tok == ']') == 1: # Exclude '[]' case. + yield '\n' + ' ' * level + yield tok + if tok == '[': + level += 1 + if tok == ',': + yield '\n' + ' ' * level + prev_tok = tok + + token_gen = GenerateTokens(value, 0) + ret = ''.join((PrettyGlue if pretty else PlainGlue)(token_gen)) + # Add terminating '\n' for dict |value| or multi-line output. + if isinstance(value, dict) or '\n' in ret: + return ret + '\n' + return ret def FromGNString(input_string): @@ -106,7 +186,8 @@ def FromGNString(input_string): The main use cases for this is for other types, in particular lists. When using string interpolation on a list (as in the top example) the embedded strings will be quoted and escaped according to GN rules so the list can be - re-parsed to get the same result.""" + re-parsed to get the same result. + """ parser = GNValueParser(input_string) return parser.Parse() @@ -120,7 +201,7 @@ def FromGNArgs(input_string): gn assignments, this returns a Python dict, i.e.: - FromGNArgs("foo=true\nbar=1\n") -> { 'foo': True, 'bar': 1 }. + FromGNArgs('foo=true\nbar=1\n') -> { 'foo': True, 'bar': 1 }. Only simple types and lists supported; variables, structs, calls and other, more complicated things are not. @@ -137,7 +218,11 @@ def UnescapeGNString(value): Be careful not to feed with input from a Python parsing function like 'ast' because it will do Python unescaping, which will be incorrect when - fed into the GN unescaper.""" + fed into the GN unescaper. + + Args: + value: Input string to unescape. + """ result = '' i = 0 while i < len(value): @@ -158,7 +243,7 @@ def UnescapeGNString(value): def _IsDigitOrMinus(char): - return char in "-0123456789" + return char in '-0123456789' class GNValueParser(object): @@ -167,10 +252,13 @@ class GNValueParser(object): Normally you would use the wrapper function FromGNValue() below. If you expect input as a specific type, you can also call one of the Parse* - functions directly. All functions throw GNException on invalid input. """ - def __init__(self, string): + functions directly. All functions throw GNError on invalid input. + """ + + def __init__(self, string, checkout_root=_CHROMIUM_ROOT): self.input = string self.cur = 0 + self.checkout_root = checkout_root def IsDone(self): return self.cur == len(self.input) @@ -189,9 +277,8 @@ class GNValueParser(object): continue regex_match = IMPORT_RE.match(line) if not regex_match: - raise GNException('Not a valid import string: %s' % line) - import_path = os.path.join( - os.path.dirname(__file__), os.pardir, regex_match.group(1)) + raise GNError('Not a valid import string: %s' % line) + import_path = os.path.join(self.checkout_root, regex_match.group(1)) with open(import_path) as f: imported_args = f.read() self.input = self.input.replace(line, imported_args) @@ -220,31 +307,37 @@ class GNValueParser(object): def Parse(self): """Converts a string representing a printed GN value to the Python type. - See additional usage notes on FromGNString above. + See additional usage notes on FromGNString() above. - - GN booleans ('true', 'false') will be converted to Python booleans. + * GN booleans ('true', 'false') will be converted to Python booleans. - - GN numbers ('123') will be converted to Python numbers. + * GN numbers ('123') will be converted to Python numbers. - - GN strings (double-quoted as in '"asdf"') will be converted to Python + * GN strings (double-quoted as in '"asdf"') will be converted to Python strings with GN escaping rules. GN string interpolation (embedded variables preceded by $) are not supported and will be returned as literals. - - GN lists ('[1, "asdf", 3]') will be converted to Python lists. + * GN lists ('[1, "asdf", 3]') will be converted to Python lists. - - GN scopes ('{ ... }') are not supported.""" + * GN scopes ('{ ... }') are not supported. + + Raises: + GNError: Parse fails. + """ result = self._ParseAllowTrailing() self.ConsumeWhitespace() if not self.IsDone(): - raise GNException("Trailing input after parsing:\n " + - self.input[self.cur:]) + raise GNError("Trailing input after parsing:\n " + self.input[self.cur:]) return result def ParseArgs(self): """Converts a whitespace-separated list of ident=literals to a dict. - See additional usage notes on FromGNArgs, above. + See additional usage notes on FromGNArgs(), above. + + Raises: + GNError: Parse fails. """ d = {} @@ -255,21 +348,22 @@ class GNValueParser(object): ident = self._ParseIdent() self.ConsumeWhitespace() if self.input[self.cur] != '=': - raise GNException("Unexpected token: " + self.input[self.cur:]) + raise GNError("Unexpected token: " + self.input[self.cur:]) self.cur += 1 self.ConsumeWhitespace() val = self._ParseAllowTrailing() self.ConsumeWhitespace() self.ConsumeComment() + self.ConsumeWhitespace() d[ident] = val return d def _ParseAllowTrailing(self): - """Internal version of Parse that doesn't check for trailing stuff.""" + """Internal version of Parse() that doesn't check for trailing stuff.""" self.ConsumeWhitespace() if self.IsDone(): - raise GNException("Expected input to parse.") + raise GNError("Expected input to parse.") next_char = self.input[self.cur] if next_char == '[': @@ -283,14 +377,14 @@ class GNValueParser(object): elif self._ConstantFollows('false'): return False else: - raise GNException("Unexpected token: " + self.input[self.cur:]) + raise GNError("Unexpected token: " + self.input[self.cur:]) def _ParseIdent(self): ident = '' next_char = self.input[self.cur] if not next_char.isalpha() and not next_char=='_': - raise GNException("Expected an identifier: " + self.input[self.cur:]) + raise GNError("Expected an identifier: " + self.input[self.cur:]) ident += next_char self.cur += 1 @@ -306,7 +400,7 @@ class GNValueParser(object): def ParseNumber(self): self.ConsumeWhitespace() if self.IsDone(): - raise GNException('Expected number but got nothing.') + raise GNError('Expected number but got nothing.') begin = self.cur @@ -318,17 +412,17 @@ class GNValueParser(object): number_string = self.input[begin:self.cur] if not len(number_string) or number_string == '-': - raise GNException("Not a valid number.") + raise GNError('Not a valid number.') return int(number_string) def ParseString(self): self.ConsumeWhitespace() if self.IsDone(): - raise GNException('Expected string but got nothing.') + raise GNError('Expected string but got nothing.') if self.input[self.cur] != '"': - raise GNException('Expected string beginning in a " but got:\n ' + - self.input[self.cur:]) + raise GNError('Expected string beginning in a " but got:\n ' + + self.input[self.cur:]) self.cur += 1 # Skip over quote. begin = self.cur @@ -336,12 +430,11 @@ class GNValueParser(object): if self.input[self.cur] == '\\': self.cur += 1 # Skip over the backslash. if self.IsDone(): - raise GNException("String ends in a backslash in:\n " + - self.input) + raise GNError('String ends in a backslash in:\n ' + self.input) self.cur += 1 if self.IsDone(): - raise GNException('Unterminated string:\n ' + self.input[begin:]) + raise GNError('Unterminated string:\n ' + self.input[begin:]) end = self.cur self.cur += 1 # Consume trailing ". @@ -351,16 +444,15 @@ class GNValueParser(object): def ParseList(self): self.ConsumeWhitespace() if self.IsDone(): - raise GNException('Expected list but got nothing.') + raise GNError('Expected list but got nothing.') # Skip over opening '['. if self.input[self.cur] != '[': - raise GNException("Expected [ for list but got:\n " + - self.input[self.cur:]) + raise GNError('Expected [ for list but got:\n ' + self.input[self.cur:]) self.cur += 1 self.ConsumeWhitespace() if self.IsDone(): - raise GNException("Unterminated list:\n " + self.input) + raise GNError('Unterminated list:\n ' + self.input) list_result = [] previous_had_trailing_comma = True @@ -370,7 +462,7 @@ class GNValueParser(object): return list_result if not previous_had_trailing_comma: - raise GNException("List items not separated by comma.") + raise GNError('List items not separated by comma.') list_result += [ self._ParseAllowTrailing() ] self.ConsumeWhitespace() @@ -384,13 +476,19 @@ class GNValueParser(object): self.cur += 1 self.ConsumeWhitespace() - raise GNException("Unterminated list:\n " + self.input) + raise GNError('Unterminated list:\n ' + self.input) def _ConstantFollows(self, constant): - """Returns true if the given constant follows immediately at the current - location in the input. If it does, the text is consumed and the function - returns true. Otherwise, returns false and the current position is - unchanged.""" + """Checks and maybe consumes a string constant at current input location. + + Param: + constant: The string constant to check. + + Returns: + True if |constant| follows immediately at the current location in the + input. In this case, the string is consumed as a side effect. Otherwise, + returns False and the current position is unchanged. + """ end = self.cur + len(constant) if end > len(self.input): return False # Not enough room. |