Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / tools / grit / grit / tool / build.py
1 #!/usr/bin/env python
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5
6 '''The 'grit build' tool along with integration for this tool with the
7 SCons build system.
8 '''
9
10 import filecmp
11 import getopt
12 import os
13 import shutil
14 import sys
15
16 from grit import grd_reader
17 from grit import util
18 from grit.tool import interface
19 from grit import shortcuts
20
21
22 # It would be cleaner to have each module register itself, but that would
23 # require importing all of them on every run of GRIT.
24 '''Map from <output> node types to modules under grit.format.'''
25 _format_modules = {
26   'android':                  'android_xml',
27   'c_format':                 'c_format',
28   'chrome_messages_json':     'chrome_messages_json',
29   'data_package':             'data_pack',
30   'js_map_format':            'js_map_format',
31   'rc_all':                   'rc',
32   'rc_translateable':         'rc',
33   'rc_nontranslateable':      'rc',
34   'rc_header':                'rc_header',
35   'resource_map_header':      'resource_map',
36   'resource_map_source':      'resource_map',
37   'resource_file_map_source': 'resource_map',
38 }
39 _format_modules.update(
40     (type, 'policy_templates.template_formatter') for type in
41         [ 'adm', 'admx', 'adml', 'reg', 'doc', 'json',
42           'plist', 'plist_strings', 'ios_plist' ])
43
44
45 def GetFormatter(type):
46   modulename = 'grit.format.' + _format_modules[type]
47   __import__(modulename)
48   module = sys.modules[modulename]
49   try:
50     return module.Format
51   except AttributeError:
52     return module.GetFormatter(type)
53
54
55 class RcBuilder(interface.Tool):
56   '''A tool that builds RC files and resource header files for compilation.
57
58 Usage:  grit build [-o OUTPUTDIR] [-D NAME[=VAL]]*
59
60 All output options for this tool are specified in the input file (see
61 'grit help' for details on how to specify the input file - it is a global
62 option).
63
64 Options:
65
66   -a FILE           Assert that the given file is an output. There can be
67                     multiple "-a" flags listed for multiple outputs. If a "-a"
68                     or "--assert-file-list" argument is present, then the list
69                     of asserted files must match the output files or the tool
70                     will fail. The use-case is for the build system to maintain
71                     separate lists of output files and to catch errors if the
72                     build system's list and the grit list are out-of-sync.
73
74   --assert-file-list  Provide a file listing multiple asserted output files.
75                     There is one file name per line. This acts like specifying
76                     each file with "-a" on the command line, but without the
77                     possibility of running into OS line-length limits for very
78                     long lists.
79
80   -o OUTPUTDIR      Specify what directory output paths are relative to.
81                     Defaults to the current directory.
82
83   -D NAME[=VAL]     Specify a C-preprocessor-like define NAME with optional
84                     value VAL (defaults to 1) which will be used to control
85                     conditional inclusion of resources.
86
87   -E NAME=VALUE     Set environment variable NAME to VALUE (within grit).
88
89   -f FIRSTIDSFILE   Path to a python file that specifies the first id of
90                     value to use for resources.  A non-empty value here will
91                     override the value specified in the <grit> node's
92                     first_ids_file.
93
94   -w WHITELISTFILE  Path to a file containing the string names of the
95                     resources to include.  Anything not listed is dropped.
96
97   -t PLATFORM       Specifies the platform the build is targeting; defaults
98                     to the value of sys.platform. The value provided via this
99                     flag should match what sys.platform would report for your
100                     target platform; see grit.node.base.EvaluateCondition.
101
102   -h HEADERFORMAT   Custom format string to use for generating rc header files.
103                     The string should have two placeholders: {textual_id}
104                     and {numeric_id}. E.g. "#define {textual_id} {numeric_id}"
105                     Otherwise it will use the default "#define SYMBOL 1234"
106
107   --output-all-resource-defines
108   --no-output-all-resource-defines  If specified, overrides the value of the
109                     output_all_resource_defines attribute of the root <grit>
110                     element of the input .grd file.
111
112 Conditional inclusion of resources only affects the output of files which
113 control which resources get linked into a binary, e.g. it affects .rc files
114 meant for compilation but it does not affect resource header files (that define
115 IDs).  This helps ensure that values of IDs stay the same, that all messages
116 are exported to translation interchange files (e.g. XMB files), etc.
117 '''
118
119   def ShortDescription(self):
120     return 'A tool that builds RC files for compilation.'
121
122   def Run(self, opts, args):
123     self.output_directory = '.'
124     first_ids_file = None
125     whitelist_filenames = []
126     assert_output_files = []
127     target_platform = None
128     depfile = None
129     depdir = None
130     rc_header_format = None
131     output_all_resource_defines = None
132     (own_opts, args) = getopt.getopt(args, 'a:o:D:E:f:w:t:h:',
133         ('depdir=','depfile=','assert-file-list=',
134          'output-all-resource-defines',
135          'no-output-all-resource-defines',))
136     for (key, val) in own_opts:
137       if key == '-a':
138         assert_output_files.append(val)
139       elif key == '--assert-file-list':
140         with open(val) as f:
141           assert_output_files += f.read().splitlines()
142       elif key == '-o':
143         self.output_directory = val
144       elif key == '-D':
145         name, val = util.ParseDefine(val)
146         self.defines[name] = val
147       elif key == '-E':
148         (env_name, env_value) = val.split('=', 1)
149         os.environ[env_name] = env_value
150       elif key == '-f':
151         # TODO(joi@chromium.org): Remove this override once change
152         # lands in WebKit.grd to specify the first_ids_file in the
153         # .grd itself.
154         first_ids_file = val
155       elif key == '-w':
156         whitelist_filenames.append(val)
157       elif key == '--output-all-resource-defines':
158         output_all_resource_defines = True
159       elif key == '--no-output-all-resource-defines':
160         output_all_resource_defines = False
161       elif key == '-t':
162         target_platform = val
163       elif key == '-h':
164         rc_header_format = val
165       elif key == '--depdir':
166         depdir = val
167       elif key == '--depfile':
168         depfile = val
169
170     if len(args):
171       print 'This tool takes no tool-specific arguments.'
172       return 2
173     self.SetOptions(opts)
174     if self.scons_targets:
175       self.VerboseOut('Using SCons targets to identify files to output.\n')
176     else:
177       self.VerboseOut('Output directory: %s (absolute path: %s)\n' %
178                       (self.output_directory,
179                        os.path.abspath(self.output_directory)))
180
181     if whitelist_filenames:
182       self.whitelist_names = set()
183       for whitelist_filename in whitelist_filenames:
184         self.VerboseOut('Using whitelist: %s\n' % whitelist_filename);
185         whitelist_contents = util.ReadFile(whitelist_filename, util.RAW_TEXT)
186         self.whitelist_names.update(whitelist_contents.strip().split('\n'))
187
188     self.res = grd_reader.Parse(opts.input,
189                                 debug=opts.extra_verbose,
190                                 first_ids_file=first_ids_file,
191                                 defines=self.defines,
192                                 target_platform=target_platform)
193
194     # If the output_all_resource_defines option is specified, override the value
195     # found in the grd file.
196     if output_all_resource_defines is not None:
197       self.res.SetShouldOutputAllResourceDefines(output_all_resource_defines)
198
199     # Set an output context so that conditionals can use defines during the
200     # gathering stage; we use a dummy language here since we are not outputting
201     # a specific language.
202     self.res.SetOutputLanguage('en')
203     if rc_header_format:
204       self.res.AssignRcHeaderFormat(rc_header_format)
205     self.res.RunGatherers()
206     self.Process()
207
208     if assert_output_files:
209       if not self.CheckAssertedOutputFiles(assert_output_files):
210         return 2
211
212     if depfile and depdir:
213       self.GenerateDepfile(depfile, depdir)
214
215     return 0
216
217   def __init__(self, defines=None):
218     # Default file-creation function is built-in open().  Only done to allow
219     # overriding by unit test.
220     self.fo_create = open
221
222     # key/value pairs of C-preprocessor like defines that are used for
223     # conditional output of resources
224     self.defines = defines or {}
225
226     # self.res is a fully-populated resource tree if Run()
227     # has been called, otherwise None.
228     self.res = None
229
230     # Set to a list of filenames for the output nodes that are relative
231     # to the current working directory.  They are in the same order as the
232     # output nodes in the file.
233     self.scons_targets = None
234
235     # The set of names that are whitelisted to actually be included in the
236     # output.
237     self.whitelist_names = None
238
239   @staticmethod
240   def AddWhitelistTags(start_node, whitelist_names):
241     # Walk the tree of nodes added attributes for the nodes that shouldn't
242     # be written into the target files (skip markers).
243     from grit.node import include
244     from grit.node import message
245     from grit.node import structure
246     for node in start_node:
247       # Same trick data_pack.py uses to see what nodes actually result in
248       # real items.
249       if (isinstance(node, include.IncludeNode) or
250           isinstance(node, message.MessageNode) or
251           isinstance(node, structure.StructureNode)):
252         text_ids = node.GetTextualIds()
253         # Mark the item to be skipped if it wasn't in the whitelist.
254         if text_ids and text_ids[0] not in whitelist_names:
255           node.SetWhitelistMarkedAsSkip(True)
256
257   @staticmethod
258   def ProcessNode(node, output_node, outfile):
259     '''Processes a node in-order, calling its formatter before and after
260     recursing to its children.
261
262     Args:
263       node: grit.node.base.Node subclass
264       output_node: grit.node.io.OutputNode
265       outfile: open filehandle
266     '''
267     base_dir = util.dirname(output_node.GetOutputFilename())
268
269     formatter = GetFormatter(output_node.GetType())
270     formatted = formatter(node, output_node.GetLanguage(), output_dir=base_dir)
271     outfile.writelines(formatted)
272
273
274   def Process(self):
275     # Update filenames with those provided by SCons if we're being invoked
276     # from SCons.  The list of SCons targets also includes all <structure>
277     # node outputs, but it starts with our output files, in the order they
278     # occur in the .grd
279     if self.scons_targets:
280       assert len(self.scons_targets) >= len(self.res.GetOutputFiles())
281       outfiles = self.res.GetOutputFiles()
282       for ix in range(len(outfiles)):
283         outfiles[ix].output_filename = os.path.abspath(
284           self.scons_targets[ix])
285     else:
286       for output in self.res.GetOutputFiles():
287         output.output_filename = os.path.abspath(os.path.join(
288           self.output_directory, output.GetFilename()))
289
290     # If there are whitelisted names, tag the tree once up front, this way
291     # while looping through the actual output, it is just an attribute check.
292     if self.whitelist_names:
293       self.AddWhitelistTags(self.res, self.whitelist_names)
294
295     for output in self.res.GetOutputFiles():
296       self.VerboseOut('Creating %s...' % output.GetFilename())
297
298       # Microsoft's RC compiler can only deal with single-byte or double-byte
299       # files (no UTF-8), so we make all RC files UTF-16 to support all
300       # character sets.
301       if output.GetType() in ('rc_header', 'resource_map_header',
302           'resource_map_source', 'resource_file_map_source'):
303         encoding = 'cp1252'
304       elif output.GetType() in ('android', 'c_format', 'js_map_format', 'plist',
305                                 'plist_strings', 'doc', 'json'):
306         encoding = 'utf_8'
307       elif output.GetType() in ('chrome_messages_json'):
308         # Chrome Web Store currently expects BOM for UTF-8 files :-(
309         encoding = 'utf-8-sig'
310       else:
311         # TODO(gfeher) modify here to set utf-8 encoding for admx/adml
312         encoding = 'utf_16'
313
314       # Set the context, for conditional inclusion of resources
315       self.res.SetOutputLanguage(output.GetLanguage())
316       self.res.SetOutputContext(output.GetContext())
317       self.res.SetDefines(self.defines)
318
319       # Make the output directory if it doesn't exist.
320       self.MakeDirectoriesTo(output.GetOutputFilename())
321
322       # Write the results to a temporary file and only overwrite the original
323       # if the file changed.  This avoids unnecessary rebuilds.
324       outfile = self.fo_create(output.GetOutputFilename() + '.tmp', 'wb')
325
326       if output.GetType() != 'data_package':
327         outfile = util.WrapOutputStream(outfile, encoding)
328
329       # Iterate in-order through entire resource tree, calling formatters on
330       # the entry into a node and on exit out of it.
331       with outfile:
332         self.ProcessNode(self.res, output, outfile)
333
334       # Now copy from the temp file back to the real output, but on Windows,
335       # only if the real output doesn't exist or the contents of the file
336       # changed.  This prevents identical headers from being written and .cc
337       # files from recompiling (which is painful on Windows).
338       if not os.path.exists(output.GetOutputFilename()):
339         os.rename(output.GetOutputFilename() + '.tmp',
340                   output.GetOutputFilename())
341       else:
342         # CHROMIUM SPECIFIC CHANGE.
343         # This clashes with gyp + vstudio, which expect the output timestamp
344         # to change on a rebuild, even if nothing has changed.
345         #files_match = filecmp.cmp(output.GetOutputFilename(),
346         #    output.GetOutputFilename() + '.tmp')
347         #if (output.GetType() != 'rc_header' or not files_match
348         #    or sys.platform != 'win32'):
349         shutil.copy2(output.GetOutputFilename() + '.tmp',
350                      output.GetOutputFilename())
351         os.remove(output.GetOutputFilename() + '.tmp')
352
353       self.VerboseOut(' done.\n')
354
355     # Print warnings if there are any duplicate shortcuts.
356     warnings = shortcuts.GenerateDuplicateShortcutsWarnings(
357         self.res.UberClique(), self.res.GetTcProject())
358     if warnings:
359       print '\n'.join(warnings)
360
361     # Print out any fallback warnings, and missing translation errors, and
362     # exit with an error code if there are missing translations in a non-pseudo
363     # and non-official build.
364     warnings = (self.res.UberClique().MissingTranslationsReport().
365         encode('ascii', 'replace'))
366     if warnings:
367       self.VerboseOut(warnings)
368     if self.res.UberClique().HasMissingTranslations():
369       print self.res.UberClique().missing_translations_
370       sys.exit(-1)
371
372
373   def CheckAssertedOutputFiles(self, assert_output_files):
374     '''Checks that the asserted output files are specified in the given list.
375
376     Returns true if the asserted files are present. If they are not, returns
377     False and prints the failure.
378     '''
379     # Compare the absolute path names, sorted.
380     asserted = sorted([os.path.abspath(i) for i in assert_output_files])
381     actual = sorted([
382         os.path.abspath(os.path.join(self.output_directory, i.GetFilename()))
383         for i in self.res.GetOutputFiles()])
384
385     if asserted != actual:
386       missing = list(set(actual) - set(asserted))
387       extra = list(set(asserted) - set(actual))
388       error = '''Asserted file list does not match.
389
390 Expected output files:
391 %s
392 Actual output files:
393 %s
394 Missing output files:
395 %s
396 Extra output files:
397 %s
398 '''
399       print error % ('\n'.join(asserted), '\n'.join(actual), '\n'.join(missing),
400           '\n'.join(extra))
401       return False
402     return True
403
404
405   def GenerateDepfile(self, depfile, depdir):
406     '''Generate a depfile that contains the imlicit dependencies of the input
407     grd. The depfile will be in the same format as a makefile, and will contain
408     references to files relative to |depdir|. It will be put in |depfile|.
409
410     For example, supposing we have three files in a directory src/
411
412     src/
413       blah.grd    <- depends on input{1,2}.xtb
414       input1.xtb
415       input2.xtb
416
417     and we run
418
419       grit -i blah.grd -o ../out/gen --depdir ../out --depfile ../out/gen/blah.rd.d
420
421     from the directory src/ we will generate a depfile ../out/gen/blah.grd.d
422     that has the contents
423
424       gen/blah.h: ../src/input1.xtb ../src/input2.xtb
425
426     Where "gen/blah.h" is the first output (Ninja expects the .d file to list
427     the first output in cases where there is more than one).
428
429     Note that all paths in the depfile are relative to ../out, the depdir.
430     '''
431     depfile = os.path.abspath(depfile)
432     depdir = os.path.abspath(depdir)
433     infiles = self.res.GetInputFiles()
434
435     # Get the first output file relative to the depdir.
436     outputs = self.res.GetOutputFiles()
437     output_file = os.path.relpath(os.path.join(
438           self.output_directory, outputs[0].GetFilename()), depdir)
439
440     # The path prefix to prepend to dependencies in the depfile.
441     prefix = os.path.relpath(os.getcwd(), depdir)
442     deps_text = ' '.join([os.path.join(prefix, i) for i in infiles])
443
444     depfile_contents = output_file + ': ' + deps_text
445     self.MakeDirectoriesTo(depfile)
446     outfile = self.fo_create(depfile, 'wb')
447     outfile.writelines(depfile_contents)
448
449   @staticmethod
450   def MakeDirectoriesTo(file):
451     '''Creates directories necessary to contain |file|.'''
452     dir = os.path.split(file)[0]
453     if not os.path.exists(dir):
454       os.makedirs(dir)