codekingpro/portable-devtools
114k
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
20import os.path
21import shlex
22import subprocess
23import xml.etree.ElementTree as ET
24from xml.sax.saxutils import escape
25
26import gyp
27import gyp.common
28import gyp.msvs_emulation
29
30generator_wants_static_library_dependencies_adjusted = False
31
32generator_default_variables = {}
33
34for 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
38for unused in [
39 "RULE_INPUT_PATH",
40 "RULE_INPUT_ROOT",
41 "RULE_INPUT_NAME",
42 "RULE_INPUT_DIRNAME",
43 "RULE_INPUT_EXT",
44 "EXECUTABLE_PREFIX",
45 "EXECUTABLE_SUFFIX",
46 "STATIC_LIB_PREFIX",
47 "STATIC_LIB_SUFFIX",
48 "SHARED_LIB_PREFIX",
49 "SHARED_LIB_SUFFIX",
50 "CONFIGURATION_NAME",
51]:
52 generator_default_variables[unused] = ""
53
54# Include dirs will occasionally use the SHARED_INTERMEDIATE_DIR variable as
55# part of the path when dealing with generated headers. This value will be
56# replaced dynamically for each configuration.
57generator_default_variables["SHARED_INTERMEDIATE_DIR"] = "$SHARED_INTERMEDIATE_DIR"
58
59
60def CalculateVariables(default_variables, params):
61 generator_flags = params.get("generator_flags", {})
62 for key, val in generator_flags.items():
63 default_variables.setdefault(key, val)
64 flavor = gyp.common.GetFlavor(params)
65 default_variables.setdefault("OS", flavor)
66 if flavor == "win":
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(
80 target_list,
81 target_dicts,
82 shared_intermediate_dirs,
83 config_name,
84 params,
85 compiler_path,
86):
87 """Calculate the set of include directories to be used.
88
89 Returns:
90 A list including all the include_dir's specified for every target followed
91 by any include directories that were added as cflag compiler options.
92 """
93
94 gyp_includes_set = set()
95 compiler_includes_list = []
96
97 # Find compiler's default include dirs.
98 if compiler_path:
99 command = shlex.split(compiler_path)
100 command.extend(["-E", "-xc++", "-v", "-"])
101 proc = subprocess.Popen(
102 args=command,
103 stdin=subprocess.PIPE,
104 stdout=subprocess.PIPE,
105 stderr=subprocess.PIPE,
106 )
107 output = proc.communicate()[1].decode("utf-8")
108 # Extract the list of include dirs from the output, which has this format:
109 # ...
110 # #include "..." search starts here:
111 # #include <...> search starts here:
112 # /usr/include/c++/4.6
113 # /usr/local/include
114 # End of search list.
115 # ...
116 in_include_list = False
117 for line in output.splitlines():
118 if line.startswith("#include"):
119 in_include_list = True
120 continue
121 if line.startswith("End of search list."):
122 break
123 if in_include_list:
124 include_dir = line.strip()
125 if include_dir not in compiler_includes_list:
126 compiler_includes_list.append(include_dir)
127
128 flavor = gyp.common.GetFlavor(params)
129 if flavor == "win":
130 generator_flags = params.get("generator_flags", {})
131 for target_name in target_list:
132 target = target_dicts[target_name]
133 if config_name in target["configurations"]:
134 config = target["configurations"][config_name]
135
136 # Look for any include dirs that were explicitly added via cflags. This
137 # may be done in gyp files to force certain includes to come at the end.
138 # TODO(jgreenwald): Change the gyp files to not abuse cflags for this, and
139 # remove this.
140 if flavor == "win":
141 msvs_settings = gyp.msvs_emulation.MsvsSettings(target, generator_flags)
142 cflags = msvs_settings.GetCflags(config_name)
143 else:
144 cflags = config["cflags"]
145 for cflag in cflags:
146 if cflag.startswith("-I"):
147 include_dir = cflag[2:]
148 if include_dir not in compiler_includes_list:
149 compiler_includes_list.append(include_dir)
150
151 # Find standard gyp include dirs.
152 if "include_dirs" in config:
153 include_dirs = config["include_dirs"]
154 for shared_intermediate_dir in shared_intermediate_dirs:
155 for include_dir in include_dirs:
156 include_dir = include_dir.replace(
157 "$SHARED_INTERMEDIATE_DIR", shared_intermediate_dir
158 )
159 if not os.path.isabs(include_dir):
160 base_dir = os.path.dirname(target_name)
161
162 include_dir = base_dir + "/" + include_dir
163 include_dir = os.path.abspath(include_dir)
164
165 gyp_includes_set.add(include_dir)
166
167 # Generate a list that has all the include dirs.
168 all_includes_list = list(gyp_includes_set)
169 all_includes_list.sort()
170 for compiler_include in compiler_includes_list:
171 if compiler_include not in gyp_includes_set:
172 all_includes_list.append(compiler_include)
173
174 # All done.
175 return all_includes_list
176
177
178def GetCompilerPath(target_list, data, options):
179 """Determine a command that can be used to invoke the compiler.
180
181 Returns:
182 If this is a gyp project that has explicit make settings, try to determine
183 the compiler from that. Otherwise, see if a compiler was specified via the
184 CC_target environment variable.
185 """
186 # First, see if the compiler is configured in make's settings.
187 build_file, _, _ = gyp.common.ParseQualifiedTarget(target_list[0])
188 make_global_settings_dict = data[build_file].get("make_global_settings", {})
189 for key, value in make_global_settings_dict:
190 if key in ["CC", "CXX"]:
191 return os.path.join(options.toplevel_dir, value)
192
193 # Check to see if the compiler was specified as an environment variable.
194 for key in ["CC_target", "CC", "CXX"]:
195 compiler = os.environ.get(key)
196 if compiler:
197 return compiler
198
199 return "gcc"
200
201
202def GetAllDefines(target_list, target_dicts, data, config_name, params, compiler_path):
203 """Calculate the defines for a project.
204
205 Returns:
206 A dict that includes explicit defines declared in gyp files along with all
207 of the default defines that the compiler uses.
208 """
209
210 # Get defines declared in the gyp files.
211 all_defines = {}
212 flavor = gyp.common.GetFlavor(params)
213 if flavor == "win":
214 generator_flags = params.get("generator_flags", {})
215 for target_name in target_list:
216 target = target_dicts[target_name]
217
218 if flavor == "win":
219 msvs_settings = gyp.msvs_emulation.MsvsSettings(target, generator_flags)
220 extra_defines = msvs_settings.GetComputedDefines(config_name)
221 else:
222 extra_defines = []
223 if config_name in target["configurations"]:
224 config = target["configurations"][config_name]
225 target_defines = config["defines"]
226 else:
227 target_defines = []
228 for define in target_defines + extra_defines:
229 split_define = define.split("=", 1)
230 if len(split_define) == 1:
231 split_define.append("1")
232 if split_define[0].strip() in all_defines:
233 # Already defined
234 continue
235 all_defines[split_define[0].strip()] = split_define[1].strip()
236 # Get default compiler defines (if possible).
237 if flavor == "win":
238 return all_defines # Default defines already processed in the loop above.
239 if compiler_path:
240 command = shlex.split(compiler_path)
241 command.extend(["-E", "-dM", "-"])
242 cpp_proc = subprocess.Popen(
243 args=command, cwd=".", stdin=subprocess.PIPE, stdout=subprocess.PIPE
244 )
245 cpp_output = cpp_proc.communicate()[0].decode("utf-8")
246 cpp_lines = cpp_output.split("\n")
247 for cpp_line in cpp_lines:
248 if not cpp_line.strip():
249 continue
250 cpp_line_parts = cpp_line.split(" ", 2)
251 key = cpp_line_parts[1]
252 val = cpp_line_parts[2] if len(cpp_line_parts) >= 3 else "1"
253 all_defines[key] = val
254
255 return all_defines
256
257
258def WriteIncludePaths(out, eclipse_langs, include_dirs):
259 """Write the includes section of a CDT settings export file."""
260
261 out.write(
262 ' <section name="org.eclipse.cdt.internal.ui.wizards.'
263 'settingswizards.IncludePaths">\n'
264 )
265 out.write(' <language name="holder for library settings"></language>\n')
266 for lang in eclipse_langs:
267 out.write(' <language name="%s">\n' % lang)
268 for include_dir in include_dirs:
269 out.write(
270 ' <includepath workspace_path="false">%s</includepath>\n'
271 % include_dir
272 )
273 out.write(" </language>\n")
274 out.write(" </section>\n")
275
276
277def WriteMacros(out, eclipse_langs, defines):
278 """Write the macros section of a CDT settings export file."""
279
280 out.write(
281 ' <section name="org.eclipse.cdt.internal.ui.wizards.'
282 'settingswizards.Macros">\n'
283 )
284 out.write(' <language name="holder for library settings"></language>\n')
285 for lang in eclipse_langs:
286 out.write(' <language name="%s">\n' % lang)
287 for key in sorted(defines):
288 out.write(
289 " <macro><name>%s</name><value>%s</value></macro>\n"
290 % (escape(key), escape(defines[key]))
291 )
292 out.write(" </language>\n")
293 out.write(" </section>\n")
294
295
296def GenerateOutputForConfig(target_list, target_dicts, data, params, config_name):
297 options = params["options"]
298 generator_flags = params.get("generator_flags", {})
299
300 # build_dir: relative path from source root to our output files.
301 # e.g. "out/Debug"
302 build_dir = os.path.join(generator_flags.get("output_dir", "out"), config_name)
303
304 toplevel_build = os.path.join(options.toplevel_dir, build_dir)
305 # Ninja uses out/Debug/gen while make uses out/Debug/obj/gen as the
306 # SHARED_INTERMEDIATE_DIR. Include both possible locations.
307 shared_intermediate_dirs = [
308 os.path.join(toplevel_build, "obj", "gen"),
309 os.path.join(toplevel_build, "gen"),
310 ]
311
312 GenerateCdtSettingsFile(
313 target_list,
314 target_dicts,
315 data,
316 params,
317 config_name,
318 os.path.join(toplevel_build, "eclipse-cdt-settings.xml"),
319 options,
320 shared_intermediate_dirs,
321 )
322 GenerateClasspathFile(
323 target_list,
324 target_dicts,
325 options.toplevel_dir,
326 toplevel_build,
327 os.path.join(toplevel_build, "eclipse-classpath.xml"),
328 )
329
330
331def GenerateCdtSettingsFile(
332 target_list,
333 target_dicts,
334 data,
335 params,
336 config_name,
337 out_name,
338 options,
339 shared_intermediate_dirs,
340):
341 gyp.common.EnsureDirExists(out_name)
342 with open(out_name, "w") as out:
343 out.write('<?xml version="1.0" encoding="UTF-8"?>\n')
344 out.write("<cdtprojectproperties>\n")
345
346 eclipse_langs = [
347 "C++ Source File",
348 "C Source File",
349 "Assembly Source File",
350 "GNU C++",
351 "GNU C",
352 "Assembly",
353 ]
354 compiler_path = GetCompilerPath(target_list, data, options)
355 include_dirs = GetAllIncludeDirectories(
356 target_list,
357 target_dicts,
358 shared_intermediate_dirs,
359 config_name,
360 params,
361 compiler_path,
362 )
363 WriteIncludePaths(out, eclipse_langs, include_dirs)
364 defines = GetAllDefines(
365 target_list, target_dicts, data, config_name, params, compiler_path
366 )
367 WriteMacros(out, eclipse_langs, defines)
368
369 out.write("</cdtprojectproperties>\n")
370
371
372def GenerateClasspathFile(
373 target_list, target_dicts, toplevel_dir, toplevel_build, out_name
374):
375 """Generates a classpath file suitable for symbol navigation and code
376 completion of Java code (such as in Android projects) by finding all
377 .java and .jar files used as action inputs."""
378 gyp.common.EnsureDirExists(out_name)
379 result = ET.Element("classpath")
380
381 def AddElements(kind, paths):
382 # First, we need to normalize the paths so they are all relative to the
383 # toplevel dir.
384 rel_paths = set()
385 for path in paths:
386 if os.path.isabs(path):
387 rel_paths.add(os.path.relpath(path, toplevel_dir))
388 else:
389 rel_paths.add(path)
390
391 for path in sorted(rel_paths):
392 entry_element = ET.SubElement(result, "classpathentry")
393 entry_element.set("kind", kind)
394 entry_element.set("path", path)
395
396 AddElements("lib", GetJavaJars(target_list, target_dicts, toplevel_dir))
397 AddElements("src", GetJavaSourceDirs(target_list, target_dicts, toplevel_dir))
398 # Include the standard JRE container and a dummy out folder
399 AddElements("con", ["org.eclipse.jdt.launching.JRE_CONTAINER"])
400 # Include a dummy out folder so that Eclipse doesn't use the default /bin
401 # folder in the root of the project.
402 AddElements("output", [os.path.join(toplevel_build, ".eclipse-java-build")])
403
404 ET.ElementTree(result).write(out_name)
405
406
407def GetJavaJars(target_list, target_dicts, toplevel_dir):
408 """Generates a sequence of all .jars used as inputs."""
409 for target_name in target_list:
410 target = target_dicts[target_name]
411 for action in target.get("actions", []):
412 for input_ in action["inputs"]:
413 if os.path.splitext(input_)[1] == ".jar" and not input_.startswith("$"):
414 if os.path.isabs(input_):
415 yield input_
416 else:
417 yield os.path.join(os.path.dirname(target_name), input_)
418
419
420def GetJavaSourceDirs(target_list, target_dicts, toplevel_dir):
421 """Generates a sequence of all likely java package root directories."""
422 for target_name in target_list:
423 target = target_dicts[target_name]
424 for action in target.get("actions", []):
425 for input_ in action["inputs"]:
426 if os.path.splitext(input_)[1] == ".java" and not input_.startswith(
427 "$"
428 ):
429 dir_ = os.path.dirname(
430 os.path.join(os.path.dirname(target_name), input_)
431 )
432 # If there is a parent 'src' or 'java' folder, navigate up to it -
433 # these are canonical package root names in Chromium. This will
434 # break if 'src' or 'java' exists in the package structure. This
435 # could be further improved by inspecting the java file for the
436 # package name if this proves to be too fragile in practice.
437 parent_search = dir_
438 while os.path.basename(parent_search) not in ["src", "java"]:
439 parent_search, _ = os.path.split(parent_search)
440 if not parent_search or parent_search == toplevel_dir:
441 # Didn't find a known root, just return the original path
442 yield dir_
443 break
444 else:
445 yield parent_search
446
447
448def GenerateOutput(target_list, target_dicts, data, params):
449 """Generate an XML settings file that can be imported into a CDT project."""
450
451 if params["options"].generator_output:
452 raise NotImplementedError("--generator_output not implemented for eclipse")
453
454 if user_config := params.get("generator_flags", {}).get("config", None):
455 GenerateOutputForConfig(target_list, target_dicts, data, params, user_config)
456 else:
457 config_names = target_dicts[target_list[0]]["configurations"]
458 for config_name in config_names:
459 GenerateOutputForConfig(
460 target_list, target_dicts, data, params, config_name
461 )
462 