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