[M120 Migration][VD] Fix url crash in RequestCertificateConfirm
[platform/framework/web/chromium-efl.git] / build / gn_helpers.py
1 # Copyright 2014 The Chromium Authors
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4
5 """Helper functions useful when writing scripts that integrate with GN.
6
7 The main functions are ToGNString() and FromGNString(), to convert between
8 serialized GN veriables and Python variables.
9
10 To use in an arbitrary Python file in the build:
11
12   import os
13   import sys
14
15   sys.path.append(os.path.join(os.path.dirname(__file__),
16                                os.pardir, os.pardir, 'build'))
17   import gn_helpers
18
19 Where the sequence of parameters to join is the relative path from your source
20 file to the build directory.
21 """
22
23 import json
24 import os
25 import re
26 import shlex
27 import shutil
28 import sys
29
30
31 _CHROMIUM_ROOT = os.path.abspath(
32     os.path.join(os.path.dirname(__file__), os.pardir))
33
34 BUILD_VARS_FILENAME = 'build_vars.json'
35 IMPORT_RE = re.compile(r'^import\("//(\S+)"\)')
36
37
38 class GNError(Exception):
39   pass
40
41
42 # Computes ASCII code of an element of encoded Python 2 str / Python 3 bytes.
43 _Ord = ord if sys.version_info.major < 3 else lambda c: c
44
45
46 def _TranslateToGnChars(s):
47   for decoded_ch in s.encode('utf-8'):  # str in Python 2, bytes in Python 3.
48     code = _Ord(decoded_ch)  # int
49     if code in (34, 36, 92):  # For '"', '$', or '\\'.
50       yield '\\' + chr(code)
51     elif 32 <= code < 127:
52       yield chr(code)
53     else:
54       yield '$0x%02X' % code
55
56
57 def ToGNString(value, pretty=False):
58   """Returns a stringified GN equivalent of a Python value.
59
60   Args:
61     value: The Python value to convert.
62     pretty: Whether to pretty print. If true, then non-empty lists are rendered
63         recursively with one item per line, with indents. Otherwise lists are
64         rendered without new line.
65   Returns:
66     The stringified GN equivalent to |value|.
67
68   Raises:
69     GNError: |value| cannot be printed to GN.
70   """
71
72   if sys.version_info.major < 3:
73     basestring_compat = basestring
74   else:
75     basestring_compat = str
76
77   # Emits all output tokens without intervening whitespaces.
78   def GenerateTokens(v, level):
79     if isinstance(v, basestring_compat):
80       yield '"' + ''.join(_TranslateToGnChars(v)) + '"'
81
82     elif isinstance(v, bool):
83       yield 'true' if v else 'false'
84
85     elif isinstance(v, int):
86       yield str(v)
87
88     elif isinstance(v, list):
89       yield '['
90       for i, item in enumerate(v):
91         if i > 0:
92           yield ','
93         for tok in GenerateTokens(item, level + 1):
94           yield tok
95       yield ']'
96
97     elif isinstance(v, dict):
98       if level > 0:
99         yield '{'
100       for key in sorted(v):
101         if not isinstance(key, basestring_compat):
102           raise GNError('Dictionary key is not a string.')
103         if not key or key[0].isdigit() or not key.replace('_', '').isalnum():
104           raise GNError('Dictionary key is not a valid GN identifier.')
105         yield key  # No quotations.
106         yield '='
107         for tok in GenerateTokens(v[key], level + 1):
108           yield tok
109       if level > 0:
110         yield '}'
111
112     else:  # Not supporting float: Add only when needed.
113       raise GNError('Unsupported type when printing to GN.')
114
115   can_start = lambda tok: tok and tok not in ',}]='
116   can_end = lambda tok: tok and tok not in ',{[='
117
118   # Adds whitespaces, trying to keep everything (except dicts) in 1 line.
119   def PlainGlue(gen):
120     prev_tok = None
121     for i, tok in enumerate(gen):
122       if i > 0:
123         if can_end(prev_tok) and can_start(tok):
124           yield '\n'  # New dict item.
125         elif prev_tok == '[' and tok == ']':
126           yield '  '  # Special case for [].
127         elif tok != ',':
128           yield ' '
129       yield tok
130       prev_tok = tok
131
132   # Adds whitespaces so non-empty lists can span multiple lines, with indent.
133   def PrettyGlue(gen):
134     prev_tok = None
135     level = 0
136     for i, tok in enumerate(gen):
137       if i > 0:
138         if can_end(prev_tok) and can_start(tok):
139           yield '\n' + '  ' * level  # New dict item.
140         elif tok == '=' or prev_tok in '=':
141           yield ' '  # Separator before and after '=', on same line.
142       if tok in ']}':
143         level -= 1
144       # Exclude '[]' and '{}' cases.
145       if int(prev_tok == '[') + int(tok == ']') == 1 or \
146          int(prev_tok == '{') + int(tok == '}') == 1:
147         yield '\n' + '  ' * level
148       yield tok
149       if tok in '[{':
150         level += 1
151       if tok == ',':
152         yield '\n' + '  ' * level
153       prev_tok = tok
154
155   token_gen = GenerateTokens(value, 0)
156   ret = ''.join((PrettyGlue if pretty else PlainGlue)(token_gen))
157   # Add terminating '\n' for dict |value| or multi-line output.
158   if isinstance(value, dict) or '\n' in ret:
159     return ret + '\n'
160   return ret
161
162
163 def FromGNString(input_string):
164   """Converts the input string from a GN serialized value to Python values.
165
166   For details on supported types see GNValueParser.Parse() below.
167
168   If your GN script did:
169     something = [ "file1", "file2" ]
170     args = [ "--values=$something" ]
171   The command line would look something like:
172     --values="[ \"file1\", \"file2\" ]"
173   Which when interpreted as a command line gives the value:
174     [ "file1", "file2" ]
175
176   You can parse this into a Python list using GN rules with:
177     input_values = FromGNValues(options.values)
178   Although the Python 'ast' module will parse many forms of such input, it
179   will not handle GN escaping properly, nor GN booleans. You should use this
180   function instead.
181
182
183   A NOTE ON STRING HANDLING:
184
185   If you just pass a string on the command line to your Python script, or use
186   string interpolation on a string variable, the strings will not be quoted:
187     str = "asdf"
188     args = [ str, "--value=$str" ]
189   Will yield the command line:
190     asdf --value=asdf
191   The unquoted asdf string will not be valid input to this function, which
192   accepts only quoted strings like GN scripts. In such cases, you can just use
193   the Python string literal directly.
194
195   The main use cases for this is for other types, in particular lists. When
196   using string interpolation on a list (as in the top example) the embedded
197   strings will be quoted and escaped according to GN rules so the list can be
198   re-parsed to get the same result.
199   """
200   parser = GNValueParser(input_string)
201   return parser.Parse()
202
203
204 def FromGNArgs(input_string):
205   """Converts a string with a bunch of gn arg assignments into a Python dict.
206
207   Given a whitespace-separated list of
208
209     <ident> = (integer | string | boolean | <list of the former>)
210
211   gn assignments, this returns a Python dict, i.e.:
212
213     FromGNArgs('foo=true\nbar=1\n') -> { 'foo': True, 'bar': 1 }.
214
215   Only simple types and lists supported; variables, structs, calls
216   and other, more complicated things are not.
217
218   This routine is meant to handle only the simple sorts of values that
219   arise in parsing --args.
220   """
221   parser = GNValueParser(input_string)
222   return parser.ParseArgs()
223
224
225 def UnescapeGNString(value):
226   """Given a string with GN escaping, returns the unescaped string.
227
228   Be careful not to feed with input from a Python parsing function like
229   'ast' because it will do Python unescaping, which will be incorrect when
230   fed into the GN unescaper.
231
232   Args:
233     value: Input string to unescape.
234   """
235   result = ''
236   i = 0
237   while i < len(value):
238     if value[i] == '\\':
239       if i < len(value) - 1:
240         next_char = value[i + 1]
241         if next_char in ('$', '"', '\\'):
242           # These are the escaped characters GN supports.
243           result += next_char
244           i += 1
245         else:
246           # Any other backslash is a literal.
247           result += '\\'
248     else:
249       result += value[i]
250     i += 1
251   return result
252
253
254 def _IsDigitOrMinus(char):
255   return char in '-0123456789'
256
257
258 class GNValueParser(object):
259   """Duplicates GN parsing of values and converts to Python types.
260
261   Normally you would use the wrapper function FromGNValue() below.
262
263   If you expect input as a specific type, you can also call one of the Parse*
264   functions directly. All functions throw GNError on invalid input.
265   """
266
267   def __init__(self, string, checkout_root=_CHROMIUM_ROOT):
268     self.input = string
269     self.cur = 0
270     self.checkout_root = checkout_root
271
272   def IsDone(self):
273     return self.cur == len(self.input)
274
275   def ReplaceImports(self):
276     """Replaces import(...) lines with the contents of the imports.
277
278     Recurses on itself until there are no imports remaining, in the case of
279     nested imports.
280     """
281     lines = self.input.splitlines()
282     if not any(line.startswith('import(') for line in lines):
283       return
284     for line in lines:
285       if not line.startswith('import('):
286         continue
287       regex_match = IMPORT_RE.match(line)
288       if not regex_match:
289         raise GNError('Not a valid import string: %s' % line)
290       import_path = os.path.join(self.checkout_root, regex_match.group(1))
291       with open(import_path) as f:
292         imported_args = f.read()
293       self.input = self.input.replace(line, imported_args)
294     # Call ourselves again if we've just replaced an import() with additional
295     # imports.
296     self.ReplaceImports()
297
298
299   def _ConsumeWhitespace(self):
300     while not self.IsDone() and self.input[self.cur] in ' \t\n':
301       self.cur += 1
302
303   def ConsumeCommentAndWhitespace(self):
304     self._ConsumeWhitespace()
305
306     # Consume each comment, line by line.
307     while not self.IsDone() and self.input[self.cur] == '#':
308       # Consume the rest of the comment, up until the end of the line.
309       while not self.IsDone() and self.input[self.cur] != '\n':
310         self.cur += 1
311       # Move the cursor to the next line (if there is one).
312       if not self.IsDone():
313         self.cur += 1
314
315       self._ConsumeWhitespace()
316
317   def Parse(self):
318     """Converts a string representing a printed GN value to the Python type.
319
320     See additional usage notes on FromGNString() above.
321
322     * GN booleans ('true', 'false') will be converted to Python booleans.
323
324     * GN numbers ('123') will be converted to Python numbers.
325
326     * GN strings (double-quoted as in '"asdf"') will be converted to Python
327       strings with GN escaping rules. GN string interpolation (embedded
328       variables preceded by $) are not supported and will be returned as
329       literals.
330
331     * GN lists ('[1, "asdf", 3]') will be converted to Python lists.
332
333     * GN scopes ('{ ... }') are not supported.
334
335     Raises:
336       GNError: Parse fails.
337     """
338     result = self._ParseAllowTrailing()
339     self.ConsumeCommentAndWhitespace()
340     if not self.IsDone():
341       raise GNError("Trailing input after parsing:\n  " + self.input[self.cur:])
342     return result
343
344   def ParseArgs(self):
345     """Converts a whitespace-separated list of ident=literals to a dict.
346
347     See additional usage notes on FromGNArgs(), above.
348
349     Raises:
350       GNError: Parse fails.
351     """
352     d = {}
353
354     self.ReplaceImports()
355     self.ConsumeCommentAndWhitespace()
356
357     while not self.IsDone():
358       ident = self._ParseIdent()
359       self.ConsumeCommentAndWhitespace()
360       if self.input[self.cur] != '=':
361         raise GNError("Unexpected token: " + self.input[self.cur:])
362       self.cur += 1
363       self.ConsumeCommentAndWhitespace()
364       val = self._ParseAllowTrailing()
365       self.ConsumeCommentAndWhitespace()
366       d[ident] = val
367
368     return d
369
370   def _ParseAllowTrailing(self):
371     """Internal version of Parse() that doesn't check for trailing stuff."""
372     self.ConsumeCommentAndWhitespace()
373     if self.IsDone():
374       raise GNError("Expected input to parse.")
375
376     next_char = self.input[self.cur]
377     if next_char == '[':
378       return self.ParseList()
379     elif next_char == '{':
380       return self.ParseScope()
381     elif _IsDigitOrMinus(next_char):
382       return self.ParseNumber()
383     elif next_char == '"':
384       return self.ParseString()
385     elif self._ConstantFollows('true'):
386       return True
387     elif self._ConstantFollows('false'):
388       return False
389     else:
390       raise GNError("Unexpected token: " + self.input[self.cur:])
391
392   def _ParseIdent(self):
393     ident = ''
394
395     next_char = self.input[self.cur]
396     if not next_char.isalpha() and not next_char=='_':
397       raise GNError("Expected an identifier: " + self.input[self.cur:])
398
399     ident += next_char
400     self.cur += 1
401
402     next_char = self.input[self.cur]
403     while next_char.isalpha() or next_char.isdigit() or next_char=='_':
404       ident += next_char
405       self.cur += 1
406       next_char = self.input[self.cur]
407
408     return ident
409
410   def ParseNumber(self):
411     self.ConsumeCommentAndWhitespace()
412     if self.IsDone():
413       raise GNError('Expected number but got nothing.')
414
415     begin = self.cur
416
417     # The first character can include a negative sign.
418     if not self.IsDone() and _IsDigitOrMinus(self.input[self.cur]):
419       self.cur += 1
420     while not self.IsDone() and self.input[self.cur].isdigit():
421       self.cur += 1
422
423     number_string = self.input[begin:self.cur]
424     if not len(number_string) or number_string == '-':
425       raise GNError('Not a valid number.')
426     return int(number_string)
427
428   def ParseString(self):
429     self.ConsumeCommentAndWhitespace()
430     if self.IsDone():
431       raise GNError('Expected string but got nothing.')
432
433     if self.input[self.cur] != '"':
434       raise GNError('Expected string beginning in a " but got:\n  ' +
435                     self.input[self.cur:])
436     self.cur += 1  # Skip over quote.
437
438     begin = self.cur
439     while not self.IsDone() and self.input[self.cur] != '"':
440       if self.input[self.cur] == '\\':
441         self.cur += 1  # Skip over the backslash.
442         if self.IsDone():
443           raise GNError('String ends in a backslash in:\n  ' + self.input)
444       self.cur += 1
445
446     if self.IsDone():
447       raise GNError('Unterminated string:\n  ' + self.input[begin:])
448
449     end = self.cur
450     self.cur += 1  # Consume trailing ".
451
452     return UnescapeGNString(self.input[begin:end])
453
454   def ParseList(self):
455     self.ConsumeCommentAndWhitespace()
456     if self.IsDone():
457       raise GNError('Expected list but got nothing.')
458
459     # Skip over opening '['.
460     if self.input[self.cur] != '[':
461       raise GNError('Expected [ for list but got:\n  ' + self.input[self.cur:])
462     self.cur += 1
463     self.ConsumeCommentAndWhitespace()
464     if self.IsDone():
465       raise GNError('Unterminated list:\n  ' + self.input)
466
467     list_result = []
468     previous_had_trailing_comma = True
469     while not self.IsDone():
470       if self.input[self.cur] == ']':
471         self.cur += 1  # Skip over ']'.
472         return list_result
473
474       if not previous_had_trailing_comma:
475         raise GNError('List items not separated by comma.')
476
477       list_result += [ self._ParseAllowTrailing() ]
478       self.ConsumeCommentAndWhitespace()
479       if self.IsDone():
480         break
481
482       # Consume comma if there is one.
483       previous_had_trailing_comma = self.input[self.cur] == ','
484       if previous_had_trailing_comma:
485         # Consume comma.
486         self.cur += 1
487         self.ConsumeCommentAndWhitespace()
488
489     raise GNError('Unterminated list:\n  ' + self.input)
490
491   def ParseScope(self):
492     self.ConsumeCommentAndWhitespace()
493     if self.IsDone():
494       raise GNError('Expected scope but got nothing.')
495
496     # Skip over opening '{'.
497     if self.input[self.cur] != '{':
498       raise GNError('Expected { for scope but got:\n ' + self.input[self.cur:])
499     self.cur += 1
500     self.ConsumeCommentAndWhitespace()
501     if self.IsDone():
502       raise GNError('Unterminated scope:\n ' + self.input)
503
504     scope_result = {}
505     while not self.IsDone():
506       if self.input[self.cur] == '}':
507         self.cur += 1
508         return scope_result
509
510       ident = self._ParseIdent()
511       self.ConsumeCommentAndWhitespace()
512       if self.input[self.cur] != '=':
513         raise GNError("Unexpected token: " + self.input[self.cur:])
514       self.cur += 1
515       self.ConsumeCommentAndWhitespace()
516       val = self._ParseAllowTrailing()
517       self.ConsumeCommentAndWhitespace()
518       scope_result[ident] = val
519
520     raise GNError('Unterminated scope:\n ' + self.input)
521
522   def _ConstantFollows(self, constant):
523     """Checks and maybe consumes a string constant at current input location.
524
525     Param:
526       constant: The string constant to check.
527
528     Returns:
529       True if |constant| follows immediately at the current location in the
530       input. In this case, the string is consumed as a side effect. Otherwise,
531       returns False and the current position is unchanged.
532     """
533     end = self.cur + len(constant)
534     if end > len(self.input):
535       return False  # Not enough room.
536     if self.input[self.cur:end] == constant:
537       self.cur = end
538       return True
539     return False
540
541
542 def ReadBuildVars(output_directory):
543   """Parses $output_directory/build_vars.json into a dict."""
544   with open(os.path.join(output_directory, BUILD_VARS_FILENAME)) as f:
545     return json.load(f)
546
547
548 def CreateBuildCommand(output_directory):
549   """Returns [cmd, -C, output_directory], where |cmd| is auto{siso,ninja}."""
550   suffix = '.bat' if sys.platform.startswith('win32') else ''
551   # Prefer the version on PATH, but fallback to known version if PATH doesn't
552   # have one (e.g. on bots).
553   if not shutil.which(f'autoninja{suffix}'):
554     third_party_prefix = os.path.join(_CHROMIUM_ROOT, 'third_party')
555     ninja_prefix = os.path.join(third_party_prefix, 'ninja', '')
556     siso_prefix = os.path.join(third_party_prefix, 'siso', '')
557     # Also - bots configure reclient manually, and so do not use the "auto"
558     # wrappers.
559     ninja_cmd = [f'{ninja_prefix}ninja{suffix}']
560     siso_cmd = [f'{siso_prefix}siso{suffix}', 'ninja']
561   else:
562     ninja_cmd = [f'autoninja{suffix}']
563     siso_cmd = [f'autosiso{suffix}']
564
565   if output_directory and os.path.relpath(output_directory) != '.':
566     ninja_cmd += ['-C', output_directory]
567     siso_cmd += ['-C', output_directory]
568   siso_deps = os.path.exists(os.path.join(output_directory, '.siso_deps'))
569   ninja_deps = os.path.exists(os.path.join(output_directory, '.ninja_deps'))
570   if siso_deps and ninja_deps:
571     raise Exception('Found both .siso_deps and .ninja_deps in '
572                     f'{output_directory}. Not sure which build tool to use. '
573                     'Please delete one, or better, run "gn clean".')
574   if siso_deps:
575     return siso_cmd
576   return ninja_cmd