• 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# 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