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
|
|
|
|
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). |
10
|
|
|
|
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. |
16
|
|
|
|
17
|
|
|
This generator has no automated tests, so expect it to be broken. |
18
|
|
|
""" |
19
|
|
|
|
20
|
|
|
from xml.sax.saxutils import escape |
21
|
|
|
import os.path |
22
|
|
|
import subprocess |
23
|
|
|
import gyp |
24
|
|
|
import gyp.common |
25
|
|
|
import gyp.msvs_emulation |
26
|
|
|
import shlex |
27
|
|
|
import xml.etree.cElementTree as ET |
28
|
|
|
|
29
|
|
|
generator_wants_static_library_dependencies_adjusted = False |
30
|
|
|
|
31
|
|
|
generator_default_variables = { |
32
|
|
|
} |
33
|
|
|
|
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 |
37
|
|
|
|
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] = '' |
45
|
|
|
|
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' |
51
|
|
|
|
52
|
|
|
|
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) |
59
|
|
|
if flavor == 'win': |
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', []) |
67
|
|
|
|
68
|
|
|
gyp.msvs_emulation.CalculateCommonVariables(default_variables, params) |
69
|
|
|
|
70
|
|
|
|
71
|
|
|
def CalculateGeneratorInputInfo(params): |
72
|
|
|
"""Calculate the generator specific info that gets fed to input (called by |
73
|
|
|
gyp).""" |
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 |
78
|
|
|
|
79
|
|
|
|
80
|
|
|
def GetAllIncludeDirectories(target_list, target_dicts, |
81
|
|
|
shared_intermediate_dirs, config_name, params, |
82
|
|
|
compiler_path): |
83
|
|
|
"""Calculate the set of include directories to be used. |
84
|
|
|
|
85
|
|
|
Returns: |
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. |
88
|
|
|
""" |
89
|
|
|
|
90
|
|
|
gyp_includes_set = set() |
91
|
|
|
compiler_includes_list = [] |
92
|
|
|
|
93
|
|
|
# Find compiler's default include dirs. |
94
|
|
|
if compiler_path: |
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: |
101
|
|
|
# ... |
102
|
|
|
# #include "..." search starts here: |
103
|
|
|
# #include <...> search starts here: |
104
|
|
|
# /usr/include/c++/4.6 |
105
|
|
|
# /usr/local/include |
106
|
|
|
# End of search list. |
107
|
|
|
# ... |
108
|
|
|
in_include_list = False |
109
|
|
|
for line in output.splitlines(): |
110
|
|
|
if line.startswith('#include'): |
111
|
|
|
in_include_list = True |
112
|
|
|
continue |
113
|
|
|
if line.startswith('End of search list.'): |
114
|
|
|
break |
115
|
|
|
if in_include_list: |
116
|
|
|
include_dir = line.strip() |
117
|
|
|
if include_dir not in compiler_includes_list: |
118
|
|
|
compiler_includes_list.append(include_dir) |
119
|
|
|
|
120
|
|
|
flavor = gyp.common.GetFlavor(params) |
121
|
|
|
if flavor == 'win': |
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] |
127
|
|
|
|
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 |
131
|
|
|
# remove this. |
132
|
|
|
if flavor == 'win': |
133
|
|
|
msvs_settings = gyp.msvs_emulation.MsvsSettings(target, generator_flags) |
134
|
|
|
cflags = msvs_settings.GetCflags(config_name) |
135
|
|
|
else: |
136
|
|
|
cflags = config['cflags'] |
137
|
|
|
for cflag in 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) |
142
|
|
|
|
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) |
152
|
|
|
|
153
|
|
|
include_dir = base_dir + '/' + include_dir |
154
|
|
|
include_dir = os.path.abspath(include_dir) |
155
|
|
|
|
156
|
|
|
gyp_includes_set.add(include_dir) |
157
|
|
|
|
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) |
164
|
|
|
|
165
|
|
|
# All done. |
166
|
|
|
return all_includes_list |
167
|
|
|
|
168
|
|
|
|
169
|
|
|
def GetCompilerPath(target_list, data, options): |
170
|
|
|
"""Determine a command that can be used to invoke the compiler. |
171
|
|
|
|
172
|
|
|
Returns: |
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. |
176
|
|
|
""" |
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) |
183
|
|
|
|
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) |
187
|
|
|
if compiler: |
188
|
|
|
return compiler |
189
|
|
|
|
190
|
|
|
return 'gcc' |
191
|
|
|
|
192
|
|
|
|
193
|
|
|
def GetAllDefines(target_list, target_dicts, data, config_name, params, |
194
|
|
|
compiler_path): |
195
|
|
|
"""Calculate the defines for a project. |
196
|
|
|
|
197
|
|
|
Returns: |
198
|
|
|
A dict that includes explict defines declared in gyp files along with all of |
199
|
|
|
the default defines that the compiler uses. |
200
|
|
|
""" |
201
|
|
|
|
202
|
|
|
# Get defines declared in the gyp files. |
203
|
|
|
all_defines = {} |
204
|
|
|
flavor = gyp.common.GetFlavor(params) |
205
|
|
|
if flavor == 'win': |
206
|
|
|
generator_flags = params.get('generator_flags', {}) |
207
|
|
|
for target_name in target_list: |
208
|
|
|
target = target_dicts[target_name] |
209
|
|
|
|
210
|
|
|
if flavor == 'win': |
211
|
|
|
msvs_settings = gyp.msvs_emulation.MsvsSettings(target, generator_flags) |
212
|
|
|
extra_defines = msvs_settings.GetComputedDefines(config_name) |
213
|
|
|
else: |
214
|
|
|
extra_defines = [] |
215
|
|
|
if config_name in target['configurations']: |
216
|
|
|
config = target['configurations'][config_name] |
217
|
|
|
target_defines = config['defines'] |
218
|
|
|
else: |
219
|
|
|
target_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: |
225
|
|
|
# Already defined |
226
|
|
|
continue |
227
|
|
|
all_defines[split_define[0].strip()] = split_define[1].strip() |
228
|
|
|
# Get default compiler defines (if possible). |
229
|
|
|
if flavor == 'win': |
230
|
|
|
return all_defines # Default defines already processed in the loop above. |
231
|
|
|
if compiler_path: |
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(): |
240
|
|
|
continue |
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] |
245
|
|
|
else: |
246
|
|
|
val = '1' |
247
|
|
|
all_defines[key] = val |
248
|
|
|
|
249
|
|
|
return all_defines |
250
|
|
|
|
251
|
|
|
|
252
|
|
|
def WriteIncludePaths(out, eclipse_langs, include_dirs): |
253
|
|
|
"""Write the includes section of a CDT settings export file.""" |
254
|
|
|
|
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' % |
262
|
|
|
include_dir) |
263
|
|
|
out.write(' </language>\n') |
264
|
|
|
out.write(' </section>\n') |
265
|
|
|
|
266
|
|
|
|
267
|
|
|
def WriteMacros(out, eclipse_langs, defines): |
268
|
|
|
"""Write the macros section of a CDT settings export file.""" |
269
|
|
|
|
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') |
280
|
|
|
|
281
|
|
|
|
282
|
|
|
def GenerateOutputForConfig(target_list, target_dicts, data, params, |
283
|
|
|
config_name): |
284
|
|
|
options = params['options'] |
285
|
|
|
generator_flags = params.get('generator_flags', {}) |
286
|
|
|
|
287
|
|
|
# build_dir: relative path from source root to our output files. |
288
|
|
|
# e.g. "out/Debug" |
289
|
|
|
build_dir = os.path.join(generator_flags.get('output_dir', 'out'), |
290
|
|
|
config_name) |
291
|
|
|
|
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')] |
297
|
|
|
|
298
|
|
|
GenerateCdtSettingsFile(target_list, |
299
|
|
|
target_dicts, |
300
|
|
|
data, |
301
|
|
|
params, |
302
|
|
|
config_name, |
303
|
|
|
os.path.join(toplevel_build, |
304
|
|
|
'eclipse-cdt-settings.xml'), |
305
|
|
|
options, |
306
|
|
|
shared_intermediate_dirs) |
307
|
|
|
GenerateClasspathFile(target_list, |
308
|
|
|
target_dicts, |
309
|
|
|
options.toplevel_dir, |
310
|
|
|
toplevel_build, |
311
|
|
|
os.path.join(toplevel_build, |
312
|
|
|
'eclipse-classpath.xml')) |
313
|
|
|
|
314
|
|
|
|
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') |
322
|
|
|
|
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) |
333
|
|
|
|
334
|
|
|
out.write('</cdtprojectproperties>\n') |
335
|
|
|
|
336
|
|
|
|
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') |
344
|
|
|
|
345
|
|
|
def AddElements(kind, paths): |
346
|
|
|
# First, we need to normalize the paths so they are all relative to the |
347
|
|
|
# toplevel dir. |
348
|
|
|
rel_paths = set() |
349
|
|
|
for path in paths: |
350
|
|
|
if os.path.isabs(path): |
351
|
|
|
rel_paths.add(os.path.relpath(path, toplevel_dir)) |
352
|
|
|
else: |
353
|
|
|
rel_paths.add(path) |
354
|
|
|
|
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) |
359
|
|
|
|
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')]) |
367
|
|
|
|
368
|
|
|
ET.ElementTree(result).write(out_name) |
369
|
|
|
|
370
|
|
|
|
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_): |
379
|
|
|
yield input_ |
380
|
|
|
else: |
381
|
|
|
yield os.path.join(os.path.dirname(target_name), input_) |
382
|
|
|
|
383
|
|
|
|
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), |
393
|
|
|
input_)) |
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. |
399
|
|
|
parent_search = dir_ |
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 |
404
|
|
|
yield dir_ |
405
|
|
|
break |
406
|
|
|
else: |
407
|
|
|
yield parent_search |
408
|
|
|
|
409
|
|
|
|
410
|
|
|
def GenerateOutput(target_list, target_dicts, data, params): |
411
|
|
|
"""Generate an XML settings file that can be imported into a CDT project.""" |
412
|
|
|
|
413
|
|
|
if params['options'].generator_output: |
414
|
|
|
raise NotImplementedError("--generator_output not implemented for eclipse") |
415
|
|
|
|
416
|
|
|
user_config = params.get('generator_flags', {}).get('config', None) |
417
|
|
|
if user_config: |
418
|
|
|
GenerateOutputForConfig(target_list, target_dicts, data, params, |
419
|
|
|
user_config) |
420
|
|
|
else: |
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, |
424
|
|
|
config_name) |
425
|
|
|
|
426
|
|
|
|