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