• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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