Home | History | Annotate | Download | only in build
      1 # Copyright 2014 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 """Converts a given gypi file to a python scope and writes the result to stdout.
      6 
      7 USING THIS SCRIPT IN CHROMIUM
      8 
      9 Forking Python to run this script in the middle of GN is slow, especially on
     10 Windows, and it makes both the GYP and GN files harder to follow. You can't
     11 use "git grep" to find files in the GN build any more, and tracking everything
     12 in GYP down requires a level of indirection. Any calls will have to be removed
     13 and cleaned up once the GYP-to-GN transition is complete.
     14 
     15 As a result, we only use this script when the list of files is large and
     16 frequently-changing. In these cases, having one canonical list outweights the
     17 downsides.
     18 
     19 As of this writing, the GN build is basically complete. It's likely that all
     20 large and frequently changing targets where this is appropriate use this
     21 mechanism already. And since we hope to turn down the GYP build soon, the time
     22 horizon is also relatively short. As a result, it is likely that no additional
     23 uses of this script should every be added to the build. During this later part
     24 of the transition period, we should be focusing more and more on the absolute
     25 readability of the GN build.
     26 
     27 
     28 HOW TO USE
     29 
     30 It is assumed that the file contains a toplevel dictionary, and this script
     31 will return that dictionary as a GN "scope" (see example below). This script
     32 does not know anything about GYP and it will not expand variables or execute
     33 conditions.
     34 
     35 It will strip conditions blocks.
     36 
     37 A variables block at the top level will be flattened so that the variables
     38 appear in the root dictionary. This way they can be returned to the GN code.
     39 
     40 Say your_file.gypi looked like this:
     41   {
     42      'sources': [ 'a.cc', 'b.cc' ],
     43      'defines': [ 'ENABLE_DOOM_MELON' ],
     44   }
     45 
     46 You would call it like this:
     47   gypi_values = exec_script("//build/gypi_to_gn.py",
     48                             [ rebase_path("your_file.gypi") ],
     49                             "scope",
     50                             [ "your_file.gypi" ])
     51 
     52 Notes:
     53  - The rebase_path call converts the gypi file from being relative to the
     54    current build file to being system absolute for calling the script, which
     55    will have a different current directory than this file.
     56 
     57  - The "scope" parameter tells GN to interpret the result as a series of GN
     58    variable assignments.
     59 
     60  - The last file argument to exec_script tells GN that the given file is a
     61    dependency of the build so Ninja can automatically re-run GN if the file
     62    changes.
     63 
     64 Read the values into a target like this:
     65   component("mycomponent") {
     66     sources = gypi_values.sources
     67     defines = gypi_values.defines
     68   }
     69 
     70 Sometimes your .gypi file will include paths relative to a different
     71 directory than the current .gn file. In this case, you can rebase them to
     72 be relative to the current directory.
     73   sources = rebase_path(gypi_values.sources, ".",
     74                         "//path/gypi/input/values/are/relative/to")
     75 
     76 This script will tolerate a 'variables' in the toplevel dictionary or not. If
     77 the toplevel dictionary just contains one item called 'variables', it will be
     78 collapsed away and the result will be the contents of that dictinoary. Some
     79 .gypi files are written with or without this, depending on how they expect to
     80 be embedded into a .gyp file.
     81 
     82 This script also has the ability to replace certain substrings in the input.
     83 Generally this is used to emulate GYP variable expansion. If you passed the
     84 argument "--replace=<(foo)=bar" then all instances of "<(foo)" in strings in
     85 the input will be replaced with "bar":
     86 
     87   gypi_values = exec_script("//build/gypi_to_gn.py",
     88                             [ rebase_path("your_file.gypi"),
     89                               "--replace=<(foo)=bar"],
     90                             "scope",
     91                             [ "your_file.gypi" ])
     92 
     93 """
     94 
     95 import gn_helpers
     96 from optparse import OptionParser
     97 import sys
     98 
     99 def LoadPythonDictionary(path):
    100   file_string = open(path).read()
    101   try:
    102     file_data = eval(file_string, {'__builtins__': None}, None)
    103   except SyntaxError, e:
    104     e.filename = path
    105     raise
    106   except Exception, e:
    107     raise Exception("Unexpected error while reading %s: %s" % (path, str(e)))
    108 
    109   assert isinstance(file_data, dict), "%s does not eval to a dictionary" % path
    110 
    111   # Flatten any variables to the top level.
    112   if 'variables' in file_data:
    113     file_data.update(file_data['variables'])
    114     del file_data['variables']
    115 
    116   # Strip all elements that this script can't process.
    117   elements_to_strip = [
    118     'conditions',
    119     'target_conditions',
    120     'targets',
    121     'includes',
    122     'actions',
    123   ]
    124   for element in elements_to_strip:
    125     if element in file_data:
    126       del file_data[element]
    127 
    128   return file_data
    129 
    130 
    131 def ReplaceSubstrings(values, search_for, replace_with):
    132   """Recursively replaces substrings in a value.
    133 
    134   Replaces all substrings of the "search_for" with "repace_with" for all
    135   strings occurring in "values". This is done by recursively iterating into
    136   lists as well as the keys and values of dictionaries."""
    137   if isinstance(values, str):
    138     return values.replace(search_for, replace_with)
    139 
    140   if isinstance(values, list):
    141     return [ReplaceSubstrings(v, search_for, replace_with) for v in values]
    142 
    143   if isinstance(values, dict):
    144     # For dictionaries, do the search for both the key and values.
    145     result = {}
    146     for key, value in values.items():
    147       new_key = ReplaceSubstrings(key, search_for, replace_with)
    148       new_value = ReplaceSubstrings(value, search_for, replace_with)
    149       result[new_key] = new_value
    150     return result
    151 
    152   # Assume everything else is unchanged.
    153   return values
    154 
    155 def main():
    156   parser = OptionParser()
    157   parser.add_option("-r", "--replace", action="append",
    158     help="Replaces substrings. If passed a=b, replaces all substrs a with b.")
    159   (options, args) = parser.parse_args()
    160 
    161   if len(args) != 1:
    162     raise Exception("Need one argument which is the .gypi file to read.")
    163 
    164   data = LoadPythonDictionary(args[0])
    165   if options.replace:
    166     # Do replacements for all specified patterns.
    167     for replace in options.replace:
    168       split = replace.split('=')
    169       # Allow "foo=" to replace with nothing.
    170       if len(split) == 1:
    171         split.append('')
    172       assert len(split) == 2, "Replacement must be of the form 'key=value'."
    173       data = ReplaceSubstrings(data, split[0], split[1])
    174 
    175   # Sometimes .gypi files use the GYP syntax with percents at the end of the
    176   # variable name (to indicate not to overwrite a previously-defined value):
    177   #   'foo%': 'bar',
    178   # Convert these to regular variables.
    179   for key in data:
    180     if len(key) > 1 and key[len(key) - 1] == '%':
    181       data[key[:-1]] = data[key]
    182       del data[key]
    183 
    184   print gn_helpers.ToGNString(data)
    185 
    186 if __name__ == '__main__':
    187   try:
    188     main()
    189   except Exception, e:
    190     print str(e)
    191     sys.exit(1)
    192