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