Home | History | Annotate | Download | only in grit
      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 '''SCons integration for GRIT.
      7 '''
      8 
      9 # NOTE: DO NOT IMPORT ANY GRIT STUFF HERE - we import lazily so that
     10 # grit and its dependencies aren't imported until actually needed.
     11 
     12 import os
     13 import types
     14 
     15 def _IsDebugEnabled():
     16   return 'GRIT_DEBUG' in os.environ and os.environ['GRIT_DEBUG'] == '1'
     17 
     18 def _SourceToFile(source):
     19   '''Return the path to the source file, given the 'source' argument as provided
     20   by SCons to the _Builder or _Emitter functions.
     21   '''
     22   # Get the filename of the source.  The 'source' parameter can be a string,
     23   # a "node", or a list of strings or nodes.
     24   if isinstance(source, types.ListType):
     25     source = str(source[0])
     26   else:
     27     source = str(source)
     28   return source
     29 
     30 
     31 def _ParseRcFlags(flags):
     32   """Gets a mapping of defines.
     33 
     34   Args:
     35     flags: env['RCFLAGS']; the input defines.
     36 
     37   Returns:
     38     A tuple of (defines, res_file):
     39       defines: A mapping of {name: val}
     40       res_file: None, or the specified res file for static file dependencies.
     41   """
     42   from grit import util
     43 
     44   defines = {}
     45   res_file = None
     46   # Get the CPP defines from the environment.
     47   res_flag = '--res_file='
     48   for flag in flags:
     49     if flag.startswith(res_flag):
     50       res_file = flag[len(res_flag):]
     51       continue
     52     if flag.startswith('/D'):
     53       flag = flag[2:]
     54     name, val = util.ParseDefine(flag)
     55     # Only apply to first instance of a given define
     56     if name not in defines:
     57       defines[name] = val
     58   return (defines, res_file)
     59 
     60 
     61 def _Builder(target, source, env):
     62   print _SourceToFile(source)
     63 
     64   from grit import grit_runner
     65   from grit.tool import build
     66   options = grit_runner.Options()
     67   # This sets options to default values
     68   options.ReadOptions([])
     69   options.input = _SourceToFile(source)
     70 
     71   # TODO(joi) Check if we can get the 'verbose' option from the environment.
     72 
     73   builder = build.RcBuilder(defines=_ParseRcFlags(env['RCFLAGS'])[0])
     74 
     75   # To ensure that our output files match what we promised SCons, we
     76   # use the list of targets provided by SCons and update the file paths in
     77   # our .grd input file with the targets.
     78   builder.scons_targets = [str(t) for t in target]
     79   builder.Run(options, [])
     80   return None  # success
     81 
     82 
     83 def _GetOutputFiles(grd, base_dir):
     84   """Processes outputs listed in the grd into rc_headers and rc_alls.
     85 
     86   Note that anything that's not an rc_header is classified as an rc_all.
     87 
     88   Args:
     89     grd: An open GRD reader.
     90 
     91   Returns:
     92     A tuple of (rc_headers, rc_alls, lang_folders):
     93       rc_headers: Outputs marked as rc_header.
     94       rc_alls: All other outputs.
     95       lang_folders: The output language folders.
     96   """
     97   rc_headers = []
     98   rc_alls = []
     99   lang_folders = {}
    100 
    101   # Explicit output files.
    102   for output in grd.GetOutputFiles():
    103     path = os.path.join(base_dir, output.GetFilename())
    104     if (output.GetType() == 'rc_header'):
    105       rc_headers.append(path)
    106     else:
    107       rc_alls.append(path)
    108     if _IsDebugEnabled():
    109       print 'GRIT: Added target %s' % path
    110     if output.attrs['lang'] != '':
    111       lang_folders[output.attrs['lang']] = os.path.dirname(path)
    112 
    113   return (rc_headers, rc_alls, lang_folders)
    114 
    115 
    116 def _ProcessNodes(grd, base_dir, lang_folders):
    117   """Processes the GRD nodes to figure out file dependencies.
    118 
    119   Args:
    120     grd: An open GRD reader.
    121     base_dir: The base directory for filenames.
    122     lang_folders: THe output language folders.
    123 
    124   Returns:
    125     A tuple of (structure_outputs, translated_files, static_files):
    126       structure_outputs: Structures marked as sconsdep.
    127       translated_files: Files that are structures or skeletons, and get
    128         translated by GRIT.
    129       static_files: Files that are includes, and are used directly by res files.
    130   """
    131   structure_outputs = []
    132   translated_files = []
    133   static_files = []
    134 
    135   # Go through nodes, figuring out resources.  Also output certain resources
    136   # as build targets, based on the sconsdep flag.
    137   for node in grd.ActiveDescendants():
    138     with node:
    139       file = node.ToRealPath(node.GetInputPath())
    140       if node.name == 'structure':
    141         translated_files.append(os.path.abspath(file))
    142         # TODO(joi) Should remove the "if sconsdep is true" thing as it is a
    143         # hack - see grit/node/structure.py
    144         if node.HasFileForLanguage() and node.attrs['sconsdep'] == 'true':
    145           for lang in lang_folders:
    146             path = node.FileForLanguage(lang, lang_folders[lang],
    147                                         create_file=False,
    148                                         return_if_not_generated=False)
    149             if path:
    150               structure_outputs.append(path)
    151               if _IsDebugEnabled():
    152                 print 'GRIT: Added target %s' % path
    153       elif (node.name == 'skeleton' or (node.name == 'file' and node.parent and
    154                                         node.parent.name == 'translations')):
    155         translated_files.append(os.path.abspath(file))
    156       elif node.name == 'include':
    157         # If it's added by file name and the file isn't easy to find, don't make
    158         # it a dependency.  This could add some build flakiness, but it doesn't
    159         # work otherwise.
    160         if node.attrs['filenameonly'] != 'true' or os.path.exists(file):
    161           static_files.append(os.path.abspath(file))
    162         # If it's output from mk, look in the output directory.
    163         elif node.attrs['mkoutput'] == 'true':
    164           static_files.append(os.path.join(base_dir, os.path.basename(file)))
    165 
    166   return (structure_outputs, translated_files, static_files)
    167 
    168 
    169 def _SetDependencies(env, base_dir, res_file, rc_alls, translated_files,
    170                      static_files):
    171   """Sets dependencies in the environment.
    172 
    173   Args:
    174     env: The SCons environment.
    175     base_dir: The base directory for filenames.
    176     res_file: The res_file specified in the RC flags.
    177     rc_alls: All non-rc_header outputs.
    178     translated_files: Files that are structures or skeletons, and get
    179       translated by GRIT.
    180     static_files: Files that are includes, and are used directly by res files.
    181   """
    182   if res_file:
    183     env.Depends(os.path.join(base_dir, res_file), static_files)
    184   else:
    185     # Make a best effort dependency setup when no res file is specified.
    186     translated_files.extend(static_files)
    187 
    188   for rc_all in rc_alls:
    189     env.Depends(rc_all, translated_files)
    190 
    191 
    192 def _Emitter(target, source, env):
    193   """Modifies the list of targets to include all outputs.
    194 
    195   Note that this also sets up the dependencies, even though it's an emitter
    196   rather than a scanner.  This is so that the resource header file doesn't show
    197   as having dependencies.
    198 
    199   Args:
    200     target: The list of targets to emit for.
    201     source: The source or list of sources for the target.
    202     env: The SCons environment.
    203 
    204   Returns:
    205     A tuple of (targets, sources).
    206   """
    207   from grit import grd_reader
    208   from grit import util
    209 
    210   (defines, res_file) = _ParseRcFlags(env['RCFLAGS'])
    211 
    212   grd = grd_reader.Parse(_SourceToFile(source), debug=_IsDebugEnabled())
    213   # TODO(jperkins): This is a hack to get an output context set for the reader.
    214   # This should really be smarter about the language.
    215   grd.SetOutputLanguage('en')
    216   grd.SetDefines(defines)
    217 
    218   base_dir = util.dirname(str(target[0]))
    219   (rc_headers, rc_alls, lang_folders) = _GetOutputFiles(grd, base_dir)
    220   (structure_outputs, translated_files, static_files) = _ProcessNodes(grd,
    221       base_dir, lang_folders)
    222 
    223   rc_alls.extend(structure_outputs)
    224   _SetDependencies(env, base_dir, res_file, rc_alls, translated_files,
    225                    static_files)
    226 
    227   targets = rc_headers
    228   targets.extend(rc_alls)
    229 
    230   # Return target and source lists.
    231   return (targets, source)
    232 
    233 
    234 # Function name is mandated by newer versions of SCons.
    235 def generate(env):
    236   # Importing this module should be possible whenever this function is invoked
    237   # since it should only be invoked by SCons.
    238   import SCons.Builder
    239   import SCons.Action
    240 
    241   # The varlist parameter tells SCons that GRIT needs to be invoked again
    242   # if RCFLAGS has changed since last compilation.
    243   build_action = SCons.Action.FunctionAction(_Builder, varlist=['RCFLAGS'])
    244   emit_action = SCons.Action.FunctionAction(_Emitter, varlist=['RCFLAGS'])
    245 
    246   builder = SCons.Builder.Builder(action=build_action, emitter=emit_action,
    247                                   src_suffix='.grd')
    248 
    249   # Add our builder and scanner to the environment.
    250   env.Append(BUILDERS = {'GRIT': builder})
    251 
    252 
    253 # Function name is mandated by newer versions of SCons.
    254 def exists(env):
    255   return 1
    256