blob: 82586586873dcf0336b5f50d3b841d7dbfac2448 [file] [log] [blame]
[email protected]530bf7e82014-04-06 04:35:101# Copyright 2014 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
brettw9dffb542016-01-22 18:40:035"""Helper functions useful when writing scripts that integrate with GN.
6
Samuel Huang1ea5180e2020-05-25 16:29:407The main functions are ToGNString() and FromGNString(), to convert between
brettw81687d82016-01-29 23:23:108serialized GN veriables and Python variables.
9
Samuel Huang1ea5180e2020-05-25 16:29:4010To use in an arbitrary Python file in the build:
brettw81687d82016-01-29 23:23:1011
12 import os
13 import sys
14
15 sys.path.append(os.path.join(os.path.dirname(__file__),
Samuel Huang1ea5180e2020-05-25 16:29:4016 os.pardir, os.pardir, 'build'))
brettw81687d82016-01-29 23:23:1017 import gn_helpers
18
19Where the sequence of parameters to join is the relative path from your source
Samuel Huang1ea5180e2020-05-25 16:29:4020file to the build directory.
21"""
[email protected]530bf7e82014-04-06 04:35:1022
Andrew Grieve169e377d2020-08-10 18:12:1423import json
Ben Pastene00156a22020-03-23 18:13:1024import os
25import re
Raul Tambre4197d3a2019-03-19 15:04:2026import sys
27
28
Ben Pasteneb52227f2020-06-01 22:41:3029_CHROMIUM_ROOT = os.path.join(os.path.dirname(__file__), os.pardir)
30
Andrew Grieve169e377d2020-08-10 18:12:1431BUILD_VARS_FILENAME = 'build_vars.json'
Ben Pastene00156a22020-03-23 18:13:1032IMPORT_RE = re.compile(r'^import\("//(\S+)"\)')
33
34
Samuel Huang1ea5180e2020-05-25 16:29:4035class GNError(Exception):
[email protected]530bf7e82014-04-06 04:35:1036 pass
37
38
Samuel Huang007ab8b82020-06-05 21:44:3439# Computes ASCII code of an element of encoded Python 2 str / Python 3 bytes.
40_Ord = ord if sys.version_info.major < 3 else lambda c: c
41
42
43def _TranslateToGnChars(s):
44 for decoded_ch in s.encode('utf-8'): # str in Python 2, bytes in Python 3.
45 code = _Ord(decoded_ch) # int
46 if code in (34, 36, 92): # For '"', '$', or '\\'.
47 yield '\\' + chr(code)
48 elif 32 <= code < 127:
49 yield chr(code)
50 else:
51 yield '$0x%02X' % code
52
53
54def ToGNString(value, pretty=False):
Samuel Huang1ea5180e2020-05-25 16:29:4055 """Returns a stringified GN equivalent of a Python value.
[email protected]530bf7e82014-04-06 04:35:1056
Samuel Huang1ea5180e2020-05-25 16:29:4057 Args:
58 value: The Python value to convert.
Samuel Huang007ab8b82020-06-05 21:44:3459 pretty: Whether to pretty print. If true, then non-empty lists are rendered
60 recursively with one item per line, with indents. Otherwise lists are
61 rendered without new line.
Samuel Huang1ea5180e2020-05-25 16:29:4062 Returns:
63 The stringified GN equivalent to |value|.
64
65 Raises:
66 GNError: |value| cannot be printed to GN.
67 """
brettw9dffb542016-01-22 18:40:0368
Samuel Huang007ab8b82020-06-05 21:44:3469 if sys.version_info.major < 3:
70 basestring_compat = basestring
71 else:
72 basestring_compat = str
hashimoto7bf1d0a2016-04-04 01:01:5173
Samuel Huang007ab8b82020-06-05 21:44:3474 # Emits all output tokens without intervening whitespaces.
75 def GenerateTokens(v, level):
76 if isinstance(v, basestring_compat):
77 yield '"' + ''.join(_TranslateToGnChars(v)) + '"'
[email protected]530bf7e82014-04-06 04:35:1078
Samuel Huang007ab8b82020-06-05 21:44:3479 elif isinstance(v, bool):
80 yield 'true' if v else 'false'
[email protected]530bf7e82014-04-06 04:35:1081
Samuel Huang007ab8b82020-06-05 21:44:3482 elif isinstance(v, int):
83 yield str(v)
[email protected]530bf7e82014-04-06 04:35:1084
Samuel Huang007ab8b82020-06-05 21:44:3485 elif isinstance(v, list):
86 yield '['
87 for i, item in enumerate(v):
88 if i > 0:
89 yield ','
90 for tok in GenerateTokens(item, level + 1):
91 yield tok
92 yield ']'
[email protected]530bf7e82014-04-06 04:35:1093
Samuel Huang007ab8b82020-06-05 21:44:3494 elif isinstance(v, dict):
95 if level > 0:
96 raise GNError('Attempting to recursively print a dictionary.')
97 for key in sorted(v):
98 if not isinstance(key, basestring_compat):
99 raise GNError('Dictionary key is not a string.')
100 if not key or key[0].isdigit() or not key.replace('_', '').isalnum():
101 raise GNError('Dictionary key is not a valid GN identifier.')
102 yield key # No quotations.
103 yield '='
104 for tok in GenerateTokens(value[key], level + 1):
105 yield tok
106
107 else: # Not supporting float: Add only when needed.
108 raise GNError('Unsupported type when printing to GN.')
109
110 can_start = lambda tok: tok and tok not in ',]='
111 can_end = lambda tok: tok and tok not in ',[='
112
113 # Adds whitespaces, trying to keep everything (except dicts) in 1 line.
114 def PlainGlue(gen):
115 prev_tok = None
116 for i, tok in enumerate(gen):
117 if i > 0:
118 if can_end(prev_tok) and can_start(tok):
119 yield '\n' # New dict item.
120 elif prev_tok == '[' and tok == ']':
121 yield ' ' # Special case for [].
122 elif tok != ',':
123 yield ' '
124 yield tok
125 prev_tok = tok
126
127 # Adds whitespaces so non-empty lists can span multiple lines, with indent.
128 def PrettyGlue(gen):
129 prev_tok = None
130 level = 0
131 for i, tok in enumerate(gen):
132 if i > 0:
133 if can_end(prev_tok) and can_start(tok):
134 yield '\n' + ' ' * level # New dict item.
135 elif tok == '=' or prev_tok in '=':
136 yield ' ' # Separator before and after '=', on same line.
137 if tok == ']':
138 level -= 1
139 if int(prev_tok == '[') + int(tok == ']') == 1: # Exclude '[]' case.
140 yield '\n' + ' ' * level
141 yield tok
142 if tok == '[':
143 level += 1
144 if tok == ',':
145 yield '\n' + ' ' * level
146 prev_tok = tok
147
148 token_gen = GenerateTokens(value, 0)
149 ret = ''.join((PrettyGlue if pretty else PlainGlue)(token_gen))
150 # Add terminating '\n' for dict |value| or multi-line output.
151 if isinstance(value, dict) or '\n' in ret:
152 return ret + '\n'
153 return ret
brettw9dffb542016-01-22 18:40:03154
155
yyanagisawa18ef9302016-10-07 02:35:17156def FromGNString(input_string):
brettw9dffb542016-01-22 18:40:03157 """Converts the input string from a GN serialized value to Python values.
158
159 For details on supported types see GNValueParser.Parse() below.
160
161 If your GN script did:
162 something = [ "file1", "file2" ]
163 args = [ "--values=$something" ]
164 The command line would look something like:
165 --values="[ \"file1\", \"file2\" ]"
166 Which when interpreted as a command line gives the value:
167 [ "file1", "file2" ]
168
169 You can parse this into a Python list using GN rules with:
170 input_values = FromGNValues(options.values)
171 Although the Python 'ast' module will parse many forms of such input, it
172 will not handle GN escaping properly, nor GN booleans. You should use this
173 function instead.
174
175
176 A NOTE ON STRING HANDLING:
177
178 If you just pass a string on the command line to your Python script, or use
179 string interpolation on a string variable, the strings will not be quoted:
180 str = "asdf"
181 args = [ str, "--value=$str" ]
182 Will yield the command line:
183 asdf --value=asdf
184 The unquoted asdf string will not be valid input to this function, which
185 accepts only quoted strings like GN scripts. In such cases, you can just use
186 the Python string literal directly.
187
188 The main use cases for this is for other types, in particular lists. When
189 using string interpolation on a list (as in the top example) the embedded
190 strings will be quoted and escaped according to GN rules so the list can be
Samuel Huang1ea5180e2020-05-25 16:29:40191 re-parsed to get the same result.
192 """
yyanagisawa18ef9302016-10-07 02:35:17193 parser = GNValueParser(input_string)
brettw9dffb542016-01-22 18:40:03194 return parser.Parse()
195
196
yyanagisawa18ef9302016-10-07 02:35:17197def FromGNArgs(input_string):
dpranke65d84dc02016-04-06 00:07:18198 """Converts a string with a bunch of gn arg assignments into a Python dict.
199
200 Given a whitespace-separated list of
201
202 <ident> = (integer | string | boolean | <list of the former>)
203
204 gn assignments, this returns a Python dict, i.e.:
205
Samuel Huang1ea5180e2020-05-25 16:29:40206 FromGNArgs('foo=true\nbar=1\n') -> { 'foo': True, 'bar': 1 }.
dpranke65d84dc02016-04-06 00:07:18207
208 Only simple types and lists supported; variables, structs, calls
209 and other, more complicated things are not.
210
211 This routine is meant to handle only the simple sorts of values that
212 arise in parsing --args.
213 """
yyanagisawa18ef9302016-10-07 02:35:17214 parser = GNValueParser(input_string)
dpranke65d84dc02016-04-06 00:07:18215 return parser.ParseArgs()
216
217
brettw9dffb542016-01-22 18:40:03218def UnescapeGNString(value):
219 """Given a string with GN escaping, returns the unescaped string.
220
221 Be careful not to feed with input from a Python parsing function like
222 'ast' because it will do Python unescaping, which will be incorrect when
Samuel Huang1ea5180e2020-05-25 16:29:40223 fed into the GN unescaper.
224
225 Args:
226 value: Input string to unescape.
227 """
brettw9dffb542016-01-22 18:40:03228 result = ''
229 i = 0
230 while i < len(value):
231 if value[i] == '\\':
232 if i < len(value) - 1:
233 next_char = value[i + 1]
234 if next_char in ('$', '"', '\\'):
235 # These are the escaped characters GN supports.
236 result += next_char
237 i += 1
238 else:
239 # Any other backslash is a literal.
240 result += '\\'
241 else:
242 result += value[i]
243 i += 1
244 return result
245
246
247def _IsDigitOrMinus(char):
Samuel Huang1ea5180e2020-05-25 16:29:40248 return char in '-0123456789'
brettw9dffb542016-01-22 18:40:03249
250
251class GNValueParser(object):
252 """Duplicates GN parsing of values and converts to Python types.
253
254 Normally you would use the wrapper function FromGNValue() below.
255
256 If you expect input as a specific type, you can also call one of the Parse*
Samuel Huang1ea5180e2020-05-25 16:29:40257 functions directly. All functions throw GNError on invalid input.
258 """
Ben Pasteneb52227f2020-06-01 22:41:30259
260 def __init__(self, string, checkout_root=_CHROMIUM_ROOT):
brettw9dffb542016-01-22 18:40:03261 self.input = string
262 self.cur = 0
Ben Pasteneb52227f2020-06-01 22:41:30263 self.checkout_root = checkout_root
brettw9dffb542016-01-22 18:40:03264
265 def IsDone(self):
266 return self.cur == len(self.input)
267
Ben Pastene00156a22020-03-23 18:13:10268 def ReplaceImports(self):
269 """Replaces import(...) lines with the contents of the imports.
270
271 Recurses on itself until there are no imports remaining, in the case of
272 nested imports.
273 """
274 lines = self.input.splitlines()
275 if not any(line.startswith('import(') for line in lines):
276 return
277 for line in lines:
278 if not line.startswith('import('):
279 continue
280 regex_match = IMPORT_RE.match(line)
281 if not regex_match:
Samuel Huang1ea5180e2020-05-25 16:29:40282 raise GNError('Not a valid import string: %s' % line)
Ben Pasteneb52227f2020-06-01 22:41:30283 import_path = os.path.join(self.checkout_root, regex_match.group(1))
Ben Pastene00156a22020-03-23 18:13:10284 with open(import_path) as f:
285 imported_args = f.read()
286 self.input = self.input.replace(line, imported_args)
287 # Call ourselves again if we've just replaced an import() with additional
288 # imports.
289 self.ReplaceImports()
290
291
brettw9dffb542016-01-22 18:40:03292 def ConsumeWhitespace(self):
293 while not self.IsDone() and self.input[self.cur] in ' \t\n':
294 self.cur += 1
295
Ben Pastene9b24d852018-11-06 00:42:09296 def ConsumeComment(self):
297 if self.IsDone() or self.input[self.cur] != '#':
298 return
299
300 # Consume each comment, line by line.
301 while not self.IsDone() and self.input[self.cur] == '#':
302 # Consume the rest of the comment, up until the end of the line.
303 while not self.IsDone() and self.input[self.cur] != '\n':
304 self.cur += 1
305 # Move the cursor to the next line (if there is one).
306 if not self.IsDone():
307 self.cur += 1
308
brettw9dffb542016-01-22 18:40:03309 def Parse(self):
310 """Converts a string representing a printed GN value to the Python type.
311
Samuel Huang1ea5180e2020-05-25 16:29:40312 See additional usage notes on FromGNString() above.
brettw9dffb542016-01-22 18:40:03313
Samuel Huang1ea5180e2020-05-25 16:29:40314 * GN booleans ('true', 'false') will be converted to Python booleans.
brettw9dffb542016-01-22 18:40:03315
Samuel Huang1ea5180e2020-05-25 16:29:40316 * GN numbers ('123') will be converted to Python numbers.
brettw9dffb542016-01-22 18:40:03317
Samuel Huang1ea5180e2020-05-25 16:29:40318 * GN strings (double-quoted as in '"asdf"') will be converted to Python
brettw9dffb542016-01-22 18:40:03319 strings with GN escaping rules. GN string interpolation (embedded
Julien Brianceau96dfe4d82017-08-01 09:03:13320 variables preceded by $) are not supported and will be returned as
brettw9dffb542016-01-22 18:40:03321 literals.
322
Samuel Huang1ea5180e2020-05-25 16:29:40323 * GN lists ('[1, "asdf", 3]') will be converted to Python lists.
brettw9dffb542016-01-22 18:40:03324
Samuel Huang1ea5180e2020-05-25 16:29:40325 * GN scopes ('{ ... }') are not supported.
326
327 Raises:
328 GNError: Parse fails.
329 """
brettw9dffb542016-01-22 18:40:03330 result = self._ParseAllowTrailing()
331 self.ConsumeWhitespace()
332 if not self.IsDone():
Samuel Huang1ea5180e2020-05-25 16:29:40333 raise GNError("Trailing input after parsing:\n " + self.input[self.cur:])
brettw9dffb542016-01-22 18:40:03334 return result
335
dpranke65d84dc02016-04-06 00:07:18336 def ParseArgs(self):
337 """Converts a whitespace-separated list of ident=literals to a dict.
338
Samuel Huang1ea5180e2020-05-25 16:29:40339 See additional usage notes on FromGNArgs(), above.
340
341 Raises:
342 GNError: Parse fails.
dpranke65d84dc02016-04-06 00:07:18343 """
344 d = {}
345
Ben Pastene00156a22020-03-23 18:13:10346 self.ReplaceImports()
dpranke65d84dc02016-04-06 00:07:18347 self.ConsumeWhitespace()
Ben Pastene9b24d852018-11-06 00:42:09348 self.ConsumeComment()
dpranke65d84dc02016-04-06 00:07:18349 while not self.IsDone():
350 ident = self._ParseIdent()
351 self.ConsumeWhitespace()
352 if self.input[self.cur] != '=':
Samuel Huang1ea5180e2020-05-25 16:29:40353 raise GNError("Unexpected token: " + self.input[self.cur:])
dpranke65d84dc02016-04-06 00:07:18354 self.cur += 1
355 self.ConsumeWhitespace()
356 val = self._ParseAllowTrailing()
357 self.ConsumeWhitespace()
Ben Pastene9b24d852018-11-06 00:42:09358 self.ConsumeComment()
Ben Pasteneb75f8ec2020-06-04 22:09:28359 self.ConsumeWhitespace()
dpranke65d84dc02016-04-06 00:07:18360 d[ident] = val
361
362 return d
363
brettw9dffb542016-01-22 18:40:03364 def _ParseAllowTrailing(self):
Samuel Huang1ea5180e2020-05-25 16:29:40365 """Internal version of Parse() that doesn't check for trailing stuff."""
brettw9dffb542016-01-22 18:40:03366 self.ConsumeWhitespace()
367 if self.IsDone():
Samuel Huang1ea5180e2020-05-25 16:29:40368 raise GNError("Expected input to parse.")
brettw9dffb542016-01-22 18:40:03369
370 next_char = self.input[self.cur]
371 if next_char == '[':
372 return self.ParseList()
373 elif _IsDigitOrMinus(next_char):
374 return self.ParseNumber()
375 elif next_char == '"':
376 return self.ParseString()
377 elif self._ConstantFollows('true'):
378 return True
379 elif self._ConstantFollows('false'):
380 return False
381 else:
Samuel Huang1ea5180e2020-05-25 16:29:40382 raise GNError("Unexpected token: " + self.input[self.cur:])
brettw9dffb542016-01-22 18:40:03383
dpranke65d84dc02016-04-06 00:07:18384 def _ParseIdent(self):
yyanagisawa18ef9302016-10-07 02:35:17385 ident = ''
dpranke65d84dc02016-04-06 00:07:18386
387 next_char = self.input[self.cur]
388 if not next_char.isalpha() and not next_char=='_':
Samuel Huang1ea5180e2020-05-25 16:29:40389 raise GNError("Expected an identifier: " + self.input[self.cur:])
dpranke65d84dc02016-04-06 00:07:18390
yyanagisawa18ef9302016-10-07 02:35:17391 ident += next_char
dpranke65d84dc02016-04-06 00:07:18392 self.cur += 1
393
394 next_char = self.input[self.cur]
395 while next_char.isalpha() or next_char.isdigit() or next_char=='_':
yyanagisawa18ef9302016-10-07 02:35:17396 ident += next_char
dpranke65d84dc02016-04-06 00:07:18397 self.cur += 1
398 next_char = self.input[self.cur]
399
yyanagisawa18ef9302016-10-07 02:35:17400 return ident
dpranke65d84dc02016-04-06 00:07:18401
brettw9dffb542016-01-22 18:40:03402 def ParseNumber(self):
403 self.ConsumeWhitespace()
404 if self.IsDone():
Samuel Huang1ea5180e2020-05-25 16:29:40405 raise GNError('Expected number but got nothing.')
brettw9dffb542016-01-22 18:40:03406
407 begin = self.cur
408
409 # The first character can include a negative sign.
410 if not self.IsDone() and _IsDigitOrMinus(self.input[self.cur]):
411 self.cur += 1
412 while not self.IsDone() and self.input[self.cur].isdigit():
413 self.cur += 1
414
415 number_string = self.input[begin:self.cur]
416 if not len(number_string) or number_string == '-':
Samuel Huang1ea5180e2020-05-25 16:29:40417 raise GNError('Not a valid number.')
brettw9dffb542016-01-22 18:40:03418 return int(number_string)
419
420 def ParseString(self):
421 self.ConsumeWhitespace()
422 if self.IsDone():
Samuel Huang1ea5180e2020-05-25 16:29:40423 raise GNError('Expected string but got nothing.')
brettw9dffb542016-01-22 18:40:03424
425 if self.input[self.cur] != '"':
Samuel Huang1ea5180e2020-05-25 16:29:40426 raise GNError('Expected string beginning in a " but got:\n ' +
427 self.input[self.cur:])
brettw9dffb542016-01-22 18:40:03428 self.cur += 1 # Skip over quote.
429
430 begin = self.cur
431 while not self.IsDone() and self.input[self.cur] != '"':
432 if self.input[self.cur] == '\\':
433 self.cur += 1 # Skip over the backslash.
434 if self.IsDone():
Samuel Huang1ea5180e2020-05-25 16:29:40435 raise GNError('String ends in a backslash in:\n ' + self.input)
brettw9dffb542016-01-22 18:40:03436 self.cur += 1
437
438 if self.IsDone():
Samuel Huang1ea5180e2020-05-25 16:29:40439 raise GNError('Unterminated string:\n ' + self.input[begin:])
brettw9dffb542016-01-22 18:40:03440
441 end = self.cur
442 self.cur += 1 # Consume trailing ".
443
444 return UnescapeGNString(self.input[begin:end])
445
446 def ParseList(self):
447 self.ConsumeWhitespace()
448 if self.IsDone():
Samuel Huang1ea5180e2020-05-25 16:29:40449 raise GNError('Expected list but got nothing.')
brettw9dffb542016-01-22 18:40:03450
451 # Skip over opening '['.
452 if self.input[self.cur] != '[':
Samuel Huang1ea5180e2020-05-25 16:29:40453 raise GNError('Expected [ for list but got:\n ' + self.input[self.cur:])
brettw9dffb542016-01-22 18:40:03454 self.cur += 1
455 self.ConsumeWhitespace()
456 if self.IsDone():
Samuel Huang1ea5180e2020-05-25 16:29:40457 raise GNError('Unterminated list:\n ' + self.input)
brettw9dffb542016-01-22 18:40:03458
459 list_result = []
460 previous_had_trailing_comma = True
461 while not self.IsDone():
462 if self.input[self.cur] == ']':
463 self.cur += 1 # Skip over ']'.
464 return list_result
465
466 if not previous_had_trailing_comma:
Samuel Huang1ea5180e2020-05-25 16:29:40467 raise GNError('List items not separated by comma.')
brettw9dffb542016-01-22 18:40:03468
469 list_result += [ self._ParseAllowTrailing() ]
470 self.ConsumeWhitespace()
471 if self.IsDone():
472 break
473
474 # Consume comma if there is one.
475 previous_had_trailing_comma = self.input[self.cur] == ','
476 if previous_had_trailing_comma:
477 # Consume comma.
478 self.cur += 1
479 self.ConsumeWhitespace()
480
Samuel Huang1ea5180e2020-05-25 16:29:40481 raise GNError('Unterminated list:\n ' + self.input)
brettw9dffb542016-01-22 18:40:03482
483 def _ConstantFollows(self, constant):
Samuel Huang1ea5180e2020-05-25 16:29:40484 """Checks and maybe consumes a string constant at current input location.
485
486 Param:
487 constant: The string constant to check.
488
489 Returns:
490 True if |constant| follows immediately at the current location in the
491 input. In this case, the string is consumed as a side effect. Otherwise,
492 returns False and the current position is unchanged.
493 """
brettw9dffb542016-01-22 18:40:03494 end = self.cur + len(constant)
dprankee031ec22016-04-02 00:17:34495 if end > len(self.input):
brettw9dffb542016-01-22 18:40:03496 return False # Not enough room.
497 if self.input[self.cur:end] == constant:
498 self.cur = end
499 return True
500 return False
Andrew Grieve169e377d2020-08-10 18:12:14501
502
503def ReadBuildVars(output_directory):
504 """Parses $output_directory/build_vars.json into a dict."""
505 with open(os.path.join(output_directory, BUILD_VARS_FILENAME)) as f:
506 return json.load(f)