1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602
|
# Copyright 2014 The Chromium Authors
# 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 shutil
import sys
_CHROMIUM_ROOT = os.path.abspath(
os.path.join(os.path.dirname(__file__), os.pardir))
ARGS_GN_FILENAME = 'args.gn'
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 = regex_match.group(1)
if import_path.startswith("//"):
import_path = os.path.join(self.checkout_root, import_path[2:])
elif sys.platform.startswith('win32'):
if import_path.startswith("/"):
# gn users '/C:/path/to/foo.gn', not 'C:/path/to/foo.gn' on windows
import_path = import_path[1:]
else:
raise GNError('Need /-prefix for an absolute path: %s' % import_path)
if not os.path.isabs(import_path):
raise GNError('Unable to use relative path in import path: %s' %
import_path)
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)
def ReadArgsGN(output_directory):
"""Parses $output_directory/args.gn into a dict."""
fname = os.path.join(output_directory, ARGS_GN_FILENAME)
if not os.path.exists(fname):
return {}
with open(fname) as f:
return FromGNArgs(f.read())
def CreateBuildCommand(output_directory):
"""Returns [cmd, -C, output_directory].
Where |cmd| is one of: siso ninja, ninja, or autoninja.
"""
suffix = '.bat' if sys.platform.startswith('win32') else ''
# Prefer the version on PATH, but fallback to known version if PATH doesn't
# have one (e.g. on bots).
if not shutil.which(f'autoninja{suffix}'):
third_party_prefix = os.path.join(_CHROMIUM_ROOT, 'third_party')
ninja_prefix = os.path.join(third_party_prefix, 'ninja', '')
siso_prefix = os.path.join(third_party_prefix, 'siso', 'cipd', '')
# Also - bots configure reclient manually, and so do not use the "auto"
# wrappers.
ninja_cmd = [f'{ninja_prefix}ninja{suffix}']
siso_cmd = [f'{siso_prefix}siso{suffix}', 'ninja']
else:
ninja_cmd = [f'autoninja{suffix}']
siso_cmd = list(ninja_cmd)
if output_directory and os.path.abspath(output_directory) != os.path.abspath(
os.curdir):
ninja_cmd += ['-C', output_directory]
siso_cmd += ['-C', output_directory]
siso_deps = os.path.exists(os.path.join(output_directory, '.siso_deps'))
ninja_deps = os.path.exists(os.path.join(output_directory, '.ninja_deps'))
if siso_deps and ninja_deps:
raise Exception('Found both .siso_deps and .ninja_deps in '
f'{output_directory}. Not sure which build tool to use. '
'Please delete one, or better, run "gn clean".')
if siso_deps:
return siso_cmd
return ninja_cmd
|