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