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