1# Copyright (c) 2012 Google Inc. 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"""GYP backend that generates Eclipse CDT settings files.
6
7This backend DOES NOT generate Eclipse CDT projects. Instead, it generates XML
8files that can be imported into an Eclipse CDT project. The XML file contains a
9list of include paths and symbols (i.e. defines).
10
11Because a full .cproject definition is not created by this generator, it's not
12possible to properly define the include dirs and symbols for each file
13individually.  Instead, one set of includes/symbols is generated for the entire
14project.  This works fairly well (and is a vast improvement in general), but may
15still result in a few indexer issues here and there.
16
17This generator has no automated tests, so expect it to be broken.
18"""
19
20from xml.sax.saxutils import escape
21import os.path
22import subprocess
23import gyp
24import gyp.common
25import gyp.msvs_emulation
26import shlex
27
28generator_wants_static_library_dependencies_adjusted = False
29
30generator_default_variables = {
31}
32
33for dirname in ['INTERMEDIATE_DIR', 'PRODUCT_DIR', 'LIB_DIR', 'SHARED_LIB_DIR']:
34  # Some gyp steps fail if these are empty(!).
35  generator_default_variables[dirname] = 'dir'
36
37for unused in ['RULE_INPUT_PATH', 'RULE_INPUT_ROOT', 'RULE_INPUT_NAME',
38               'RULE_INPUT_DIRNAME', 'RULE_INPUT_EXT',
39               'EXECUTABLE_PREFIX', 'EXECUTABLE_SUFFIX',
40               'STATIC_LIB_PREFIX', 'STATIC_LIB_SUFFIX',
41               'SHARED_LIB_PREFIX', 'SHARED_LIB_SUFFIX',
42               'CONFIGURATION_NAME']:
43  generator_default_variables[unused] = ''
44
45# Include dirs will occasionally use the SHARED_INTERMEDIATE_DIR variable as
46# part of the path when dealing with generated headers.  This value will be
47# replaced dynamically for each configuration.
48generator_default_variables['SHARED_INTERMEDIATE_DIR'] = \
49    '$SHARED_INTERMEDIATE_DIR'
50
51
52def CalculateVariables(default_variables, params):
53  generator_flags = params.get('generator_flags', {})
54  for key, val in generator_flags.items():
55    default_variables.setdefault(key, val)
56  flavor = gyp.common.GetFlavor(params)
57  default_variables.setdefault('OS', flavor)
58  if flavor == 'win':
59    # Copy additional generator configuration data from VS, which is shared
60    # by the Eclipse generator.
61    import gyp.generator.msvs as msvs_generator
62    generator_additional_non_configuration_keys = getattr(msvs_generator,
63        'generator_additional_non_configuration_keys', [])
64    generator_additional_path_sections = getattr(msvs_generator,
65        'generator_additional_path_sections', [])
66
67    gyp.msvs_emulation.CalculateCommonVariables(default_variables, params)
68
69
70def CalculateGeneratorInputInfo(params):
71  """Calculate the generator specific info that gets fed to input (called by
72  gyp)."""
73  generator_flags = params.get('generator_flags', {})
74  if generator_flags.get('adjust_static_libraries', False):
75    global generator_wants_static_library_dependencies_adjusted
76    generator_wants_static_library_dependencies_adjusted = True
77
78
79def GetAllIncludeDirectories(target_list, target_dicts,
80                             shared_intermediate_dirs, config_name, params):
81  """Calculate the set of include directories to be used.
82
83  Returns:
84    A list including all the include_dir's specified for every target followed
85    by any include directories that were added as cflag compiler options.
86  """
87
88  gyp_includes_set = set()
89  compiler_includes_list = []
90
91  flavor = gyp.common.GetFlavor(params)
92  if flavor == 'win':
93    generator_flags = params.get('generator_flags', {})
94  for target_name in target_list:
95    target = target_dicts[target_name]
96    if config_name in target['configurations']:
97      config = target['configurations'][config_name]
98
99      # Look for any include dirs that were explicitly added via cflags. This
100      # may be done in gyp files to force certain includes to come at the end.
101      # TODO(jgreenwald): Change the gyp files to not abuse cflags for this, and
102      # remove this.
103      if flavor == 'win':
104        msvs_settings = gyp.msvs_emulation.MsvsSettings(target, generator_flags)
105        cflags = msvs_settings.GetCflags(config_name)
106      else:
107        cflags = config['cflags']
108      for cflag in cflags:
109        include_dir = ''
110        if cflag.startswith('-I'):
111          include_dir = cflag[2:]
112        if include_dir and not include_dir in compiler_includes_list:
113          compiler_includes_list.append(include_dir)
114
115      # Find standard gyp include dirs.
116      if config.has_key('include_dirs'):
117        include_dirs = config['include_dirs']
118        for shared_intermediate_dir in shared_intermediate_dirs:
119          for include_dir in include_dirs:
120            include_dir = include_dir.replace('$SHARED_INTERMEDIATE_DIR',
121                                              shared_intermediate_dir)
122            if not os.path.isabs(include_dir):
123              base_dir = os.path.dirname(target_name)
124
125              include_dir = base_dir + '/' + include_dir
126              include_dir = os.path.abspath(include_dir)
127
128            if not include_dir in gyp_includes_set:
129              gyp_includes_set.add(include_dir)
130
131
132  # Generate a list that has all the include dirs.
133  all_includes_list = list(gyp_includes_set)
134  all_includes_list.sort()
135  for compiler_include in compiler_includes_list:
136    if not compiler_include in gyp_includes_set:
137      all_includes_list.append(compiler_include)
138
139  # All done.
140  return all_includes_list
141
142
143def GetCompilerPath(target_list, target_dicts, data):
144  """Determine a command that can be used to invoke the compiler.
145
146  Returns:
147    If this is a gyp project that has explicit make settings, try to determine
148    the compiler from that.  Otherwise, see if a compiler was specified via the
149    CC_target environment variable.
150  """
151
152  # First, see if the compiler is configured in make's settings.
153  build_file, _, _ = gyp.common.ParseQualifiedTarget(target_list[0])
154  make_global_settings_dict = data[build_file].get('make_global_settings', {})
155  for key, value in make_global_settings_dict:
156    if key in ['CC', 'CXX']:
157      return value
158
159  # Check to see if the compiler was specified as an environment variable.
160  for key in ['CC_target', 'CC', 'CXX']:
161    compiler = os.environ.get(key)
162    if compiler:
163      return compiler
164
165  return 'gcc'
166
167
168def GetAllDefines(target_list, target_dicts, data, config_name, params):
169  """Calculate the defines for a project.
170
171  Returns:
172    A dict that includes explict defines declared in gyp files along with all of
173    the default defines that the compiler uses.
174  """
175
176  # Get defines declared in the gyp files.
177  all_defines = {}
178  flavor = gyp.common.GetFlavor(params)
179  if flavor == 'win':
180    generator_flags = params.get('generator_flags', {})
181  for target_name in target_list:
182    target = target_dicts[target_name]
183
184    if flavor == 'win':
185      msvs_settings = gyp.msvs_emulation.MsvsSettings(target, generator_flags)
186      extra_defines = msvs_settings.GetComputedDefines(config_name)
187    else:
188      extra_defines = []
189    if config_name in target['configurations']:
190      config = target['configurations'][config_name]
191      target_defines = config['defines']
192    else:
193      target_defines = []
194    for define in target_defines + extra_defines:
195      split_define = define.split('=', 1)
196      if len(split_define) == 1:
197        split_define.append('1')
198      if split_define[0].strip() in all_defines:
199        # Already defined
200        continue
201      all_defines[split_define[0].strip()] = split_define[1].strip()
202  # Get default compiler defines (if possible).
203  if flavor == 'win':
204    return all_defines  # Default defines already processed in the loop above.
205  cc_target = GetCompilerPath(target_list, target_dicts, data)
206  if cc_target:
207    command = shlex.split(cc_target)
208    command.extend(['-E', '-dM', '-'])
209    cpp_proc = subprocess.Popen(args=command, cwd='.',
210                                stdin=subprocess.PIPE, stdout=subprocess.PIPE)
211    cpp_output = cpp_proc.communicate()[0]
212    cpp_lines = cpp_output.split('\n')
213    for cpp_line in cpp_lines:
214      if not cpp_line.strip():
215        continue
216      cpp_line_parts = cpp_line.split(' ', 2)
217      key = cpp_line_parts[1]
218      if len(cpp_line_parts) >= 3:
219        val = cpp_line_parts[2]
220      else:
221        val = '1'
222      all_defines[key] = val
223
224  return all_defines
225
226
227def WriteIncludePaths(out, eclipse_langs, include_dirs):
228  """Write the includes section of a CDT settings export file."""
229
230  out.write('  <section name="org.eclipse.cdt.internal.ui.wizards.' \
231            'settingswizards.IncludePaths">\n')
232  out.write('    <language name="holder for library settings"></language>\n')
233  for lang in eclipse_langs:
234    out.write('    <language name="%s">\n' % lang)
235    for include_dir in include_dirs:
236      out.write('      <includepath workspace_path="false">%s</includepath>\n' %
237                include_dir)
238    out.write('    </language>\n')
239  out.write('  </section>\n')
240
241
242def WriteMacros(out, eclipse_langs, defines):
243  """Write the macros section of a CDT settings export file."""
244
245  out.write('  <section name="org.eclipse.cdt.internal.ui.wizards.' \
246            'settingswizards.Macros">\n')
247  out.write('    <language name="holder for library settings"></language>\n')
248  for lang in eclipse_langs:
249    out.write('    <language name="%s">\n' % lang)
250    for key in sorted(defines.iterkeys()):
251      out.write('      <macro><name>%s</name><value>%s</value></macro>\n' %
252                (escape(key), escape(defines[key])))
253    out.write('    </language>\n')
254  out.write('  </section>\n')
255
256
257def GenerateOutputForConfig(target_list, target_dicts, data, params,
258                            config_name):
259  options = params['options']
260  generator_flags = params.get('generator_flags', {})
261
262  # build_dir: relative path from source root to our output files.
263  # e.g. "out/Debug"
264  build_dir = os.path.join(generator_flags.get('output_dir', 'out'),
265                           config_name)
266
267  toplevel_build = os.path.join(options.toplevel_dir, build_dir)
268  # Ninja uses out/Debug/gen while make uses out/Debug/obj/gen as the
269  # SHARED_INTERMEDIATE_DIR. Include both possible locations.
270  shared_intermediate_dirs = [os.path.join(toplevel_build, 'obj', 'gen'),
271                              os.path.join(toplevel_build, 'gen')]
272
273  out_name = os.path.join(toplevel_build, 'eclipse-cdt-settings.xml')
274  gyp.common.EnsureDirExists(out_name)
275  out = open(out_name, 'w')
276
277  out.write('<?xml version="1.0" encoding="UTF-8"?>\n')
278  out.write('<cdtprojectproperties>\n')
279
280  eclipse_langs = ['C++ Source File', 'C Source File', 'Assembly Source File',
281                   'GNU C++', 'GNU C', 'Assembly']
282  include_dirs = GetAllIncludeDirectories(target_list, target_dicts,
283                                          shared_intermediate_dirs, config_name,
284                                          params)
285  WriteIncludePaths(out, eclipse_langs, include_dirs)
286  defines = GetAllDefines(target_list, target_dicts, data, config_name, params)
287  WriteMacros(out, eclipse_langs, defines)
288
289  out.write('</cdtprojectproperties>\n')
290  out.close()
291
292
293def GenerateOutput(target_list, target_dicts, data, params):
294  """Generate an XML settings file that can be imported into a CDT project."""
295
296  if params['options'].generator_output:
297    raise NotImplementedError, "--generator_output not implemented for eclipse"
298
299  user_config = params.get('generator_flags', {}).get('config', None)
300  if user_config:
301    GenerateOutputForConfig(target_list, target_dicts, data, params,
302                            user_config)
303  else:
304    config_names = target_dicts[target_list[0]]['configurations'].keys()
305    for config_name in config_names:
306      GenerateOutputForConfig(target_list, target_dicts, data, params,
307                              config_name)
308
309