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