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.
6 '''The 'grit build' tool along with integration for this tool with the
16 from grit import grd_reader
18 from grit.tool import interface
19 from grit import shortcuts
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.'''
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',
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',
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' ])
45 def GetFormatter(type):
46 modulename = 'grit.format.' + _format_modules[type]
47 __import__(modulename)
48 module = sys.modules[modulename]
51 except AttributeError:
52 return module.GetFormatter(type)
55 class RcBuilder(interface.Tool):
56 '''A tool that builds RC files and resource header files for compilation.
58 Usage: grit build [-o OUTPUTDIR] [-D NAME[=VAL]]*
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
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.
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
80 -o OUTPUTDIR Specify what directory output paths are relative to.
81 Defaults to the current directory.
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.
87 -E NAME=VALUE Set environment variable NAME to VALUE (within grit).
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
94 -w WHITELISTFILE Path to a file containing the string names of the
95 resources to include. Anything not listed is dropped.
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.
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"
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.
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.
119 def ShortDescription(self):
120 return 'A tool that builds RC files for compilation.'
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
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:
138 assert_output_files.append(val)
139 elif key == '--assert-file-list':
141 assert_output_files += f.read().splitlines()
143 self.output_directory = val
145 name, val = util.ParseDefine(val)
146 self.defines[name] = val
148 (env_name, env_value) = val.split('=', 1)
149 os.environ[env_name] = env_value
151 # TODO(joi@chromium.org): Remove this override once change
152 # lands in WebKit.grd to specify the first_ids_file in the
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
162 target_platform = val
164 rc_header_format = val
165 elif key == '--depdir':
167 elif key == '--depfile':
171 print 'This tool takes no tool-specific arguments.'
173 self.SetOptions(opts)
174 if self.scons_targets:
175 self.VerboseOut('Using SCons targets to identify files to output.\n')
177 self.VerboseOut('Output directory: %s (absolute path: %s)\n' %
178 (self.output_directory,
179 os.path.abspath(self.output_directory)))
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'))
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)
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)
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')
204 self.res.AssignRcHeaderFormat(rc_header_format)
205 self.res.RunGatherers()
208 if assert_output_files:
209 if not self.CheckAssertedOutputFiles(assert_output_files):
212 if depfile and depdir:
213 self.GenerateDepfile(depfile, depdir)
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
222 # key/value pairs of C-preprocessor like defines that are used for
223 # conditional output of resources
224 self.defines = defines or {}
226 # self.res is a fully-populated resource tree if Run()
227 # has been called, otherwise None.
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
235 # The set of names that are whitelisted to actually be included in the
237 self.whitelist_names = None
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
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)
258 def ProcessNode(node, output_node, outfile):
259 '''Processes a node in-order, calling its formatter before and after
260 recursing to its children.
263 node: grit.node.base.Node subclass
264 output_node: grit.node.io.OutputNode
265 outfile: open filehandle
267 base_dir = util.dirname(output_node.GetOutputFilename())
269 formatter = GetFormatter(output_node.GetType())
270 formatted = formatter(node, output_node.GetLanguage(), output_dir=base_dir)
271 outfile.writelines(formatted)
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
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])
286 for output in self.res.GetOutputFiles():
287 output.output_filename = os.path.abspath(os.path.join(
288 self.output_directory, output.GetFilename()))
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)
295 for output in self.res.GetOutputFiles():
296 self.VerboseOut('Creating %s...' % output.GetFilename())
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
301 if output.GetType() in ('rc_header', 'resource_map_header',
302 'resource_map_source', 'resource_file_map_source'):
304 elif output.GetType() in ('android', 'c_format', 'js_map_format', 'plist',
305 'plist_strings', 'doc', 'json'):
307 elif output.GetType() in ('chrome_messages_json'):
308 # Chrome Web Store currently expects BOM for UTF-8 files :-(
309 encoding = 'utf-8-sig'
311 # TODO(gfeher) modify here to set utf-8 encoding for admx/adml
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)
319 # Make the output directory if it doesn't exist.
320 self.MakeDirectoriesTo(output.GetOutputFilename())
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')
326 if output.GetType() != 'data_package':
327 outfile = util.WrapOutputStream(outfile, encoding)
329 # Iterate in-order through entire resource tree, calling formatters on
330 # the entry into a node and on exit out of it.
332 self.ProcessNode(self.res, output, outfile)
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())
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')
353 self.VerboseOut(' done.\n')
355 # Print warnings if there are any duplicate shortcuts.
356 warnings = shortcuts.GenerateDuplicateShortcutsWarnings(
357 self.res.UberClique(), self.res.GetTcProject())
359 print '\n'.join(warnings)
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'))
367 self.VerboseOut(warnings)
368 if self.res.UberClique().HasMissingTranslations():
369 print self.res.UberClique().missing_translations_
373 def CheckAssertedOutputFiles(self, assert_output_files):
374 '''Checks that the asserted output files are specified in the given list.
376 Returns true if the asserted files are present. If they are not, returns
377 False and prints the failure.
379 # Compare the absolute path names, sorted.
380 asserted = sorted([os.path.abspath(i) for i in assert_output_files])
382 os.path.abspath(os.path.join(self.output_directory, i.GetFilename()))
383 for i in self.res.GetOutputFiles()])
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.
390 Expected output files:
394 Missing output files:
399 print error % ('\n'.join(asserted), '\n'.join(actual), '\n'.join(missing),
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|.
410 For example, supposing we have three files in a directory src/
413 blah.grd <- depends on input{1,2}.xtb
419 grit -i blah.grd -o ../out/gen --depdir ../out --depfile ../out/gen/blah.rd.d
421 from the directory src/ we will generate a depfile ../out/gen/blah.grd.d
422 that has the contents
424 gen/blah.h: ../src/input1.xtb ../src/input2.xtb
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).
429 Note that all paths in the depfile are relative to ../out, the depdir.
431 depfile = os.path.abspath(depfile)
432 depdir = os.path.abspath(depdir)
433 infiles = self.res.GetInputFiles()
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)
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])
444 depfile_contents = output_file + ': ' + deps_text
445 self.MakeDirectoriesTo(depfile)
446 outfile = self.fo_create(depfile, 'wb')
447 outfile.writelines(depfile_contents)
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):