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