1# Copyright (c) 2012 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 5'''The 'grit build' tool. 6''' 7 8from __future__ import print_function 9 10import codecs 11import filecmp 12import getopt 13import gzip 14import os 15import shutil 16import sys 17 18import six 19 20from grit import grd_reader 21from grit import shortcuts 22from grit import util 23from grit.format import minifier 24from grit.node import brotli_util 25from grit.node import include 26from grit.node import message 27from grit.node import structure 28from grit.tool import interface 29 30 31# It would be cleaner to have each module register itself, but that would 32# require importing all of them on every run of GRIT. 33'''Map from <output> node types to modules under grit.format.''' 34_format_modules = { 35 'android': 'android_xml', 36 'c_format': 'c_format', 37 'chrome_messages_json': 'chrome_messages_json', 38 'chrome_messages_json_gzip': 'chrome_messages_json', 39 'data_package': 'data_pack', 40 'policy_templates': 'policy_templates_json', 41 'rc_all': 'rc', 42 'rc_header': 'rc_header', 43 'rc_nontranslateable': 'rc', 44 'rc_translateable': 'rc', 45 'resource_file_map_source': 'resource_map', 46 'resource_map_header': 'resource_map', 47 'resource_map_source': 'resource_map', 48} 49 50def GetFormatter(type): 51 modulename = 'grit.format.' + _format_modules[type] 52 __import__(modulename) 53 module = sys.modules[modulename] 54 try: 55 return module.Format 56 except AttributeError: 57 return module.GetFormatter(type) 58 59 60class RcBuilder(interface.Tool): 61 '''A tool that builds RC files and resource header files for compilation. 62 63Usage: grit build [-o OUTPUTDIR] [-D NAME[=VAL]]* 64 65All output options for this tool are specified in the input file (see 66'grit help' for details on how to specify the input file - it is a global 67option). 68 69Options: 70 71 -a FILE Assert that the given file is an output. There can be 72 multiple "-a" flags listed for multiple outputs. If a "-a" 73 or "--assert-file-list" argument is present, then the list 74 of asserted files must match the output files or the tool 75 will fail. The use-case is for the build system to maintain 76 separate lists of output files and to catch errors if the 77 build system's list and the grit list are out-of-sync. 78 79 --assert-file-list Provide a file listing multiple asserted output files. 80 There is one file name per line. This acts like specifying 81 each file with "-a" on the command line, but without the 82 possibility of running into OS line-length limits for very 83 long lists. 84 85 -o OUTPUTDIR Specify what directory output paths are relative to. 86 Defaults to the current directory. 87 88 -p FILE Specify a file containing a pre-determined mapping from 89 resource names to resource ids which will be used to assign 90 resource ids to those resources. Resources not found in this 91 file will be assigned ids normally. The motivation is to run 92 your app's startup and have it dump the resources it loads, 93 and then pass these via this flag. This will pack startup 94 resources together, thus reducing paging while all other 95 resources are unperturbed. The file should have the format: 96 RESOURCE_ONE_NAME 123 97 RESOURCE_TWO_NAME 124 98 99 -D NAME[=VAL] Specify a C-preprocessor-like define NAME with optional 100 value VAL (defaults to 1) which will be used to control 101 conditional inclusion of resources. 102 103 -E NAME=VALUE Set environment variable NAME to VALUE (within grit). 104 105 -f FIRSTIDSFILE Path to a python file that specifies the first id of 106 value to use for resources. A non-empty value here will 107 override the value specified in the <grit> node's 108 first_ids_file. 109 110 -w ALLOWLISTFILE Path to a file containing the string names of the 111 resources to include. Anything not listed is dropped. 112 113 -t PLATFORM Specifies the platform the build is targeting; defaults 114 to the value of sys.platform. The value provided via this 115 flag should match what sys.platform would report for your 116 target platform; see grit.node.base.EvaluateCondition. 117 118 --allowlist-support 119 Generate code to support extracting a resource allowlist 120 from executables. 121 122 --write-only-new flag 123 If flag is non-0, write output files to a temporary file 124 first, and copy it to the real output only if the new file 125 is different from the old file. This allows some build 126 systems to realize that dependent build steps might be 127 unnecessary, at the cost of comparing the output data at 128 grit time. 129 130 --depend-on-stamp 131 If specified along with --depfile and --depdir, the depfile 132 generated will depend on a stampfile instead of the first 133 output in the input .grd file. 134 135 --js-minifier A command to run the Javascript minifier. If not set then 136 Javascript won't be minified. The command should read the 137 original Javascript from standard input, and output the 138 minified Javascript to standard output. A non-zero exit 139 status will be taken as indicating failure. 140 141 --css-minifier A command to run the CSS minifier. If not set then CSS won't 142 be minified. The command should read the original CSS from 143 standard input, and output the minified CSS to standard 144 output. A non-zero exit status will be taken as indicating 145 failure. 146 147 --brotli The full path to the brotli executable generated by 148 third_party/brotli/BUILD.gn, required if any entries use 149 compress="brotli". 150 151Conditional inclusion of resources only affects the output of files which 152control which resources get linked into a binary, e.g. it affects .rc files 153meant for compilation but it does not affect resource header files (that define 154IDs). This helps ensure that values of IDs stay the same, that all messages 155are exported to translation interchange files (e.g. XMB files), etc. 156''' 157 158 def ShortDescription(self): 159 return 'A tool that builds RC files for compilation.' 160 161 def Run(self, opts, args): 162 brotli_util.SetBrotliCommand(None) 163 os.environ['cwd'] = os.getcwd() 164 self.output_directory = '.' 165 first_ids_file = None 166 predetermined_ids_file = None 167 allowlist_filenames = [] 168 assert_output_files = [] 169 target_platform = None 170 depfile = None 171 depdir = None 172 allowlist_support = False 173 write_only_new = False 174 depend_on_stamp = False 175 js_minifier = None 176 css_minifier = None 177 replace_ellipsis = True 178 (own_opts, args) = getopt.getopt( 179 args, 'a:p:o:D:E:f:w:t:', 180 ('depdir=', 'depfile=', 'assert-file-list=', 'help', 181 'output-all-resource-defines', 'no-output-all-resource-defines', 182 'no-replace-ellipsis', 'depend-on-stamp', 'js-minifier=', 183 'css-minifier=', 'write-only-new=', 'allowlist-support', 'brotli=')) 184 for (key, val) in own_opts: 185 if key == '-a': 186 assert_output_files.append(val) 187 elif key == '--assert-file-list': 188 with open(val) as f: 189 assert_output_files += f.read().splitlines() 190 elif key == '-o': 191 self.output_directory = val 192 elif key == '-D': 193 name, val = util.ParseDefine(val) 194 self.defines[name] = val 195 elif key == '-E': 196 (env_name, env_value) = val.split('=', 1) 197 os.environ[env_name] = env_value 198 elif key == '-f': 199 # TODO(joi@chromium.org): Remove this override once change 200 # lands in WebKit.grd to specify the first_ids_file in the 201 # .grd itself. 202 first_ids_file = val 203 elif key == '-w': 204 allowlist_filenames.append(val) 205 elif key == '--no-replace-ellipsis': 206 replace_ellipsis = False 207 elif key == '-p': 208 predetermined_ids_file = val 209 elif key == '-t': 210 target_platform = val 211 elif key == '--depdir': 212 depdir = val 213 elif key == '--depfile': 214 depfile = val 215 elif key == '--write-only-new': 216 write_only_new = val != '0' 217 elif key == '--depend-on-stamp': 218 depend_on_stamp = True 219 elif key == '--js-minifier': 220 js_minifier = val 221 elif key == '--css-minifier': 222 css_minifier = val 223 elif key == '--allowlist-support': 224 allowlist_support = True 225 elif key == '--brotli': 226 brotli_util.SetBrotliCommand([os.path.abspath(val)]) 227 elif key == '--help': 228 self.ShowUsage() 229 sys.exit(0) 230 231 if len(args): 232 print('This tool takes no tool-specific arguments.') 233 return 2 234 self.SetOptions(opts) 235 self.VerboseOut('Output directory: %s (absolute path: %s)\n' % 236 (self.output_directory, 237 os.path.abspath(self.output_directory))) 238 239 if allowlist_filenames: 240 self.allowlist_names = set() 241 for allowlist_filename in allowlist_filenames: 242 self.VerboseOut('Using allowlist: %s\n' % allowlist_filename) 243 allowlist_contents = util.ReadFile(allowlist_filename, 'utf-8') 244 self.allowlist_names.update(allowlist_contents.strip().split('\n')) 245 246 if js_minifier: 247 minifier.SetJsMinifier(js_minifier) 248 249 if css_minifier: 250 minifier.SetCssMinifier(css_minifier) 251 252 self.write_only_new = write_only_new 253 254 self.res = grd_reader.Parse(opts.input, 255 debug=opts.extra_verbose, 256 first_ids_file=first_ids_file, 257 predetermined_ids_file=predetermined_ids_file, 258 defines=self.defines, 259 target_platform=target_platform) 260 261 # Set an output context so that conditionals can use defines during the 262 # gathering stage; we use a dummy language here since we are not outputting 263 # a specific language. 264 self.res.SetOutputLanguage('en') 265 self.res.SetAllowlistSupportEnabled(allowlist_support) 266 self.res.RunGatherers() 267 268 # Replace ... with the single-character version. http://crbug.com/621772 269 if replace_ellipsis: 270 for node in self.res: 271 if isinstance(node, message.MessageNode): 272 node.SetReplaceEllipsis(True) 273 274 self.Process() 275 276 if assert_output_files: 277 if not self.CheckAssertedOutputFiles(assert_output_files): 278 return 2 279 280 if depfile and depdir: 281 self.GenerateDepfile(depfile, depdir, first_ids_file, depend_on_stamp) 282 283 return 0 284 285 def __init__(self, defines=None): 286 # Default file-creation function is codecs.open(). Only done to allow 287 # overriding by unit test. 288 self.fo_create = codecs.open 289 290 # key/value pairs of C-preprocessor like defines that are used for 291 # conditional output of resources 292 self.defines = defines or {} 293 294 # self.res is a fully-populated resource tree if Run() 295 # has been called, otherwise None. 296 self.res = None 297 298 # The set of names that are allowlisted to actually be included in the 299 # output. 300 self.allowlist_names = None 301 302 # Whether to compare outputs to their old contents before writing. 303 self.write_only_new = False 304 305 @staticmethod 306 def AddAllowlistTags(start_node, allowlist_names): 307 # Walk the tree of nodes added attributes for the nodes that shouldn't 308 # be written into the target files (skip markers). 309 for node in start_node: 310 # Same trick data_pack.py uses to see what nodes actually result in 311 # real items. 312 if (isinstance(node, include.IncludeNode) or 313 isinstance(node, message.MessageNode) or 314 isinstance(node, structure.StructureNode)): 315 text_ids = node.GetTextualIds() 316 # Mark the item to be skipped if it wasn't in the allowlist. 317 if text_ids and text_ids[0] not in allowlist_names: 318 node.SetAllowlistMarkedAsSkip(True) 319 320 @staticmethod 321 def ProcessNode(node, output_node, outfile): 322 '''Processes a node in-order, calling its formatter before and after 323 recursing to its children. 324 325 Args: 326 node: grit.node.base.Node subclass 327 output_node: grit.node.io.OutputNode 328 outfile: open filehandle 329 ''' 330 base_dir = util.dirname(output_node.GetOutputFilename()) 331 332 formatter = GetFormatter(output_node.GetType()) 333 formatted = formatter(node, output_node.GetLanguage(), output_dir=base_dir) 334 # NB: Formatters may be generators or return lists. The writelines API 335 # accepts iterables as a shortcut to calling write directly. That means 336 # you can pass strings (iteration yields characters), but not bytes (as 337 # iteration yields integers). Python 2 worked due to its quirks with 338 # bytes/string implementation, but Python 3 fails. It's also a bit more 339 # inefficient to call write once per character/byte. Handle all of this 340 # ourselves by calling write directly on strings/bytes before falling back 341 # to writelines. 342 if isinstance(formatted, (six.string_types, six.binary_type)): 343 outfile.write(formatted) 344 else: 345 outfile.writelines(formatted) 346 if output_node.GetType() == 'data_package': 347 with open(output_node.GetOutputFilename() + '.info', 'w') as infofile: 348 if node.info: 349 # We terminate with a newline so that when these files are 350 # concatenated later we consistently terminate with a newline so 351 # consumers can account for terminating newlines. 352 infofile.writelines(['\n'.join(node.info), '\n']) 353 354 @staticmethod 355 def _EncodingForOutputType(output_type): 356 # Microsoft's RC compiler can only deal with single-byte or double-byte 357 # files (no UTF-8), so we make all RC files UTF-16 to support all 358 # character sets. 359 if output_type in ('rc_header', 'resource_file_map_source', 360 'resource_map_header', 'resource_map_source'): 361 return 'cp1252' 362 if output_type in ('android', 'c_format', 'plist', 'plist_strings', 'doc', 363 'json', 'android_policy', 'chrome_messages_json', 364 'chrome_messages_json_gzip', 'policy_templates'): 365 return 'utf_8' 366 # TODO(gfeher) modify here to set utf-8 encoding for admx/adml 367 return 'utf_16' 368 369 def Process(self): 370 for output in self.res.GetOutputFiles(): 371 output.output_filename = os.path.abspath(os.path.join( 372 self.output_directory, output.GetOutputFilename())) 373 374 # If there are allowlisted names, tag the tree once up front, this way 375 # while looping through the actual output, it is just an attribute check. 376 if self.allowlist_names: 377 self.AddAllowlistTags(self.res, self.allowlist_names) 378 379 for output in self.res.GetOutputFiles(): 380 self.VerboseOut('Creating %s...' % output.GetOutputFilename()) 381 382 # Set the context, for conditional inclusion of resources 383 self.res.SetOutputLanguage(output.GetLanguage()) 384 self.res.SetOutputContext(output.GetContext()) 385 self.res.SetFallbackToDefaultLayout(output.GetFallbackToDefaultLayout()) 386 self.res.SetDefines(self.defines) 387 388 # Assign IDs only once to ensure that all outputs use the same IDs. 389 if self.res.GetIdMap() is None: 390 self.res.InitializeIds() 391 392 # Make the output directory if it doesn't exist. 393 self.MakeDirectoriesTo(output.GetOutputFilename()) 394 395 # Write the results to a temporary file and only overwrite the original 396 # if the file changed. This avoids unnecessary rebuilds. 397 out_filename = output.GetOutputFilename() 398 tmp_filename = out_filename + '.tmp' 399 tmpfile = self.fo_create(tmp_filename, 'wb') 400 401 output_type = output.GetType() 402 if output_type != 'data_package': 403 encoding = self._EncodingForOutputType(output_type) 404 tmpfile = util.WrapOutputStream(tmpfile, encoding) 405 406 # Iterate in-order through entire resource tree, calling formatters on 407 # the entry into a node and on exit out of it. 408 with tmpfile: 409 self.ProcessNode(self.res, output, tmpfile) 410 411 if output_type == 'chrome_messages_json_gzip': 412 gz_filename = tmp_filename + '.gz' 413 with open(tmp_filename, 'rb') as tmpfile, open(gz_filename, 'wb') as f: 414 with gzip.GzipFile(filename='', mode='wb', fileobj=f, mtime=0) as fgz: 415 shutil.copyfileobj(tmpfile, fgz) 416 os.remove(tmp_filename) 417 tmp_filename = gz_filename 418 419 # Now copy from the temp file back to the real output, but on Windows, 420 # only if the real output doesn't exist or the contents of the file 421 # changed. This prevents identical headers from being written and .cc 422 # files from recompiling (which is painful on Windows). 423 if not os.path.exists(out_filename): 424 os.rename(tmp_filename, out_filename) 425 else: 426 # CHROMIUM SPECIFIC CHANGE. 427 # This clashes with gyp + vstudio, which expect the output timestamp 428 # to change on a rebuild, even if nothing has changed, so only do 429 # it when opted in. 430 if not self.write_only_new: 431 write_file = True 432 else: 433 files_match = filecmp.cmp(out_filename, tmp_filename) 434 write_file = not files_match 435 if write_file: 436 shutil.copy2(tmp_filename, out_filename) 437 os.remove(tmp_filename) 438 439 self.VerboseOut(' done.\n') 440 441 # Print warnings if there are any duplicate shortcuts. 442 warnings = shortcuts.GenerateDuplicateShortcutsWarnings( 443 self.res.UberClique(), self.res.GetTcProject()) 444 if warnings: 445 print('\n'.join(warnings)) 446 447 # Print out any fallback warnings, and missing translation errors, and 448 # exit with an error code if there are missing translations in a non-pseudo 449 # and non-official build. 450 warnings = (self.res.UberClique().MissingTranslationsReport(). 451 encode('ascii', 'replace')) 452 if warnings: 453 self.VerboseOut(warnings) 454 if self.res.UberClique().HasMissingTranslations(): 455 print(self.res.UberClique().missing_translations_) 456 sys.exit(-1) 457 458 459 def CheckAssertedOutputFiles(self, assert_output_files): 460 '''Checks that the asserted output files are specified in the given list. 461 462 Returns true if the asserted files are present. If they are not, returns 463 False and prints the failure. 464 ''' 465 # Compare the absolute path names, sorted. 466 asserted = sorted([os.path.abspath(i) for i in assert_output_files]) 467 actual = sorted([ 468 os.path.abspath(os.path.join(self.output_directory, 469 i.GetOutputFilename())) 470 for i in self.res.GetOutputFiles()]) 471 472 if asserted != actual: 473 missing = list(set(asserted) - set(actual)) 474 extra = list(set(actual) - set(asserted)) 475 error = '''Asserted file list does not match. 476 477Expected output files: 478%s 479Actual output files: 480%s 481Missing output files: 482%s 483Extra output files: 484%s 485''' 486 print(error % ('\n'.join(asserted), '\n'.join(actual), '\n'.join(missing), 487 ' \n'.join(extra))) 488 return False 489 return True 490 491 492 def GenerateDepfile(self, depfile, depdir, first_ids_file, depend_on_stamp): 493 '''Generate a depfile that contains the imlicit dependencies of the input 494 grd. The depfile will be in the same format as a makefile, and will contain 495 references to files relative to |depdir|. It will be put in |depfile|. 496 497 For example, supposing we have three files in a directory src/ 498 499 src/ 500 blah.grd <- depends on input{1,2}.xtb 501 input1.xtb 502 input2.xtb 503 504 and we run 505 506 grit -i blah.grd -o ../out/gen \ 507 --depdir ../out \ 508 --depfile ../out/gen/blah.rd.d 509 510 from the directory src/ we will generate a depfile ../out/gen/blah.grd.d 511 that has the contents 512 513 gen/blah.h: ../src/input1.xtb ../src/input2.xtb 514 515 Where "gen/blah.h" is the first output (Ninja expects the .d file to list 516 the first output in cases where there is more than one). If the flag 517 --depend-on-stamp is specified, "gen/blah.rd.d.stamp" will be used that is 518 'touched' whenever a new depfile is generated. 519 520 Note that all paths in the depfile are relative to ../out, the depdir. 521 ''' 522 depfile = os.path.abspath(depfile) 523 depdir = os.path.abspath(depdir) 524 infiles = self.res.GetInputFiles() 525 526 # We want to trigger a rebuild if the first ids change. 527 if first_ids_file is not None: 528 infiles.append(first_ids_file) 529 530 if (depend_on_stamp): 531 output_file = depfile + ".stamp" 532 # Touch the stamp file before generating the depfile. 533 with open(output_file, 'a'): 534 os.utime(output_file, None) 535 else: 536 # Get the first output file relative to the depdir. 537 outputs = self.res.GetOutputFiles() 538 output_file = os.path.join(self.output_directory, 539 outputs[0].GetOutputFilename()) 540 541 output_file = os.path.relpath(output_file, depdir) 542 # The path prefix to prepend to dependencies in the depfile. 543 prefix = os.path.relpath(os.getcwd(), depdir) 544 deps_text = ' '.join([os.path.join(prefix, i) for i in infiles]) 545 546 depfile_contents = output_file + ': ' + deps_text 547 self.MakeDirectoriesTo(depfile) 548 outfile = self.fo_create(depfile, 'w', encoding='utf-8') 549 outfile.write(depfile_contents) 550 551 @staticmethod 552 def MakeDirectoriesTo(file): 553 '''Creates directories necessary to contain |file|.''' 554 dir = os.path.split(file)[0] 555 if not os.path.exists(dir): 556 os.makedirs(dir) 557