1#!/usr/bin/env python
2
3# Copyright (c) 2012 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# This script is wrapper for Chromium that adds some support for how GYP
8# is invoked by Chromium beyond what can be done in the gclient hooks.
9
10import glob
11import gyp_environment
12import os
13import re
14import shlex
15import subprocess
16import string
17import sys
18import vs_toolchain
19
20script_dir = os.path.dirname(os.path.realpath(__file__))
21chrome_src = os.path.abspath(os.path.join(script_dir, os.pardir))
22
23sys.path.insert(0, os.path.join(chrome_src, 'tools', 'gyp', 'pylib'))
24import gyp
25
26# Assume this file is in a one-level-deep subdirectory of the source root.
27SRC_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
28
29# Add paths so that pymod_do_main(...) can import files.
30sys.path.insert(1, os.path.join(chrome_src, 'build', 'android', 'gyp'))
31sys.path.insert(1, os.path.join(chrome_src, 'tools'))
32sys.path.insert(1, os.path.join(chrome_src, 'tools', 'generate_shim_headers'))
33sys.path.insert(1, os.path.join(chrome_src, 'tools', 'grit'))
34sys.path.insert(1, os.path.join(chrome_src, 'chrome', 'tools', 'build'))
35sys.path.insert(1, os.path.join(chrome_src, 'chromecast', 'tools', 'build'))
36sys.path.insert(1, os.path.join(chrome_src, 'native_client', 'build'))
37sys.path.insert(1, os.path.join(chrome_src, 'native_client_sdk', 'src',
38    'build_tools'))
39sys.path.insert(1, os.path.join(chrome_src, 'remoting', 'tools', 'build'))
40sys.path.insert(1, os.path.join(chrome_src, 'third_party', 'liblouis'))
41sys.path.insert(1, os.path.join(chrome_src, 'third_party', 'WebKit',
42    'Source', 'build', 'scripts'))
43
44# On Windows, Psyco shortens warm runs of build/gyp_chromium by about
45# 20 seconds on a z600 machine with 12 GB of RAM, from 90 down to 70
46# seconds.  Conversely, memory usage of build/gyp_chromium with Psyco
47# maxes out at about 158 MB vs. 132 MB without it.
48#
49# Psyco uses native libraries, so we need to load a different
50# installation depending on which OS we are running under. It has not
51# been tested whether using Psyco on our Mac and Linux builds is worth
52# it (the GYP running time is a lot shorter, so the JIT startup cost
53# may not be worth it).
54if sys.platform == 'win32':
55  try:
56    sys.path.insert(0, os.path.join(chrome_src, 'third_party', 'psyco_win32'))
57    import psyco
58  except:
59    psyco = None
60else:
61  psyco = None
62
63
64def GetSupplementalFiles():
65  """Returns a list of the supplemental files that are included in all GYP
66  sources."""
67  return glob.glob(os.path.join(chrome_src, '*', 'supplement.gypi'))
68
69
70def ProcessGypDefinesItems(items):
71  """Converts a list of strings to a list of key-value pairs."""
72  result = []
73  for item in items:
74    tokens = item.split('=', 1)
75    # Some GYP variables have hyphens, which we don't support.
76    if len(tokens) == 2:
77      result += [(tokens[0], tokens[1])]
78    else:
79      # No value supplied, treat it as a boolean and set it. Note that we
80      # use the string '1' here so we have a consistent definition whether
81      # you do 'foo=1' or 'foo'.
82      result += [(tokens[0], '1')]
83  return result
84
85
86def GetGypVars(supplemental_files):
87  """Returns a dictionary of all GYP vars."""
88  # Find the .gyp directory in the user's home directory.
89  home_dot_gyp = os.environ.get('GYP_CONFIG_DIR', None)
90  if home_dot_gyp:
91    home_dot_gyp = os.path.expanduser(home_dot_gyp)
92  if not home_dot_gyp:
93    home_vars = ['HOME']
94    if sys.platform in ('cygwin', 'win32'):
95      home_vars.append('USERPROFILE')
96    for home_var in home_vars:
97      home = os.getenv(home_var)
98      if home != None:
99        home_dot_gyp = os.path.join(home, '.gyp')
100        if not os.path.exists(home_dot_gyp):
101          home_dot_gyp = None
102        else:
103          break
104
105  if home_dot_gyp:
106    include_gypi = os.path.join(home_dot_gyp, "include.gypi")
107    if os.path.exists(include_gypi):
108      supplemental_files += [include_gypi]
109
110  # GYP defines from the supplemental.gypi files.
111  supp_items = []
112  for supplement in supplemental_files:
113    with open(supplement, 'r') as f:
114      try:
115        file_data = eval(f.read(), {'__builtins__': None}, None)
116      except SyntaxError, e:
117        e.filename = os.path.abspath(supplement)
118        raise
119      variables = file_data.get('variables', [])
120      for v in variables:
121        supp_items += [(v, str(variables[v]))]
122
123  # GYP defines from the environment.
124  env_items = ProcessGypDefinesItems(
125      shlex.split(os.environ.get('GYP_DEFINES', '')))
126
127  # GYP defines from the command line. We can't use optparse since we want
128  # to ignore all arguments other than "-D".
129  cmdline_input_items = []
130  for i in range(len(sys.argv))[1:]:
131    if sys.argv[i].startswith('-D'):
132      if sys.argv[i] == '-D' and i + 1 < len(sys.argv):
133        cmdline_input_items += [sys.argv[i + 1]]
134      elif len(sys.argv[i]) > 2:
135        cmdline_input_items += [sys.argv[i][2:]]
136  cmdline_items = ProcessGypDefinesItems(cmdline_input_items)
137
138  vars_dict = dict(supp_items + env_items + cmdline_items)
139  return vars_dict
140
141
142def GetOutputDirectory():
143  """Returns the output directory that GYP will use."""
144  # GYP generator flags from the command line. We can't use optparse since we
145  # want to ignore all arguments other than "-G".
146  needle = '-Goutput_dir='
147  cmdline_input_items = []
148  for item in sys.argv[1:]:
149    if item.startswith(needle):
150      return item[len(needle):]
151
152  env_items = shlex.split(os.environ.get('GYP_GENERATOR_FLAGS', ''))
153  needle = 'output_dir='
154  for item in env_items:
155    if item.startswith(needle):
156      return item[len(needle):]
157
158  return "out"
159
160
161def additional_include_files(supplemental_files, args=[]):
162  """
163  Returns a list of additional (.gypi) files to include, without duplicating
164  ones that are already specified on the command line. The list of supplemental
165  include files is passed in as an argument.
166  """
167  # Determine the include files specified on the command line.
168  # This doesn't cover all the different option formats you can use,
169  # but it's mainly intended to avoid duplicating flags on the automatic
170  # makefile regeneration which only uses this format.
171  specified_includes = set()
172  for arg in args:
173    if arg.startswith('-I') and len(arg) > 2:
174      specified_includes.add(os.path.realpath(arg[2:]))
175
176  result = []
177  def AddInclude(path):
178    if os.path.realpath(path) not in specified_includes:
179      result.append(path)
180
181  # Always include common.gypi.
182  AddInclude(os.path.join(script_dir, 'common.gypi'))
183
184  # Optionally add supplemental .gypi files if present.
185  for supplement in supplemental_files:
186    AddInclude(supplement)
187
188  return result
189
190
191if __name__ == '__main__':
192  # Disabling garbage collection saves about 1 second out of 16 on a Linux
193  # z620 workstation. Since this is a short-lived process it's not a problem to
194  # leak a few cyclyc references in order to spare the CPU cycles for
195  # scanning the heap.
196  import gc
197  gc.disable()
198
199  args = sys.argv[1:]
200
201  use_analyzer = len(args) and args[0] == '--analyzer'
202  if use_analyzer:
203    args.pop(0)
204    os.environ['GYP_GENERATORS'] = 'analyzer'
205    args.append('-Gconfig_path=' + args.pop(0))
206    args.append('-Ganalyzer_output_path=' + args.pop(0))
207
208  if int(os.environ.get('GYP_CHROMIUM_NO_ACTION', 0)):
209    print 'Skipping gyp_chromium due to GYP_CHROMIUM_NO_ACTION env var.'
210    sys.exit(0)
211
212  # Use the Psyco JIT if available.
213  if psyco:
214    psyco.profile()
215    print "Enabled Psyco JIT."
216
217  # Fall back on hermetic python if we happen to get run under cygwin.
218  # TODO(bradnelson): take this out once this issue is fixed:
219  #    http://code.google.com/p/gyp/issues/detail?id=177
220  if sys.platform == 'cygwin':
221    import find_depot_tools
222    depot_tools_path = find_depot_tools.add_depot_tools_to_path()
223    python_dir = sorted(glob.glob(os.path.join(depot_tools_path,
224                                               'python2*_bin')))[-1]
225    env = os.environ.copy()
226    env['PATH'] = python_dir + os.pathsep + env.get('PATH', '')
227    p = subprocess.Popen(
228       [os.path.join(python_dir, 'python.exe')] + sys.argv,
229       env=env, shell=False)
230    p.communicate()
231    sys.exit(p.returncode)
232
233  # This could give false positives since it doesn't actually do real option
234  # parsing.  Oh well.
235  gyp_file_specified = False
236  for arg in args:
237    if arg.endswith('.gyp'):
238      gyp_file_specified = True
239      break
240
241  gyp_environment.SetEnvironment()
242
243  # If we didn't get a file, check an env var, and then fall back to
244  # assuming 'all.gyp' from the same directory as the script.
245  if not gyp_file_specified:
246    gyp_file = os.environ.get('CHROMIUM_GYP_FILE')
247    if gyp_file:
248      # Note that CHROMIUM_GYP_FILE values can't have backslashes as
249      # path separators even on Windows due to the use of shlex.split().
250      args.extend(shlex.split(gyp_file))
251    else:
252      args.append(os.path.join(script_dir, 'all.gyp'))
253
254  # There shouldn't be a circular dependency relationship between .gyp files,
255  # but in Chromium's .gyp files, on non-Mac platforms, circular relationships
256  # currently exist.  The check for circular dependencies is currently
257  # bypassed on other platforms, but is left enabled on the Mac, where a
258  # violation of the rule causes Xcode to misbehave badly.
259  # TODO(mark): Find and kill remaining circular dependencies, and remove this
260  # option.  http://crbug.com/35878.
261  # TODO(tc): Fix circular dependencies in ChromiumOS then add linux2 to the
262  # list.
263  if sys.platform not in ('darwin',):
264    args.append('--no-circular-check')
265
266  # We explicitly don't support the make gyp generator (crbug.com/348686). Be
267  # nice and fail here, rather than choking in gyp.
268  if re.search(r'(^|,|\s)make($|,|\s)', os.environ.get('GYP_GENERATORS', '')):
269    print 'Error: make gyp generator not supported (check GYP_GENERATORS).'
270    sys.exit(1)
271
272  # If CHROMIUM_GYP_SYNTAX_CHECK is set to 1, it will invoke gyp with --check
273  # to enfore syntax checking.
274  syntax_check = os.environ.get('CHROMIUM_GYP_SYNTAX_CHECK')
275  if syntax_check and int(syntax_check):
276    args.append('--check')
277
278  supplemental_includes = GetSupplementalFiles()
279  gyp_vars_dict = GetGypVars(supplemental_includes)
280
281  # TODO(dmikurube): Remove these checks and messages after a while.
282  if ('linux_use_tcmalloc' in gyp_vars_dict or
283      'android_use_tcmalloc' in gyp_vars_dict):
284    print '*****************************************************************'
285    print '"linux_use_tcmalloc" and "android_use_tcmalloc" are deprecated!'
286    print '-----------------------------------------------------------------'
287    print 'You specify "linux_use_tcmalloc" or "android_use_tcmalloc" in'
288    print 'your GYP_DEFINES. Please switch them into "use_allocator" now.'
289    print 'See http://crbug.com/345554 for the details.'
290    print '*****************************************************************'
291
292  # Automatically turn on crosscompile support for platforms that need it.
293  # (The Chrome OS build sets CC_host / CC_target which implicitly enables
294  # this mode.)
295  if all(('ninja' in os.environ.get('GYP_GENERATORS', ''),
296          gyp_vars_dict.get('OS') in ['android', 'ios'],
297          'GYP_CROSSCOMPILE' not in os.environ)):
298    os.environ['GYP_CROSSCOMPILE'] = '1'
299  if gyp_vars_dict.get('OS') == 'android':
300    args.append('--check')
301
302  args.extend(
303      ['-I' + i for i in additional_include_files(supplemental_includes, args)])
304
305  args.extend(['-D', 'gyp_output_dir=' + GetOutputDirectory()])
306
307  if not use_analyzer:
308    print 'Updating projects from gyp files...'
309    sys.stdout.flush()
310
311  # Off we go...
312  gyp_rc = gyp.main(args)
313
314  if not use_analyzer:
315    vs2013_runtime_dll_dirs = vs_toolchain.SetEnvironmentAndGetRuntimeDllDirs()
316    if vs2013_runtime_dll_dirs:
317      x64_runtime, x86_runtime = vs2013_runtime_dll_dirs
318      vs_toolchain.CopyVsRuntimeDlls(
319        os.path.join(chrome_src, GetOutputDirectory()),
320        (x86_runtime, x64_runtime))
321
322  sys.exit(gyp_rc)
323