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