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