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
_Ord = ord if sys.version_info.major < 3 else lambda c: c
def _TranslateToGnChars(s):
for decoded_ch in s.encode('utf-8'): code = _Ord(decoded_ch) if code in (34, 36, 92): yield '\\' + chr(code)
elif 32 <= code < 127:
yield chr(code)
else:
yield '$0x%02X' % code
def ToGNString(value, pretty=False):
if sys.version_info.major < 3:
basestring_compat = basestring
else:
basestring_compat = str
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 yield '='
for tok in GenerateTokens(v[key], level + 1):
yield tok
if level > 0:
yield '}'
else: 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 ',{[='
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' elif prev_tok == '[' and tok == ']':
yield ' ' elif tok != ',':
yield ' '
yield tok
prev_tok = tok
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 elif tok == '=' or prev_tok in '=':
yield ' ' if tok in ']}':
level -= 1
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))
if isinstance(value, dict) or '\n' in ret:
return ret + '\n'
return ret
def FromGNString(input_string):
parser = GNValueParser(input_string)
return parser.Parse()
def FromGNArgs(input_string):
parser = GNValueParser(input_string)
return parser.ParseArgs()
def UnescapeGNString(value):
result = ''
i = 0
while i < len(value):
if value[i] == '\\':
if i < len(value) - 1:
next_char = value[i + 1]
if next_char in ('$', '"', '\\'):
result += next_char
i += 1
else:
result += '\\'
else:
result += value[i]
i += 1
return result
def _IsDigitOrMinus(char):
return char in '-0123456789'
class GNValueParser(object):
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):
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)
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()
while not self.IsDone() and self.input[self.cur] == '#':
while not self.IsDone() and self.input[self.cur] != '\n':
self.cur += 1
if not self.IsDone():
self.cur += 1
self._ConsumeWhitespace()
def Parse(self):
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):
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):
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
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
begin = self.cur
while not self.IsDone() and self.input[self.cur] != '"':
if self.input[self.cur] == '\\':
self.cur += 1 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
return UnescapeGNString(self.input[begin:end])
def ParseList(self):
self.ConsumeCommentAndWhitespace()
if self.IsDone():
raise GNError('Expected list but got nothing.')
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 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
previous_had_trailing_comma = self.input[self.cur] == ','
if previous_had_trailing_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.')
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):
end = self.cur + len(constant)
if end > len(self.input):
return False if self.input[self.cur:end] == constant:
self.cur = end
return True
return False
def ReadBuildVars(output_directory):
with open(os.path.join(output_directory, BUILD_VARS_FILENAME)) as f:
return json.load(f)