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.
5 """GYP backend that generates Eclipse CDT settings files.
7 This backend DOES NOT generate Eclipse CDT projects. Instead, it generates XML
8 files that can be imported into an Eclipse CDT project. The XML file contains a
9 list of include paths and symbols (i.e. defines).
11 Because a full .cproject definition is not created by this generator, it's not
12 possible to properly define the include dirs and symbols for each file
13 individually. Instead, one set of includes/symbols is generated for the entire
14 project. This works fairly well (and is a vast improvement in general), but may
15 still result in a few indexer issues here and there.
17 This generator has no automated tests, so expect it to be broken.
20 from xml.sax.saxutils import escape
25 import gyp.msvs_emulation
27 import xml.etree.cElementTree as ET
29 generator_wants_static_library_dependencies_adjusted = False
31 generator_default_variables = {
34 for dirname in ['INTERMEDIATE_DIR', 'PRODUCT_DIR', 'LIB_DIR', 'SHARED_LIB_DIR']:
35 # Some gyp steps fail if these are empty(!), so we convert them to variables
36 generator_default_variables[dirname] = '$' + dirname
38 for unused in ['RULE_INPUT_PATH', 'RULE_INPUT_ROOT', 'RULE_INPUT_NAME',
39 'RULE_INPUT_DIRNAME', 'RULE_INPUT_EXT',
40 'EXECUTABLE_PREFIX', 'EXECUTABLE_SUFFIX',
41 'STATIC_LIB_PREFIX', 'STATIC_LIB_SUFFIX',
42 'SHARED_LIB_PREFIX', 'SHARED_LIB_SUFFIX',
43 'CONFIGURATION_NAME']:
44 generator_default_variables[unused] = ''
46 # Include dirs will occasionally use the SHARED_INTERMEDIATE_DIR variable as
47 # part of the path when dealing with generated headers. This value will be
48 # replaced dynamically for each configuration.
49 generator_default_variables['SHARED_INTERMEDIATE_DIR'] = \
50 '$SHARED_INTERMEDIATE_DIR'
53 def CalculateVariables(default_variables, params):
54 generator_flags = params.get('generator_flags', {})
55 for key, val in generator_flags.items():
56 default_variables.setdefault(key, val)
57 flavor = gyp.common.GetFlavor(params)
58 default_variables.setdefault('OS', flavor)
60 # Copy additional generator configuration data from VS, which is shared
61 # by the Eclipse generator.
62 import gyp.generator.msvs as msvs_generator
63 generator_additional_non_configuration_keys = getattr(msvs_generator,
64 'generator_additional_non_configuration_keys', [])
65 generator_additional_path_sections = getattr(msvs_generator,
66 'generator_additional_path_sections', [])
68 gyp.msvs_emulation.CalculateCommonVariables(default_variables, params)
71 def CalculateGeneratorInputInfo(params):
72 """Calculate the generator specific info that gets fed to input (called by
74 generator_flags = params.get('generator_flags', {})
75 if generator_flags.get('adjust_static_libraries', False):
76 global generator_wants_static_library_dependencies_adjusted
77 generator_wants_static_library_dependencies_adjusted = True
80 def GetAllIncludeDirectories(target_list, target_dicts,
81 shared_intermediate_dirs, config_name, params,
83 """Calculate the set of include directories to be used.
86 A list including all the include_dir's specified for every target followed
87 by any include directories that were added as cflag compiler options.
90 gyp_includes_set = set()
91 compiler_includes_list = []
93 # Find compiler's default include dirs.
95 command = shlex.split(compiler_path)
96 command.extend(['-E', '-xc++', '-v', '-'])
97 proc = subprocess.Popen(args=command, stdin=subprocess.PIPE,
98 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
99 output = proc.communicate()[1]
100 # Extract the list of include dirs from the output, which has this format:
102 # #include "..." search starts here:
103 # #include <...> search starts here:
104 # /usr/include/c++/4.6
106 # End of search list.
108 in_include_list = False
109 for line in output.splitlines():
110 if line.startswith('#include'):
111 in_include_list = True
113 if line.startswith('End of search list.'):
116 include_dir = line.strip()
117 if include_dir not in compiler_includes_list:
118 compiler_includes_list.append(include_dir)
120 flavor = gyp.common.GetFlavor(params)
122 generator_flags = params.get('generator_flags', {})
123 for target_name in target_list:
124 target = target_dicts[target_name]
125 if config_name in target['configurations']:
126 config = target['configurations'][config_name]
128 # Look for any include dirs that were explicitly added via cflags. This
129 # may be done in gyp files to force certain includes to come at the end.
130 # TODO(jgreenwald): Change the gyp files to not abuse cflags for this, and
133 msvs_settings = gyp.msvs_emulation.MsvsSettings(target, generator_flags)
134 cflags = msvs_settings.GetCflags(config_name)
136 cflags = config['cflags']
138 if cflag.startswith('-I'):
139 include_dir = cflag[2:]
140 if include_dir not in compiler_includes_list:
141 compiler_includes_list.append(include_dir)
143 # Find standard gyp include dirs.
144 if config.has_key('include_dirs'):
145 include_dirs = config['include_dirs']
146 for shared_intermediate_dir in shared_intermediate_dirs:
147 for include_dir in include_dirs:
148 include_dir = include_dir.replace('$SHARED_INTERMEDIATE_DIR',
149 shared_intermediate_dir)
150 if not os.path.isabs(include_dir):
151 base_dir = os.path.dirname(target_name)
153 include_dir = base_dir + '/' + include_dir
154 include_dir = os.path.abspath(include_dir)
156 gyp_includes_set.add(include_dir)
158 # Generate a list that has all the include dirs.
159 all_includes_list = list(gyp_includes_set)
160 all_includes_list.sort()
161 for compiler_include in compiler_includes_list:
162 if not compiler_include in gyp_includes_set:
163 all_includes_list.append(compiler_include)
166 return all_includes_list
169 def GetCompilerPath(target_list, data, options):
170 """Determine a command that can be used to invoke the compiler.
173 If this is a gyp project that has explicit make settings, try to determine
174 the compiler from that. Otherwise, see if a compiler was specified via the
175 CC_target environment variable.
177 # First, see if the compiler is configured in make's settings.
178 build_file, _, _ = gyp.common.ParseQualifiedTarget(target_list[0])
179 make_global_settings_dict = data[build_file].get('make_global_settings', {})
180 for key, value in make_global_settings_dict:
181 if key in ['CC', 'CXX']:
182 return os.path.join(options.toplevel_dir, value)
184 # Check to see if the compiler was specified as an environment variable.
185 for key in ['CC_target', 'CC', 'CXX']:
186 compiler = os.environ.get(key)
193 def GetAllDefines(target_list, target_dicts, data, config_name, params,
195 """Calculate the defines for a project.
198 A dict that includes explict defines declared in gyp files along with all of
199 the default defines that the compiler uses.
202 # Get defines declared in the gyp files.
204 flavor = gyp.common.GetFlavor(params)
206 generator_flags = params.get('generator_flags', {})
207 for target_name in target_list:
208 target = target_dicts[target_name]
211 msvs_settings = gyp.msvs_emulation.MsvsSettings(target, generator_flags)
212 extra_defines = msvs_settings.GetComputedDefines(config_name)
215 if config_name in target['configurations']:
216 config = target['configurations'][config_name]
217 target_defines = config['defines']
220 for define in target_defines + extra_defines:
221 split_define = define.split('=', 1)
222 if len(split_define) == 1:
223 split_define.append('1')
224 if split_define[0].strip() in all_defines:
227 all_defines[split_define[0].strip()] = split_define[1].strip()
228 # Get default compiler defines (if possible).
230 return all_defines # Default defines already processed in the loop above.
232 command = shlex.split(compiler_path)
233 command.extend(['-E', '-dM', '-'])
234 cpp_proc = subprocess.Popen(args=command, cwd='.',
235 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
236 cpp_output = cpp_proc.communicate()[0]
237 cpp_lines = cpp_output.split('\n')
238 for cpp_line in cpp_lines:
239 if not cpp_line.strip():
241 cpp_line_parts = cpp_line.split(' ', 2)
242 key = cpp_line_parts[1]
243 if len(cpp_line_parts) >= 3:
244 val = cpp_line_parts[2]
247 all_defines[key] = val
252 def WriteIncludePaths(out, eclipse_langs, include_dirs):
253 """Write the includes section of a CDT settings export file."""
255 out.write(' <section name="org.eclipse.cdt.internal.ui.wizards.' \
256 'settingswizards.IncludePaths">\n')
257 out.write(' <language name="holder for library settings"></language>\n')
258 for lang in eclipse_langs:
259 out.write(' <language name="%s">\n' % lang)
260 for include_dir in include_dirs:
261 out.write(' <includepath workspace_path="false">%s</includepath>\n' %
263 out.write(' </language>\n')
264 out.write(' </section>\n')
267 def WriteMacros(out, eclipse_langs, defines):
268 """Write the macros section of a CDT settings export file."""
270 out.write(' <section name="org.eclipse.cdt.internal.ui.wizards.' \
271 'settingswizards.Macros">\n')
272 out.write(' <language name="holder for library settings"></language>\n')
273 for lang in eclipse_langs:
274 out.write(' <language name="%s">\n' % lang)
275 for key in sorted(defines.iterkeys()):
276 out.write(' <macro><name>%s</name><value>%s</value></macro>\n' %
277 (escape(key), escape(defines[key])))
278 out.write(' </language>\n')
279 out.write(' </section>\n')
282 def GenerateOutputForConfig(target_list, target_dicts, data, params,
284 options = params['options']
285 generator_flags = params.get('generator_flags', {})
287 # build_dir: relative path from source root to our output files.
289 build_dir = os.path.join(generator_flags.get('output_dir', 'out'),
292 toplevel_build = os.path.join(options.toplevel_dir, build_dir)
293 # Ninja uses out/Debug/gen while make uses out/Debug/obj/gen as the
294 # SHARED_INTERMEDIATE_DIR. Include both possible locations.
295 shared_intermediate_dirs = [os.path.join(toplevel_build, 'obj', 'gen'),
296 os.path.join(toplevel_build, 'gen')]
298 GenerateCdtSettingsFile(target_list,
303 os.path.join(toplevel_build,
304 'eclipse-cdt-settings.xml'),
306 shared_intermediate_dirs)
307 GenerateClasspathFile(target_list,
309 options.toplevel_dir,
311 os.path.join(toplevel_build,
312 'eclipse-classpath.xml'))
315 def GenerateCdtSettingsFile(target_list, target_dicts, data, params,
316 config_name, out_name, options,
317 shared_intermediate_dirs):
318 gyp.common.EnsureDirExists(out_name)
319 with open(out_name, 'w') as out:
320 out.write('<?xml version="1.0" encoding="UTF-8"?>\n')
321 out.write('<cdtprojectproperties>\n')
323 eclipse_langs = ['C++ Source File', 'C Source File', 'Assembly Source File',
324 'GNU C++', 'GNU C', 'Assembly']
325 compiler_path = GetCompilerPath(target_list, data, options)
326 include_dirs = GetAllIncludeDirectories(target_list, target_dicts,
327 shared_intermediate_dirs,
328 config_name, params, compiler_path)
329 WriteIncludePaths(out, eclipse_langs, include_dirs)
330 defines = GetAllDefines(target_list, target_dicts, data, config_name,
331 params, compiler_path)
332 WriteMacros(out, eclipse_langs, defines)
334 out.write('</cdtprojectproperties>\n')
337 def GenerateClasspathFile(target_list, target_dicts, toplevel_dir,
338 toplevel_build, out_name):
339 '''Generates a classpath file suitable for symbol navigation and code
340 completion of Java code (such as in Android projects) by finding all
341 .java and .jar files used as action inputs.'''
342 gyp.common.EnsureDirExists(out_name)
343 result = ET.Element('classpath')
345 def AddElements(kind, paths):
346 # First, we need to normalize the paths so they are all relative to the
350 if os.path.isabs(path):
351 rel_paths.add(os.path.relpath(path, toplevel_dir))
355 for path in sorted(rel_paths):
356 entry_element = ET.SubElement(result, 'classpathentry')
357 entry_element.set('kind', kind)
358 entry_element.set('path', path)
360 AddElements('lib', GetJavaJars(target_list, target_dicts, toplevel_dir))
361 AddElements('src', GetJavaSourceDirs(target_list, target_dicts, toplevel_dir))
362 # Include the standard JRE container and a dummy out folder
363 AddElements('con', ['org.eclipse.jdt.launching.JRE_CONTAINER'])
364 # Include a dummy out folder so that Eclipse doesn't use the default /bin
365 # folder in the root of the project.
366 AddElements('output', [os.path.join(toplevel_build, '.eclipse-java-build')])
368 ET.ElementTree(result).write(out_name)
371 def GetJavaJars(target_list, target_dicts, toplevel_dir):
372 '''Generates a sequence of all .jars used as inputs.'''
373 for target_name in target_list:
374 target = target_dicts[target_name]
375 for action in target.get('actions', []):
376 for input_ in action['inputs']:
377 if os.path.splitext(input_)[1] == '.jar' and not input_.startswith('$'):
378 if os.path.isabs(input_):
381 yield os.path.join(os.path.dirname(target_name), input_)
384 def GetJavaSourceDirs(target_list, target_dicts, toplevel_dir):
385 '''Generates a sequence of all likely java package root directories.'''
386 for target_name in target_list:
387 target = target_dicts[target_name]
388 for action in target.get('actions', []):
389 for input_ in action['inputs']:
390 if (os.path.splitext(input_)[1] == '.java' and
391 not input_.startswith('$')):
392 dir_ = os.path.dirname(os.path.join(os.path.dirname(target_name),
394 # If there is a parent 'src' or 'java' folder, navigate up to it -
395 # these are canonical package root names in Chromium. This will
396 # break if 'src' or 'java' exists in the package structure. This
397 # could be further improved by inspecting the java file for the
398 # package name if this proves to be too fragile in practice.
400 while os.path.basename(parent_search) not in ['src', 'java']:
401 parent_search, _ = os.path.split(parent_search)
402 if not parent_search or parent_search == toplevel_dir:
403 # Didn't find a known root, just return the original path
410 def GenerateOutput(target_list, target_dicts, data, params):
411 """Generate an XML settings file that can be imported into a CDT project."""
413 if params['options'].generator_output:
414 raise NotImplementedError("--generator_output not implemented for eclipse")
416 user_config = params.get('generator_flags', {}).get('config', None)
418 GenerateOutputForConfig(target_list, target_dicts, data, params,
421 config_names = target_dicts[target_list[0]]['configurations'].keys()
422 for config_name in config_names:
423 GenerateOutputForConfig(target_list, target_dicts, data, params,