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 -o OUTPUTDIR Specify what directory output paths are relative to.
67 Defaults to the current directory.
69 -D NAME[=VAL] Specify a C-preprocessor-like define NAME with optional
70 value VAL (defaults to 1) which will be used to control
71 conditional inclusion of resources.
73 -E NAME=VALUE Set environment variable NAME to VALUE (within grit).
75 -f FIRSTIDSFILE Path to a python file that specifies the first id of
76 value to use for resources. A non-empty value here will
77 override the value specified in the <grit> node's
80 -w WHITELISTFILE Path to a file containing the string names of the
81 resources to include. Anything not listed is dropped.
83 -t PLATFORM Specifies the platform the build is targeting; defaults
84 to the value of sys.platform. The value provided via this
85 flag should match what sys.platform would report for your
86 target platform; see grit.node.base.EvaluateCondition.
88 -h HEADERFORMAT Custom format string to use for generating rc header files.
89 The string should have two placeholders: {textual_id}
90 and {numeric_id}. E.g. "#define {textual_id} {numeric_id}"
91 Otherwise it will use the default "#define SYMBOL 1234"
93 Conditional inclusion of resources only affects the output of files which
94 control which resources get linked into a binary, e.g. it affects .rc files
95 meant for compilation but it does not affect resource header files (that define
96 IDs). This helps ensure that values of IDs stay the same, that all messages
97 are exported to translation interchange files (e.g. XMB files), etc.
100 def ShortDescription(self):
101 return 'A tool that builds RC files for compilation.'
103 def Run(self, opts, args):
104 self.output_directory = '.'
105 first_ids_file = None
106 whitelist_filenames = []
107 target_platform = None
110 rc_header_format = None
111 (own_opts, args) = getopt.getopt(args, 'o:D:E:f:w:t:h:', ('depdir=','depfile='))
112 for (key, val) in own_opts:
114 self.output_directory = val
116 name, val = util.ParseDefine(val)
117 self.defines[name] = val
119 (env_name, env_value) = val.split('=', 1)
120 os.environ[env_name] = env_value
122 # TODO(joi@chromium.org): Remove this override once change
123 # lands in WebKit.grd to specify the first_ids_file in the
127 whitelist_filenames.append(val)
129 target_platform = val
131 rc_header_format = val
132 elif key == '--depdir':
134 elif key == '--depfile':
138 print 'This tool takes no tool-specific arguments.'
140 self.SetOptions(opts)
141 if self.scons_targets:
142 self.VerboseOut('Using SCons targets to identify files to output.\n')
144 self.VerboseOut('Output directory: %s (absolute path: %s)\n' %
145 (self.output_directory,
146 os.path.abspath(self.output_directory)))
148 if whitelist_filenames:
149 self.whitelist_names = set()
150 for whitelist_filename in whitelist_filenames:
151 self.VerboseOut('Using whitelist: %s\n' % whitelist_filename);
152 whitelist_contents = util.ReadFile(whitelist_filename, util.RAW_TEXT)
153 self.whitelist_names.update(whitelist_contents.strip().split('\n'))
155 self.res = grd_reader.Parse(opts.input,
156 debug=opts.extra_verbose,
157 first_ids_file=first_ids_file,
158 defines=self.defines,
159 target_platform=target_platform)
160 # Set an output context so that conditionals can use defines during the
161 # gathering stage; we use a dummy language here since we are not outputting
162 # a specific language.
163 self.res.SetOutputLanguage('en')
165 self.res.AssignRcHeaderFormat(rc_header_format)
166 self.res.RunGatherers()
169 if depfile and depdir:
170 self.GenerateDepfile(opts.input, depfile, depdir)
174 def __init__(self, defines=None):
175 # Default file-creation function is built-in open(). Only done to allow
176 # overriding by unit test.
177 self.fo_create = open
179 # key/value pairs of C-preprocessor like defines that are used for
180 # conditional output of resources
181 self.defines = defines or {}
183 # self.res is a fully-populated resource tree if Run()
184 # has been called, otherwise None.
187 # Set to a list of filenames for the output nodes that are relative
188 # to the current working directory. They are in the same order as the
189 # output nodes in the file.
190 self.scons_targets = None
192 # The set of names that are whitelisted to actually be included in the
194 self.whitelist_names = None
197 def AddWhitelistTags(start_node, whitelist_names):
198 # Walk the tree of nodes added attributes for the nodes that shouldn't
199 # be written into the target files (skip markers).
200 from grit.node import include
201 from grit.node import message
202 for node in start_node:
203 # Same trick data_pack.py uses to see what nodes actually result in
205 if (isinstance(node, include.IncludeNode) or
206 isinstance(node, message.MessageNode)):
207 text_ids = node.GetTextualIds()
208 # Mark the item to be skipped if it wasn't in the whitelist.
209 if text_ids and text_ids[0] not in whitelist_names:
210 node.SetWhitelistMarkedAsSkip(True)
213 def ProcessNode(node, output_node, outfile):
214 '''Processes a node in-order, calling its formatter before and after
215 recursing to its children.
218 node: grit.node.base.Node subclass
219 output_node: grit.node.io.OutputNode
220 outfile: open filehandle
222 base_dir = util.dirname(output_node.GetOutputFilename())
224 formatter = GetFormatter(output_node.GetType())
225 formatted = formatter(node, output_node.GetLanguage(), output_dir=base_dir)
226 outfile.writelines(formatted)
230 # Update filenames with those provided by SCons if we're being invoked
231 # from SCons. The list of SCons targets also includes all <structure>
232 # node outputs, but it starts with our output files, in the order they
234 if self.scons_targets:
235 assert len(self.scons_targets) >= len(self.res.GetOutputFiles())
236 outfiles = self.res.GetOutputFiles()
237 for ix in range(len(outfiles)):
238 outfiles[ix].output_filename = os.path.abspath(
239 self.scons_targets[ix])
241 for output in self.res.GetOutputFiles():
242 output.output_filename = os.path.abspath(os.path.join(
243 self.output_directory, output.GetFilename()))
245 # If there are whitelisted names, tag the tree once up front, this way
246 # while looping through the actual output, it is just an attribute check.
247 if self.whitelist_names:
248 self.AddWhitelistTags(self.res, self.whitelist_names)
250 for output in self.res.GetOutputFiles():
251 self.VerboseOut('Creating %s...' % output.GetFilename())
253 # Microsoft's RC compiler can only deal with single-byte or double-byte
254 # files (no UTF-8), so we make all RC files UTF-16 to support all
256 if output.GetType() in ('rc_header', 'resource_map_header',
257 'resource_map_source', 'resource_file_map_source'):
259 elif output.GetType() in ('android', 'c_format', 'js_map_format', 'plist',
260 'plist_strings', 'doc', 'json'):
262 elif output.GetType() in ('chrome_messages_json'):
263 # Chrome Web Store currently expects BOM for UTF-8 files :-(
264 encoding = 'utf-8-sig'
266 # TODO(gfeher) modify here to set utf-8 encoding for admx/adml
269 # Set the context, for conditional inclusion of resources
270 self.res.SetOutputLanguage(output.GetLanguage())
271 self.res.SetOutputContext(output.GetContext())
272 self.res.SetDefines(self.defines)
274 # Make the output directory if it doesn't exist.
275 self.MakeDirectoriesTo(output.GetOutputFilename())
277 # Write the results to a temporary file and only overwrite the original
278 # if the file changed. This avoids unnecessary rebuilds.
279 outfile = self.fo_create(output.GetOutputFilename() + '.tmp', 'wb')
281 if output.GetType() != 'data_package':
282 outfile = util.WrapOutputStream(outfile, encoding)
284 # Iterate in-order through entire resource tree, calling formatters on
285 # the entry into a node and on exit out of it.
287 self.ProcessNode(self.res, output, outfile)
289 # Now copy from the temp file back to the real output, but on Windows,
290 # only if the real output doesn't exist or the contents of the file
291 # changed. This prevents identical headers from being written and .cc
292 # files from recompiling (which is painful on Windows).
293 if not os.path.exists(output.GetOutputFilename()):
294 os.rename(output.GetOutputFilename() + '.tmp',
295 output.GetOutputFilename())
297 # CHROMIUM SPECIFIC CHANGE.
298 # This clashes with gyp + vstudio, which expect the output timestamp
299 # to change on a rebuild, even if nothing has changed.
300 #files_match = filecmp.cmp(output.GetOutputFilename(),
301 # output.GetOutputFilename() + '.tmp')
302 #if (output.GetType() != 'rc_header' or not files_match
303 # or sys.platform != 'win32'):
304 shutil.copy2(output.GetOutputFilename() + '.tmp',
305 output.GetOutputFilename())
306 os.remove(output.GetOutputFilename() + '.tmp')
308 self.VerboseOut(' done.\n')
310 # Print warnings if there are any duplicate shortcuts.
311 warnings = shortcuts.GenerateDuplicateShortcutsWarnings(
312 self.res.UberClique(), self.res.GetTcProject())
314 print '\n'.join(warnings)
316 # Print out any fallback warnings, and missing translation errors, and
317 # exit with an error code if there are missing translations in a non-pseudo
318 # and non-official build.
319 warnings = (self.res.UberClique().MissingTranslationsReport().
320 encode('ascii', 'replace'))
322 self.VerboseOut(warnings)
323 if self.res.UberClique().HasMissingTranslations():
324 print self.res.UberClique().missing_translations_
327 def GenerateDepfile(self, input_filename, depfile, depdir):
328 '''Generate a depfile that contains the imlicit dependencies of the input
329 grd. The depfile will be in the same format as a makefile, and will contain
330 references to files relative to |depdir|. It will be put in |depfile|.
332 For example, supposing we have three files in a directory src/
335 blah.grd <- depends on input{1,2}.xtb
341 grit -i blah.grd -o ../out/gen --depdir ../out --depfile ../out/gen/blah.rd.d
343 from the directory src/ we will generate a depfile ../out/gen/blah.grd.d
344 that has the contents
346 gen/blah.grd.d: ../src/input1.xtb ../src/input2.xtb
348 Note that all paths in the depfile are relative to ../out, the depdir.
350 depfile = os.path.abspath(depfile)
351 depdir = os.path.abspath(depdir)
352 # The path prefix to prepend to dependencies in the depfile.
353 prefix = os.path.relpath(os.getcwd(), depdir)
354 # The path that the depfile refers to itself by.
355 self_ref_depfile = os.path.relpath(depfile, depdir)
356 infiles = self.res.GetInputFiles()
357 deps_text = ' '.join([os.path.join(prefix, i) for i in infiles])
358 depfile_contents = self_ref_depfile + ': ' + deps_text
359 self.MakeDirectoriesTo(depfile)
360 outfile = self.fo_create(depfile, 'wb')
361 outfile.writelines(depfile_contents)
364 def MakeDirectoriesTo(file):
365 '''Creates directories necessary to contain |file|.'''
366 dir = os.path.split(file)[0]
367 if not os.path.exists(dir):