diff options
Diffstat (limited to 'third_party/libwebrtc/build/gn_helpers.py')
-rw-r--r-- | third_party/libwebrtc/build/gn_helpers.py | 542 |
1 files changed, 542 insertions, 0 deletions
diff --git a/third_party/libwebrtc/build/gn_helpers.py b/third_party/libwebrtc/build/gn_helpers.py new file mode 100644 index 0000000000..c6e4129b21 --- /dev/null +++ b/third_party/libwebrtc/build/gn_helpers.py @@ -0,0 +1,542 @@ +# Copyright 2014 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Helper functions useful when writing scripts that integrate with GN. + +The main functions are ToGNString() and FromGNString(), to convert between +serialized GN veriables and Python variables. + +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')) + import gn_helpers + +Where the sequence of parameters to join is the relative path from your source +file to the build directory. +""" + +import json +import os +import re +import sys + + +_CHROMIUM_ROOT = os.path.join(os.path.dirname(__file__), os.pardir) + +BUILD_VARS_FILENAME = 'build_vars.json' +IMPORT_RE = re.compile(r'^import\("//(\S+)"\)') + + +class GNError(Exception): + pass + + +# 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 + + +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: + yield '{' + 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(v[key], level + 1): + yield tok + if level > 0: + yield '}' + + 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 in ']}': + level -= 1 + # Exclude '[]' and '{}' cases. + if int(prev_tok == '[') + int(tok == ']') == 1 or \ + int(prev_tok == '{') + int(tok == '}') == 1: + yield '\n' + ' ' * level + yield tok + if tok in '[{': + 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): + """Converts the input string from a GN serialized value to Python values. + + For details on supported types see GNValueParser.Parse() below. + + If your GN script did: + something = [ "file1", "file2" ] + args = [ "--values=$something" ] + The command line would look something like: + --values="[ \"file1\", \"file2\" ]" + Which when interpreted as a command line gives the value: + [ "file1", "file2" ] + + You can parse this into a Python list using GN rules with: + input_values = FromGNValues(options.values) + Although the Python 'ast' module will parse many forms of such input, it + will not handle GN escaping properly, nor GN booleans. You should use this + function instead. + + + A NOTE ON STRING HANDLING: + + If you just pass a string on the command line to your Python script, or use + string interpolation on a string variable, the strings will not be quoted: + str = "asdf" + args = [ str, "--value=$str" ] + Will yield the command line: + asdf --value=asdf + The unquoted asdf string will not be valid input to this function, which + accepts only quoted strings like GN scripts. In such cases, you can just use + the Python string literal directly. + + 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. + """ + parser = GNValueParser(input_string) + return parser.Parse() + + +def FromGNArgs(input_string): + """Converts a string with a bunch of gn arg assignments into a Python dict. + + Given a whitespace-separated list of + + <ident> = (integer | string | boolean | <list of the former>) + + gn assignments, this returns a Python dict, i.e.: + + 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. + + This routine is meant to handle only the simple sorts of values that + arise in parsing --args. + """ + parser = GNValueParser(input_string) + return parser.ParseArgs() + + +def UnescapeGNString(value): + """Given a string with GN escaping, returns the unescaped string. + + 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. + + Args: + value: Input string to unescape. + """ + result = '' + i = 0 + while i < len(value): + if value[i] == '\\': + if i < len(value) - 1: + next_char = value[i + 1] + if next_char in ('$', '"', '\\'): + # These are the escaped characters GN supports. + result += next_char + i += 1 + else: + # Any other backslash is a literal. + result += '\\' + else: + result += value[i] + i += 1 + return result + + +def _IsDigitOrMinus(char): + return char in '-0123456789' + + +class GNValueParser(object): + """Duplicates GN parsing of values and converts to Python types. + + 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 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) + + def ReplaceImports(self): + """Replaces import(...) lines with the contents of the imports. + + Recurses on itself until there are no imports remaining, in the case of + nested imports. + """ + lines = self.input.splitlines() + if not any(line.startswith('import(') for line in lines): + return + for line in lines: + if not line.startswith('import('): + continue + regex_match = IMPORT_RE.match(line) + if not regex_match: + 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) + # Call ourselves again if we've just replaced an import() with additional + # imports. + self.ReplaceImports() + + + def _ConsumeWhitespace(self): + while not self.IsDone() and self.input[self.cur] in ' \t\n': + self.cur += 1 + + def ConsumeCommentAndWhitespace(self): + self._ConsumeWhitespace() + + # Consume each comment, line by line. + while not self.IsDone() and self.input[self.cur] == '#': + # Consume the rest of the comment, up until the end of the line. + while not self.IsDone() and self.input[self.cur] != '\n': + self.cur += 1 + # Move the cursor to the next line (if there is one). + if not self.IsDone(): + self.cur += 1 + + self._ConsumeWhitespace() + + def Parse(self): + """Converts a string representing a printed GN value to the Python type. + + See additional usage notes on FromGNString() above. + + * GN booleans ('true', 'false') will be converted to Python booleans. + + * GN numbers ('123') will be converted to Python numbers. + + * 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 scopes ('{ ... }') are not supported. + + Raises: + GNError: Parse fails. + """ + result = self._ParseAllowTrailing() + self.ConsumeCommentAndWhitespace() + if not self.IsDone(): + 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. + + Raises: + GNError: Parse fails. + """ + d = {} + + self.ReplaceImports() + self.ConsumeCommentAndWhitespace() + + while not self.IsDone(): + ident = self._ParseIdent() + self.ConsumeCommentAndWhitespace() + if self.input[self.cur] != '=': + raise GNError("Unexpected token: " + self.input[self.cur:]) + self.cur += 1 + self.ConsumeCommentAndWhitespace() + val = self._ParseAllowTrailing() + self.ConsumeCommentAndWhitespace() + d[ident] = val + + return d + + def _ParseAllowTrailing(self): + """Internal version of Parse() that doesn't check for trailing stuff.""" + self.ConsumeCommentAndWhitespace() + if self.IsDone(): + raise GNError("Expected input to parse.") + + next_char = self.input[self.cur] + if next_char == '[': + return self.ParseList() + elif next_char == '{': + return self.ParseScope() + elif _IsDigitOrMinus(next_char): + return self.ParseNumber() + elif next_char == '"': + return self.ParseString() + elif self._ConstantFollows('true'): + return True + elif self._ConstantFollows('false'): + return False + else: + 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 GNError("Expected an identifier: " + self.input[self.cur:]) + + ident += next_char + self.cur += 1 + + next_char = self.input[self.cur] + while next_char.isalpha() or next_char.isdigit() or next_char=='_': + ident += next_char + self.cur += 1 + next_char = self.input[self.cur] + + return ident + + def ParseNumber(self): + self.ConsumeCommentAndWhitespace() + if self.IsDone(): + raise GNError('Expected number but got nothing.') + + begin = self.cur + + # The first character can include a negative sign. + if not self.IsDone() and _IsDigitOrMinus(self.input[self.cur]): + self.cur += 1 + while not self.IsDone() and self.input[self.cur].isdigit(): + self.cur += 1 + + number_string = self.input[begin:self.cur] + if not len(number_string) or number_string == '-': + raise GNError('Not a valid number.') + return int(number_string) + + def ParseString(self): + self.ConsumeCommentAndWhitespace() + if self.IsDone(): + raise GNError('Expected string but got nothing.') + + if 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 + while not self.IsDone() and self.input[self.cur] != '"': + if self.input[self.cur] == '\\': + self.cur += 1 # Skip over the backslash. + if self.IsDone(): + raise GNError('String ends in a backslash in:\n ' + self.input) + self.cur += 1 + + if self.IsDone(): + raise GNError('Unterminated string:\n ' + self.input[begin:]) + + end = self.cur + self.cur += 1 # Consume trailing ". + + return UnescapeGNString(self.input[begin:end]) + + def ParseList(self): + self.ConsumeCommentAndWhitespace() + if self.IsDone(): + raise GNError('Expected list but got nothing.') + + # Skip over opening '['. + if self.input[self.cur] != '[': + raise GNError('Expected [ for list but got:\n ' + self.input[self.cur:]) + self.cur += 1 + self.ConsumeCommentAndWhitespace() + if self.IsDone(): + raise GNError('Unterminated list:\n ' + self.input) + + list_result = [] + previous_had_trailing_comma = True + while not self.IsDone(): + if self.input[self.cur] == ']': + self.cur += 1 # Skip over ']'. + return list_result + + if not previous_had_trailing_comma: + raise GNError('List items not separated by comma.') + + list_result += [ self._ParseAllowTrailing() ] + self.ConsumeCommentAndWhitespace() + if self.IsDone(): + break + + # Consume comma if there is one. + previous_had_trailing_comma = self.input[self.cur] == ',' + if previous_had_trailing_comma: + # Consume comma. + self.cur += 1 + self.ConsumeCommentAndWhitespace() + + raise GNError('Unterminated list:\n ' + self.input) + + def ParseScope(self): + self.ConsumeCommentAndWhitespace() + if self.IsDone(): + raise GNError('Expected scope but got nothing.') + + # Skip over opening '{'. + if self.input[self.cur] != '{': + raise GNError('Expected { for scope but got:\n ' + self.input[self.cur:]) + self.cur += 1 + self.ConsumeCommentAndWhitespace() + if self.IsDone(): + raise GNError('Unterminated scope:\n ' + self.input) + + scope_result = {} + while not self.IsDone(): + if self.input[self.cur] == '}': + self.cur += 1 + return scope_result + + ident = self._ParseIdent() + self.ConsumeCommentAndWhitespace() + if self.input[self.cur] != '=': + raise GNError("Unexpected token: " + self.input[self.cur:]) + self.cur += 1 + self.ConsumeCommentAndWhitespace() + val = self._ParseAllowTrailing() + self.ConsumeCommentAndWhitespace() + scope_result[ident] = val + + raise GNError('Unterminated scope:\n ' + self.input) + + def _ConstantFollows(self, constant): + """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. + if self.input[self.cur:end] == constant: + self.cur = end + return True + return False + + +def ReadBuildVars(output_directory): + """Parses $output_directory/build_vars.json into a dict.""" + with open(os.path.join(output_directory, BUILD_VARS_FILENAME)) as f: + return json.load(f) |